@sabaiway/agent-workflow-kit 14.17.0 → 14.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,34 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
4
4
  is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
5
5
  every `migrations/<version>-<slug>.md` newer than it, in semver order.
6
6
 
7
+ ## 14.18.0 — Jev connects from ONE command you run: the Recommendations item hands it over, the guide prints it, and one leaf names it (AD-165)
8
+
9
+ **While `TYPESAFE_API_KEY` is not in the environment the agent's commands run in, the Recommendations item `jev-connect`
10
+ now carries you to the result.** Its apply is `HAND-APPLY: node <kit>/tools/jev-connect.mjs` — a command YOU run in a
11
+ terminal of your own; the agent hands it over and never runs it. In 14.17.0 the apply ran the read-only guide, which
12
+ sets nothing: a user who ran it believed Jev was connected. The rule now: a recommendation reaches the result.
13
+
14
+ - **`jev-connect`, the one command.** It refuses without a terminal (exit 3), asks for the key with no echo, verifies
15
+ it with one request to api.typesafe.ai (the vendor's sample body, the key in a header from an in-process fetch —
16
+ never an argument, never a shell trace; the four documented error codes print the vendor's meanings), and saves it
17
+ as managed `export` lines in your shell's startup files: zsh → `~/.zshrc` (under `ZDOTDIR` when set); bash →
18
+ `~/.bashrc` and the login file bash reads (`.bash_profile`, `.bash_login` or `.profile`, the first that exists;
19
+ `.bash_profile` created when none; written even when it sources `.bashrc`). Then: `connected — quit the agent and
20
+ start it again from a new terminal`, exit 0. Every earlier line that starts `export TYPESAFE_API_KEY=` is replaced in
21
+ place by the managed line, so no `if` branch is emptied; a linked file or directory is never written through, nor a
22
+ file outside your home; a new file is created 0600.
23
+ Another shell, or Windows: the key is verified but not saved, the last line says what to set by hand, exit 4. A key
24
+ the vendor refuses: `nothing written`, exit 1. `--unverified` saves without the request. The key's value never
25
+ appears in any output.
26
+ - **The guide shrinks to two steps.** `/agent-workflow-kit jev` prints the key mark and the connect line, then the
27
+ optional vendor skill with its own restart line and first prompt. The shell key line, the curl line, the response
28
+ and the error meanings left the guide; its mode doc lost the walk — no command the guide prints is run by the agent.
29
+ - **One leaf, no restatement.** `tools/jev-facts.mjs` holds the key rule, the connect line and the vendor's request
30
+ facts; the guide, the advisor and the command import it. The advisor's WHAT now names the environment the agent's
31
+ commands run in, never "this host".
32
+ - Closes items 23–26 and 28–30 of Issue-027 (the unguarded `homedir()`, the missing restart after the skill install,
33
+ the key in curl's argv, `ZDOTDIR`, the `&& unset k`, the WHAT, the login bash that never reads `~/.bashrc`).
34
+
7
35
  ## 14.17.0 — the Recommendations screen offers the Jev walk while `TYPESAFE_API_KEY` is not set, and the walk starts with the key (AD-164 revised)
8
36
 
9
37
  **While `TYPESAFE_API_KEY` is not in the environment the agent's commands run in, the Recommendations screen now
package/README.md CHANGED
@@ -235,7 +235,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
235
235
  | `/agent-workflow-kit status` | any time | **read-only** single view of **versions + deployment + settings + bridges**: which members (kit / memory / engine / the two bridges) are installed and at what version (with an honest "installed on this machine" note when one is behind) and — in a project — what's deployed (`docs/ai`, the version stamps, and the **visibility**: visible / hidden / unclear), and the **feature-spec adoption state** (`not adopted` / `adopting (N draft)` / `adopted (N live, M draft)` / `could not be read`, plus `declined` when recorded), plus your settings (orchestration recipes, attribution, velocity) and the bridges' readiness. The two version axes (package number vs deployment-structure head) stay decoupled. Never writes, never commits, never runs a subscription CLI. |
236
236
  | `/agent-workflow-kit now` | any time | **read-only** answer to where work stands in four blocks: **NOW** names every plan in flight, its phase, current and next step, and the tree evidence; **STEPS** gives every ledger row its evidence-derived status; **QUEUE** folds named rows under their full heading paths and calls position priority only in the judged backlog; **CAMPAIGN** shows the source-size practice figures and config state. The plan's own text never sets a status; a contradicted claim prints beside it. A block that cannot be derived is WITHHELD with its cause; NOW has two halves, plans and tree, each independently a value or WITHHELD, and NOW itself is WITHHELD only when both are. Never writes, never commits, never runs a subscription CLI. |
237
237
  | `/agent-workflow-kit tier` | any time | **read-only** guide to the epic, story and task tier: what each level is, and the next step of the walk, with the command that runs it wherever a tool does. Never writes, never commits, never runs a subscription CLI. |
238
- | `/agent-workflow-kit jev` | any time | **read-only** guide to Jev (TypeSafe), a decision model: what it is, why it pays here, what it is not, the three steps to a first request and three prompts where it pays. Opens no connection and never prints the key. |
238
+ | `/agent-workflow-kit jev` | any time | **read-only** guide to Jev (TypeSafe), a decision model: what it is, why it pays here, what it is not, the two steps to a connected key (the connect command, the optional vendor skill) and three prompts where it pays. Opens no connection and never prints the key. |
239
239
  | `/agent-workflow-kit recipes` | any time | **read-only** orchestration advisor: presents five named recipes for composing the carriers of a step — the bridges and the full-tool executor subagent — into plan → execute → review — **Solo / Reviewed / Council / Delegated / Subagent** — plans + recommends one for your environment (degrading with a stated reason when a backend isn't ready, or when the executor vehicle is missing/unusable), and offers the choice. The activity/slot registry (six activities) is rendered by `procedures` and by `set-recipe --help`. The orchestrator runs it via the bridge skills or the executor vehicle and **always commits**; the kit never executes a recipe, never runs a subscription CLI, never commits. |
240
240
  | `/agent-workflow-kit procedures <activity>` | any time | **read-only** activity-procedures advisor: prints a named activity's ordered steps (`plan-authoring` / `plan-execution` / `routine` / `feedback-triage` / `epic` / `task`) read **live** from the engine, plus the **resolved recipe per slot** from your `docs/ai/orchestration.json` (agent-writable via `set-recipe`, or hand-edited) + carrier readiness (`plan-authoring`: author, fold, review; default Reviewed when a backend is ready, Council on request; slot-aware incl. Delegated and Subagent) — and, for every dispatched backend, the **full driving contract at the point of use** (exact copy-pasteable invocation, grounding levers like agy's `--facts`/`--decided`, the round-2 `--continue` delta, guarded passthrough), verbatim from the bridge manifests (drift-guarded; each wrapper's `--help` prints the same) — except `feedback-triage`, whose record-bound block carries the check with `--excerpts`, the resolved member lines (ready bridge commands, the ready lens brief, or explicit skipped members), and either the record round-table command for a bridge obligation or the no-receipt fact otherwise: the record is the artifact. `--override <slot>=<value>` adjusts one slot per run. Composes with `recipes`; never writes, never commits, never runs a subscription CLI. |
241
241
  | `/agent-workflow-kit set-recipe` | any time | **config writer** for `docs/ai/orchestration.json`: tell the agent your preference in plain language and it maps it to explicit `--set <activity>.<slot>=<value>` / `--unset` ops; the kit validates, **previews by default**, and writes only on `--write` (deployment-gated, atomic, symlink/TOCTOU-safe), resolving the effective recipe vs live readiness. Writes **only** that file — **never runs a backend or a subagent, never commits**; hand-editing stays fully supported. |
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: agent-workflow-kit
3
3
  description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow". New to the epic, story and task tier? Run `/agent-workflow-kit tier`. A typed decision with a confidence from Jev? Run `/agent-workflow-kit jev`.
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '14.17.0'
6
+ version: '14.18.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-kit",
5
5
  "kind": "composition-root",
6
- "version": "14.17.0",
6
+ "version": "14.18.0",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-kit",
3
- "version": "14.17.0",
3
+ "version": "14.18.0",
4
4
  "description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -2,15 +2,15 @@
2
2
 
3
3
  <!-- opt-in-capability: jev-connect -->
4
4
 
5
- Read-only. The Jev guide: what Jev (TypeSafe) is, why it pays in this workflow, what it is not, the three steps to a first request and three prompts where it pays. The guide writes no file, opens no connection, never prints the key's value and never commits.
5
+ Read-only. The Jev guide: what Jev (TypeSafe) is, why it pays in this workflow, what it is not, the two steps to a connected key and three prompts where it pays. The guide writes no file, opens no connection, never prints the key's value and never commits.
6
6
 
7
7
  Run `node ${CLAUDE_SKILL_DIR}/tools/jev-guide.mjs --dir <project> --json`.
8
8
 
9
- Render the returned envelope compactly in the user's conversational language. Never paste the JSON. Keep every path and every command verbatim, each command on its own line so it runs as printed; translate only the prose around them. Keep every vendor-quoted text verbatim and in quotes, never translated or reflowed: the not-a-replacement sentence, the curl line, the response, the three field meanings and the four error meanings.
9
+ Render the returned envelope compactly in the user's conversational language. Never paste the JSON. Keep every path and every command verbatim, each command on its own line so it runs as printed; translate only the prose around them. Keep the one vendor-quoted text verbatim and in quotes, never translated or reflowed: the not-a-replacement sentence.
10
10
 
11
- Walk the steps with the user in order, one at a time; move on when the step's check passes or the user skips it. STEP 1: the user runs the key line in a terminal of their own, never the agent. STEP 2: the user runs the install lines — they change the agent's own configuration outside the project. STEP 3: run the curl line only on the user's explicit yes in the chat — it is a network call that sends the sample text to api.typesafe.ai — and never with `-v`, `--trace` or any flag that prints request headers; under an OS sandbox the host must be reachable, and a blocked host is named by the sandbox: the user allows it or runs the line in their own terminal.
11
+ The connect line is the user's: hand it over as printed and never run it — the command refuses without a terminal, and a terminal is not consent. The install lines are the user's too: they change the agent's own configuration outside the project. No command the guide prints is run by the agent.
12
12
 
13
- Keep this order: the fixed text (WHAT, WHY HERE, NOT, SOURCES); then the three steps — the key with its line or its editor sentence and its mark, the vendor skill with its install lines and its mark, the first request with its curl line and its check; then the three prompts where it pays.
13
+ Keep this order: the fixed text (WHAT, WHY HERE, NOT, SOURCES); then the two steps — the key with its connect line and its mark, the vendor skill with its install lines, its mark and its first prompt; then the three prompts where it pays.
14
14
 
15
15
  Exit codes:
16
16
 
@@ -20,4 +20,4 @@ Exit codes:
20
20
  | 1 | Nothing rendered: a stat of the `--dir` does not report a directory; the cause is printed on stderr. |
21
21
  | 2 | Usage: an unknown argument, a `--dir` without a value, or `--help` / `-h` combined with anything. |
22
22
 
23
- **Invariants:** the guide is read-only and opens no connection · the agent runs STEP 3's curl line only on the user's explicit yes · the key's value never printed · compact user-language render with paths, commands and vendor quotes verbatim · process.exitCode, never process.exit().
23
+ **Invariants:** the guide is read-only and opens no connection · the connect line is run by the user in a terminal of their own, never by the agent · the key's value never printed · compact user-language render with paths, commands and vendor quotes verbatim · process.exitCode, never process.exit().
@@ -17,7 +17,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
17
17
 
18
18
  1. **`--cwd` is REQUIRED** — the target project is explicit, never inferred from the shell's current directory (a subdirectory invocation still advises on the named root).
19
19
  2. **Verdict first (present-even-when-empty, opening at the `## Recommendations (agent-workflow)` header).** With everything optimal the body is exactly `no recommendations — flow optimal.` — the empty-state line ALONE is the verdict, zero added lines. Every other state opens the body with ONE verdict line composed from the frozen templates: `{K} item(s) need attention` leads when any item's severity is `attention` (a CONFIGURED declaration that is broken, drifted, degrading or invalid — the deployment needs review); `{N} optional recommendation(s), apply any you want` covers the `optional` class (offers to enable an unconfigured capability), led by `nothing is broken` ONLY when no item needs attention AND no probe check was skipped (a skipped probe could hide an attention-class problem — the claim never overreaches); `optimality NOT attested — {M} probe check(s) skipped` is appended last whenever probe checks were skipped. (The `(s)` invariant form IS the pinned pluralization — no singular/plural branching.) Items render attention-first, each tagged `needs attention:` / `optional:`. A failed probe renders as a stated `⚠ skipped item …` line — never a crash, never a fabricated item.
20
- 3. **The apply-through-agent lane — an explicit informed-consent checkpoint:** after presenting, OFFER to apply; the user selects items in plain language; for each selected item you **surface its posture note inline** where one exists (the per-item notes below cover exactly the risk-marked keys; for `sandbox-lane` the note INCLUDES the sandbox-lanes ladder — present the whole ladder inline at the consent moment, never as a bare pointer). The user **explicitly confirms**, and **only then** — **no command runs before confirmation** — do you run the rendered command. Do NOT infer safety from the presence or absence of an `--apply` flag: most items' rendered command IS the mutation and completes on that one run, **including a no-`--apply` mutation** such as `family-freshness`'s `npx … init`. Some items instead render a **dry-run preview** that changes nothing and prints an explicit follow-up `--apply`/mutating command to run NEXT (e.g. `sandbox-lane`'s ack-write — a NEUTRAL recipe fingerprint into the family-owned `docs/ai/acks.json`, never a security key — and the `gates-declaration` seeder): after the SAME confirmation you run that printed follow-up command — no second ask, no improvisation (each writer keeps its own consent semantics: previews, `--apply` flags, refusals). An item marked **HAND-APPLY** (e.g. the `read-lane` stale/missing arms) is **never run by you and never written by the kit** — hand the user its rendered line together with its posture note; HAND-APPLY is **maintainer territory wherever it renders** — the apply slot or the `recipe:` line. For `worktrees-dir` the two classes COMPOSE in one item and the ORDER is part of the contract: its `recipe:` line is the HAND-APPLY grant advice, its apply one-liner is the dry-run ack preview, and you **wait for the maintainer to confirm the grant is applied (or the terminal fallback chosen)** before running that preview and the exact `--apply` command it prints — the ack RECORDS a choice that must already have been made, and against a trusted host NO the item renders no ack at all.
20
+ 3. **The apply-through-agent lane — an explicit informed-consent checkpoint:** after presenting, OFFER to apply; the user selects items in plain language; for each selected item you **surface its posture note inline** where one exists (the per-item notes below cover exactly the risk-marked keys; for `sandbox-lane` the note INCLUDES the sandbox-lanes ladder — present the whole ladder inline at the consent moment, never as a bare pointer). The user **explicitly confirms**, and **only then** — **no command runs before confirmation** — do you run the rendered command. Do NOT infer safety from the presence or absence of an `--apply` flag: most items' rendered command IS the mutation and completes on that one run, **including a no-`--apply` mutation** such as `family-freshness`'s `npx … init`. Some items instead render a **dry-run preview** that changes nothing and prints an explicit follow-up `--apply`/mutating command to run NEXT (e.g. `sandbox-lane`'s ack-write — a NEUTRAL recipe fingerprint into the family-owned `docs/ai/acks.json`, never a security key — and the `gates-declaration` seeder): after the SAME confirmation you run that printed follow-up command — no second ask, no improvisation (each writer keeps its own consent semantics: previews, `--apply` flags, refusals). An item marked **HAND-APPLY** (e.g. the `read-lane` stale/missing arms) is **never run by you and never written by the kit on your behalf** (a kit command the user runs in a terminal of their own is the user's own write) — hand the user its rendered line together with its posture note; HAND-APPLY is **maintainer territory wherever it renders** — the apply slot or the `recipe:` line. For `worktrees-dir` the two classes COMPOSE in one item and the ORDER is part of the contract: its `recipe:` line is the HAND-APPLY grant advice, its apply one-liner is the dry-run ack preview, and you **wait for the maintainer to confirm the grant is applied (or the terminal fallback chosen)** before running that preview and the exact `--apply` command it prints — the ack RECORDS a choice that must already have been made, and against a trusted host NO the item renders no ack at all.
21
21
  4. Registry strings (benefits + item texts) are frozen tool data, fact-true, ONE line under the pinned shape cap — posture/risk detail lives in the notes below at the consent moment, never inline in the overview. The dual velocity+security wording (`safer — blast radius bounded by the OS sandbox, not human attention`) rides ONLY the items with a real security delta (the autonomy render, the sandbox provisioning); the bridge-wrappers item claims **velocity only**.
22
22
 
23
23
  **The `executor-vehicle` item (the carrier the project already CONFIGURED — beside the `agents` offer, never the same item).** It fires on ONE pair of facts: a slot in `docs/ai/orchestration.json` names `subagent` while the executor vehicle (`.claude/agents/executor.md`) is `missing` or `unusable` — so every such slot silently degrades to solo. That is a configured declaration that is broken, hence **`attention`**, not an offer; its text names the state, its reason and how many configured slots are affected across the six activities (`plan-authoring`, `plan-execution`, `routine`, `feedback-triage`, `epic`, `task`), covering the subagent-capable slots `plan-authoring.author`, `plan-authoring.fold`, `plan-execution.execute`, `routine.carrier`, `epic.author`, `task.author` and `task.execute`. **`placed` and `customized` render NOTHING** (a working vehicle is not a recommendation), a config naming no `subagent` slot renders nothing, and a config the validated reader refuses is a stated **skip**, never an item computed over a shape nothing accepted. It is deliberately DISTINCT from its two neighbours, neither of which can observe this state: the `agents` offer converges the moment nothing is left to PLACE — which a present-but-unusable executor also satisfies — and the review-recipe probe skips a slot configured `subagent` by construction. Its apply is the agents writer's own `cheap-agents.mjs --apply` (the same writer the `agents` item previews; here the rendered line IS the mutation, so read the item's state before confirming), and its `recipe:` line is the hidden-mode reconcile follow-up (`hide-footprint --reconcile`), for a hidden deployment only. Honest limit on convergence: the apply places an ABSENT vehicle, so it clears `missing`; it STOPs on a symlink, so on `unusable` the item's apply line carries a `HAND-APPLY:` precondition naming each refusal the advisor observes; an executor whose frontmatter is invalid is re-derived by the apply itself. Risk profile: writes under `.claude/agents/` only — the executor's body re-derived from `docs/ai/vehicles.json`, every other vehicle create-only — never `settings.json`, never a commit.
@@ -38,7 +38,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
38
38
  - ADDITIONAL `gates-inert` arms (the third outcomes) — two further arms, and they differ in whether anything is BROKEN. **`producer-unrecognized`** — a checker with no producer anywhere in the declaration, on a tracked tree the changed-line coverage domain cannot reach (`.ts`/`.tsx`/`.jsx`/`.mts`/`.cts` strictly outnumber `.mjs`/`.cjs`/`.js`): the dead pair is real, so this arm is **HAND-APPLY** and the two remedies are marking the real producer with `"lcovProducer": true` or dropping the checker. Never a `node --test` prescription over a project that has no such suite, never the fill preview, and never an acknowledgement — a dead pair is broken, not narrow, and removing a producer after an acknowledgement lands right back in this arm. **`coverage-domain-narrow`** — the producer/checker pair IS live and the tree is still dominated by what the domain excludes: nothing is broken, and the honest sentence is that certification covers the assessable minority. Its apply is the consent-gated **ack writer** preview (a NEUTRAL fingerprint into `docs/ai/acks.json` as `coverageDomainAck`, never a security key); after the SAME confirmation you run the `--apply` it prints. The fingerprint binds the FACT — the verdict plus the unsupported extensions present, never the file counts — so an acknowledged project stays quiet as it grows and re-fires when a new unsupported language arrives or the verdict flips. The census reads the TRACKED tree with a read-only `git ls-files`; a tree it cannot read (a non-git deployment) becomes a stated skip, so optimality is withheld rather than assumed. Risk profile: no enforcement change of any kind — one acknowledgement recorded in a family-owned file, and one hand edit that stays the maintainer's.
39
39
  - `spec-adoption` — the feature-spec layer's ADOPTION STATE, judged from `docs/ai/specs/` alone through the same census `spec-check --all` runs, so a store the checker would refuse to observe is never counted here either. Both arms are OFFERS (`optional`) — the layer is opt-in, and `attention` stays reserved for a configured declaration that is broken. **`not-adopted`** (no store): every plan then cites zero governing specs by default and nothing says adoption never started; its apply is the spec-layer ensure one-liner (`ensure-configs --reconcile --only specs`, the same seed `upgrade` runs — it writes the reader/checker pairs and the store root, create-only, and skips with a stated reason where no Node evidence exists), and its `recipe:` line carries a NAMED **HAND-APPLY alternative** — the DECLINE preview (`ack-write --lane spec-adoption`, a dry-run that prints the exact `--apply`), which records `specAdoptionAck` in the family-owned `docs/ai/acks.json`. **The two are exclusive, and the order is part of the contract:** ask which the maintainer wants BEFORE the confirmation; on "seed" run the apply; on "decline" run NOTHING — hand over the preview line and let them run it and its printed `--apply`; never run the seed after a decline was chosen, and never run the decline preview for them. **`adopting`** (a store with no live contract): an optional item naming the draft count; its apply is that same decline preview (a dry-run; after the SAME confirmation you run the `--apply` it prints), and the honest remedy is authoring a contract from the skill's `SPEC_TEMPLATE.md`. A recorded decline silences BOTH arms and shows on `status` as ` — declined`; one live contract makes the decline moot. An unreadable store is a stated skip, never `flow optimal`. Risk profile: the seed writes only create-only files under `scripts/` and `docs/ai/specs/`; the decline writes one key into the ack store; nothing here authors a spec or blocks a plan.
40
40
  - `profile-gap` — each item's apply is a PREVIEW that writes nothing, and consent takes two steps: the confirmed pick runs the preview and you relay every line it prints (the environment-resolved slot values, each checker `entry:` line with its trust-chain disclosure, a Communication region the insert plans in the same run); the same line with ` --apply` appended runs only on a SECOND yes over those lines, never carried by the first. Items whose apply lines are byte-equal are one writer and take one answer: the tier preview's `--apply` writes the absent slots, the store seed and the queue seed together; the checker verb writes only its own entries; the insert writes every region it plans, all or nothing, so a `communication-absent` skip beside a declined `story-sessions-section` gets its own ask after the insert's full preview. A no records nothing and the item stays on the screen. A DECLINE is asked apart from later, before anything runs, and runs `node ${CLAUDE_SKILL_DIR}/tools/tier-preview.mjs --decline --cwd <project-root>` only when the user refuses EVERY item the tier preview then reports offered: it records each of them at the shipped lineage, and a declined item leaves the screen until a lineage step changes the profile. An undecidable entry is a stated skip naming its reason, the remedy by reason: a prior → the upgrade's `lens` reconcile; an absent region → the insert; a customized region → a hand edit only (the lens preserves it), so that skip stands and withholds `flow optimal`. Risk profile: the tier preview's `--apply` rewrites `docs/ai/orchestration.json` in canonical form with the `_README` note and creates `docs/ai/epics/.gitkeep` and `docs/plans/queue.md` create-only; the insert adds regions to `docs/ai/agent_rules.md`; the checker verb adds entries to the PRIVILEGED `docs/ai/gates.json`; the decline writes `docs/ai/profile-declines.json`; nothing here commits.
41
- - `jev-connect` — the apply runs the read-only Jev guide and writes nothing; then walk its three steps by the jev mode doc. On bash or zsh the walk's key line appends the key in plain text to the user's startup file (readable by any process running as that user, and by other local accounts when the file is world-readable in a traversable home, as a new ~/.zshrc in a default macOS home is) and is run by the user, never the agent; the first request sends the sample text to api.typesafe.ai only on the user's yes; the vendor keeps data under its processing agreement and offers zero retention only to enterprise accounts. The item reads the environment the agent's commands run in: a host setting that filters it shows the item while the key is set in the user's shell. The decline is the HAND-APPLY alternative on the recipe line, asked apart BEFORE the confirmation (the `spec-adoption` order): on "connect" run the apply, on "decline" run nothing and hand over the line, never the apply after a decline was chosen; the decline is recorded per project as `jevConnectAck` in `docs/ai/acks.json`. Risk profile: a plain-text key in the user's startup file, written by the user; one network call on the user's yes.
41
+ - `jev-connect` — the apply is HAND-APPLY: the user runs the connect command in a terminal of their own, never the agent (the command refuses without a terminal, and a terminal is not consent). The command reads the key with no echo, makes one network call to api.typesafe.ai with the vendor's sample text only on the user's own run, and writes the key in plain text as managed export lines in the shell's startup files (bash: ~/.bashrc and the login file, created as ~/.bash_profile when none exists) (readable by any process running as that user, and by other local accounts when one is world-readable in a traversable home); the vendor keeps data under its processing agreement and offers zero retention only to enterprise accounts. The item reads the environment the agent's commands run in: a host setting that filters it shows the item while the key is set in the user's shell. The decline is the HAND-APPLY alternative on the recipe line, asked apart BEFORE the confirmation (the `spec-adoption` order): on "connect" hand over the apply line, on "decline" run nothing and hand over the recipe line; the decline is recorded per project as `jevConnectAck` in `docs/ai/acks.json`. Risk profile: a plain-text key in the user's startup files, written by a command the user runs; one network call from that run.
42
42
  - `adr-store-migration` — other items write project files too; what is unique here is that the crossing **overwrites and deletes files the project already has**: it replaces the deployed enforcement scripts in `scripts/` (the directional subset — only basenames the project already has; a locally-edited copy is snapshotted first, never silently clobbered) and, where a retired archive file exists, DELETES it once conservation has been proven. That is why it is **HAND-APPLY** and why the command shown in the apply slot is a **`--dry-run`** — it writes nothing and prints the whole plan. `--apply` is a SEPARATE step, run only after that plan has been shown and **fresh consent** obtained for it; the consent flow executes only the apply slot, so an item that needs consent AFTER its preview cannot use that lane at all. Every write is idempotent and the run is re-runnable to completion after any interruption, so a re-run repairs rather than double-applies. It never commits. Risk profile: overwrite + delete of existing project files, gated on a preview you have actually read.
43
43
 
44
44
  **Sandbox lanes (what to DO with the `sandbox-lane` recipe, per host class):**
@@ -103,7 +103,7 @@ const CATALOG = [
103
103
  invocation: invocationOf('jev'),
104
104
  group: 'Inspect',
105
105
  kind: READ_ONLY,
106
- oneLine: 'Guide to Jev (TypeSafe), a decision model: what it is, why it pays here, the steps to a first request and three prompts where it pays; the guide opens no connection.',
106
+ oneLine: 'Guide to Jev (TypeSafe), a decision model: what it is, why it pays here, the two steps to a connected key and three prompts where it pays; the guide opens no connection.',
107
107
  },
108
108
  {
109
109
  key: 'backends',
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+ // spec:jev-guide — docs/ai/specs/kit/jev-guide/jev-connect.md
3
+ // The one command that connects Jev (TypeSafe) on a host. The USER runs it in a terminal of their
4
+ // own: it reads the key with no echo, verifies it with one request, saves it as managed export lines
5
+ // in the shell's startup files and says to restart the agent. It never prints the key, never puts it in
6
+ // an argv, and refuses when stdin is not a terminal, so a non-interactive run (the agent's) cannot
7
+ // reach the prompt — the mode doc keeps the rule that the agent never runs it.
8
+ import { lstatSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import { homedir } from 'node:os';
10
+ import { basename, dirname, isAbsolute, join, relative, sep } from 'node:path';
11
+ import { fail } from '../references/scripts/markdown-blocks.mjs';
12
+ import { isDirectRun } from './direct-run.mjs';
13
+ import { JEV_ENDPOINT, JEV_ERROR_MEANINGS, JEV_SAMPLE_BODY, KEY_VARIABLE } from './jev-facts.mjs';
14
+
15
+ export const EXIT = Object.freeze({ saved: 0, notVerified: 1, usage: 2, noTerminal: 3, notSaved: 4 });
16
+ export const MARK = '# agent-workflow jev';
17
+ export const TIMEOUT_MS = 15000;
18
+ const LOGIN_FILES = ['.bash_profile', '.bash_login', '.profile'];
19
+ export const TERMINAL_SENTENCE = 'run this in a terminal of your own: it asks for the key there, so the agent never sees it';
20
+ export const CONNECTED = 'connected — quit the agent and start it again from a new terminal';
21
+ export const SAVED_IN_PART = 'saved in part — add the export line by hand where a line above says not saved, then restart the agent';
22
+ export const NOT_SAVED_SENTENCE = `not saved — set ${KEY_VARIABLE} to your key in your shell's startup file, in an editor and in that shell's own syntax, then restart the agent`;
23
+ export const EDITOR_SENTENCE = `verified, ${NOT_SAVED_SENTENCE}`;
24
+ const HELP = `jev-connect — connects Jev (TypeSafe) on this host; run it in a terminal of your own.
25
+
26
+ Usage:
27
+ node jev-connect.mjs [--unverified]
28
+
29
+ Asks for ${KEY_VARIABLE} with no echo, verifies the key with one request to api.typesafe.ai, saves it
30
+ as managed export lines in your shell's startup files (bash: ~/.bashrc and the login file bash
31
+ reads; zsh: ~/.zshrc, under ZDOTDIR when set) and says to restart the agent. The key is never printed and never put in an argument.
32
+
33
+ --unverified save without the request (no network from here).
34
+ --help answered only when it is the whole invocation.
35
+
36
+ Exit codes: 0 saved; 1 not verified, nothing written; 2 usage; 3 no terminal, aborted or no key,
37
+ nothing written; 4 not saved (another shell, Windows, a linked startup file or directory) — the
38
+ last line says what to set by hand.`;
39
+
40
+ const parseArgv = (argv) => {
41
+ if (argv.includes('--help') || argv.includes('-h')) {
42
+ if (argv.length !== 1) throw fail(EXIT.usage, '--help is answered only when it is the whole invocation');
43
+ return { help: true };
44
+ }
45
+ return argv.reduce((parsed, arg) => {
46
+ if (arg === '--unverified') return { ...parsed, unverified: true };
47
+ throw fail(EXIT.usage, `unknown argument "${arg}" — run with --help`);
48
+ }, { help: false, unverified: false });
49
+ };
50
+
51
+ // The no-echo read: raw mode on a terminal stdin, restored on every path. Resolves the first line
52
+ // (CR, LF or CRLF ends it; bytes after it are dropped), null on Ctrl-C / Ctrl-D.
53
+ export const readSecret = (prompt, { stdin, stdout }) => new Promise((resolve, reject) => {
54
+ let buffer = '';
55
+ const finish = (fn) => {
56
+ stdin.removeListener('data', onData);
57
+ try { stdin.setRawMode(false); } catch { /* the terminal is gone; the value still resolves */ }
58
+ stdin.pause();
59
+ stdout.write('\n');
60
+ fn();
61
+ };
62
+ const onData = (chunk) => {
63
+ for (const char of String(chunk)) {
64
+ if (char === '\u0003' || char === '\u0004') return finish(() => resolve(null));
65
+ if (char === '\r' || char === '\n') return finish(() => resolve(buffer.trim()));
66
+ if (char === '\u007f' || char === '\b') buffer = buffer.slice(0, -1);
67
+ else if (char >= ' ') buffer += char;
68
+ }
69
+ return undefined;
70
+ };
71
+ try {
72
+ stdout.write(prompt);
73
+ stdin.setRawMode(true);
74
+ stdin.setEncoding('utf8');
75
+ stdin.on('data', onData);
76
+ stdin.resume();
77
+ } catch (error) {
78
+ finish(() => reject(error));
79
+ }
80
+ });
81
+
82
+ const scrub = (text, key) => String(text).split(key).join('<key>');
83
+
84
+ // One request: the vendor's sample body, the key in a header, no redirect, a bounded wait. The
85
+ // verdict is a line to print; `ok` says whether the save may follow. Nothing here writes.
86
+ export const verifyKey = async (key, io = {}) => {
87
+ const fetchImpl = io.fetch ?? globalThis.fetch;
88
+ const timeoutMs = io.timeoutMs ?? TIMEOUT_MS;
89
+ let response;
90
+ try {
91
+ response = await fetchImpl(JEV_ENDPOINT, {
92
+ method: 'POST',
93
+ headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
94
+ body: JSON.stringify(JEV_SAMPLE_BODY),
95
+ redirect: 'error',
96
+ signal: AbortSignal.timeout(timeoutMs),
97
+ });
98
+ } catch (error) {
99
+ const cause = error?.name === 'TimeoutError' ? `no answer within ${timeoutMs / 1000} s` : (error?.cause?.message ?? error?.message ?? String(error));
100
+ return { ok: false, line: `not verified: ${scrub(cause, key)}` };
101
+ }
102
+ if (response.status !== 200) {
103
+ const meaning = JEV_ERROR_MEANINGS[response.status];
104
+ return { ok: false, line: `not verified: HTTP ${response.status}${meaning ? ` — ${meaning}` : ''}` };
105
+ }
106
+ let body;
107
+ try { body = await response.json(); } catch { return { ok: false, line: 'not verified: HTTP 200 with an answer that is not JSON' }; }
108
+ const answer = body?.answers?.department;
109
+ if (typeof answer?.choice !== 'string' || typeof answer?.confidence !== 'number') {
110
+ return { ok: false, line: 'not verified: HTTP 200 without a department choice and a confidence' };
111
+ }
112
+ return { ok: true, line: `verified: HTTP 200 — department ${scrub(answer.choice, key)}, confidence ${answer.confidence}` };
113
+ };
114
+
115
+ // The startup files the key goes to, by a closed rule over the shell: zsh reads ~/.zshrc (under
116
+ // ZDOTDIR when set); an interactive bash reads ~/.bashrc and a login bash the first of
117
+ // .bash_profile, .bash_login, .profile that exists — created as .bash_profile when none does. Both
118
+ // bash files always: no file content decides the set, so a login file's own older export is
119
+ // rewritten too. Any other shell, and Windows, gets no target.
120
+ export const targetsFor = ({ shell, home, zdotdir, platform = 'linux' }, deps = {}) => {
121
+ const exists = deps.exists ?? ((path) => { try { lstatSync(path); return true; } catch { return false; } });
122
+ if (platform === 'win32' || typeof home !== 'string' || home === '') return [];
123
+ const name = typeof shell === 'string' ? basename(shell) : '';
124
+ if (name === 'zsh') return [join(typeof zdotdir === 'string' && zdotdir !== '' ? zdotdir : home, '.zshrc')];
125
+ if (name !== 'bash') return [];
126
+ const login = LOGIN_FILES.map((file) => join(home, file)).find(exists) ?? join(home, LOGIN_FILES[0]);
127
+ return [join(home, '.bashrc'), login];
128
+ };
129
+
130
+ export const managedLine = (key) => `export ${KEY_VARIABLE}='${key.replace(/'/g, `'\\''`)}' ${MARK}`;
131
+ const isExportLine = (line) => new RegExp(`^\\s*export\\s+${KEY_VARIABLE}=`).test(line);
132
+
133
+ // Replace matching export lines in place, their indent and CR kept, so no if/case branch is
134
+ // emptied; append when none match.
135
+ export const upsertManagedLine = (text, line) => {
136
+ const lines = text.split('\n');
137
+ if (lines.some(isExportLine)) {
138
+ return lines.map((item) => (isExportLine(item)
139
+ ? `${item.match(/^\s*/)[0]}${line}${item.endsWith('\r') ? '\r' : ''}` : item)).join('\n');
140
+ }
141
+ const body = text.length === 0 || text.endsWith('\n') ? text : `${text}\n`;
142
+ return `${body}${line}\n`;
143
+ };
144
+
145
+ // The directories between the home and the target, each lstat'ed: a link among them is never written
146
+ // through (a stow'd ZDOTDIR is a dotfiles repo); a target outside the home is not written at all.
147
+ const parentRefusal = (path, home, lstat) => {
148
+ const rel = relative(home, dirname(path));
149
+ if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return `${path} is outside the home`;
150
+ let dir = home;
151
+ for (const part of rel.split(sep).filter(Boolean)) {
152
+ dir = join(dir, part);
153
+ if (lstat(dir).isSymbolicLink()) return `${path} — ${dir} is a link`;
154
+ }
155
+ return null;
156
+ };
157
+
158
+ // One line per target: `saved: <path>` or `not saved: <path> — <why>`. A link — the target or a
159
+ // directory below the home — or a non-regular entry is never written through; a new file is created
160
+ // 0600; an existing file keeps its mode.
161
+ export const saveKey = (key, targets, home, deps = {}) => targets.map((path) => {
162
+ const lstat = deps.lstat ?? lstatSync;
163
+ const read = deps.read ?? ((at) => readFileSync(at, 'utf8'));
164
+ const write = deps.write ?? writeFileSync;
165
+ let stat = null;
166
+ try {
167
+ const refusal = parentRefusal(path, home, lstat);
168
+ if (refusal) return { path, saved: false, line: `not saved: ${refusal} — add the export line by hand` };
169
+ stat = lstat(path);
170
+ } catch (error) {
171
+ if (error?.code !== 'ENOENT') return { path, saved: false, line: `not saved: ${path} — ${error?.code ?? error?.message}` };
172
+ }
173
+ if (stat?.isSymbolicLink()) return { path, saved: false, line: `not saved: ${path} is a link — add the export line by hand` };
174
+ if (stat && !stat.isFile()) return { path, saved: false, line: `not saved: ${path} is not a regular file` };
175
+ try {
176
+ write(path, upsertManagedLine(stat ? read(path) : '', managedLine(key)), stat ? {} : { mode: 0o600 });
177
+ return { path, saved: true, line: `saved: ${path}` };
178
+ } catch (error) {
179
+ return { path, saved: false, line: `not saved: ${path} — ${error?.code ?? error?.message}` };
180
+ }
181
+ });
182
+
183
+ const safeHome = (env) => {
184
+ if (typeof env.HOME === 'string' && env.HOME !== '') return env.HOME;
185
+ try { return homedir(); } catch { return ''; }
186
+ };
187
+
188
+ export const main = async (argv, io = {}) => {
189
+ const log = io.log ?? console.log;
190
+ const error = io.error ?? console.error;
191
+ let parsed;
192
+ try { parsed = parseArgv(argv); } catch (err) { error(err.message); return EXIT.usage; }
193
+ if (parsed.help) { log(HELP); return EXIT.saved; }
194
+ const stdin = io.stdin ?? process.stdin;
195
+ const stdout = io.stdout ?? process.stdout;
196
+ if (!stdin.isTTY) { error(TERMINAL_SENTENCE); return EXIT.noTerminal; }
197
+ let key;
198
+ try { key = await readSecret(`${KEY_VARIABLE}: `, { stdin, stdout }); } catch (err) { error(`the terminal read failed: ${err?.message ?? err} — nothing written`); return EXIT.noTerminal; }
199
+ if (key === null) { error('aborted — nothing written'); return EXIT.noTerminal; }
200
+ if (key === '') { error('no key entered — nothing written'); return EXIT.noTerminal; }
201
+ if (parsed.unverified) log('not verified: skipped (--unverified)');
202
+ else {
203
+ const verdict = await verifyKey(key, io);
204
+ log(verdict.line);
205
+ if (!verdict.ok) { log('nothing written'); return EXIT.notVerified; }
206
+ }
207
+ const env = io.env ?? process.env;
208
+ const home = io.home ?? safeHome(env);
209
+ const targets = targetsFor({ shell: env.SHELL, home, zdotdir: env.ZDOTDIR, platform: io.platform ?? process.platform }, io);
210
+ const results = saveKey(key, targets, home, io);
211
+ for (const result of results) log(result.line);
212
+ const saved = results.filter((result) => result.saved).length;
213
+ if (results.length > 0 && saved === results.length) { log(CONNECTED); return EXIT.saved; }
214
+ if (saved > 0) log(SAVED_IN_PART);
215
+ else log(parsed.unverified ? NOT_SAVED_SENTENCE : EDITOR_SENTENCE);
216
+ return EXIT.notSaved;
217
+ };
218
+
219
+ if (isDirectRun(import.meta.url)) process.exitCode = await main(process.argv.slice(2));
@@ -0,0 +1,40 @@
1
+ // spec:jev-guide — docs/ai/specs/kit/jev-guide/index.md
2
+ // The one leaf the Jev surfaces share: the key rule, the connect line, and the vendor facts the
3
+ // connect command's one request uses. No side effect, no import beyond node:path — the read-only
4
+ // guide imports it under its closed import graph, the advisor and the connect command beside it.
5
+ import { join } from 'node:path';
6
+
7
+ export const KEY_VARIABLE = 'TYPESAFE_API_KEY';
8
+
9
+ // The ONE key rule: set exactly when the variable is a string that is not empty after trimming.
10
+ export const keySet = (env) => typeof env[KEY_VARIABLE] === 'string' && env[KEY_VARIABLE].trim() !== '';
11
+
12
+ // A minimal POSIX quoting for the one path the connect line carries: bare when every byte is safe,
13
+ // else single-quoted with the shell's own escape for a single quote.
14
+ const SAFE = /^[A-Za-z0-9_./:=+@%,-]+$/;
15
+ export const quoteArg = (value) => (SAFE.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`);
16
+
17
+ // The command the user runs in a terminal of their own; the guide prints it, the advisor's apply
18
+ // carries it. `toolsDir` is the kit's tools directory (the file beside this one).
19
+ export const connectLine = (toolsDir) => `node ${quoteArg(join(toolsDir, 'jev-connect.mjs'))}`;
20
+
21
+ // The vendor's smallest documented request (docs.typesafe.ai quickstart, read 2026-09-29) and the
22
+ // meanings of its documented error codes, quoted as the vendor prints them.
23
+ export const JEV_ENDPOINT = 'https://api.typesafe.ai/v1/systemone';
24
+ export const JEV_SAMPLE_BODY = Object.freeze({
25
+ state: 'Help! My payouts have been failing for 3 days.',
26
+ model: 'jev-latest',
27
+ questions: {
28
+ department: {
29
+ type: 'choice',
30
+ instructions: 'Which team should handle this?',
31
+ criteria: { billing: 'Payments, invoicing, refunds', technical: 'Bugs, outages, integrations', sales: 'Pricing, upgrades, new accounts' },
32
+ },
33
+ },
34
+ });
35
+ export const JEV_ERROR_MEANINGS = Object.freeze({
36
+ 401: 'Missing or invalid API key. Check the `Authorization` header.',
37
+ 422: 'The request body failed validation — for example a missing required field or a malformed question. The body details the offending field.',
38
+ 429: 'You have exceeded your rate limit. Back off and retry after a short delay.',
39
+ 529: 'TypeSafe is temporarily overloaded. Retry after a short delay.',
40
+ });
@@ -2,14 +2,16 @@
2
2
  // spec:jev-guide — docs/ai/specs/kit/jev-guide/index.md
3
3
  import { statSync } from 'node:fs';
4
4
  import { homedir } from 'node:os';
5
- import { basename, join, resolve } from 'node:path';
5
+ import { basename, dirname, join, resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
6
7
  import { fail } from '../references/scripts/markdown-blocks.mjs';
7
8
  import { isDirectRun } from './direct-run.mjs';
9
+ import { KEY_VARIABLE, connectLine, keySet } from './jev-facts.mjs';
8
10
 
9
11
  const SCHEMA = 1;
10
- const KEY_VARIABLE = 'TYPESAFE_API_KEY';
12
+ const TOOLS = dirname(fileURLToPath(import.meta.url));
11
13
  const SKILL_FILE = join('typesafe-ai', 'SKILL.md');
12
- const HELP = `jev — read-only: what Jev (TypeSafe) is, why it pays here, and the steps to a first request.
14
+ const HELP = `jev — read-only: what Jev (TypeSafe) is, why it pays here, and the two steps to a connected key.
13
15
 
14
16
  Usage:
15
17
  node jev-guide.mjs [--dir PROJECT] [--json]
@@ -20,7 +22,7 @@ Usage:
20
22
 
21
23
  Exit codes: 0 rendered; 1 nothing rendered (a --dir that is not a directory); 2 usage.
22
24
 
23
- Reads only: the --dir, the home directory, the key variable's presence, the SHELL variable and five skill paths; no network.`;
25
+ Reads only: the --dir, the home directory, the key variable's presence and five skill paths; no network.`;
24
26
 
25
27
  export const JEV_TEXT = Object.freeze([
26
28
  'WHAT: Jev is a decision model: it takes a state and typed questions (choice, score, noul) and returns typed answers with probabilities and a confidence; it does not write text.',
@@ -42,7 +44,13 @@ export const JEV_TEXT = Object.freeze([
42
44
  'code.claude.com for claude plugin list, read 2026-09-29.',
43
45
  ]);
44
46
 
47
+ const KEY_GET = 'Get a key at console.typesafe.ai and set it on every host the agent runs on.';
48
+ const KEY_CONNECT = 'Connect it from a terminal of your own, never through the agent: the command asks for the key with no echo, checks it with one request and, for bash or zsh, saves it to your shell\'s startup files.';
49
+ const KEY_RESTART = 'Then quit the agent, start it again from a new terminal, and run /agent-workflow-kit jev again: the key mark should read set.';
50
+ const KEY_NEVER = 'Never paste the key into the chat or into a project file.';
51
+ const KEY_NOT_SET = `key: ${KEY_VARIABLE} not set. A host setting that filters the environment of the agent's commands also hides it. If it still reads not set after a restart and no such setting applies, the agent may not have been started from a shell that reads its startup file: export the variable in the file the agent's launcher reads, then restart.`;
45
52
  const INSTALL = [
53
+ 'Optional, for your own code and prompts — the vendor skill:',
46
54
  'Claude Code:',
47
55
  'claude plugin marketplace add typesafe-ai/skills',
48
56
  'claude plugin install typesafe@typesafe-ai',
@@ -55,28 +63,8 @@ const NOT_SEEN = [
55
63
  'in a Claude Code session (the vendor); ask any other agent to use the TypeSafe skill (the vendor); or run Claude Code\'s own',
56
64
  'claude plugin list',
57
65
  ];
58
- const PROFILES = Object.freeze({ zsh: '~/.zshrc', bash: '~/.bashrc' });
59
- const KEY_LINE = 'set +x; printf \'TYPESAFE_API_KEY: \' && read -rs k && echo && { echo; printf \'export TYPESAFE_API_KEY=%q\' "$k"; echo; } >> PROFILE && unset k';
60
- const KEY_GET = 'Get a key at console.typesafe.ai and set it on every host the agent runs on.';
61
- const KEY_RUN = 'Run this line yourself in a terminal of your own, outside the agent: it reads the key with no echo, so the key never passes through the agent or its transcript.';
62
- const KEY_EDITOR = 'Set TYPESAFE_API_KEY to your key in your shell\'s startup file, in an editor and in that shell\'s own syntax, never through the chat or a project file.';
63
- const KEY_RESTART = 'Then quit the agent, start it again from a new terminal, and run /agent-workflow-kit jev again: the key mark reads set.';
64
- const KEY_NEVER = 'Never paste the key into the chat or into a project file.';
65
- const keyNotSet = (profile) => `key: ${KEY_VARIABLE} not set. A host setting that filters the environment of the agent's commands also hides it. If it still reads not set after a restart and no such setting applies, the agent may not have been started from a shell that reads ${profile ?? 'that startup file'}: export the variable in the file the agent's launcher reads, then restart.`;
66
- const CURL = 'set +x; curl -q -sS -w \' HTTP %{http_code}\' -X POST https://api.typesafe.ai/v1/systemone -H "Authorization: Bearer $TYPESAFE_API_KEY" -H "Content-Type: application/json" -d \'{"state":"Help! My payouts have been failing for 3 days.","model":"jev-latest","questions":{"department":{"type":"choice","instructions":"Which team should handle this?","criteria":{"billing":"Payments, invoicing, refunds","technical":"Bugs, outages, integrations","sales":"Pricing, upgrades, new accounts"}}}}\'';
67
- const FIRST_USE = [
68
- 'The vendor\'s smallest documented request, reading the key from the environment:',
69
- CURL,
70
- 'The check: HTTP 200 and a response whose answers.department carries a choice and a confidence; any other code reads by the four error meanings below.',
71
- 'Its documented response:',
72
- '{"model":"jev-1.13.0","answers":{"department":{"type":"choice","choice":"billing","probabilities":{"billing":0.88,"technical":0.12,"sales":0.0},"confidence":0.81}},"usage":{"input_tokens":318,"output_tokens":34}}',
73
- 'choice: "The highest-probability option."',
74
- 'probabilities: "Every option mapped to its probability (floats that sum to 1)."',
75
- 'confidence: "How certain the model is, derived from probabilities."',
76
- '401: "Missing or invalid API key. Check the `Authorization` header."',
77
- '422: "The request body failed validation — for example a missing required field or a malformed question. The body details the offending field."',
78
- '429: "You have exceeded your rate limit. Back off and retry after a short delay."',
79
- '529: "TypeSafe is temporarily overloaded. Retry after a short delay."',
66
+ const AFTER_INSTALL = [
67
+ 'After an install, quit the agent and start it again from a new terminal.',
80
68
  'Then give the agent this prompt: using the TypeSafe skill, route these three tickets to billing, technical or sales, one request each, and print each choice and confidence — "I was charged twice this month.", "The app crashes when I upload a file.", "Is there a discount for a yearly plan?".',
81
69
  'Success: three choices, each with a confidence.',
82
70
  ];
@@ -109,18 +97,18 @@ const parseArgv = (argv) => {
109
97
  const statIs = (path, kind) => {
110
98
  try { return kind === 'file' ? statSync(path).isFile() : statSync(path).isDirectory(); } catch { return false; }
111
99
  };
100
+ const homeOf = (io) => {
101
+ if (io.home !== undefined) return io.home;
102
+ try { return homedir(); } catch { return ''; }
103
+ };
112
104
  const skillPaths = (dir, home) => [...new Set([join(dir, '.claude', 'skills'), join(dir, '.agents', 'skills'),
113
- join(home, '.claude', 'skills'), join(home, '.codex', 'skills'), join(home, '.cursor', 'skills')]
105
+ ...(home === '' ? [] : [join(home, '.claude', 'skills'), join(home, '.codex', 'skills'), join(home, '.cursor', 'skills')])]
114
106
  .map((root) => join(root, SKILL_FILE)))];
115
- export const keySet = (env) => typeof env[KEY_VARIABLE] === 'string' && env[KEY_VARIABLE].trim() !== '';
116
- const profileOf = (env) => (typeof env.SHELL === 'string' && Object.hasOwn(PROFILES, basename(env.SHELL)) ? PROFILES[basename(env.SHELL)] : null);
117
107
 
118
- const renderSteps = (found, set, profile) => [
119
- ['STEP 1 — the key', KEY_GET, ...(profile ? [KEY_RUN, KEY_LINE.replace('PROFILE', profile)] : [KEY_EDITOR]), KEY_RESTART,
120
- KEY_NEVER, set ? `key: ${KEY_VARIABLE} set.` : keyNotSet(profile)],
108
+ const renderSteps = (found, set, connect) => [
109
+ ['STEP 1 — the key', KEY_GET, KEY_CONNECT, connect, KEY_RESTART, KEY_NEVER, set ? `key: ${KEY_VARIABLE} set.` : KEY_NOT_SET],
121
110
  ['STEP 2 — the vendor skill', ...INSTALL,
122
- ...(found.length ? found.map((path) => `skill: found at ${path}`) : NOT_SEEN)],
123
- ['STEP 3 — the first request', ...FIRST_USE],
111
+ ...(found.length ? found.map((path) => `skill: found at ${path}`) : NOT_SEEN), ...AFTER_INSTALL],
124
112
  ];
125
113
 
126
114
  export const main = (argv, io = {}) => {
@@ -133,14 +121,13 @@ export const main = (argv, io = {}) => {
133
121
  if (parsed.help) { log(HELP); return 0; }
134
122
  const dir = resolve(io.cwd ?? process.cwd(), parsed.dir);
135
123
  if (!statIs(dir, 'directory')) { error(`${dir} is not a directory`); return 1; }
136
- const found = skillPaths(dir, io.home ?? homedir()).filter((path) => statIs(path, 'file'));
124
+ const found = skillPaths(dir, homeOf(io)).filter((path) => statIs(path, 'file'));
137
125
  const env = io.env ?? process.env;
138
126
  const set = keySet(env);
139
- const profile = profileOf(env);
140
- const steps = renderSteps(found, set, profile);
127
+ const connect = connectLine(io.toolsDir ?? TOOLS);
128
+ const steps = renderSteps(found, set, connect);
141
129
  log(parsed.json
142
- ? JSON.stringify({ schema: SCHEMA, command: 'jev', dir, text: JEV_TEXT, skill: { found }, key: { set, profile },
143
- steps, prompts: PROMPTS }, null, 2)
130
+ ? JSON.stringify({ schema: SCHEMA, command: 'jev', dir, text: JEV_TEXT, skill: { found }, key: { set }, connect, steps, prompts: PROMPTS }, null, 2)
144
131
  : [...JEV_TEXT, ...steps.flat(), 'WHERE IT PAYS IN THIS WORKFLOW', ...PROMPTS].join('\n'));
145
132
  return 0;
146
133
  };
@@ -102,7 +102,7 @@ import {
102
102
  import { ADOPTION, STORE_DIR_REL as SPEC_STORE_DIR_REL, SPEC_ADOPTION_LANE, declineFingerprint, readDeclineAck, surveySpecAdoption } from './spec-adoption.mjs';
103
103
  import { ENSURE_OPS } from './ensure-vocabulary.mjs';
104
104
  import { composeProfileGapScreen } from './profile-gap-screen.mjs';
105
- import { keySet } from './jev-guide.mjs';
105
+ import { connectLine, keySet } from './jev-facts.mjs';
106
106
 
107
107
  // The upgrade ensure that seeds the spec store — the not-adopted item's apply; pinned to the vocabulary.
108
108
  const SPEC_LAYER_ENSURE = ENSURE_OPS.includes('specs') ? 'specs' : null;
@@ -283,7 +283,7 @@ export const WHATS = Object.freeze({
283
283
  'worktrees-dir': 'write access to the worktrees parent dir {dir} is not confirmed — provision may still stop',
284
284
  'spec-adoption': 'feature-spec store absent (docs/ai/specs) — no feature contract can govern a plan here yet; seed the store, or record the decline',
285
285
  'profile-gap': '{what}',
286
- 'jev-connect': 'TYPESAFE_API_KEY is not set on this host — Jev (TypeSafe), a typed decision model, is not connected machine-wide',
286
+ 'jev-connect': 'Jev (TypeSafe) is not connected: TYPESAFE_API_KEY is not in the environment the agent\'s commands run in',
287
287
  'spec-adoption.adopting': 'feature-spec store: {n} draft spec(s), no live contract — nothing governs a plan through it yet; land a live contract, or record the decline',
288
288
  });
289
289
 
@@ -1608,14 +1608,14 @@ const probeMcpChannel = ({ root, deps, add, skip }) => {
1608
1608
  }
1609
1609
  };
1610
1610
 
1611
- // The Jev offer (contract: kit/jev-guide, part jev-offer). The key is judged first, through the
1612
- // guide's own rule over the injected environment only; the ack store is read only when it is not set.
1611
+ // The Jev offer (contract: kit/jev-guide, part jev-offer). The key is judged first, through the facts leaf's one rule over
1612
+ // the injected environment; the apply is the connect command the USER runs in a terminal of their own (HAND-APPLY), never a guide.
1613
1613
  export const probeJevConnect = ({ root, deps, add, skip }) => {
1614
1614
  try {
1615
1615
  if (keySet(deps.getenv ?? {})) return;
1616
1616
  if (readAckValue(root, deps, ACKS_JEV_CONNECT_KEY) === JEV_CONNECT_DECLINE) return;
1617
1617
  const decline = `node ${q(toolPath('ack-write.mjs'))} --lane jev-connect --fingerprint ${JEV_CONNECT_DECLINE} --cwd ${q(root)}`;
1618
- add('jev-connect', WHATS['jev-connect'], `node ${q(toolPath('jev-guide.mjs'))} --dir ${q(root)} --json`, 'jev-connect',
1618
+ add('jev-connect', WHATS['jev-connect'], `HAND-APPLY: ${connectLine(HERE)}`, 'jev-connect',
1619
1619
  `HAND-APPLY alternative (instead of the apply, never after it): decline the offer by recording it — ${decline}`);
1620
1620
  } catch (err) {
1621
1621
  skip('jev-connect', err);