@sabaiway/agent-workflow-kit 14.18.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,33 @@ 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
+
7
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)
8
35
 
9
36
  **While `TYPESAFE_API_KEY` is not in the environment the agent's commands run in, the Recommendations item `jev-connect`
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 two steps to a connected key (the connect command, the optional vendor skill) and three prompts where it pays. Opens no connection and never prints the key. |
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.18.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.18.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.18.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,11 +2,11 @@
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 two steps to a connected key and three prompts where it pays. The guide writes no file, opens no connection, never prints the key's value and never commits.
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 the one vendor-quoted text verbatim and in quotes, never translated or reflowed: the not-a-replacement sentence.
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
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
 
@@ -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 is HAND-APPLY: the user runs the connect command in a terminal of their own, never the agent (the command refuses without a terminal, and a terminal is not consent). The command reads the key with no echo, makes one network call to api.typesafe.ai with the vendor's sample text only on the user's own run, and writes the key in plain text as managed export lines in the shell's startup files (bash: ~/.bashrc and the login file, created as ~/.bash_profile when none exists) (readable by any process running as that user, and by other local accounts when one is world-readable in a traversable home); the vendor keeps data under its processing agreement and offers zero retention only to enterprise accounts. The item reads the environment the agent's commands run in: a host setting that filters it shows the item while the key is set in the user's shell. The decline is the HAND-APPLY alternative on the recipe line, asked apart BEFORE the confirmation (the `spec-adoption` order): on "connect" hand over the apply line, on "decline" run nothing and hand over the recipe line; the decline is recorded per project as `jevConnectAck` in `docs/ai/acks.json`. Risk profile: a plain-text key in the user's startup files, written by a command the user runs; one network call from that run.
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):**
@@ -1,41 +1,49 @@
1
1
  #!/usr/bin/env node
2
2
  // spec:jev-guide — docs/ai/specs/kit/jev-guide/jev-connect.md
3
- // The one command that connects Jev (TypeSafe) on a host. The USER runs it in a terminal of their
4
- // own: it reads the key with no echo, verifies it with one request, saves it as managed export lines
5
- // in the shell's startup files and says to restart the agent. It never prints the key, never puts it in
6
- // an argv, and refuses when stdin is not a terminal, so a non-interactive run (the agent's) cannot
7
- // reach the prompt — the mode doc keeps the rule that the agent never runs it.
8
- import { lstatSync, readFileSync, writeFileSync } from 'node:fs';
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';
9
11
  import { homedir } from 'node:os';
10
12
  import { basename, dirname, isAbsolute, join, relative, sep } from 'node:path';
11
13
  import { fail } from '../references/scripts/markdown-blocks.mjs';
12
14
  import { isDirectRun } from './direct-run.mjs';
13
- import { JEV_ENDPOINT, JEV_ERROR_MEANINGS, JEV_SAMPLE_BODY, KEY_VARIABLE } from './jev-facts.mjs';
15
+ import { JEV_ENDPOINT, JEV_ERROR_MEANINGS, JEV_SAMPLE_BODY, KEY_VARIABLE, RESTART_STEP } from './jev-facts.mjs';
14
16
 
15
17
  export const EXIT = Object.freeze({ saved: 0, notVerified: 1, usage: 2, noTerminal: 3, notSaved: 4 });
16
18
  export const MARK = '# agent-workflow jev';
17
19
  export const TIMEOUT_MS = 15000;
18
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')`;
19
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';
20
- export const CONNECTED = 'connected — quit the agent and start it again from a new terminal';
21
- export const SAVED_IN_PART = 'saved in part — add the export line by hand where a line above says not saved, then restart the agent';
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';
22
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`;
23
29
  export const EDITOR_SENTENCE = `verified, ${NOT_SAVED_SENTENCE}`;
24
- const HELP = `jev-connect — connects Jev (TypeSafe) on this host; run it in a terminal of your own.
30
+ const HELP = `jev-connect — saves the Jev (TypeSafe) key on this host; run it in a terminal of your own.
25
31
 
26
32
  Usage:
27
33
  node jev-connect.mjs [--unverified]
28
34
 
29
- Asks for ${KEY_VARIABLE} with no echo, verifies the key with one request to api.typesafe.ai, saves it
30
- as managed export lines in your shell's startup files (bash: ~/.bashrc and the login file bash
31
- reads; zsh: ~/.zshrc, under ZDOTDIR when set) and says to restart the agent. The key is never printed and never put in an argument.
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.
32
40
 
33
41
  --unverified save without the request (no network from here).
34
42
  --help answered only when it is the whole invocation.
35
43
 
36
- Exit codes: 0 saved; 1 not verified, nothing written; 2 usage; 3 no terminal, aborted or no key,
37
- nothing written; 4 not saved (another shell, Windows, a linked startup file or directory) — the
38
- last line says what to set by hand.`;
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.`;
39
47
 
40
48
  const parseArgv = (argv) => {
41
49
  if (argv.includes('--help') || argv.includes('-h')) {
@@ -112,22 +120,31 @@ export const verifyKey = async (key, io = {}) => {
112
120
  return { ok: true, line: `verified: HTTP 200 — department ${scrub(answer.choice, key)}, confidence ${answer.confidence}` };
113
121
  };
114
122
 
115
- // The startup files the key goes to, by a closed rule over the shell: zsh reads ~/.zshrc (under
116
- // ZDOTDIR when set); an interactive bash reads ~/.bashrc and a login bash the first of
117
- // .bash_profile, .bash_login, .profile that exists — created as .bash_profile when none does. Both
118
- // bash files always: no file content decides the set, so a login file's own older export is
119
- // rewritten too. Any other shell, and Windows, gets no target.
120
- export const targetsFor = ({ shell, home, zdotdir, platform = 'linux' }, deps = {}) => {
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 = {}) => {
121
130
  const exists = deps.exists ?? ((path) => { try { lstatSync(path); return true; } catch { return false; } });
122
- if (platform === 'win32' || typeof home !== 'string' || home === '') return [];
131
+ if (platform === 'win32') return [{ form: 'windows', path: null }];
132
+ if (typeof home !== 'string' || home === '') return [];
123
133
  const name = typeof shell === 'string' ? basename(shell) : '';
124
- if (name === 'zsh') return [join(typeof zdotdir === 'string' && zdotdir !== '' ? zdotdir : home, '.zshrc')];
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
+ }
125
140
  if (name !== 'bash') return [];
126
141
  const login = LOGIN_FILES.map((file) => join(home, file)).find(exists) ?? join(home, LOGIN_FILES[0]);
127
- return [join(home, '.bashrc'), login];
142
+ return [exported(join(home, '.bashrc')), exported(login)];
128
143
  };
129
144
 
130
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}`;
131
148
  const isExportLine = (line) => new RegExp(`^\\s*export\\s+${KEY_VARIABLE}=`).test(line);
132
149
 
133
150
  // Replace matching export lines in place, their indent and CR kept, so no if/case branch is
@@ -143,42 +160,73 @@ export const upsertManagedLine = (text, line) => {
143
160
  };
144
161
 
145
162
  // The directories between the home and the target, each lstat'ed: a link among them is never written
146
- // through (a stow'd ZDOTDIR is a dotfiles repo); a target outside the home is not written at all.
147
- const parentRefusal = (path, home, lstat) => {
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) => {
148
166
  const rel = relative(home, dirname(path));
149
- if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return `${path} is outside the home`;
167
+ if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return { refusal: `${path} is outside the home` };
168
+ const missing = [];
150
169
  let dir = home;
151
170
  for (const part of rel.split(sep).filter(Boolean)) {
152
171
  dir = join(dir, part);
153
- if (lstat(dir).isSymbolicLink()) return `${path} — ${dir} is a link`;
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` };
154
180
  }
155
- return null;
181
+ return { refusal: null, missing };
156
182
  };
157
183
 
158
- // One line per target: `saved: <path>` or `not saved: <path> — <why>`. A link — the target or a
159
- // directory below the home — or a non-regular entry is never written through; a new file is created
160
- // 0600; an existing file keeps its mode.
161
- export const saveKey = (key, targets, home, deps = {}) => targets.map((path) => {
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) => {
162
188
  const lstat = deps.lstat ?? lstatSync;
163
189
  const read = deps.read ?? ((at) => readFileSync(at, 'utf8'));
164
190
  const write = deps.write ?? writeFileSync;
191
+ const mkdir = deps.mkdir ?? mkdirSync;
192
+ const no = (line) => ({ path, saved: false, line });
165
193
  let stat = null;
166
194
  try {
167
- const refusal = parentRefusal(path, home, lstat);
168
- if (refusal) return { path, saved: false, line: `not saved: ${refusal} — add the export line by hand` };
169
- stat = lstat(path);
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);
170
199
  } catch (error) {
171
- if (error?.code !== 'ENOENT') return { path, saved: false, line: `not saved: ${path} — ${error?.code ?? error?.message}` };
200
+ if (error?.code !== 'ENOENT') return no(`not saved: ${path} — ${error?.code ?? error?.message}`);
172
201
  }
173
- if (stat?.isSymbolicLink()) return { path, saved: false, line: `not saved: ${path} is a link — add the export line by hand` };
174
- if (stat && !stat.isFile()) return { path, saved: false, line: `not saved: ${path} is not a regular file` };
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`);
175
204
  try {
176
- write(path, upsertManagedLine(stat ? read(path) : '', managedLine(key)), stat ? {} : { mode: 0o600 });
205
+ const text = form === 'fish' ? `${fishLine(key)}\n` : upsertManagedLine(stat ? read(path) : '', managedLine(key));
206
+ write(path, text, stat ? {} : { mode: 0o600 });
177
207
  return { path, saved: true, line: `saved: ${path}` };
178
208
  } catch (error) {
179
- return { path, saved: false, line: `not saved: ${path} — ${error?.code ?? error?.message}` };
209
+ return no(`not saved: ${path} — ${error?.code ?? error?.message}`);
180
210
  }
181
- });
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)));
182
230
 
183
231
  const safeHome = (env) => {
184
232
  if (typeof env.HOME === 'string' && env.HOME !== '') return env.HOME;
@@ -206,12 +254,15 @@ export const main = async (argv, io = {}) => {
206
254
  }
207
255
  const env = io.env ?? process.env;
208
256
  const home = io.home ?? safeHome(env);
209
- const targets = targetsFor({ shell: env.SHELL, home, zdotdir: env.ZDOTDIR, platform: io.platform ?? process.platform }, io);
257
+ const targets = targetsFor({ shell: env.SHELL, home, zdotdir: env.ZDOTDIR, xdgConfigHome: env.XDG_CONFIG_HOME,
258
+ platform: io.platform ?? process.platform }, io);
210
259
  const results = saveKey(key, targets, home, io);
211
260
  for (const result of results) log(result.line);
212
261
  const saved = results.filter((result) => result.saved).length;
213
- if (results.length > 0 && saved === results.length) { log(CONNECTED); return EXIT.saved; }
262
+ const verified = parsed.unverified ? '' : 'verified, ';
263
+ if (results.length > 0 && saved === results.length) { log(ALL_SAVED); return EXIT.saved; }
214
264
  if (saved > 0) log(SAVED_IN_PART);
265
+ else if (results.length > 0) log(`${verified}${RETRY_SENTENCE}`);
215
266
  else log(parsed.unverified ? NOT_SAVED_SENTENCE : EDITOR_SENTENCE);
216
267
  return EXIT.notSaved;
217
268
  };
@@ -15,8 +15,26 @@ const SAFE = /^[A-Za-z0-9_./:=+@%,-]+$/;
15
15
  export const quoteArg = (value) => (SAFE.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`);
16
16
 
17
17
  // The command the user runs in a terminal of their own; the guide prints it, the advisor's apply
18
- // carries it. `toolsDir` is the kit's tools directory (the file beside this one).
19
- export const connectLine = (toolsDir) => `node ${quoteArg(join(toolsDir, 'jev-connect.mjs'))}`;
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
+ };
20
38
 
21
39
  // The vendor's smallest documented request (docs.typesafe.ai quickstart, read 2026-09-29) and the
22
40
  // meanings of its documented error codes, quoted as the vendor prints them.
@@ -1,17 +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';
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, connectLine, keySet } from './jev-facts.mjs';
9
+ import { KEY_VARIABLE, RESTART_STEP, connectLine, keySet, placeOf } 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 connected key.
14
+ const HELP = `jev — read-only: what Jev (TypeSafe) is, why it pays here, and the two steps to a set key.
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 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.`;
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.',
@@ -45,10 +45,10 @@ export const JEV_TEXT = Object.freeze([
45
45
  ]);
46
46
 
47
47
  const KEY_GET = 'Get a key at console.typesafe.ai and set it on every host the agent runs on.';
48
- const KEY_CONNECT = 'Connect it from a terminal of your own, never through the agent: the command asks for the key with no echo, checks it with one request and, for bash or zsh, saves it to your shell\'s startup files.';
49
- const KEY_RESTART = 'Then quit the agent, start it again from a new terminal, and run /agent-workflow-kit jev again: the key mark should read set.';
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
50
  const KEY_NEVER = 'Never paste the key into the chat or into a project file.';
51
- const KEY_NOT_SET = `key: ${KEY_VARIABLE} not set. A host setting that filters the environment of the agent's commands also hides it. If it still reads not set after a restart and no such setting applies, the agent may not have been started from a shell that reads its startup file: export the variable in the file the agent's launcher reads, then restart.`;
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
52
  const INSTALL = [
53
53
  'Optional, for your own code and prompts — the vendor skill:',
54
54
  'Claude Code:',
@@ -101,12 +101,17 @@ const homeOf = (io) => {
101
101
  if (io.home !== undefined) return io.home;
102
102
  try { return homedir(); } catch { return ''; }
103
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') });
104
108
  const skillPaths = (dir, home) => [...new Set([join(dir, '.claude', 'skills'), join(dir, '.agents', 'skills'),
105
109
  ...(home === '' ? [] : [join(home, '.claude', 'skills'), join(home, '.codex', 'skills'), join(home, '.cursor', 'skills')])]
106
110
  .map((root) => join(root, SKILL_FILE)))];
107
111
 
108
- const renderSteps = (found, set, connect) => [
109
- ['STEP 1 — the key', KEY_GET, KEY_CONNECT, connect, KEY_RESTART, KEY_NEVER, set ? `key: ${KEY_VARIABLE} set.` : KEY_NOT_SET],
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],
110
115
  ['STEP 2 — the vendor skill', ...INSTALL,
111
116
  ...(found.length ? found.map((path) => `skill: found at ${path}`) : NOT_SEEN), ...AFTER_INSTALL],
112
117
  ];
@@ -124,10 +129,12 @@ export const main = (argv, io = {}) => {
124
129
  const found = skillPaths(dir, homeOf(io)).filter((path) => statIs(path, 'file'));
125
130
  const env = io.env ?? process.env;
126
131
  const set = keySet(env);
127
- const connect = connectLine(io.toolsDir ?? TOOLS);
128
- const steps = renderSteps(found, set, connect);
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);
129
136
  log(parsed.json
130
- ? JSON.stringify({ schema: SCHEMA, command: 'jev', dir, text: JEV_TEXT, skill: { found }, key: { set }, connect, 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)
131
138
  : [...JEV_TEXT, ...steps.flat(), 'WHERE IT PAYS IN THIS WORKFLOW', ...PROMPTS].join('\n'));
132
139
  return 0;
133
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 { connectLine, keySet } from './jev-facts.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;
@@ -1612,11 +1612,14 @@ const probeMcpChannel = ({ root, deps, add, skip }) => {
1612
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'], `HAND-APPLY: ${connectLine(HERE)}`, '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) {