@sabaiway/agent-workflow-kit 14.19.0 → 14.20.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,44 @@ 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.20.0 — the kit installs the Jev vendor skill: pinned, one verified copy per agent skill root, by one command you run (AD-168)
8
+
9
+ **Once `TYPESAFE_API_KEY` is set, the Recommendations item `jev-skill` offers the vendor's Jev skill for every agent the
10
+ family drives.** Its apply is `HAND-APPLY: node <kit>/tools/jev-skill.mjs --apply --claude-dir <dir>`: after your yes
11
+ in the chat the agent hands the line over and you run it in a terminal of your own. It writes a verified copy of the
12
+ skill into the three agent skill roots: Codex `~/.agents/skills`, Claude Code `<claude dir>/skills`
13
+ (`CLAUDE_CONFIG_DIR` when absolute, else `~/.claude`; the line always names the dir the agent resolved) and
14
+ Antigravity CLI `~/.gemini/config/skills`. Then restart the agent from a new terminal.
15
+
16
+ - **Pinned, never updated silently.** The kit ships the vendor's `SKILL.md` and MIT `LICENSE` at v0.5.7 (commit
17
+ 65a39f39) under `references/vendor/typesafe-ai/`, the skill text as `SKILL.md.pinned` so no agent loads the kit's own
18
+ copy; the command checks both digests before any write and downloads nothing. A copy at an earlier pin is replaced
19
+ only by your own `--apply`.
20
+ - **Your files stay yours.** Each target is judged by content: `current`, `earlier`, `absent` or `foreign`. A foreign
21
+ copy, a link, an extra file or another digest is left untouched and named. The command writes through no link below
22
+ your home or below a Claude dir outside it, makes its temp and aside directories beside a root, never inside one,
23
+ re-checks each target before its rename, and lists a leftover from an interrupted run instead of removing it.
24
+ - **A dry run first.** Without `--apply` the command writes nothing and prints the plan, the leftovers and, when
25
+ something is to install or replace, the apply line; the agent runs only this form, and only when you ask to see the
26
+ plan.
27
+ - **One jev item at a time.** `jev-connect` while the key is not set, `jev-skill` after. A decline is recorded per
28
+ project as `jevSkillAck` in `docs/ai/acks.json` (`ack-write --lane jev-skill`).
29
+ - **The guide.** `/agent-workflow-kit jev` STEP 2 prints the kit route first; the vendor's own routes follow as
30
+ alternatives, then one mark per target.
31
+ - **Delegated runs.** The bundled codex and agy bridge docs say a delegated run reads the skill from its root, never
32
+ inherits the key, and leaves Jev requests to the orchestrator.
33
+ - Recorded debt (Issue-027 (39)-(41)): a cleanup removal that fails after a refused rename ends the run with an
34
+ exception; a home or Claude dir that is not normalized (`/x/claude/.`) is walked at a wrong path; on POSIX a
35
+ backslash in a Claude dir name is split as a separator.
36
+
37
+ ## 14.19.1 — the ADR navigator stamps a cap it honours (AD-167)
38
+
39
+ `references/scripts/archive-decisions.mjs` and its test: byte-identical to memory 8.1.3. `--write-navigator` stamps
40
+ `maxLines` 200 while `docs/ai/adr/log.md` fits and the navigator's own line count past it, so a navigator past 200
41
+ lines no longer fails the docs gate. An upgrade keeps a project's deployed `scripts/archive-decisions.mjs` byte for
42
+ byte: a project already on the ADR store copies `archive-decisions.mjs` and `archive-decisions.test.mjs` from the
43
+ kit's `references/scripts/` into its `scripts/`, then runs `--write-navigator` once. Nothing else in the kit changed.
44
+
7
45
  ## 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
46
 
9
47
  **`jev-connect` no longer prints `connected`.** A saved key reaches the agent only once the agent's environment is read
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.19.0'
6
+ version: '14.20.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
@@ -2,7 +2,7 @@
2
2
  name: antigravity-cli-bridge
3
3
  description: Delegate work to Google's Antigravity CLI (`agy`) — the successor to Gemini CLI — to reach Gemini, Claude, and GPT-OSS models under a Google AI Pro/Ultra subscription from the terminal. Use when the user wants to run a headless `agy` prompt, hand a focused task or second-opinion review to `agy`, install or authenticate Antigravity CLI, check or economise its quota/models, bridge project context into `agy`, set up a second delegated-execution backend beside Codex, or troubleshoot `agy` flags, models, auth, conversations, or its headless behaviour.
4
4
  metadata:
5
- version: '5.7.0'
5
+ version: '5.7.1'
6
6
  ---
7
7
 
8
8
  # antigravity-cli-bridge
@@ -234,6 +234,13 @@ AGY_MODEL="Gemini 3.7 Flash (Low)" agy-run \
234
234
  "Without me pointing you at any file, name a project-specific skill under .agents/skills/ here and cite its path."
235
235
  ```
236
236
 
237
+ **The Jev skill in a delegated run.** A headless `agy-run` loads the vendor's Jev skill from the user root the kit's
238
+ `jev-skill` command fills, `~/.gemini/config/skills` — measured 2026-10-02 on agy 1.2.14: a probe skill under that
239
+ root was listed, and none under `~/.gemini/antigravity-cli/skills`. The run never inherits `TYPESAFE_API_KEY`: the
240
+ wrapper unsets every `*_API_KEY` before agy starts. Jev requests are the orchestrator's own, made in the user's
241
+ environment, and their answers are passed into a brief as text; whether a headless agy command reaches a network
242
+ host is governed by the user's agy permissions and sandbox settings, not by the bridge.
243
+
237
244
  ## How the main agent drives `agy` efficiently
238
245
 
239
246
  See [`references/driving-agy.md`](references/driving-agy.md) for the full playbook (delegation
@@ -343,7 +343,7 @@ DEFAULT_AGY_REVIEW_MODEL="Gemini 3.8 Flash (High)"
343
343
  # Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
344
344
  # version (drift-guarded by agy-review.test.mjs against capability.json).
345
345
  AW_RECEIPT_BACKEND="agy"
346
- AW_BRIDGE_VERSION="5.7.0" # aw-version-anchor
346
+ AW_BRIDGE_VERSION="5.7.1" # aw-version-anchor
347
347
  # `-` not `:-` so an EXPLICIT empty AGY_MODEL= survives (drop --model, use settings.json — agy.sh:52).
348
348
  AGY_MODEL="${AGY_MODEL-$DEFAULT_AGY_REVIEW_MODEL}"
349
349
  # D5 control-byte screen — IMMEDIATELY after resolution, BEFORE the off-frontier advisory (or any
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "antigravity-cli-bridge",
5
5
  "kind": "execution-backend",
6
- "version": "5.7.0",
6
+ "version": "5.7.1",
7
7
  "provides": ["review", "probe"],
8
8
  "posture": { "model": "Gemini 3.8 Flash (High)" },
9
9
  "roles": {
@@ -2,7 +2,7 @@
2
2
  name: codex-cli-bridge
3
3
  description: Delegate work to the OpenAI Codex CLI (`codex`) under a ChatGPT subscription — run plan/instruction EXECUTION in a sandboxed workspace, or get a read-only ADVISORY review of a plan or working-tree diff — as a second delegated-execution backend beside Antigravity. Use when the user wants to hand a bounded coding task or plan to `codex exec`, get a second-opinion review from codex, install or authenticate Codex CLI, understand its sandbox/network/approval policy, drive codex efficiently from the main agent (exec vs review, resume, the commit boundary), bridge project context (`AGENTS.md`) into codex, or troubleshoot codex flags, models, auth, or its no-TTY headless behaviour.
4
4
  metadata:
5
- version: '3.8.0'
5
+ version: '3.8.1'
6
6
  ---
7
7
 
8
8
  # codex-cli-bridge
@@ -236,6 +236,12 @@ avoided). And the execution contract tells codex: if the project declares **no**
236
236
  set, **STOP and report** rather than invent checks. Pass `--skip-git-repo-check` to codex only when
237
237
  you truly mean it.
238
238
 
239
+ **The Jev skill in a delegated run.** A `codex-exec` run loads the vendor's Jev skill from the user root the kit's
240
+ `jev-skill` command fills, `~/.agents/skills` — measured 2026-10-02: a run under `--ignore-user-config` listed the
241
+ skills of that root. The run never inherits `TYPESAFE_API_KEY`: the wrappers unset every `*_API_KEY` before codex
242
+ starts. codex exec has no network, so Jev requests are the orchestrator's own, made in the user's environment, and
243
+ their answers are passed into a brief as text.
244
+
239
245
  ## How the main agent drives `codex` efficiently
240
246
 
241
247
  See [`references/driving-codex.md`](references/driving-codex.md) for the full playbook. Essentials:
@@ -373,7 +373,7 @@ CHATGPT_LOGIN_GUARD="Logged in using ChatGPT"
373
373
  # receipt this wrapper mints; scripts/release/version-sync.mjs bumps it under the one-anchor-per-file
374
374
  # rule, so a release can never leave it behind (the AD-053 drift class).
375
375
  AW_RECEIPT_BACKEND="codex"
376
- AW_BRIDGE_VERSION="3.8.0" # aw-version-anchor
376
+ AW_BRIDGE_VERSION="3.8.1" # aw-version-anchor
377
377
  # The kill grace handed to timeout(1) as --kill-after, and recorded in the receipt as killGraceS:
378
378
  # ONE constant, so the number the ledger checks against the dispatch deadline is the number the run
379
379
  # actually applied.
@@ -284,7 +284,7 @@ DEFAULT_CODEX_EFFORT="high"
284
284
  # Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
285
285
  # version (drift-guarded by codex-review.test.mjs against capability.json).
286
286
  AW_RECEIPT_BACKEND="codex"
287
- AW_BRIDGE_VERSION="3.8.0" # aw-version-anchor
287
+ AW_BRIDGE_VERSION="3.8.1" # aw-version-anchor
288
288
  CODEX_MODEL="${CODEX_MODEL:-$DEFAULT_CODEX_MODEL}"
289
289
  CODEX_EFFORT="${CODEX_EFFORT:-$DEFAULT_CODEX_EFFORT}"
290
290
  # Generous hard cap for a slow review at the pinned effort (subscription latency varies).
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "codex-cli-bridge",
5
5
  "kind": "execution-backend",
6
- "version": "3.8.0",
6
+ "version": "3.8.1",
7
7
  "posture": { "model": "gpt-6-astra", "effort": "high", "tier": null },
8
8
  "provides": ["execute", "review"],
9
9
  "roles": {
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.19.0",
6
+ "version": "14.20.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.19.0",
3
+ "version": "14.20.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",
@@ -1,14 +1,15 @@
1
1
  ### Mode: jev
2
2
 
3
3
  <!-- opt-in-capability: jev-connect -->
4
+ <!-- opt-in-capability: jev-skill -->
4
5
 
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
+ Read-only. The Jev guide: what Jev (TypeSafe) is, why it pays in this workflow, what it is not, the two steps — a set key and the vendor skill installed by the kit, pinned — and three prompts where it pays. The guide writes no file, opens no connection, never prints the key's value and never commits.
6
7
 
7
8
  Run `node ${CLAUDE_SKILL_DIR}/tools/jev-guide.mjs --dir <project> --json`.
8
9
 
9
10
  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
11
 
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
+ The connect line and the skill's apply line are the user's: hand each over as printed and never run it — the connect command refuses without a terminal, and a terminal is not consent for either. Only when the user asks to see the plan, run the skill command without `--apply` yourself: a read-only dry run that writes nothing. The vendor's 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
13
 
13
14
  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
15
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md
6
6
 
7
- The **read-only deployment advisor** — the deterministic section every `upgrade` run ends with, also invocable on its own. It computes what in THIS deployment is configured sub-optimally (allowlist not seeded, autonomy render drifted, sandbox unavailable, gates undeclared, a declared gate matrix that verifies nothing, bridge friction, sandbox-mask clutter, an unacknowledged sandbox recipe, a configured `subagent` carrier whose executor vehicle cannot carry it, a difference from the reference profile the project has not declined, Jev not connected on this host) and renders **verdict-first**: one composed verdict line, then each item as **{severity · what is sub-optimal · the benefit in ONE plain line · an optional `recipe:` line (the `sandbox-lane` live recipe — egress hosts + resolved writable dirs — the `worktrees-dir` hand-apply-first grant advice, or the `agents` / `executor-vehicle` hidden-mode reconcile follow-up) · the exact consent-gated apply one-liner}**. The tool computes deterministic English DATA; **you PRESENT the section in the user's conversational language** — every fact, count and item from the tool, nothing added or dropped; commands, paths, hosts and rule strings stay **byte-exact**; show the raw tool block on request (the AD-032 report-contract lane — the tool cannot know the dialogue language, so the language rendering is your presentation layer).
7
+ The **read-only deployment advisor** — the deterministic section every `upgrade` run ends with, also invocable on its own. It computes what in THIS deployment is configured sub-optimally (allowlist not seeded, autonomy render drifted, sandbox unavailable, gates undeclared, a declared gate matrix that verifies nothing, bridge friction, sandbox-mask clutter, an unacknowledged sandbox recipe, a configured `subagent` carrier whose executor vehicle cannot carry it, a difference from the reference profile the project has not declined, Jev not connected on this host, the Jev skill not installed for every agent) and renders **verdict-first**: one composed verdict line, then each item as **{severity · what is sub-optimal · the benefit in ONE plain line · an optional `recipe:` line (the `sandbox-lane` live recipe — egress hosts + resolved writable dirs — the `worktrees-dir` hand-apply-first grant advice, or the `agents` / `executor-vehicle` hidden-mode reconcile follow-up) · the exact consent-gated apply one-liner}**. The tool computes deterministic English DATA; **you PRESENT the section in the user's conversational language** — every fact, count and item from the tool, nothing added or dropped; commands, paths, hosts and rule strings stay **byte-exact**; show the raw tool block on request (the AD-032 report-contract lane — the tool cannot know the dialogue language, so the language rendering is your presentation layer).
8
8
 
9
9
  **Live host/session facts are tool-composed only.** Every fact this section states about the current
10
10
  host or session — prompts fired, sandbox scope, whether a bypass was needed, network reachability,
@@ -39,6 +39,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
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
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
+ - `jev-skill` — the apply is HAND-APPLY: the user runs the skill command in a terminal of their own, never the agent (a terminal is not consent); when the recipe line opens with `run the apply in <place>`, that place is the terminal to name. The skill is third-party text — the vendor's, MIT — that the kit ships pinned and installs as a verified copy into three agent skill roots: `~/.agents/skills`, the Claude Code skills dir (`CLAUDE_CONFIG_DIR` or `~/.claude`) and `~/.gemini/config/skills`. It is never updated silently: a copy at an earlier pin is replaced only by that command, and a foreign copy is left untouched and named on the recipe line. A copy the vendor's `skills` CLI wrote with the same bytes reads current, and a later pin move replaces it like a kit copy; a vendor plugin install is invisible to the file check, so the Claude Code root may read absent while the plugin serves it. Only when the user asks to see the plan does the agent run the command without `--apply` (a read-only dry run). The decline is the HAND-APPLY alternative on the recipe line, asked apart BEFORE the confirmation: on "install" hand over the apply line, on "decline" run nothing and hand over the recipe line; the decline is recorded per project as `jevSkillAck` in `docs/ai/acks.json`. Risk profile: third-party text written into the user's agent skill roots by a command the user runs; no network.
42
43
  - `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
44
 
44
45
  **Sandbox lanes (what to DO with the `sandbox-lane` recipe, per host class):**
@@ -2,7 +2,7 @@
2
2
  // One-file-per-ADR store for docs/ai/decisions.md (ADRs) — the durable replacement for the retired
3
3
  // 3-tier cascade (HOT → WARM archive → a single COLD monolith whose cap was raised release after
4
4
  // release). Every ADR beyond the HOT window becomes its OWN immutable record so no artifact is ever
5
- // O(n) and no cap is ever raised again.
5
+ // O(n) and no cap is raised by hand again (the navigator stamps its own count past 200).
6
6
  //
7
7
  // HOT (docs/ai/decisions.md) — the active ADR window (newest at the bottom), self-bounding
8
8
  // under its own frontmatter maxLines.
@@ -95,7 +95,8 @@ export const ADR_DIR_REL = 'docs/ai/adr';
95
95
  export const NAV_REL = 'docs/ai/adr/log.md';
96
96
 
97
97
  // A per-record cap (generous vs the largest real ADR ~74 lines; a body over it is a genuine smell) and
98
- // the navigator cap (bounded — exceeding it is the future plateau-shard trigger, Decision 8).
98
+ // the navigator's FLOOR: the navigator stamps 200 while it fits and its own line count past it, since
99
+ // nothing rotates a governing row out (docs/ai/specs/memory/adr-navigator.md).
99
100
  export const RECORD_CAP = 400;
100
101
  export const NAV_MAXLINES = 200;
101
102
  // How many most-recent ids the navigator's recent window carries (status-annotated, incl. supersessions).
@@ -701,8 +702,8 @@ export const computeGoverningIds = (entries) => {
701
702
  return new Set(entries.filter((e) => e.status === IN_FORCE_STATUS && !superseded.has(e.id)).map((e) => e.id));
702
703
  };
703
704
 
704
- const NAV_FRONTMATTER = (today) =>
705
- `---\ntype: reference\nlastUpdated: ${today}\nscope: permanent\nstaleAfter: never\nowner: none\nmaxLines: ${NAV_MAXLINES}\n---\n`;
705
+ const NAV_FRONTMATTER = (today, cap) =>
706
+ `---\ntype: reference\nlastUpdated: ${today}\nscope: permanent\nstaleAfter: never\nowner: none\nmaxLines: ${cap}\n---\n`;
706
707
 
707
708
  // Build docs/ai/adr/log.md from the WHOLE corpus (HOT entries ∪ adr/ records). Governing heads
708
709
  // (accepted ∧ not-superseded) sorted newest-first; superseded ADRs drop OUT (still reachable by
@@ -744,7 +745,9 @@ export const buildNavigator = (corpus, today) => {
744
745
  '',
745
746
  ...recent,
746
747
  ].join('\n');
747
- return `${NAV_FRONTMATTER(today)}\n${body}\n`;
748
+ // The cap sits on one line whatever its value, so the count with the floor is the final count.
749
+ const count = lineCountOf(`${NAV_FRONTMATTER(today, NAV_MAXLINES)}\n${body}\n`);
750
+ return `${NAV_FRONTMATTER(today, Math.max(NAV_MAXLINES, count))}\n${body}\n`;
748
751
  };
749
752
 
750
753
  // ── snapshot (durable, pre-delete — Decision 5) ─────────────────────────────────────────
@@ -800,6 +800,47 @@ describe('1.5 navigator — governing heads (computed), superseded drop out but
800
800
  });
801
801
  });
802
802
 
803
+ // ── the navigator's stamped cap (docs/ai/specs/memory/adr-navigator.md) ─────────────────
804
+ //
805
+ // The frame is 38 lines (15 ADRs or more), so 162 governing heads make exactly 200, 163 make 201 and 170 make 208.
806
+
807
+ const capOf = (text) => Number(text.match(/^maxLines: (\d+)$/m)[1]);
808
+ const corpusOf = (n) =>
809
+ Array.from({ length: n }, (_, i) => {
810
+ const id = String(i + 1).padStart(3, '0');
811
+ return { id, idNum: i + 1, title: `T${id}`, status: 'accepted', supersedes: [], supersededBy: [], fileName: `AD-${id}-t.md` };
812
+ });
813
+
814
+ describe('the navigator stamps a cap it honours', () => {
815
+ it('200 while it fits, its own line count past it', () => {
816
+ // spec:adr-navigator/S1
817
+ for (const heads of [2, 162]) {
818
+ const nav = buildNavigator(corpusOf(heads), '2026-07-09');
819
+ assert.equal(capOf(nav), 200, `${heads} heads keep the 200 floor`);
820
+ }
821
+ assert.equal(lineCountOf(buildNavigator(corpusOf(162), '2026-07-09')), 200);
822
+ // 165 entries, two superseded: 163 heads, 201 lines — a stamp from the entry count would read 203.
823
+ const corpus = corpusOf(165).map((e, i) => (i < 2 ? { ...e, supersededBy: ['003'] } : e));
824
+ const big = buildNavigator(corpus, '2026-07-09');
825
+ assert.equal(lineCountOf(big), 201);
826
+ assert.equal(capOf(big), 201, 'past the floor the stamp is the file\'s own count');
827
+ });
828
+
829
+ it('a written navigator past 200 lines fits its cap, --check is green, a hand-edited cap is stale', () => {
830
+ // spec:adr-navigator/S2
831
+ const root = makeRoot();
832
+ seedMigrated(root, { hotIds: ['901'], storeIds: corpusOf(169).map((e) => e.id) });
833
+ const nav = readFileSync(join(root, NAV_REL), 'utf8');
834
+ assert.ok(lineCountOf(nav) > 200, 'the fixture is past the floor');
835
+ assert.ok(lineCountOf(nav) <= capOf(nav), 'the docs gate accepts the written navigator');
836
+ assert.equal(run(['--check', '--today=2026-07-09'], root).code, 0);
837
+ writeFileSync(join(root, NAV_REL), nav.replace(/^maxLines: \d+$/m, 'maxLines: 999'));
838
+ const stale = run(['--check', '--today=2026-07-09'], root);
839
+ assert.equal(stale.code, 1);
840
+ assert.match(stale.errText, /log\.md is stale/);
841
+ });
842
+ });
843
+
803
844
  // ── item (h) degrade branches on the REAL default regenerator (no injection) ───────────
804
845
 
805
846
  describe('defaultRegenerateIndex — loud-degrade branches', () => {
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TypeSafe AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,149 @@
1
+ ---
2
+ name: typesafe-ai
3
+ license: MIT
4
+ description: >
5
+ Build AI-powered software with TypeSafe: small units of AI intelligence you
6
+ can use like programming primitives. Its System One models, including Jev,
7
+ turn natural language and application state into typed judgments and
8
+ probabilities that code can combine. Use when a feature needs programmable
9
+ common sense, when brainstorming what AI could make possible in an app, or
10
+ when an LLM prompt-and-parse step could become a structured decision.
11
+ Applications include routing, ranking, extraction, verification, and
12
+ interactive experiences; these are starting points, not the limits.
13
+ Read live docs and cookbooks to find useful patterns and discover new combinations.
14
+ ---
15
+
16
+ # Build with TypeSafe
17
+
18
+ TypeSafe makes units of AI intelligence usable like programming primitives: small
19
+ judgments you can compose into larger capabilities. Its **System One models** return
20
+ fast, focused judgments that software can consume directly. **Jev** is TypeSafe's
21
+ flagship and first System One model. It understands natural language and returns
22
+ typed answers and probabilities rather
23
+ than generating text or reasoning explanations. Code owns the workflow; the model
24
+ supplies programmable common sense where ordinary code needs semantic understanding.
25
+
26
+ ## Read the live docs
27
+
28
+ **The live TypeSafe docs are the source of truth. Read them as part of the task.**
29
+ This skill gives direction; the docs carry current concepts, prompting guidance,
30
+ API contracts, SDK usage, models, limits, and worked examples.
31
+
32
+ - Start with the [documentation index](https://docs.typesafe.ai/llms.txt) to discover
33
+ relevant pages and cookbooks. Use targeted reads rather than loading the entire site.
34
+ - Mintlify serves Markdown by appending `.md` to a page path, for example
35
+ [how to build with TypeSafe](https://docs.typesafe.ai/concepts/how-to-build-with-system-one.md).
36
+ Follow links from the index; convert extensionless documentation page links to
37
+ `.md` when useful. Resolve relative links against `https://docs.typesafe.ai`.
38
+ - Before writing an integration, read the current API or chosen SDK page and the
39
+ question guidance relevant to the design. For a new workflow, also inspect the
40
+ closest cookbook: it often shows a better decomposition than a generic classifier.
41
+ - If the index is unavailable, use the direct links below or the site's navigation.
42
+ If Markdown fetching fails, try the normal page. If live access is unavailable,
43
+ use available local docs or installed SDK types, state that limitation, and avoid
44
+ inventing version-dependent details.
45
+
46
+ | Task | Start here; follow the relevant details |
47
+ | --- | --- |
48
+ | Understand the programming model | [System One](https://docs.typesafe.ai/concepts/system-one.md), [building guide](https://docs.typesafe.ai/concepts/how-to-build-with-system-one.md) |
49
+ | Explore what to build | [Use-case map](https://docs.typesafe.ai/concepts/use-case-map.md), then relevant cookbooks from the index |
50
+ | Prepare inputs and questions | [State](https://docs.typesafe.ai/concepts/state.md), [primitives](https://docs.typesafe.ai/primitives.md), then the chosen primitive's page |
51
+ | Decide how to handle uncertainty | [Confidence](https://docs.typesafe.ai/confidence.md) |
52
+ | Write API code | [HTTP API](https://docs.typesafe.ai/api.md), [Python SDK](https://docs.typesafe.ai/sdk/python.md), or [JavaScript SDK](https://docs.typesafe.ai/sdk/javascript.md) |
53
+ | Update an older integration | [Migration guide](https://docs.typesafe.ai/migrating-to-v1.md) and the installed SDK's current reference |
54
+
55
+ ## Find the useful shape
56
+
57
+ Start from the behavior the user wants: what will the application show, select,
58
+ change, or hand off? Work backward to the judgments it needs. Keep known rules,
59
+ calculations, exact lookups, and execution in code. Preserve the user's chosen stack
60
+ and scope; add TypeSafe where semantic understanding helps.
61
+
62
+ When brainstorming or choosing an architecture, consider more than classification.
63
+ The patterns below are starting points: combine primitives around the user's goal,
64
+ including ideas that do not fit an established recipe.
65
+
66
+ - **Route and fill known arguments.** A request can select a handler and its typed
67
+ parameters. Ask useful branch-specific questions up front and consume only the
68
+ relevant answers. Explore [function calling](https://docs.typesafe.ai/cookbooks/function_calling.md)
69
+ and [speculative fan-out](https://docs.typesafe.ai/patterns/fan-out.md).
70
+ - **Select instead of generate.** Find candidate values or source spans in code,
71
+ use a judgment to select the intended one, then copy or normalize it. Code can
72
+ also assemble source text into a formatted document or reading guide. Explore
73
+ [value extraction](https://docs.typesafe.ai/cookbooks/pre_parsed_value_extraction_cookbook.md)
74
+ and [structure recovery](https://docs.typesafe.ai/cookbooks/autoformat.md).
75
+ - **Find and judge evidence.** Retrieve candidates, compare their relevance to a
76
+ query, and select useful context. Explore [reranking](https://docs.typesafe.ai/cookbooks/rerank_typesafe.md)
77
+ and [hierarchical classification](https://docs.typesafe.ai/cookbooks/hierarchical_classification.md).
78
+ - **Turn judgments into reusable data.** Score dimensions once, then let code or
79
+ user controls change weights, thresholds, rankings, and views. With labeled
80
+ outcomes, those signals can become classical ML features. Explore
81
+ [composite scoring](https://docs.typesafe.ai/patterns/composite-scoring.md) and
82
+ [feature discovery](https://docs.typesafe.ai/cookbooks/autoresearch_feature_discovery.md).
83
+ - **Verify and escalate.** Check specific claims or fields against their evidence;
84
+ send uncertain or failing cases to a person or reasoning model. Explore
85
+ [citation checks](https://docs.typesafe.ai/cookbooks/citation_check.md) and
86
+ [extraction cascades](https://docs.typesafe.ai/cookbooks/sde_cascade.md).
87
+ - **Respond to changing state.** Code can retain goals and observations while fresh
88
+ judgments guide the next bounded step. Keep inferred state distinct from observed
89
+ facts, and check freshness before applying a result to a changed situation.
90
+
91
+ For open-ended requests, offer the few directions that best serve the user's goal
92
+ and recommend a starting point. For a concrete request, choose the relevant pattern
93
+ and build; a brainstorm is not a mandatory detour.
94
+
95
+ ## Design the judgments
96
+
97
+ Choose by what the answer means, then read the relevant primitive page:
98
+
99
+ | Need | Primitive | Important distinction |
100
+ | --- | --- | --- |
101
+ | One of a defined set | [Choice](https://docs.typesafe.ai/primitives/choice.md) | Picks one option; its distribution compares competing options |
102
+ | Whether a condition holds | [Noul](https://docs.typesafe.ai/primitives/noul.md) | Probability of yes; no separate confidence; use one per label when several may apply |
103
+ | Degree along a described dimension | [Score](https://docs.typesafe.ai/primitives/score.md) | Probability-weighted position on ordered levels; use comparable per-item Scores for graded ranking |
104
+
105
+ Give each question enough relevant **state** to answer: source text, identities,
106
+ relationships, policies, and current facts. Prefer named JSON fields when context
107
+ has several parts. Put the judgment in **instructions** and define its possible
108
+ answers in **criteria**. Question IDs are for code and are not sent to the model;
109
+ include complete meaning in the question. Reference nested state with backticked
110
+ paths such as `ticket.messages[0].text`.
111
+
112
+ Ask one narrow, coherent judgment per question. Split independently useful dimensions,
113
+ without destroying the relationship being judged. A bounded action selection or
114
+ contextual interpretation is valid; atomic does not mean literal fact extraction
115
+ or a one-sentence limit. Strings work for simple questions. Use structured objects
116
+ or arrays when definitions, contrasts, exclusions, or examples clarify instructions
117
+ or criteria. Score levels must describe concrete situations and stand on their own.
118
+
119
+ Keep the needed answers available. Include a no-match outcome when nothing may fit;
120
+ use a separate presence judgment when it is independently useful. For source-value
121
+ selection, check candidate coverage: the model cannot choose an omitted value.
122
+
123
+ ## Compose and verify
124
+
125
+ **Ask independent questions over the same state together**, including useful
126
+ speculative questions. They run in parallel and cannot see one another's answers.
127
+ State each speculative premise explicitly; code consumes the applicable answers.
128
+ A second request is warranted when an earlier answer is needed to fetch evidence,
129
+ construct new state, or determine the next options. Extra questions still use tokens;
130
+ measure actual request budgets, cost, and end-to-end latency.
131
+
132
+ Use probabilities and confidence to guide behavior, with thresholds evaluated on
133
+ the user's data and consequences. Choice/Score confidence summarizes distribution
134
+ concentration, not overall workflow correctness or permission to act. A Noul near
135
+ 0.5 means similar probability for yes and no, not medium intensity. Several
136
+ acceptable alternatives can also spread probability; low confidence need not
137
+ invalidate a harmless preference choice. Ignore uncertainty on unused branches.
138
+
139
+ Keep policy explicit and raw judgments reusable. Weighted scores suit compensating
140
+ preferences; an “any serious violation” rule needs separate conditions. Changing a
141
+ weight or display filter need not rerun inference when evidence and question meanings
142
+ are unchanged. Typed output guarantees the interface, not truth. System One models
143
+ are trained for calibrated decisions; validate their performance in the target domain.
144
+
145
+ Test representative cases and the resulting application behavior. For failures,
146
+ inspect the exact state, questions, candidates, answers, composition, and observed
147
+ outcome. Separate missing evidence, model errors, code errors, and service failures.
148
+ Treat cookbook thresholds and demo results as examples to evaluate, not universal
149
+ rules or permanent model limitations. Keep API credentials server-side in web apps.
@@ -15,6 +15,7 @@ export const ACKS_COVERAGE_DOMAIN_KEY = 'coverageDomainAck';
15
15
  export const ACKS_SOURCE_SIZE_COPY_KEY = 'sourceSizeCopyAck';
16
16
  export const ACKS_SPEC_ADOPTION_KEY = 'specAdoptionAck';
17
17
  export const ACKS_JEV_CONNECT_KEY = 'jevConnectAck';
18
+ export const ACKS_JEV_SKILL_KEY = 'jevSkillAck';
18
19
 
19
20
  // The CLOSED-WORLD ack-lane registry: the lane name an advisor item renders on the writer's command line ->
20
21
  // the store key that writer sets. A lane the registry does not name is a usage refusal at the writer.
@@ -25,6 +26,7 @@ export const ACK_LANES = Object.freeze({
25
26
  'source-size-copy': ACKS_SOURCE_SIZE_COPY_KEY,
26
27
  'spec-adoption': ACKS_SPEC_ADOPTION_KEY,
27
28
  'jev-connect': ACKS_JEV_CONNECT_KEY,
29
+ 'jev-skill': ACKS_JEV_SKILL_KEY,
28
30
  });
29
31
 
30
32
  export const FINGERPRINT_LENGTH = 16;
@@ -1,8 +1,10 @@
1
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';
2
+ // The one leaf the Jev surfaces share: the key rule, the connect line, the vendor facts the connect
3
+ // command's one request uses, and the vendor skill's pin, targets, state rule and apply line. No side
4
+ // effect, no import beyond node:path and node:crypto — the read-only guide imports it under its closed
5
+ // import graph, the advisor and the two commands beside it; file primitives arrive through `io`.
6
+ import { createHash } from 'node:crypto';
7
+ import { join, posix, win32 } from 'node:path';
6
8
 
7
9
  export const KEY_VARIABLE = 'TYPESAFE_API_KEY';
8
10
 
@@ -56,3 +58,98 @@ export const JEV_ERROR_MEANINGS = Object.freeze({
56
58
  429: 'You have exceeded your rate limit. Back off and retry after a short delay.',
57
59
  529: 'TypeSafe is temporarily overloaded. Retry after a short delay.',
58
60
  });
61
+
62
+ // ── the vendor skill (part jev-skill) ─────────────────────────────────────────────────────────────
63
+ // Each target file name -> the name the kit ships it under. The shipped SKILL.md is renamed so no agent
64
+ // walking the kit's own tree (a skill folder itself) loads the vendor's text before the user's --apply.
65
+ export const SKILL_FILES = Object.freeze({ 'SKILL.md': 'SKILL.md.pinned', LICENSE: 'LICENSE' });
66
+ // Newest first; a release that moves the pin puts the new entry first and removes none.
67
+ export const SKILL_PINS = Object.freeze([Object.freeze({
68
+ tag: 'v0.5.7',
69
+ commit: '65a39f393687675ce170e6094757de20370365b9',
70
+ digests: Object.freeze({
71
+ 'SKILL.md': '71ea90d7906c6554c4f4c460ef7361b2d26f59116ccdae986dc6d997b9389f52',
72
+ LICENSE: '835f233f1d6ed84a9b9a351aba0689b47644a4137d6316911fc7957bde523b02',
73
+ }),
74
+ })]);
75
+ export const AFTER_INSTALL = 'After an install, quit the agent and start it again from a new terminal.';
76
+ export const NO_APPLY_LINE = 'the apply line cannot be printed here: a path holds a control character';
77
+
78
+ const isControl = (char) => char.charCodeAt(0) < 0x20 || char.charCodeAt(0) === 0x7f;
79
+ // Display only: every path, entry name and reason a surface prints; never a command line.
80
+ export const printable = (text) => [...String(text)].map((char) => (isControl(char) ? '?' : char)).join('');
81
+
82
+ // The Claude Code dir the agent's environment names: an absolute CLAUDE_CONFIG_DIR for the platform, else <home>/.claude.
83
+ export const claudeDirOf = (env = {}, home = '', platform) => {
84
+ const value = env.CLAUDE_CONFIG_DIR;
85
+ const set = typeof value === 'string' && value !== '';
86
+ if (set && (platform === 'win32' ? win32 : posix).isAbsolute(value)) return { dir: value, fromEnv: true, note: '' };
87
+ const dir = home === '' ? '' : join(home, '.claude');
88
+ return { dir, fromEnv: false,
89
+ note: set ? printable(`CLAUDE_CONFIG_DIR is set to ${value}, not an absolute path: the Claude Code target is ${dir}`) : '' };
90
+ };
91
+
92
+ // The host separator, read off the host's own join.
93
+ const SEP = join('a', 'b').slice(1, -1);
94
+ const within = (path, dir) => path.startsWith(dir.endsWith(SEP) ? dir : `${dir}${SEP}`);
95
+ // The three targets in their order; a later record on an earlier one's path merges into it, and a later
96
+ // record inside an earlier one's path, or holding it, carries `overlaps`.
97
+ export const skillTargets = ({ home = '', claudeDir = '' } = {}) => {
98
+ if (home === '') return [];
99
+ const records = [
100
+ { agent: 'Codex', base: home, root: join(home, '.agents', 'skills') },
101
+ { agent: 'Claude Code', base: within(claudeDir, home) ? home : claudeDir, root: join(claudeDir, 'skills') },
102
+ { agent: 'Antigravity CLI', base: home, root: join(home, '.gemini', 'config', 'skills') },
103
+ ].map((record) => ({ ...record, path: join(record.root, 'typesafe-ai') }));
104
+ return records.reduce((kept, record) => {
105
+ const same = kept.find(({ path }) => path === record.path);
106
+ if (same) return kept.map((item) => (item === same ? { ...item, agent: `${item.agent} and ${record.agent}` } : item));
107
+ const other = kept.find(({ path }) => within(record.path, path) || within(path, record.path));
108
+ return [...kept, other ? { ...record, overlaps: other.path } : record];
109
+ }, []);
110
+ };
111
+
112
+ const digestOf = (bytes) => createHash('sha256').update(bytes).digest('hex');
113
+ const codeOf = (error) => error?.code ?? error?.message ?? String(error);
114
+ // The state of one target, by content and never by name; pure over io.lstat, io.readdir, io.readFile and io.pins.
115
+ export const skillState = (target, io) => {
116
+ const foreign = (reason) => ({ state: 'foreign', tag: null, reason });
117
+ if (target.overlaps) return foreign(`overlaps ${target.overlaps}`);
118
+ const pins = io.pins ?? SKILL_PINS;
119
+ try {
120
+ const names = target.path.slice(target.base.length).split(SEP).filter(Boolean);
121
+ let at = target.base;
122
+ for (const [index, name] of names.entries()) {
123
+ at = join(at, name);
124
+ let stat;
125
+ try { stat = io.lstat(at); } catch (error) {
126
+ if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') return { state: 'absent', tag: null, reason: null };
127
+ return foreign(codeOf(error));
128
+ }
129
+ if (stat.isSymbolicLink()) return foreign(`a link at ${at}`);
130
+ if (!stat.isDirectory()) return foreign(index === names.length - 1 ? 'not a directory' : `${at} is not a directory`);
131
+ }
132
+ const entries = [...io.readdir(target.path)].map(String).sort();
133
+ if (entries.join('/') !== 'LICENSE/SKILL.md') return foreign(`holds ${entries.join(', ') || 'no entry'}`);
134
+ for (const name of entries) {
135
+ if (!io.lstat(join(target.path, name)).isFile()) return foreign(`${name} is not a regular file`);
136
+ }
137
+ const found = Object.fromEntries(entries.map((name) => [name, digestOf(io.readFile(join(target.path, name)))]));
138
+ const pinAt = pins.findIndex(({ digests }) => Object.keys(SKILL_FILES).every((name) => digests[name] === found[name]));
139
+ if (pinAt === 0) return { state: 'current', tag: pins[0].tag, reason: null };
140
+ if (pinAt > 0) return { state: 'earlier', tag: pins[pinAt].tag, reason: null };
141
+ return foreign('matches no pin');
142
+ } catch (error) {
143
+ return foreign(codeOf(error));
144
+ }
145
+ };
146
+
147
+ // The skill command's apply line, always with the Claude Code dir its caller resolved; '' when a path holds a control byte.
148
+ export const skillLine = (toolsDir, platform, claudeDir) => {
149
+ if ([...`${toolsDir}${claudeDir}`].some(isControl)) return '';
150
+ if (platform === 'win32') {
151
+ const quoted = (value) => `'${value.replace(/'/g, "''")}'`;
152
+ return `node ${quoted(`${toolsDir}\\jev-skill.mjs`)} --apply --claude-dir ${quoted(claudeDir)}`;
153
+ }
154
+ return `node ${quoteArg(join(toolsDir, 'jev-skill.mjs'))} --apply --claude-dir ${quoteArg(claudeDir)}`;
155
+ };
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  // spec:jev-guide — docs/ai/specs/kit/jev-guide/index.md
3
- import { statSync } from 'node:fs';
3
+ import { lstatSync, readFileSync, readdirSync, statSync } from 'node:fs';
4
4
  import { homedir, hostname } from 'node:os';
5
5
  import { basename, dirname, join, resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { fail } from '../references/scripts/markdown-blocks.mjs';
8
8
  import { isDirectRun } from './direct-run.mjs';
9
- import { KEY_VARIABLE, RESTART_STEP, connectLine, keySet, placeOf } from './jev-facts.mjs';
9
+ import { AFTER_INSTALL, KEY_VARIABLE, NO_APPLY_LINE, RESTART_STEP, SKILL_PINS, claudeDirOf, connectLine, keySet, placeOf, printable, skillLine, skillState, skillTargets } from './jev-facts.mjs';
10
10
 
11
11
  const SCHEMA = 1;
12
12
  const TOOLS = dirname(fileURLToPath(import.meta.url));
13
13
  const SKILL_FILE = join('typesafe-ai', 'SKILL.md');
14
- const HELP = `jev — read-only: what Jev (TypeSafe) is, why it pays here, and the two steps to a set key.
14
+ const HELP = `jev — read-only: what Jev (TypeSafe) is, why it pays here, and the two steps: the key and the vendor skill.
15
15
 
16
16
  Usage:
17
17
  node jev-guide.mjs [--dir PROJECT] [--json]
@@ -22,7 +22,7 @@ Usage:
22
22
 
23
23
  Exit codes: 0 rendered; 1 nothing rendered (a --dir that is not a directory); 2 usage.
24
24
 
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.`;
25
+ Reads only: the --dir, the home directory, the key variable's presence, CLAUDE_CONFIG_DIR, five skill paths, the three skill targets, and for the place WSL_DISTRO_NAME, SSH_CONNECTION, REMOTE_CONTAINERS, CODESPACES, the platform, the hostname and /.dockerenv; no network.`;
26
26
 
27
27
  export const JEV_TEXT = Object.freeze([
28
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.',
@@ -49,8 +49,10 @@ const KEY_CONNECT = 'Connect it from a terminal of your own, never through the a
49
49
  const KEY_RESTART = `Then ${RESTART_STEP}. Run /agent-workflow-kit jev again: the key mark should read set.`;
50
50
  const KEY_NEVER = 'Never paste the key into the chat or into a project file.';
51
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.`;
52
+ const SKILL_OPTIONAL = 'Optional, for your own code and prompts — the vendor skill. The kit installs it pinned, one verified copy per agent skill root (Codex, Claude Code, Antigravity CLI); after your yes in the chat, run this in a terminal of your own:';
53
+ const NO_HOME = 'no home directory found: the kit\'s install needs one';
52
54
  const INSTALL = [
53
- 'Optional, for your own code and prompts — the vendor skill:',
55
+ 'Or the vendor\'s own routes:',
54
56
  'Claude Code:',
55
57
  'claude plugin marketplace add typesafe-ai/skills',
56
58
  'claude plugin install typesafe@typesafe-ai',
@@ -63,8 +65,8 @@ const NOT_SEEN = [
63
65
  'in a Claude Code session (the vendor); ask any other agent to use the TypeSafe skill (the vendor); or run Claude Code\'s own',
64
66
  'claude plugin list',
65
67
  ];
66
- const AFTER_INSTALL = [
67
- 'After an install, quit the agent and start it again from a new terminal.',
68
+ const FIRST_USE = [
69
+ AFTER_INSTALL,
68
70
  '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?".',
69
71
  'Success: three choices, each with a confidence.',
70
72
  ];
@@ -109,11 +111,26 @@ const skillPaths = (dir, home) => [...new Set([join(dir, '.claude', 'skills'), j
109
111
  ...(home === '' ? [] : [join(home, '.claude', 'skills'), join(home, '.codex', 'skills'), join(home, '.cursor', 'skills')])]
110
112
  .map((root) => join(root, SKILL_FILE)))];
111
113
 
112
- const renderSteps = (found, set, connect, where) => [
114
+ // The skill targets of the part jev-skill, each judged by the facts leaf's one state rule over the guide's own reads.
115
+ const skillOf = (io, env, home, platform) => {
116
+ const { dir, note } = claudeDirOf(env, home, platform);
117
+ const fs = { lstat: lstatSync, readdir: readdirSync, readFile: readFileSync, pins: io.pins ?? SKILL_PINS };
118
+ const targets = skillTargets({ home, claudeDir: dir }).map((target) => {
119
+ const { state, tag, reason } = skillState(target, fs);
120
+ return { agent: target.agent, path: printable(target.path), state, tag, reason: reason === null ? null : printable(reason) };
121
+ });
122
+ return { targets, install: home === '' ? '' : skillLine(io.toolsDir ?? TOOLS, platform, dir), note: home === '' ? '' : note };
123
+ };
124
+ const markOf = ({ agent, path, state, tag, reason }) => `${agent} skill root: ${state} at ${path}${tag ? ` (${tag})` : ''}${
125
+ reason ? ` — ${reason}` : ''}`;
126
+ const renderSteps = (found, set, connect, where, home, skill) => [
113
127
  ['STEP 1 — the key', KEY_GET, KEY_CONNECT, ...(where ? [`Run it in ${where}:`] : []), connect, KEY_RESTART, KEY_NEVER,
114
128
  set ? `key: ${KEY_VARIABLE} set.` : KEY_NOT_SET],
115
- ['STEP 2 — the vendor skill', ...INSTALL,
116
- ...(found.length ? found.map((path) => `skill: found at ${path}`) : NOT_SEEN), ...AFTER_INSTALL],
129
+ ['STEP 2 — the vendor skill', SKILL_OPTIONAL,
130
+ ...(home === '' ? [NO_HOME] : [...(where ? [`Run it in ${where}:`] : []), skill.install || NO_APPLY_LINE,
131
+ ...(skill.note ? [skill.note] : [])]),
132
+ ...INSTALL, ...skill.targets.map(markOf),
133
+ ...(found.length ? found.map((path) => `skill: found at ${path}`) : NOT_SEEN), ...FIRST_USE],
117
134
  ];
118
135
 
119
136
  export const main = (argv, io = {}) => {
@@ -132,9 +149,11 @@ export const main = (argv, io = {}) => {
132
149
  const platform = io.platform ?? process.platform;
133
150
  const connect = connectLine(io.toolsDir ?? TOOLS, platform);
134
151
  const where = placeFor(io, env, platform);
135
- const steps = renderSteps(found, set, connect, where);
152
+ const home = homeOf(io);
153
+ const skill = skillOf(io, env, home, platform);
154
+ const steps = renderSteps(found, set, connect, where, home, skill);
136
155
  log(parsed.json
137
- ? JSON.stringify({ schema: SCHEMA, command: 'jev', dir, text: JEV_TEXT, skill: { found }, key: { set }, connect, where, steps, prompts: PROMPTS }, null, 2)
156
+ ? JSON.stringify({ schema: SCHEMA, command: 'jev', dir, text: JEV_TEXT, skill: { found, targets: skill.targets, install: skill.install }, key: { set }, connect, where, steps, prompts: PROMPTS }, null, 2)
138
157
  : [...JEV_TEXT, ...steps.flat(), 'WHERE IT PAYS IN THIS WORKFLOW', ...PROMPTS].join('\n'));
139
158
  return 0;
140
159
  };
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env node
2
+ // spec:jev-guide — docs/ai/specs/kit/jev-guide/jev-skill.md
3
+ // The one command that installs the vendor's Jev skill, pinned and digest-checked, as a verified COPY into
4
+ // the three agent skill roots. The USER runs it with --apply in a terminal of their own after their yes in
5
+ // the chat; without --apply it is a read-only dry run. It reads no secret, needs no terminal, opens no
6
+ // connection, never writes through a link below a base, and never removes what it did not mint this run.
7
+ import { createHash } from 'node:crypto';
8
+ import { lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
9
+ import { homedir } from 'node:os';
10
+ import { dirname, join, posix, win32 } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { fail } from '../references/scripts/markdown-blocks.mjs';
13
+ import { isDirectRun } from './direct-run.mjs';
14
+ import { AFTER_INSTALL, NO_APPLY_LINE, SKILL_FILES, SKILL_PINS, claudeDirOf, printable, skillLine, skillState,
15
+ skillTargets } from './jev-facts.mjs';
16
+
17
+ export const EXIT = Object.freeze({ done: 0, nothingWritten: 1, usage: 2, inPart: 4 });
18
+ const TOOLS = dirname(fileURLToPath(import.meta.url));
19
+ const VENDOR_DIR = join(TOOLS, '..', 'references', 'vendor', 'typesafe-ai');
20
+ const TMP_PREFIX = '.typesafe-ai.kit-tmp-';
21
+ const OLD_PREFIX = '.typesafe-ai.kit-old-';
22
+ const HELP = `jev-skill — installs the vendor's Jev (TypeSafe) skill, pinned, as one verified copy per agent skill root.
23
+
24
+ Usage:
25
+ node jev-skill.mjs [--apply] [--claude-dir DIR]
26
+
27
+ Without --apply: a dry run that writes nothing and prints the plan.
28
+ --apply install or replace the planned copies (Codex ~/.agents/skills, Claude Code <claude dir>/skills,
29
+ Antigravity CLI ~/.gemini/config/skills); a foreign copy is left untouched.
30
+ --claude-dir the Claude Code dir (absolute); default CLAUDE_CONFIG_DIR when absolute, else ~/.claude.
31
+ --help answered only when it is the whole invocation.
32
+
33
+ Exit codes: 0 the dry run rendered or every planned write done; 1 nothing written (the kit's own copy fails
34
+ its check, or no home directory); 2 usage; 4 applied in part (a target refused).`;
35
+
36
+ const parseArgv = (argv, platform) => {
37
+ if (argv.includes('--help') || argv.includes('-h')) {
38
+ if (argv.length !== 1) throw fail(EXIT.usage, '--help is answered only when it is the whole invocation');
39
+ return { help: true };
40
+ }
41
+ const parsed = { help: false, apply: false, claudeDir: null };
42
+ for (let index = 0; index < argv.length; index += 1) {
43
+ const arg = argv[index];
44
+ if (arg === '--apply') parsed.apply = true;
45
+ else if (arg === '--claude-dir') {
46
+ const value = argv[index + 1];
47
+ if (value === undefined || value === '' || value.startsWith('-')) throw fail(EXIT.usage, '--claude-dir takes a value');
48
+ if (!(platform === 'win32' ? win32 : posix).isAbsolute(value)) throw fail(EXIT.usage, `--claude-dir takes an absolute path, not ${printable(value)}`);
49
+ parsed.claudeDir = value;
50
+ index += 1;
51
+ } else throw fail(EXIT.usage, `unknown argument "${printable(arg)}" — run with --help`);
52
+ }
53
+ return parsed;
54
+ };
55
+
56
+ const digestOf = (bytes) => createHash('sha256').update(bytes).digest('hex');
57
+ const codeOf = (error) => error?.code ?? error?.message ?? String(error);
58
+ const DEFAULT_IO = Object.freeze({
59
+ lstat: lstatSync, readdir: readdirSync, readFile: readFileSync, mkdir: mkdirSync, mkdtemp: mkdtempSync,
60
+ write: (path, bytes) => writeFileSync(path, bytes, { flag: 'wx' }), rename: renameSync, rm: rmSync,
61
+ });
62
+ const homeOf = (io) => {
63
+ if (io.home !== undefined) return io.home;
64
+ try { return homedir(); } catch { return ''; }
65
+ };
66
+
67
+ // The kit's own pair, digest-checked against the first pin: the bytes every install writes, or the refused file.
68
+ const vendorBytes = (io) => {
69
+ const bytes = {};
70
+ for (const [name, shipped] of Object.entries(SKILL_FILES)) {
71
+ try { bytes[name] = io.readFile(join(io.vendorDir, shipped)); } catch { return { refused: shipped }; }
72
+ if (digestOf(bytes[name]) !== io.pins[0].digests[name]) return { refused: shipped };
73
+ }
74
+ return { bytes };
75
+ };
76
+
77
+ const ACTIONS = Object.freeze({ current: 'keep', earlier: 'replace', absent: 'install', foreign: 'untouched' });
78
+ // Every target with its state and its action, the effective Claude Code dir, the note and the leftovers.
79
+ export const planFor = (io) => {
80
+ const resolved = claudeDirOf(io.env, io.home, io.platform);
81
+ const claudeDir = io.claudeDir ?? resolved.dir;
82
+ const note = io.claudeDir ? '' : resolved.note;
83
+ const targets = skillTargets({ home: io.home, claudeDir }).map((target) => {
84
+ const state = skillState(target, io);
85
+ return { ...target, ...state, action: ACTIONS[state.state] };
86
+ });
87
+ const parents = [...new Set(targets.map(({ root }) => dirname(root)))];
88
+ const leftovers = parents.flatMap((parent) => {
89
+ try {
90
+ return [...io.readdir(parent)].map(String).filter((name) => name.startsWith(TMP_PREFIX) || name.startsWith(OLD_PREFIX))
91
+ .sort().map((name) => join(parent, name));
92
+ } catch { return []; }
93
+ });
94
+ return { claudeDir, note, targets, leftovers };
95
+ };
96
+
97
+ const countsOf = (targets) => Object.fromEntries(Object.values(ACTIONS)
98
+ .map((action) => [action, targets.filter((target) => target.action === action).length]));
99
+ const detailOf = (target, newest) => ({
100
+ keep: `${target.tag}, keep`, replace: `${target.tag}, replace with ${newest}`, install: 'install',
101
+ untouched: `${printable(target.reason)}, left untouched`,
102
+ })[target.action];
103
+
104
+ // One target's write: install or replace, re-checking the target before each rename. Returns its lines and outcome.
105
+ export const applyTarget = (entry, io) => {
106
+ const path = printable(entry.path);
107
+ const parent = dirname(entry.root);
108
+ const lines = [];
109
+ const minted = [];
110
+ const stateNow = () => skillState(entry, io).state;
111
+ const changed = (planned) => {
112
+ const now = stateNow();
113
+ return now === planned ? null : `changed since the plan: now ${now}`;
114
+ };
115
+ let aside = null;
116
+ const refuse = (cause) => {
117
+ lines.push(`refused: ${path} — ${cause}`);
118
+ for (const dir of minted) io.rm(dir, { recursive: true, force: true });
119
+ if (aside) {
120
+ const now = stateNow();
121
+ let kept = now === 'absent' ? null : `the target is now ${now}`;
122
+ if (kept === null) {
123
+ try {
124
+ io.rename(join(aside, 'typesafe-ai'), entry.path);
125
+ io.rm(aside, { recursive: true, force: true });
126
+ } catch (error) { kept = codeOf(error); }
127
+ }
128
+ if (kept !== null) lines.push(`kept aside: ${printable(join(aside, 'typesafe-ai'))} — ${kept}`);
129
+ }
130
+ return { lines, outcome: 'refused' };
131
+ };
132
+ try {
133
+ const below = entry.root.slice(entry.base.length).split(/[\\/]/).filter(Boolean);
134
+ below.reduce((at, name) => {
135
+ const next = join(at, name);
136
+ try { io.lstat(next); } catch (error) {
137
+ if (error?.code !== 'ENOENT') throw error;
138
+ io.mkdir(next, { mode: 0o700 });
139
+ }
140
+ return next;
141
+ }, entry.base);
142
+ const temp = io.mkdtemp(join(parent, TMP_PREFIX));
143
+ minted.push(temp);
144
+ const child = join(temp, 'typesafe-ai');
145
+ io.mkdir(child, { mode: 0o700 });
146
+ for (const name of Object.keys(SKILL_FILES)) io.write(join(child, name), io.bytes[name]);
147
+ for (const name of Object.keys(SKILL_FILES)) {
148
+ if (digestOf(io.readFile(join(child, name))) !== io.pins[0].digests[name]) return refuse('digest mismatch');
149
+ }
150
+ if (entry.state === 'earlier') {
151
+ const old = io.mkdtemp(join(parent, OLD_PREFIX));
152
+ minted.push(old);
153
+ const moved = changed('earlier');
154
+ if (moved) return refuse(moved);
155
+ io.rename(entry.path, join(old, 'typesafe-ai'));
156
+ minted.pop();
157
+ aside = old;
158
+ }
159
+ const placed = changed('absent');
160
+ if (placed) return refuse(placed);
161
+ io.rename(child, entry.path);
162
+ io.rm(temp, { recursive: true, force: true });
163
+ minted.length = 0;
164
+ if (!aside) {
165
+ lines.push(`installed: ${path} (${io.pins[0].tag})`);
166
+ return { lines, outcome: 'installed' };
167
+ }
168
+ const old = { base: aside, path: join(aside, 'typesafe-ai') };
169
+ const verified = skillState(old, io);
170
+ if (verified.state === 'earlier') io.rm(aside, { recursive: true, force: true });
171
+ else lines.push(`kept aside: ${printable(old.path)} — it reads ${verified.state}`);
172
+ lines.unshift(`replaced: ${path} (${entry.tag} → ${io.pins[0].tag})`);
173
+ return { lines, outcome: 'replaced' };
174
+ } catch (error) {
175
+ return refuse(codeOf(error));
176
+ }
177
+ };
178
+
179
+ const run = (parsed, io) => {
180
+ const pin = io.pins[0];
181
+ const { bytes, refused } = vendorBytes(io);
182
+ if (refused) {
183
+ io.error(`refused: the kit's own copy of ${refused} is missing, unreadable or does not match the pin — nothing written`);
184
+ return EXIT.nothingWritten;
185
+ }
186
+ if (io.home === '') { io.error('refused: no home directory found — nothing written'); return EXIT.nothingWritten; }
187
+ const plan = planFor({ ...io, claudeDir: parsed.claudeDir ?? undefined });
188
+ io.log(`pin: ${pin.tag}, commit ${pin.commit}`);
189
+ if (plan.note) io.log(plan.note);
190
+ const leftovers = plan.leftovers.map((path) => `leftover: ${printable(path)} — not removed; delete it by hand`);
191
+ const counts = countsOf(plan.targets);
192
+ if (!parsed.apply) {
193
+ for (const target of plan.targets) io.log(`${target.agent}: ${target.state} at ${printable(target.path)} — ${detailOf(target, pin.tag)}`);
194
+ for (const line of leftovers) io.log(line);
195
+ io.log(`dry run — nothing written. Planned: ${counts.install} to install, ${counts.replace} to replace, ${counts.keep} kept, ${counts.untouched} left untouched.`);
196
+ if (counts.install + counts.replace > 0) {
197
+ io.log('To install, run in a terminal of your own:');
198
+ io.log(skillLine(io.toolsDir, io.platform, plan.claudeDir) || NO_APPLY_LINE);
199
+ }
200
+ return EXIT.done;
201
+ }
202
+ const done = { installed: 0, replaced: 0, kept: 0, untouched: 0, refused: 0 };
203
+ for (const target of plan.targets) {
204
+ if (target.action === 'keep' || target.action === 'untouched') {
205
+ io.log(target.action === 'keep' ? `kept: ${printable(target.path)} (${target.tag})`
206
+ : `left untouched: ${printable(target.path)} — ${printable(target.reason)}`);
207
+ done[target.action === 'keep' ? 'kept' : 'untouched'] += 1;
208
+ continue;
209
+ }
210
+ const result = applyTarget(target, { ...io, bytes });
211
+ for (const line of result.lines) io.log(line);
212
+ done[result.outcome] += 1;
213
+ }
214
+ for (const line of leftovers) io.log(line);
215
+ io.log(`done — ${done.installed} installed, ${done.replaced} replaced, ${done.kept} kept, ${done.untouched} left untouched, ${done.refused} refused.`);
216
+ if (done.installed + done.replaced > 0) io.log(AFTER_INSTALL);
217
+ return done.refused === 0 ? EXIT.done : EXIT.inPart;
218
+ };
219
+
220
+ export const main = (argv, io = {}) => {
221
+ const resolved = {
222
+ ...DEFAULT_IO, log: console.log, error: console.error, env: process.env, platform: process.platform,
223
+ pins: SKILL_PINS, vendorDir: VENDOR_DIR, toolsDir: TOOLS, ...io,
224
+ };
225
+ let parsed;
226
+ try { parsed = parseArgv(argv, resolved.platform); } catch (err) { resolved.error(err.message); return EXIT.usage; }
227
+ if (parsed.help) { resolved.log(HELP); return EXIT.done; }
228
+ return run(parsed, { ...resolved, home: homeOf(io) });
229
+ };
230
+
231
+ if (isDirectRun(import.meta.url)) process.exitCode = main(process.argv.slice(2));
@@ -95,6 +95,7 @@ import {
95
95
  ACKS_COVERAGE_DOMAIN_KEY,
96
96
  ACKS_SOURCE_SIZE_COPY_KEY,
97
97
  ACKS_JEV_CONNECT_KEY,
98
+ ACKS_JEV_SKILL_KEY,
98
99
  ACK_LANES,
99
100
  factFingerprint,
100
101
  readAckValue,
@@ -102,12 +103,14 @@ import {
102
103
  import { ADOPTION, STORE_DIR_REL as SPEC_STORE_DIR_REL, SPEC_ADOPTION_LANE, declineFingerprint, readDeclineAck, surveySpecAdoption } from './spec-adoption.mjs';
103
104
  import { ENSURE_OPS } from './ensure-vocabulary.mjs';
104
105
  import { composeProfileGapScreen } from './profile-gap-screen.mjs';
105
- import { connectLine, keySet, placeOf } from './jev-facts.mjs';
106
+ import { NO_APPLY_LINE, SKILL_PINS, claudeDirOf, connectLine, keySet, placeOf, printable, skillLine, skillState, skillTargets } from './jev-facts.mjs';
106
107
 
107
108
  // The upgrade ensure that seeds the spec store — the not-adopted item's apply; pinned to the vocabulary.
108
109
  const SPEC_LAYER_ENSURE = ENSURE_OPS.includes('specs') ? 'specs' : null;
109
110
  const JEV_CONNECT_DECLINED = 'jev-connect:declined';
110
111
  const JEV_CONNECT_DECLINE = factFingerprint(JEV_CONNECT_DECLINED);
112
+ // The skill decline names the pin it declined, so a pin move brings the update offer back once.
113
+ const JEV_SKILL_DECLINED = 'jev-skill:declined:';
111
114
 
112
115
  export { ACKS_FILE, ACKS_LANE_KEY, ACKS_WORKTREES_DIR_KEY, ACKS_COVERAGE_DOMAIN_KEY, ACKS_SOURCE_SIZE_COPY_KEY, ACK_LANES, factFingerprint };
113
116
 
@@ -206,6 +209,8 @@ export const SEVERITIES = Object.freeze({
206
209
  'spec-adoption': SEVERITY_OPTIONAL,
207
210
  'profile-gap': SEVERITY_OPTIONAL,
208
211
  'jev-connect': SEVERITY_OPTIONAL,
212
+ 'jev-skill': SEVERITY_OPTIONAL,
213
+ 'jev-skill.earlier': SEVERITY_OPTIONAL,
209
214
  'spec-adoption.adopting': SEVERITY_OPTIONAL,
210
215
  });
211
216
  // The per-item render tags (frozen presentation data, same language contract as the templates).
@@ -284,6 +289,8 @@ export const WHATS = Object.freeze({
284
289
  '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
290
  'profile-gap': '{what}',
286
291
  'jev-connect': 'Jev (TypeSafe) is not connected: TYPESAFE_API_KEY is not in the environment the agent\'s commands run in',
292
+ 'jev-skill': 'Jev\'s vendor skill is not installed for every agent here: a skill root the kit fills is absent or behind the kit\'s pin',
293
+ 'jev-skill.earlier': 'Jev\'s vendor skill in the agent skill roots is at {old}, behind the kit\'s pin {new}; only copies matching a kit pin change',
287
294
  '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
295
  });
289
296
 
@@ -346,6 +353,7 @@ export const BENEFITS = Object.freeze({
346
353
  'spec-adoption': 'contracts — a plan names the contract it builds to, and a change to a governed slice is visible at review instead of after it',
347
354
  'profile-gap': 'full flow — each difference from the reference profile is previewed by its own writer; a declined one returns only at a lineage step',
348
355
  'jev-connect': 'decisions — a typed choice with a confidence in under a second for your code and the agent\'s scripts, once for every project here',
356
+ 'jev-skill': 'the TypeSafe skill for Claude Code, Codex and Antigravity CLI, one pinned verified copy per agent root, from one command you run',
349
357
  });
350
358
 
351
359
  // ── the CLOSED opt-in capability registry (OPT-IN-SHIPS-INVISIBLE) ──────────────────────────────
@@ -394,6 +402,7 @@ export const OPT_IN_CAPABILITIES = Object.freeze([
394
402
  { id: 'spec-adoption', mode: 'upgrade', advisorKey: 'spec-adoption' },
395
403
  { id: 'full-flow-profile', mode: 'upgrade', advisorKey: 'profile-gap' },
396
404
  { id: 'jev-connect', mode: 'jev', advisorKey: 'jev-connect' },
405
+ { id: 'jev-skill', mode: 'jev', advisorKey: 'jev-skill' },
397
406
  { id: 'adr-store-migration', mode: 'migrate-adr-store', advisorKey: 'adr-store-migration' },
398
407
  { id: 'review-recipe', mode: 'set-recipe', advisorKey: 'review-recipe' },
399
408
  // The execute slot is a DISTINCT opt-in from the review slot, and the same probe reports both —
@@ -1313,7 +1322,7 @@ const readReadLaneToggle = (root, deps) => {
1313
1322
  // D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
1314
1323
  // at the consent moment; the static contract test asserts EXACT bidirectional coverage
1315
1324
  // (risk-marked keys == mode-doc note keys — a dropped note goes red, not silent).
1316
- export const RISK_NOTED_KEYS = Object.freeze(['sandbox-lane', 'read-lane', 'worktrees-dir', 'adr-store-migration', 'gates-inert', 'source-size', 'gate-hook', 'mcp-channel', 'spec-adoption', 'enforcement', 'profile-gap', 'jev-connect']);
1325
+ export const RISK_NOTED_KEYS = Object.freeze(['sandbox-lane', 'read-lane', 'worktrees-dir', 'adr-store-migration', 'gates-inert', 'source-size', 'gate-hook', 'mcp-channel', 'spec-adoption', 'enforcement', 'profile-gap', 'jev-connect', 'jev-skill']);
1317
1326
 
1318
1327
  // The feature-spec layer's adoption state (contract: kit/spec-adoption). The canon lets a plan cite
1319
1328
  // zero governing specs while a project adopts the layer, and nothing ever said whether adoption had
@@ -1625,6 +1634,40 @@ export const probeJevConnect = ({ root, deps, add, skip }) => {
1625
1634
  }
1626
1635
  };
1627
1636
 
1637
+ // The Jev skill offer (contract: kit/jev-guide, part jev-offer): only once the key is set, so one jev item shows at a time.
1638
+ // Each target is judged by the facts leaf's one state rule; the apply is the skill command the USER runs (HAND-APPLY).
1639
+ export const probeJevSkill = ({ root, deps, add, skip }) => {
1640
+ try {
1641
+ const env = deps.getenv ?? {};
1642
+ if (!keySet(env)) return;
1643
+ const host = deps.jevHost ?? {};
1644
+ if (typeof host.home !== 'string' || host.home === '') return;
1645
+ const pins = deps.jevPins ?? SKILL_PINS;
1646
+ const { dir, note } = claudeDirOf(env, host.home, host.platform);
1647
+ const io = { lstat: deps.lstat ?? lstatSync, readdir: deps.readdir ?? readdirSync, readFile: deps.readFile ?? readFileSync, pins };
1648
+ const judged = skillTargets({ home: host.home, claudeDir: dir }).map((target) => ({ ...target, ...skillState(target, io) }));
1649
+ if (!judged.some(({ state }) => state === 'absent' || state === 'earlier')) return;
1650
+ const decline = factFingerprint(`${JEV_SKILL_DECLINED}${pins[0].tag}`);
1651
+ if (readAckValue(root, deps, ACKS_JEV_SKILL_KEY) === decline) return;
1652
+ const line = skillLine(HERE, host.platform, dir);
1653
+ if (line === '') {
1654
+ skip('jev-skill', new Error(NO_APPLY_LINE));
1655
+ return;
1656
+ }
1657
+ const place = placeOf({ env, platform: host.platform, hostname: host.hostname, container: host.container });
1658
+ const behind = judged.filter(({ state }) => state === 'earlier').map(({ tag }) => pins.findIndex((pin) => pin.tag === tag));
1659
+ const what = behind.length ? fillTemplate(WHATS['jev-skill.earlier'], { old: pins[Math.max(...behind)].tag, new: pins[0].tag })
1660
+ : WHATS['jev-skill'];
1661
+ const untouched = judged.filter(({ state }) => state === 'foreign')
1662
+ .map(({ path, reason }) => `left untouched: ${printable(path)} — ${printable(reason)}; `).join('');
1663
+ const record = `node ${q(toolPath('ack-write.mjs'))} --lane jev-skill --fingerprint ${decline} --cwd ${q(root)}`;
1664
+ add('jev-skill', what, `HAND-APPLY: ${line}`, behind.length ? 'jev-skill.earlier' : 'jev-skill',
1665
+ `${place ? `run the apply in ${place}; ` : ''}${untouched}${note ? `${note}; ` : ''}HAND-APPLY alternative (instead of the apply, never after it): decline the offer by recording it — ${record}`);
1666
+ } catch (err) {
1667
+ skip('jev-skill', err);
1668
+ }
1669
+ };
1670
+
1628
1671
  export const probeProfileGaps = ({ root, deps, add, skip }) => {
1629
1672
  try {
1630
1673
  const { gaps, skips } = composeProfileGapScreen({ root, deps });
@@ -1658,6 +1701,7 @@ const PROBES = Object.freeze([
1658
1701
  probeMcpChannel,
1659
1702
  probeSpecAdoption,
1660
1703
  probeJevConnect,
1704
+ probeJevSkill,
1661
1705
  probeProfileGaps,
1662
1706
  ]);
1663
1707
 
@@ -1760,9 +1804,10 @@ the consent-gated ack writer into docs/ai/acks.json (never a security key).
1760
1804
  Read-only: never writes, never commits, never runs a subscription CLI. Exit codes: 0 report
1761
1805
  rendered (items or empty); 1 error; 2 usage.`;
1762
1806
 
1763
- // The host facts of the key offer's place: the platform, the hostname (a throw reads as none), /.dockerenv a regular file.
1807
+ // The host facts of the key and skill offers: the platform, the hostname (a throw reads as none), /.dockerenv a regular file, the home.
1764
1808
  const hostFacts = () => ({ platform: process.platform, hostname: (() => { try { return hostname(); } catch { return ''; } })(),
1765
- container: (() => { try { return statSync('/.dockerenv').isFile(); } catch { return false; } })() });
1809
+ container: (() => { try { return statSync('/.dockerenv').isFile(); } catch { return false; } })(),
1810
+ home: (() => { try { return homedir(); } catch { return ''; } })() });
1766
1811
 
1767
1812
  export const main = (argv, ctx = {}) => {
1768
1813
  try {