Catalog & Sign-off

A Journey approval says "this flow works". It means more when the user the flow is for and the job it serves were vetted too. The catalog holds the three, all human-approved and linked to each other.

EntityWhatLives inLinks
Personawho the user is: a description, the account role, and optionally the session settings a run signs in with.jevitate/personas.json (the personas file, extended; no second file)n/a
Joba job story, with a priority.jevitate/jobs.jsonthe personas it serves
Journeyhow a persona does the job in this app.jevitate/journeys/metadata.job and metadata.persona

It is opt-in. A project without personas.json / jobs.json works exactly as before, and a Journey that links nothing promotes exactly as before.

Job stories

A job is a job story: When [trigger], I want to [motivation], so I can [outcome]. The trigger is a situation, not a persona; the outcome is what the user can do once the job is done, not a feature.

[
  {
    "id": "invite-teammate",
    "trigger": "a new colleague joins my team",
    "motivation": "invite them by email",
    "outcome": "work on our projects together from their first day",
    "personas": ["admin"],
    "priority": "high"
  }
]
  • trigger, motivation and outcome are required and non-empty. A job missing any of the three is refused (E_CATALOG_INPUT, exit 64), by every command that reads the catalog.
  • personas are the persona ids it serves; priority is high, medium or low.
  • The file is a bare array or {"jobs": [...]}. .jevitate/campaign/jobs.json is still read when .jevitate/jobs.json does not exist; if both exist the catalog is refused until you merge them.

Job outcomes, steps and metrics (0.10.0)

A job can also carry the shared jobs-to-be-done fields, all optional. They are what a Journeeze catalog bundle carries.

{
  "id": "invite",
  "trigger": "a colleague joins my team",
  "motivation": "invite them by email",
  "outcome": "work together from their first day",
  "personas": ["admin"],
  "kind": "core",
  "steps": [
    { "id": "choose", "name": "Choose who to invite", "stage": "define" },
    { "id": "send", "name": "Send the invitation", "stage": "execute" }
  ],
  "desiredOutcomes": [
    { "id": "invite-fast", "step": "send", "direction": "minimize", "measure": "time",
      "object": "the time it takes to get a teammate invited",
      "metric": { "kind": "duration", "from": "members-open", "to": "invite-sent", "stat": "p50" },
      "target": { "op": "<=", "value": 60, "unit": "s" } }
  ],
  "provenance": "team_hypothesis"
}
FieldWhat
kind, parentcore, setup or related; the id of a declared job this one belongs to (no cycles).
context, constraintsup to 20 one-line strings each.
stepsup to 30 solution-free job steps { id, name, stage? }; stage is a job-map stage: define, locate, prepare, confirm, execute, monitor, modify, resolve or conclude.
desiredOutcomesup to 50 outcomes { id, step?, direction, measure, object, clarifier?, gulf?, metric?, target?, guardrail?, priority? }. direction is minimize or maximize; measure is time, likelihood, effort or count; guardrail: true marks a counter-outcome that must never get worse.
metricwhere an outcome is measured: duration (from, to, stat p50 / p75 / share_under), completion (from, to), abandon / repeat / error / assisted (at) or answer. from, to and at name an anchor or the reserved job_start / job_end.
targetwhat good enough is: { op, value, unit } with op one of < <= > >= and unit s, percent or count.
provenancewhere the content came from, weakest first: ai_draft, team_hypothesis, customer_evidenced, behaviour_validated.
revision, lastValidated, extensionsa whole-number revision; a YYYY-MM-DD date; up to 16 namespaces of free-form data.

Every one of these fields is part of the job's content hash, so editing any of them makes an approved job stale (needs re-review). A Journey linked to a job can say which desired outcomes it serves with metadata.serves: ["invite-fast"].

Anchors as job-step boundaries

On a job-linked Journey, a named anchor also marks the boundary of a job step and the point an outcome metric measures from or to.

"anchors": [
  { "name": "members-open", "step": 2, "stepId": "s-3k9q1a", "jobStep": "choose", "boundary": "start" },
  { "name": "invite-sent",  "step": 5, "stepId": "s-8d2m4x", "jobStep": "send",   "boundary": "end" }
]
  • job_start and job_end are the anchors of every job: a metric can always use them, and no Journey declares them.
  • jobStep names a step of the linked job, and boundary (start or end, needs jobStep) says which end. stepId is the stable id of the step the anchor follows; it wins over step, and journey promote stamps it.
  • The anchor rules: a lowercase name matching [a-z0-9._:-]{1,64} (anchor-name), a stepId that is a step of the Journey (anchor-step-id), step and stepId naming the same step (anchor-step-mismatch), a stamped id on a Journey whose steps have ids (anchor-unstamped), and at least one named anchor on a job-linked Journey (job-anchors).
  • Warn vs enforce. Existing Journeys still load and run, and journey lint, SARIF output, the review sheet and catalog status show each problem as a warning with its fix. journey promote enforces the rules on a new promotion and on a re-promotion that changes more than step ids, refusing with E_JOURNEY_ANCHOR_RULES (exit 64). Re-approving an unchanged Journey, or one whose only change is minted step ids, warns and passes.

Reference checks

The catalog checks the references inside a job and from a Journey to its job. Each problem has a stable code, a path and a fix. catalog status (refIssues), catalog analyze and job review show them, and on loaded data they are warnings. An approval enforces the structural ones: job approve refuses a job with a duplicate step or outcome id, an outcome pointing at an unknown step, an unknown parent or a parent cycle (E_JOB_BROKEN_REF, exit 64). Other reference problems, such as a metric no Journey's anchor can measure (job.unmeasurable-metric) or a target whose unit does not fit its metric (job.target-unit-mismatch), stay warnings.

Drafting desired outcomes

jevitate job draft-outcomes invite --real             # 1-3 outcomes (--count, default 3)
jevitate job draft-outcomes invite --fake-ai --json   # the deterministic fake generator

job draft-outcomes <jobId> [--count 1-3] (MCP draft_job_outcomes) asks the generation model for a first draft of a job's desired outcomes and needs exactly one of --real / --fake-ai (else E_JOB_DRAFT_ARGS, exit 64). Drafts have a direction, measure and object, never a metric or a target. The job's provenance becomes ai_draft, and the drafted ids are listed under extensions["jevitate-draft"]. It never approves: an approved job becomes stale until a person re-approves it.

Personas

Entries of the personas file now also take description, role and approval. A persona with no session yet (no storageState, no login) is a catalog-only persona: runs skip it. See Sweeps & Personas.

{
  "personas": [
    { "name": "admin", "description": "runs the team's workspace and its billing", "role": "admin",
      "storageState": "~/.jevitate/sessions/admin.json" },
    { "name": "viewer", "description": "reads shared projects, never edits", "role": "viewer" }
  ]
}

Link a Journey by setting metadata.job to a job id and metadata.persona to a persona id. On a Journey with a job, metadata.persona is always read as a persona id (an unknown one is a dangling link).

Journey review sheet and hash-bound promotion

journey promote is the human approval gate; journey review is what the reviewer reads first: one sheet (text, Markdown or JSON) that says what the Journey does, what it changes and what it proves.

jevitate journey review checkout                              # the sheet, as text
jevitate journey review checkout --markdown --out review.md   # Markdown, to a file (attach it to a PR)
jevitate journey review checkout --json                       # the schema-checked sheet (MCP: review_journey)
jevitate journey promote checkout --reviewed-hash <hash>      # refused (E_JOURNEY_REVIEW_STALE, exit 64) if it changed since
jevitate journey promote checkout --review-sheet review.md    # the same, reading the hash from the sheet file
SectionWhat it shows
Summarygoal and success criteria; missing intent is flagged with the journey annotate command that drafts it
Stepseach step's action in plain words, its target control, objective, expected result, its own check and the parameters it uses
Side effectsthe write requests it is expected to fire; clicked controls that match the safety rules, with their rule ids; the origins it touches
Inputsparameters and secret references by name only, never a value
Proofend-state checks, per-step assertions, the journey lint result and the last journey verify --mutate verdict (stale when the Journey changed after it ran)
Change since last approvala diff of steps, assertions and side effects against the version last approved, or "first approval"
Content hashthe hash an approval binds to
Catalogthe linked job and persona with their approval state, and needs re-review when anything changed since its approval

The content hash leaves out the approval bookkeeping (promoted, approval, acceptedWeak), so promoting does not change what was approved. Without --reviewed-hash or --review-sheet, journey promote prints the sheet and binds the approval to the hash it showed. Every promotion, demo approve included, records metadata.approval and keeps the approved Journey at .jevitate/journeys/.approved/<id>.json for the next review's diff.

Approving personas and jobs

Each persona and job is draft until a person approves it. The approval records { contentHash, at, provenance } on the item, in the same file. A persona's hash covers what the persona is (id, description, role), not its session settings. --reviewed-hash refuses a changed item (E_CATALOG_REVIEW_STALE, exit 64).

jevitate persona review admin                 # the sheet: who, the jobs it serves, the Journeys linked to it
jevitate persona approve admin                # shows the sheet, asks you to type `admin` (or the hash's first 8 characters)
jevitate job review invite-teammate           # the story, its personas, which have a promoted Journey, the gaps
jevitate job approve invite-teammate --reviewed-hash <hash>   # bind the approval to exactly the sheet you read
jevitate journey promote invite --accept-unvetted "pilot: the job is reviewed next sprint"
jevitate job approve invite-teammate --accept-findings "the billing persona lands next sprint"
  • Approving is CLI only. MCP gets the read-only review_journey, review_persona, review_job, catalog_status and analyze_catalog. An approval needs an interactive terminal and a typed confirmation, else E_APPROVAL_NEEDS_HUMAN (exit 64).
  • Staleness. Editing an approved persona or job changes its hash, so it becomes stale ("needs re-review"), and so does every promoted Journey linked to it. Re-approving clears it.
  • Promotion order. journey promote and demo approve need the linked job and persona to be approved. An unknown, draft or stale link is unvetted and refused (E_JOURNEY_UNVETTED, exit 1) unless a person passes --accept-unvetted "<reason>", recorded in metadata.approval.waivers.
  • Findings. Every approval runs a pre-approval analysis against the rest of the catalog. Findings that need acknowledgment (job.unknown-persona, journey.persona-not-served) refuse the approval (E_APPROVAL_FINDINGS, exit 1) unless the approver passes --accept-findings "<reason>", recorded with the approval. It is a CLI flag only; code applies the rule to the typed findings, and a model never decides an approval. An analyzer that fails to run produces a finding that needs acknowledgment.

Readiness review

Every review sheet can show a Readiness section (--readiness; MCP readiness: true), and every approval shows it. Two layers, printed apart so advice never reads as a gate.

jevitate job review invite-teammate --readiness           # deterministic checks + INCOSE GtWR rules
jevitate job review invite-teammate --readiness --real    # ... plus the advisory Jev questions
jevitate journey review invite --readiness --real --json  # MCP: review_journey {readiness: true, real: true}
  • Deterministic checks, each pass, warn or fail with a fix-it line. A Journey: links an existing job and persona, both approved, has a goal, success criteria and per-step intent, passes journey lint, and has a current proven journey verify --mutate result. A job: trigger, motivation and outcome present, its personas exist and are approved. A persona: it says who the user is and a job serves it.
  • INCOSE Guide to Writing Requirements (GtWR) rules that can be checked mechanically run on every job story and persona, each finding citing its rule id and the characteristic it protects: gtwr:vague-term, negative, pronoun-reference, too-long (unambiguous); escape-clause, absolute (verifiable); combinator, multiple-outcomes (singular); open-ended (complete); empty-field, trigger-is-persona (conforming); outcome-is-feature (necessary).
  • Jev review (advisory), only with --real and a judgment key. Jev answers a few narrow typed questions (for a Journey: does it accomplish the outcome for this persona, do the end-state checks prove it, is every step needed, are the actions plausible for the role, does each expected result match what was recorded), each shown with its probability. An answer below 0.5 reads "not ready because … (GtWR: <characteristic>)". A Jev finding never needs an acknowledgment and never changes an exit code. Without --real, or without a key, the layer says it was skipped and the deterministic layer still runs.

Readiness never adds a second gate; the existing gates still block. Approvals always run the deterministic checks and ask Jev only when given --real (MCP real: true on promote_journey / approve_demo). Answers are cached by content hash in .jevitate/cache/jev/, which init gitignores; only answers are stored, never question or catalog text.

Catalog status and analysis

jevitate catalog status          # the matrix and the lists, as text
jevitate catalog status --json   # the schema-checked report (MCP: catalog_status)
jevitate catalog analyze                  # deterministic layer (pairs, gaps, advice)
jevitate catalog analyze --real --json    # + Jev classifications (MCP: analyze_catalog {real: true})
  • catalog status shows a jobs × personas matrix (✓ a promoted Journey covers the pair, draft only unpromoted ones, MISSING none), approved jobs with no promoted Journey, Journeys linked to nothing, dangling links, stale approvals, and every recorded approval with how it was made. It exits 0, unless you add --require-approvals [--allow-channels <list>].
  • catalog analyze finds problems between items, grouped by the INCOSE requirement-set characteristics: conflicts, duplicates, overlaps, dependencies, coverage gaps and stale items. Candidate pairs are chosen by code (bounded, never O(n²) model calls). With --real, Jev classifies each judged pair as compatible, duplicate, overlapping, conflicting or dependent, with a probability; the reason line is written by code. Update advice reads "consider updating X because Y" and is never applied. It never changes a catalog file, a Journey or an approval, and always exits 0.
  • Before every approval the same analysis runs for the item being approved: at most 8 candidate pairs, with overflow listed, never silently dropped. A conflicting or duplicate classification with probability 0.7 or higher on a pair involving that item requires --accept-findings "<reason>". Below it, and for every other relation, the finding is informational. Without --real or a key nothing is classified, so nothing gates on it.

Drafting entries and strict status (0.14.0)

  • jevitate catalog draft persona|job <id> --name <text> (MCP catalog_draft) adds a DRAFT persona or job so a Journey can link it now (--description; for a job also --trigger and --outcome, placeholders to write before approving). It never approves, and an id the catalog already has is refused.
  • jevitate catalog status --strict (MCP catalog_status with strict) exits 1 on a stale persona or job approval, or on a promoted Journey that links an unapproved or unknown entry, or no job or persona.
  • Link a Journey with journey link, and gate its hand-over with journey review --strict: Promotion-ready.

Publishing to Journeeze (0.10.0)

Journeeze can import the catalog as a catalog bundle (v1).

jevitate catalog export --format journeeze-bundle --out dist/catalog      # writes dist/catalog/bundle.json
jevitate catalog export --format journeeze-bundle --out dist/catalog \
  --check jevitate-check/check.json --check nightly/check.json --product-name "Ledgerly"
jevitate connect journeeze                 # a person, once: where the upload key is kept (CLI only)
jevitate publish journeeze --dry-run       # export, validate, resolve the connection; send nothing
jevitate publish journeeze --json          # upload and wait until it is imported or refused
  • What the bundle holds. Personas (never their session settings), jobs as written, promoted Journeys with their links (job, persona, anchors, serves), every approval reduced to {contentHash, at, channel}, the jevitate check results with their machine baselines, and the findings. Every exported approval hash can be recomputed by the reader. No session keys, typed values, query strings or personal data are exported. No media in 0.10.
  • Flags. --format journeeze-bundle --out <dir> are required. --check <file> (repeatable) names the check records to include (default jevitate-check/check.json when it exists). --product-name sets product.name (default: the project's package.json name, else its folder name). The same catalog gives the same bytes. Personal data, a credential or a job with a structural reference problem refuses the export (exit 64).
  • The upload key. connect journeeze is CLI only; the MCP tool connect_journeeze is forbidden. A person names where the key is kept (an environment variable, a secret command or a password-manager entry) or types it hidden; jevitate checks it with Journeeze, asks the person to confirm the product, and saves only a reference, bound to the Journeeze origin. The key itself is never written to disk and never reaches a model, an MCP argument or result, or a log. Each project uses only its own connection.
  • Publishing. publish journeeze [--dry-run] exports the bundle, uploads it, and waits until Journeeze reports it imported (exit 0) or refused (exit 1). In CI set JOURNEEZE_UPLOAD_KEY as a CI secret; it wins over the saved reference. Hosts are pinned to Journeeze (loopback hosts need JEVITATE_JOURNEEZE_DEV=1) and redirects are refused. Setup problems exit 64 (E_JOURNEEZE_NOT_CONNECTED, E_JOURNEEZE_KEY_REFUSED, E_JOURNEEZE_FORBIDDEN, E_JOURNEEZE_URL, …); an upload that could not finish exits 2 (E_JOURNEEZE_UNAVAILABLE, E_JOURNEEZE_BUNDLE, …). Publishing never approves anything.
  • MCP. export_catalog_bundle (format, out inside the project, check, productName) only writes inside the project and never uploads. publish_to_journeeze (dryRun) never takes or returns the key.

What 0.11.0 changes for Journeeze

  • Refusal. catalog export refuses with exit 64, naming the fix, when a promoted Journey cannot be carried by the bundle, instead of leaving it (and its baselines) out with only a warning. publish journeeze refuses the same way. Draft Journeys and checks the bundle cannot carry are still left out with a warning.
  • Anchor names. A name outside the catalog bundle v1 rule (1-64 of lowercase a-z, 0-9, ., _, -, starting with a letter or digit) is reported by journey lint and promote. See Journeys.
  • Product name. publish journeeze sends the connected product's name as product.name, not package.json's. --product-name (MCP productName) overrides it and must match whoami; --dry-run checks the key with whoami and refuses a mismatch with E_JOURNEEZE_PRODUCT_MISMATCH.
  • Demo media. An environment can declare "synthetic": true in .jevitate/environments.json. demo approve on such an environment renders the final demo under the jz-mask-v1 policy, each screenshot is proven masked before and after capture and the video on every frame, and anything unproven is left out. The bundle then carries demos[] and files[] (media under media/<id>/, sha256 re-verified), and publish journeeze uploads a deterministic ZIP. A draft demo, a demo of an older Journey version, or one approved on a non-synthetic environment is never exported.
  • Locator findings. The bundle (catalog bundle v1 minor 1) carries one finding per brittle element, derived from locator health.
  • App version. jevitate check --app-version <label> (or JEVITATE_APP_VERSION) stamps the app's release label (1-64 of A-Za-z0-9._+-) next to the build id in the check record and SARIF; the bundle exports it as check.appVersion. An invalid label is refused before anything runs.
  • Issue de-duplication. Filed issues carry both the Jevitate and the Journeeze fingerprint markers, and filing searches both first.

What "human approval" guarantees

A coding agent with a shell can run the same CLI, so jevitate has three layers, and each guarantees exactly what is written here and no more.

  1. Provenance: detection. Every approval record, and each waiver on it, carries provenance: { channel, agentSignals, user?, reason? }. The channel is tty (a person typed the confirmation at a terminal), non-interactive (--non-interactive-approval "<reason>"), ci (the same, with a CI marker set), mcp (an agent's approval, never a person's) or pr-review (0.10.0, below). agentSignals lists the names of markers found in the environment (for example CLAUDECODE, CODEX_*, CURSOR_*), never their values; user is the OS user name, never an email. This is detection, not proof: a marker is a hint, and a determined process can clear its environment, fake a TTY or edit the JSON.
  2. The typed confirmation: friction. Each CLI approval shows its sheet, applies the acknowledgment gate, then asks for the item id or the first 8 characters of the content hash, and once more for each waiver. It needs a real TTY on stdin and stdout, else E_APPROVAL_NEEDS_HUMAN; a mismatch is E_APPROVAL_NOT_CONFIRMED (both exit 64, nothing written). A process that drives a pseudo-terminal can type the answer.
  3. Git review: enforcement. The approval records are files in the repository, so the real control is review of the change that adds them. init --codeowners writes a marked CODEOWNERS block for those paths (idempotent); it does nothing alone, so enable branch protection with "Require review from Code Owners". check --require-approvals (and catalog status --require-approvals) fails any approval that is missing, stale or made over a channel not in --allow-channels (default tty).
jevitate init --codeowners "@acme/qa"                 # a marked CODEOWNERS block for those paths (idempotent)
jevitate check --suite ci.json --require-approvals    # in CI: fails on a non-tty approval

Approvals verified from pull-request reviews (pr-review, 0.10.0)

With the CODEOWNERS review in place, the approval can be the review itself. In CI, on a push to the default branch, job approve, persona approve and journey promote record the channel pr-review only after the GitHub API confirms all of the following:

  • the approved content came from a merged pull request into the default branch;
  • the pull request has an approving review on its head commit, and the content at that head is exactly the content being approved;
  • the reviewer is a person who is neither the PR's author nor a contributor of any of its commits, and has write access;
  • optionally, the reviewer is a CODEOWNER (JEVITATE_PR_REVIEW_REQUIRE_CODEOWNER=1).
# in a workflow on push to the default branch: record pr-review approvals
env:
  GITHUB_TOKEN: ${{ github.token }}
  JEVITATE_PR_REVIEW_REQUIRE_CODEOWNER: "1"   # optional
run: jevitate journey promote checkout && jevitate job approve invite-teammate

# the gate: re-verify every recorded pr-review approval through GitHub
jevitate check --suite ci.json --require-approvals --allow-channels pr-review

check --require-approvals --allow-channels pr-review (and catalog status) re-verify every recorded pr-review approval through GitHub; one that does not verify is reported as unverified, never a pass. Successful verifications are cached for 24 hours; the cache is never read on pull-request events and is refused when it is tracked by git or reached through a symlink. The token comes only from the CI environment: no flag or MCP argument can set this channel, and it is never granted for an approval with waivers or a self-heal proposal. If verification fails, the approval falls back to the usual paths.

Not yet: cryptographic signing of approvals, which would bind each approval to a person's key, is future work. Full reference: docs/catalog.md.