@sabaiway/agent-workflow-kit 14.17.0 → 14.19.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,61 @@ 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.19.0 — the Jev key the vendor's way: `jev-connect` claims no connection, names one restart step and saves for fish and Windows; the guide and the Recommendations item say where to run it (AD-166)
8
+
9
+ **`jev-connect` no longer prints `connected`.** A saved key reaches the agent only once the agent's environment is read
10
+ again, which the command cannot see; the guide's key mark is the proof. A run that saves every target now closes with
11
+ `saved — restart the agent so it reads the key: in VS Code or a VS Code fork, quit the editor completely (every window)
12
+ and open it again; in a terminal, open a new terminal and start the agent there; then ask the agent to check Jev`,
13
+ exit 0. The key still reaches the agent only through `TYPESAFE_API_KEY`: the kit takes it from no file, and nothing is
14
+ pasted into the chat.
15
+
16
+ - **One restart step.** The command's all-saved line and the guide's STEP 1 print the same step from
17
+ `tools/jev-facts.mjs`. When targets are named and none is saved, the command's last line says to fix the cause on the
18
+ line above and run the command again (exit 4); `saved in part` gives the same remedy. Only a run with no target — off
19
+ Windows, a shell the command does not save for or an empty home — keeps the editor sentence.
20
+ - **Where to run it.** The guide prints `Run it in <place>:` directly before the connect line and carries `where` in
21
+ its JSON: `a PowerShell terminal` on Windows, `the WSL distro <name> terminal`, `a terminal on SSH host <host>`, or
22
+ `a terminal inside this container`; nothing when none applies. The Recommendations item's recipe line opens with
23
+ `run the apply in <place>;`. The connect line and the apply stay pure commands; on Windows the line is quoted for
24
+ PowerShell.
25
+ - **fish and Windows save.** fish: the kit's own file `~/.config/fish/conf.d/typesafe-api-key.fish` (under an absolute
26
+ `XDG_CONFIG_HOME` when set), written whole, its missing directories created 0700, a new file 0600, never through a
27
+ link. Windows: the user environment variable `TYPESAFE_API_KEY`, set by one `powershell.exe` run that reads the key
28
+ from its input, never from an argument; a failure names its cause on a `not saved:` line.
29
+ - **The guide's not-set mark asks no hand edit.** After the restart it says to run the connect line again and follow
30
+ its last line; the guide's help names the place facts it reads.
31
+ - Closes Issue-032. Unproven on the release host and stated: a live Windows run (a stub proves the route) and the fish
32
+ readback.
33
+
34
+ ## 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)
35
+
36
+ **While `TYPESAFE_API_KEY` is not in the environment the agent's commands run in, the Recommendations item `jev-connect`
37
+ now carries you to the result.** Its apply is `HAND-APPLY: node <kit>/tools/jev-connect.mjs` — a command YOU run in a
38
+ 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
39
+ sets nothing: a user who ran it believed Jev was connected. The rule now: a recommendation reaches the result.
40
+
41
+ - **`jev-connect`, the one command.** It refuses without a terminal (exit 3), asks for the key with no echo, verifies
42
+ it with one request to api.typesafe.ai (the vendor's sample body, the key in a header from an in-process fetch —
43
+ never an argument, never a shell trace; the four documented error codes print the vendor's meanings), and saves it
44
+ as managed `export` lines in your shell's startup files: zsh → `~/.zshrc` (under `ZDOTDIR` when set); bash →
45
+ `~/.bashrc` and the login file bash reads (`.bash_profile`, `.bash_login` or `.profile`, the first that exists;
46
+ `.bash_profile` created when none; written even when it sources `.bashrc`). Then: `connected — quit the agent and
47
+ start it again from a new terminal`, exit 0. Every earlier line that starts `export TYPESAFE_API_KEY=` is replaced in
48
+ place by the managed line, so no `if` branch is emptied; a linked file or directory is never written through, nor a
49
+ file outside your home; a new file is created 0600.
50
+ Another shell, or Windows: the key is verified but not saved, the last line says what to set by hand, exit 4. A key
51
+ the vendor refuses: `nothing written`, exit 1. `--unverified` saves without the request. The key's value never
52
+ appears in any output.
53
+ - **The guide shrinks to two steps.** `/agent-workflow-kit jev` prints the key mark and the connect line, then the
54
+ optional vendor skill with its own restart line and first prompt. The shell key line, the curl line, the response
55
+ and the error meanings left the guide; its mode doc lost the walk — no command the guide prints is run by the agent.
56
+ - **One leaf, no restatement.** `tools/jev-facts.mjs` holds the key rule, the connect line and the vendor's request
57
+ facts; the guide, the advisor and the command import it. The advisor's WHAT now names the environment the agent's
58
+ commands run in, never "this host".
59
+ - Closes items 23–26 and 28–30 of Issue-027 (the unguarded `homedir()`, the missing restart after the skill install,
60
+ the key in curl's argv, `ZDOTDIR`, the `&& unset k`, the WHAT, the login bash that never reads `~/.bashrc`).
61
+
7
62
  ## 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
63
 
9
64
  **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 set 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.19.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.19.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.19.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 set 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. The `Run it in <place>:` line, when the guide printed one, stays directly before the connect line and is never folded into it; the restart step is relayed whole.
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); when the recipe line opens with `run the apply in <place>`, that place is the terminal to name. 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; zsh: ~/.zshrc), as the kit's own file ~/.config/fish/conf.d/typesafe-api-key.fish for fish, or on Windows as the user environment variable TYPESAFE_API_KEY, stored in the Windows user environment; a process already running at the save can keep its old environment and hand it to what it starts — the guide's key mark confirms the agent sees it (a file is 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 or the Windows user environment, 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,270 @@
1
+ #!/usr/bin/env node
2
+ // spec:jev-guide — docs/ai/specs/kit/jev-guide/jev-connect.md
3
+ // The one command that saves the Jev (TypeSafe) key on a host. The USER runs it in a terminal of
4
+ // their own: it reads the key with no echo, verifies it with one request, saves it where the shell
5
+ // reads it (bash, zsh, fish) or in the Windows user environment, and prints the restart step. It never
6
+ // prints the key, never puts it in an argv, and refuses when stdin is not a terminal, so a
7
+ // non-interactive run (the agent's) cannot reach the prompt — the mode doc keeps the rule that the
8
+ // agent never runs it. It claims no connection: the guide's key mark, read after the restart, is the proof.
9
+ import { spawnSync } from 'node:child_process';
10
+ import { lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
11
+ import { homedir } from 'node:os';
12
+ import { basename, dirname, isAbsolute, join, relative, sep } from 'node:path';
13
+ import { fail } from '../references/scripts/markdown-blocks.mjs';
14
+ import { isDirectRun } from './direct-run.mjs';
15
+ import { JEV_ENDPOINT, JEV_ERROR_MEANINGS, JEV_SAMPLE_BODY, KEY_VARIABLE, RESTART_STEP } from './jev-facts.mjs';
16
+
17
+ export const EXIT = Object.freeze({ saved: 0, notVerified: 1, usage: 2, noTerminal: 3, notSaved: 4 });
18
+ export const MARK = '# agent-workflow jev';
19
+ export const TIMEOUT_MS = 15000;
20
+ const LOGIN_FILES = ['.bash_profile', '.bash_login', '.profile'];
21
+ const FISH_FILE = join('fish', 'conf.d', 'typesafe-api-key.fish');
22
+ const WINDOWS_TARGET = `the Windows user environment variable ${KEY_VARIABLE}`;
23
+ const WINDOWS_SCRIPT = `$ErrorActionPreference='Stop'; [Environment]::SetEnvironmentVariable('${KEY_VARIABLE}', [Console]::In.ReadLine(), 'User')`;
24
+ export const TERMINAL_SENTENCE = 'run this in a terminal of your own: it asks for the key there, so the agent never sees it';
25
+ export const ALL_SAVED = `saved — ${RESTART_STEP}; then ask the agent to check Jev`;
26
+ export const SAVED_IN_PART = 'saved in part — fix the cause on the not saved: line above and run this command again, then restart the agent';
27
+ const RETRY_SENTENCE = 'not saved — fix the cause on the line above and run this command again';
28
+ 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`;
29
+ export const EDITOR_SENTENCE = `verified, ${NOT_SAVED_SENTENCE}`;
30
+ const HELP = `jev-connect — saves the Jev (TypeSafe) key on this host; run it in a terminal of your own.
31
+
32
+ Usage:
33
+ node jev-connect.mjs [--unverified]
34
+
35
+ Asks for ${KEY_VARIABLE} with no echo, verifies the key with one request to api.typesafe.ai and
36
+ saves it: for bash and zsh as managed export lines in your shell's startup files (bash: ~/.bashrc
37
+ and the login file bash reads; zsh: ~/.zshrc, under ZDOTDIR when set), for fish as the file
38
+ ~/.config/fish/conf.d/typesafe-api-key.fish (under XDG_CONFIG_HOME when set), on Windows as the user
39
+ environment variable. Then it prints the restart step. The key is never printed and never put in an argument.
40
+
41
+ --unverified save without the request (no network from here).
42
+ --help answered only when it is the whole invocation.
43
+
44
+ Exit codes: 0 saved; 1 not verified, nothing written; 2 usage; 3 no terminal, aborted, no key or a
45
+ failed read, nothing written;
46
+ 4 not saved (a refused or failed target, or a shell it does not save for) — the last line says what to do next.`;
47
+
48
+ const parseArgv = (argv) => {
49
+ if (argv.includes('--help') || argv.includes('-h')) {
50
+ if (argv.length !== 1) throw fail(EXIT.usage, '--help is answered only when it is the whole invocation');
51
+ return { help: true };
52
+ }
53
+ return argv.reduce((parsed, arg) => {
54
+ if (arg === '--unverified') return { ...parsed, unverified: true };
55
+ throw fail(EXIT.usage, `unknown argument "${arg}" — run with --help`);
56
+ }, { help: false, unverified: false });
57
+ };
58
+
59
+ // The no-echo read: raw mode on a terminal stdin, restored on every path. Resolves the first line
60
+ // (CR, LF or CRLF ends it; bytes after it are dropped), null on Ctrl-C / Ctrl-D.
61
+ export const readSecret = (prompt, { stdin, stdout }) => new Promise((resolve, reject) => {
62
+ let buffer = '';
63
+ const finish = (fn) => {
64
+ stdin.removeListener('data', onData);
65
+ try { stdin.setRawMode(false); } catch { /* the terminal is gone; the value still resolves */ }
66
+ stdin.pause();
67
+ stdout.write('\n');
68
+ fn();
69
+ };
70
+ const onData = (chunk) => {
71
+ for (const char of String(chunk)) {
72
+ if (char === '\u0003' || char === '\u0004') return finish(() => resolve(null));
73
+ if (char === '\r' || char === '\n') return finish(() => resolve(buffer.trim()));
74
+ if (char === '\u007f' || char === '\b') buffer = buffer.slice(0, -1);
75
+ else if (char >= ' ') buffer += char;
76
+ }
77
+ return undefined;
78
+ };
79
+ try {
80
+ stdout.write(prompt);
81
+ stdin.setRawMode(true);
82
+ stdin.setEncoding('utf8');
83
+ stdin.on('data', onData);
84
+ stdin.resume();
85
+ } catch (error) {
86
+ finish(() => reject(error));
87
+ }
88
+ });
89
+
90
+ const scrub = (text, key) => String(text).split(key).join('<key>');
91
+
92
+ // One request: the vendor's sample body, the key in a header, no redirect, a bounded wait. The
93
+ // verdict is a line to print; `ok` says whether the save may follow. Nothing here writes.
94
+ export const verifyKey = async (key, io = {}) => {
95
+ const fetchImpl = io.fetch ?? globalThis.fetch;
96
+ const timeoutMs = io.timeoutMs ?? TIMEOUT_MS;
97
+ let response;
98
+ try {
99
+ response = await fetchImpl(JEV_ENDPOINT, {
100
+ method: 'POST',
101
+ headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
102
+ body: JSON.stringify(JEV_SAMPLE_BODY),
103
+ redirect: 'error',
104
+ signal: AbortSignal.timeout(timeoutMs),
105
+ });
106
+ } catch (error) {
107
+ const cause = error?.name === 'TimeoutError' ? `no answer within ${timeoutMs / 1000} s` : (error?.cause?.message ?? error?.message ?? String(error));
108
+ return { ok: false, line: `not verified: ${scrub(cause, key)}` };
109
+ }
110
+ if (response.status !== 200) {
111
+ const meaning = JEV_ERROR_MEANINGS[response.status];
112
+ return { ok: false, line: `not verified: HTTP ${response.status}${meaning ? ` — ${meaning}` : ''}` };
113
+ }
114
+ let body;
115
+ try { body = await response.json(); } catch { return { ok: false, line: 'not verified: HTTP 200 with an answer that is not JSON' }; }
116
+ const answer = body?.answers?.department;
117
+ if (typeof answer?.choice !== 'string' || typeof answer?.confidence !== 'number') {
118
+ return { ok: false, line: 'not verified: HTTP 200 without a department choice and a confidence' };
119
+ }
120
+ return { ok: true, line: `verified: HTTP 200 — department ${scrub(answer.choice, key)}, confidence ${answer.confidence}` };
121
+ };
122
+
123
+ // The targets the key goes to, as descriptors { form, path }, by a closed rule over the platform and
124
+ // the shell: Windows → the user environment variable (one spawn); zsh reads ~/.zshrc (under ZDOTDIR
125
+ // when set); an interactive bash reads ~/.bashrc and a login bash the first of .bash_profile,
126
+ // .bash_login, .profile that exists — created as .bash_profile when none does. Both bash files
127
+ // always: no file content decides the set. fish reads its conf.d (under XDG_CONFIG_HOME when it is
128
+ // absolute). Any other shell gets no target.
129
+ export const targetsFor = ({ shell, home, zdotdir, xdgConfigHome, platform = 'linux' }, deps = {}) => {
130
+ const exists = deps.exists ?? ((path) => { try { lstatSync(path); return true; } catch { return false; } });
131
+ if (platform === 'win32') return [{ form: 'windows', path: null }];
132
+ if (typeof home !== 'string' || home === '') return [];
133
+ const name = typeof shell === 'string' ? basename(shell) : '';
134
+ const exported = (path) => ({ form: 'export', path });
135
+ if (name === 'zsh') return [exported(join(typeof zdotdir === 'string' && zdotdir !== '' ? zdotdir : home, '.zshrc'))];
136
+ if (name === 'fish') {
137
+ const config = typeof xdgConfigHome === 'string' && isAbsolute(xdgConfigHome) ? xdgConfigHome : join(home, '.config');
138
+ return [{ form: 'fish', path: join(config, FISH_FILE) }];
139
+ }
140
+ if (name !== 'bash') return [];
141
+ const login = LOGIN_FILES.map((file) => join(home, file)).find(exists) ?? join(home, LOGIN_FILES[0]);
142
+ return [exported(join(home, '.bashrc')), exported(login)];
143
+ };
144
+
145
+ export const managedLine = (key) => `export ${KEY_VARIABLE}='${key.replace(/'/g, `'\\''`)}' ${MARK}`;
146
+ // fish's single quotes take two escapes: a backslash doubled, a quote after a backslash.
147
+ export const fishLine = (key) => `set -gx ${KEY_VARIABLE} '${key.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}' ${MARK}`;
148
+ const isExportLine = (line) => new RegExp(`^\\s*export\\s+${KEY_VARIABLE}=`).test(line);
149
+
150
+ // Replace matching export lines in place, their indent and CR kept, so no if/case branch is
151
+ // emptied; append when none match.
152
+ export const upsertManagedLine = (text, line) => {
153
+ const lines = text.split('\n');
154
+ if (lines.some(isExportLine)) {
155
+ return lines.map((item) => (isExportLine(item)
156
+ ? `${item.match(/^\s*/)[0]}${line}${item.endsWith('\r') ? '\r' : ''}` : item)).join('\n');
157
+ }
158
+ const body = text.length === 0 || text.endsWith('\n') ? text : `${text}\n`;
159
+ return `${body}${line}\n`;
160
+ };
161
+
162
+ // The directories between the home and the target, each lstat'ed: a link among them is never written
163
+ // through (a stow'd ZDOTDIR is a dotfiles repo); a target outside the home is not written at all. The
164
+ // walk stops at the first missing directory: nothing below it exists, so nothing below is a link.
165
+ const walkParents = (path, home, lstat) => {
166
+ const rel = relative(home, dirname(path));
167
+ if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return { refusal: `${path} is outside the home` };
168
+ const missing = [];
169
+ let dir = home;
170
+ for (const part of rel.split(sep).filter(Boolean)) {
171
+ dir = join(dir, part);
172
+ if (missing.length > 0) { missing.push(dir); continue; }
173
+ let stat;
174
+ try { stat = lstat(dir); } catch (error) {
175
+ if (error?.code !== 'ENOENT') throw error;
176
+ missing.push(dir);
177
+ continue;
178
+ }
179
+ if (stat.isSymbolicLink()) return { refusal: `${path} — ${dir} is a link` };
180
+ }
181
+ return { refusal: null, missing };
182
+ };
183
+
184
+ // A file target: the export form upserts its managed line and creates no directory; the fish form is
185
+ // the kit's own file, written whole, its missing directories created 0700. A new file is 0600; an
186
+ // existing one keeps its mode.
187
+ const saveFile = (key, { form, path }, home, deps) => {
188
+ const lstat = deps.lstat ?? lstatSync;
189
+ const read = deps.read ?? ((at) => readFileSync(at, 'utf8'));
190
+ const write = deps.write ?? writeFileSync;
191
+ const mkdir = deps.mkdir ?? mkdirSync;
192
+ const no = (line) => ({ path, saved: false, line });
193
+ let stat = null;
194
+ try {
195
+ const { refusal, missing } = walkParents(path, home, lstat);
196
+ if (refusal) return no(`not saved: ${refusal} — nothing written`);
197
+ if (form === 'fish') for (const dir of missing) mkdir(dir, { mode: 0o700 });
198
+ if (form === 'fish' || missing.length === 0) stat = lstat(path);
199
+ } catch (error) {
200
+ if (error?.code !== 'ENOENT') return no(`not saved: ${path} — ${error?.code ?? error?.message}`);
201
+ }
202
+ if (stat?.isSymbolicLink()) return no(`not saved: ${path} is a link — nothing written`);
203
+ if (stat && !stat.isFile()) return no(`not saved: ${path} is not a regular file`);
204
+ try {
205
+ const text = form === 'fish' ? `${fishLine(key)}\n` : upsertManagedLine(stat ? read(path) : '', managedLine(key));
206
+ write(path, text, stat ? {} : { mode: 0o600 });
207
+ return { path, saved: true, line: `saved: ${path}` };
208
+ } catch (error) {
209
+ return no(`not saved: ${path} — ${error?.code ?? error?.message}`);
210
+ }
211
+ };
212
+
213
+ // The Windows target: one powershell.exe fed the key on stdin, never in an argv. The default spawn
214
+ // starts a process only on Windows; nothing the child printed is relayed.
215
+ const saveWindows = (key, deps) => {
216
+ const no = (cause) => ({ path: null, saved: false, line: `not saved: ${WINDOWS_TARGET} — ${cause}` });
217
+ if (!deps.spawn && process.platform !== 'win32') return no('not on Windows');
218
+ const timeoutMs = deps.timeoutMs ?? TIMEOUT_MS;
219
+ const result = (deps.spawn ?? spawnSync)('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', WINDOWS_SCRIPT],
220
+ { input: `${key}\n`, timeout: timeoutMs, encoding: 'utf8', windowsHide: true });
221
+ if (result.error?.code === 'ETIMEDOUT') return no(`no answer within ${timeoutMs / 1000} s`);
222
+ if (result.error) return no(result.error.code ?? result.error.message);
223
+ if (result.status !== 0) return no(`powershell.exe exited ${result.status}`);
224
+ return { path: null, saved: true, line: `saved: ${WINDOWS_TARGET}` };
225
+ };
226
+
227
+ // One line per target: `saved: <where>` or `not saved: <where> — <why>`.
228
+ export const saveKey = (key, targets, home, deps = {}) => targets.map((target) => (target.form === 'windows'
229
+ ? saveWindows(key, deps) : saveFile(key, target, home, deps)));
230
+
231
+ const safeHome = (env) => {
232
+ if (typeof env.HOME === 'string' && env.HOME !== '') return env.HOME;
233
+ try { return homedir(); } catch { return ''; }
234
+ };
235
+
236
+ export const main = async (argv, io = {}) => {
237
+ const log = io.log ?? console.log;
238
+ const error = io.error ?? console.error;
239
+ let parsed;
240
+ try { parsed = parseArgv(argv); } catch (err) { error(err.message); return EXIT.usage; }
241
+ if (parsed.help) { log(HELP); return EXIT.saved; }
242
+ const stdin = io.stdin ?? process.stdin;
243
+ const stdout = io.stdout ?? process.stdout;
244
+ if (!stdin.isTTY) { error(TERMINAL_SENTENCE); return EXIT.noTerminal; }
245
+ let key;
246
+ try { key = await readSecret(`${KEY_VARIABLE}: `, { stdin, stdout }); } catch (err) { error(`the terminal read failed: ${err?.message ?? err} — nothing written`); return EXIT.noTerminal; }
247
+ if (key === null) { error('aborted — nothing written'); return EXIT.noTerminal; }
248
+ if (key === '') { error('no key entered — nothing written'); return EXIT.noTerminal; }
249
+ if (parsed.unverified) log('not verified: skipped (--unverified)');
250
+ else {
251
+ const verdict = await verifyKey(key, io);
252
+ log(verdict.line);
253
+ if (!verdict.ok) { log('nothing written'); return EXIT.notVerified; }
254
+ }
255
+ const env = io.env ?? process.env;
256
+ const home = io.home ?? safeHome(env);
257
+ const targets = targetsFor({ shell: env.SHELL, home, zdotdir: env.ZDOTDIR, xdgConfigHome: env.XDG_CONFIG_HOME,
258
+ platform: io.platform ?? process.platform }, io);
259
+ const results = saveKey(key, targets, home, io);
260
+ for (const result of results) log(result.line);
261
+ const saved = results.filter((result) => result.saved).length;
262
+ const verified = parsed.unverified ? '' : 'verified, ';
263
+ if (results.length > 0 && saved === results.length) { log(ALL_SAVED); return EXIT.saved; }
264
+ if (saved > 0) log(SAVED_IN_PART);
265
+ else if (results.length > 0) log(`${verified}${RETRY_SENTENCE}`);
266
+ else log(parsed.unverified ? NOT_SAVED_SENTENCE : EDITOR_SENTENCE);
267
+ return EXIT.notSaved;
268
+ };
269
+
270
+ if (isDirectRun(import.meta.url)) process.exitCode = await main(process.argv.slice(2));
@@ -0,0 +1,58 @@
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). On win32 the line
19
+ // is for PowerShell: always single-quoted, a quote doubled, one backslash by concatenation.
20
+ export const connectLine = (toolsDir, platform) => (platform === 'win32'
21
+ ? `node '${`${toolsDir}\\jev-connect.mjs`.replace(/'/g, "''")}'`
22
+ : `node ${quoteArg(join(toolsDir, 'jev-connect.mjs'))}`);
23
+
24
+ // The one restart step the connect command's all-saved line and the guide's STEP 1 print.
25
+ export const RESTART_STEP = 'restart the agent so it reads the key: in VS Code or a VS Code fork, quit the editor completely (every window) and open it again; in a terminal, open a new terminal and start the agent there';
26
+
27
+ // Where the user runs the connect line, by one ordered rule; a name outside the set drops its rung.
28
+ const NAME = /^[A-Za-z0-9._-]+$/;
29
+ const named = (value) => typeof value === 'string' && NAME.test(value);
30
+ const nonEmpty = (value) => typeof value === 'string' && value !== '';
31
+ export const placeOf = ({ env = {}, platform, hostname, container } = {}) => {
32
+ if (platform === 'win32') return 'a PowerShell terminal';
33
+ if (named(env.WSL_DISTRO_NAME)) return `the WSL distro ${env.WSL_DISTRO_NAME} terminal`;
34
+ if (nonEmpty(env.SSH_CONNECTION) && named(hostname)) return `a terminal on SSH host ${hostname}`;
35
+ if (nonEmpty(env.REMOTE_CONTAINERS) || nonEmpty(env.CODESPACES) || container === true) return 'a terminal inside this container';
36
+ return '';
37
+ };
38
+
39
+ // The vendor's smallest documented request (docs.typesafe.ai quickstart, read 2026-09-29) and the
40
+ // meanings of its documented error codes, quoted as the vendor prints them.
41
+ export const JEV_ENDPOINT = 'https://api.typesafe.ai/v1/systemone';
42
+ export const JEV_SAMPLE_BODY = Object.freeze({
43
+ state: 'Help! My payouts have been failing for 3 days.',
44
+ model: 'jev-latest',
45
+ questions: {
46
+ department: {
47
+ type: 'choice',
48
+ instructions: 'Which team should handle this?',
49
+ criteria: { billing: 'Payments, invoicing, refunds', technical: 'Bugs, outages, integrations', sales: 'Pricing, upgrades, new accounts' },
50
+ },
51
+ },
52
+ });
53
+ export const JEV_ERROR_MEANINGS = Object.freeze({
54
+ 401: 'Missing or invalid API key. Check the `Authorization` header.',
55
+ 422: 'The request body failed validation — for example a missing required field or a malformed question. The body details the offending field.',
56
+ 429: 'You have exceeded your rate limit. Back off and retry after a short delay.',
57
+ 529: 'TypeSafe is temporarily overloaded. Retry after a short delay.',
58
+ });
@@ -1,15 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  // spec:jev-guide — docs/ai/specs/kit/jev-guide/index.md
3
3
  import { statSync } from 'node:fs';
4
- import { homedir } from 'node:os';
5
- import { basename, join, resolve } from 'node:path';
4
+ import { homedir, hostname } from 'node:os';
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, RESTART_STEP, connectLine, keySet, placeOf } 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 set 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, five skill paths, and for the place WSL_DISTRO_NAME, SSH_CONNECTION, REMOTE_CONTAINERS, CODESPACES, the platform, the hostname and /.dockerenv; 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 saves it for bash, zsh or fish in your shell\'s startup files, or on Windows as a user environment variable.';
49
+ const KEY_RESTART = `Then ${RESTART_STEP}. 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 that restart and no such setting applies, run the connect line again and follow its last line.`;
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,23 @@ 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
+ };
104
+ // The place's host facts: each injected, else read once from the host, a failing read reading as none.
105
+ const placeFor = (io, env, platform) => placeOf({ env, platform,
106
+ hostname: io.hostname ?? (() => { try { return hostname(); } catch { return ''; } })(),
107
+ container: io.container ?? statIs('/.dockerenv', 'file') });
112
108
  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')]
109
+ ...(home === '' ? [] : [join(home, '.claude', 'skills'), join(home, '.codex', 'skills'), join(home, '.cursor', 'skills')])]
114
110
  .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
111
 
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)],
112
+ const renderSteps = (found, set, connect, where) => [
113
+ ['STEP 1 — the key', KEY_GET, KEY_CONNECT, ...(where ? [`Run it in ${where}:`] : []), connect, KEY_RESTART, KEY_NEVER,
114
+ set ? `key: ${KEY_VARIABLE} set.` : KEY_NOT_SET],
121
115
  ['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],
116
+ ...(found.length ? found.map((path) => `skill: found at ${path}`) : NOT_SEEN), ...AFTER_INSTALL],
124
117
  ];
125
118
 
126
119
  export const main = (argv, io = {}) => {
@@ -133,14 +126,15 @@ export const main = (argv, io = {}) => {
133
126
  if (parsed.help) { log(HELP); return 0; }
134
127
  const dir = resolve(io.cwd ?? process.cwd(), parsed.dir);
135
128
  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'));
129
+ const found = skillPaths(dir, homeOf(io)).filter((path) => statIs(path, 'file'));
137
130
  const env = io.env ?? process.env;
138
131
  const set = keySet(env);
139
- const profile = profileOf(env);
140
- const steps = renderSteps(found, set, profile);
132
+ const platform = io.platform ?? process.platform;
133
+ const connect = connectLine(io.toolsDir ?? TOOLS, platform);
134
+ const where = placeFor(io, env, platform);
135
+ const steps = renderSteps(found, set, connect, where);
141
136
  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)
137
+ ? JSON.stringify({ schema: SCHEMA, command: 'jev', dir, text: JEV_TEXT, skill: { found }, key: { set }, connect, where, steps, prompts: PROMPTS }, null, 2)
144
138
  : [...JEV_TEXT, ...steps.flat(), 'WHERE IT PAYS IN THIS WORKFLOW', ...PROMPTS].join('\n'));
145
139
  return 0;
146
140
  };
@@ -31,8 +31,8 @@
31
31
  // read-only git queries. Dependency-free, Node >= 22. No side effects on import (the isDirectRun
32
32
  // idiom).
33
33
 
34
- import { readFileSync, readdirSync, lstatSync, existsSync } from 'node:fs';
35
- import { homedir } from 'node:os';
34
+ import { readFileSync, readdirSync, lstatSync, existsSync, statSync } from 'node:fs';
35
+ import { homedir, hostname } from 'node:os';
36
36
  import { dirname, join, resolve } from 'node:path';
37
37
  import { fileURLToPath } from 'node:url';
38
38
  import {
@@ -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, placeOf } 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,15 +1608,18 @@ 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
- if (keySet(deps.getenv ?? {})) return;
1615
+ const env = deps.getenv ?? {};
1616
+ if (keySet(env)) return;
1616
1617
  if (readAckValue(root, deps, ACKS_JEV_CONNECT_KEY) === JEV_CONNECT_DECLINE) return;
1618
+ const host = deps.jevHost ?? {};
1619
+ const place = placeOf({ env, platform: host.platform, hostname: host.hostname, container: host.container });
1617
1620
  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',
1619
- `HAND-APPLY alternative (instead of the apply, never after it): decline the offer by recording it — ${decline}`);
1621
+ add('jev-connect', WHATS['jev-connect'], `HAND-APPLY: ${connectLine(HERE, host.platform)}`, 'jev-connect',
1622
+ `${place ? `run the apply in ${place}; ` : ''}HAND-APPLY alternative (instead of the apply, never after it): decline the offer by recording it — ${decline}`);
1620
1623
  } catch (err) {
1621
1624
  skip('jev-connect', err);
1622
1625
  }
@@ -1757,6 +1760,10 @@ the consent-gated ack writer into docs/ai/acks.json (never a security key).
1757
1760
  Read-only: never writes, never commits, never runs a subscription CLI. Exit codes: 0 report
1758
1761
  rendered (items or empty); 1 error; 2 usage.`;
1759
1762
 
1763
+ // The host facts of the key offer's place: the platform, the hostname (a throw reads as none), /.dockerenv a regular file.
1764
+ const hostFacts = () => ({ platform: process.platform, hostname: (() => { try { return hostname(); } catch { return ''; } })(),
1765
+ container: (() => { try { return statSync('/.dockerenv').isFile(); } catch { return false; } })() });
1766
+
1760
1767
  export const main = (argv, ctx = {}) => {
1761
1768
  try {
1762
1769
  if (argv.includes('--help') || argv.includes('-h')) return { code: 0, stdout: HELP, stderr: '' };
@@ -1781,7 +1788,7 @@ export const main = (argv, ctx = {}) => {
1781
1788
  }
1782
1789
  })();
1783
1790
  if (st == null || !st.isDirectory()) throw Object.assign(new Error(`--cwd is not a directory: ${cwd}`), { exitCode: 1 });
1784
- const result = buildRecommendations({ cwd, deps: ctx.deps ?? { getenv: process.env } });
1791
+ const result = buildRecommendations({ cwd, deps: ctx.deps ?? { getenv: process.env, jevHost: hostFacts() } });
1785
1792
  if (json) return { code: 0, stdout: JSON.stringify(result, null, 2), stderr: '' };
1786
1793
  return { code: 0, stdout: formatRecommendations(result), stderr: '' };
1787
1794
  } catch (err) {