@sabaiway/agent-workflow-kit 14.16.0 → 14.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +2 -2
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/jev.md +7 -5
- package/references/modes/recommendations.md +3 -2
- package/tools/ack-store.mjs +2 -0
- package/tools/commands.mjs +1 -1
- package/tools/jev-connect.mjs +219 -0
- package/tools/jev-facts.mjs +40 -0
- package/tools/jev-guide.mjs +29 -36
- package/tools/recommendations.mjs +25 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,54 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
|
|
|
4
4
|
is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
|
|
5
5
|
every `migrations/<version>-<slug>.md` newer than it, in semver order.
|
|
6
6
|
|
|
7
|
+
## 14.18.0 — Jev connects from ONE command you run: the Recommendations item hands it over, the guide prints it, and one leaf names it (AD-165)
|
|
8
|
+
|
|
9
|
+
**While `TYPESAFE_API_KEY` is not in the environment the agent's commands run in, the Recommendations item `jev-connect`
|
|
10
|
+
now carries you to the result.** Its apply is `HAND-APPLY: node <kit>/tools/jev-connect.mjs` — a command YOU run in a
|
|
11
|
+
terminal of your own; the agent hands it over and never runs it. In 14.17.0 the apply ran the read-only guide, which
|
|
12
|
+
sets nothing: a user who ran it believed Jev was connected. The rule now: a recommendation reaches the result.
|
|
13
|
+
|
|
14
|
+
- **`jev-connect`, the one command.** It refuses without a terminal (exit 3), asks for the key with no echo, verifies
|
|
15
|
+
it with one request to api.typesafe.ai (the vendor's sample body, the key in a header from an in-process fetch —
|
|
16
|
+
never an argument, never a shell trace; the four documented error codes print the vendor's meanings), and saves it
|
|
17
|
+
as managed `export` lines in your shell's startup files: zsh → `~/.zshrc` (under `ZDOTDIR` when set); bash →
|
|
18
|
+
`~/.bashrc` and the login file bash reads (`.bash_profile`, `.bash_login` or `.profile`, the first that exists;
|
|
19
|
+
`.bash_profile` created when none; written even when it sources `.bashrc`). Then: `connected — quit the agent and
|
|
20
|
+
start it again from a new terminal`, exit 0. Every earlier line that starts `export TYPESAFE_API_KEY=` is replaced in
|
|
21
|
+
place by the managed line, so no `if` branch is emptied; a linked file or directory is never written through, nor a
|
|
22
|
+
file outside your home; a new file is created 0600.
|
|
23
|
+
Another shell, or Windows: the key is verified but not saved, the last line says what to set by hand, exit 4. A key
|
|
24
|
+
the vendor refuses: `nothing written`, exit 1. `--unverified` saves without the request. The key's value never
|
|
25
|
+
appears in any output.
|
|
26
|
+
- **The guide shrinks to two steps.** `/agent-workflow-kit jev` prints the key mark and the connect line, then the
|
|
27
|
+
optional vendor skill with its own restart line and first prompt. The shell key line, the curl line, the response
|
|
28
|
+
and the error meanings left the guide; its mode doc lost the walk — no command the guide prints is run by the agent.
|
|
29
|
+
- **One leaf, no restatement.** `tools/jev-facts.mjs` holds the key rule, the connect line and the vendor's request
|
|
30
|
+
facts; the guide, the advisor and the command import it. The advisor's WHAT now names the environment the agent's
|
|
31
|
+
commands run in, never "this host".
|
|
32
|
+
- Closes items 23–26 and 28–30 of Issue-027 (the unguarded `homedir()`, the missing restart after the skill install,
|
|
33
|
+
the key in curl's argv, `ZDOTDIR`, the `&& unset k`, the WHAT, the login bash that never reads `~/.bashrc`).
|
|
34
|
+
|
|
35
|
+
## 14.17.0 — the Recommendations screen offers the Jev walk while `TYPESAFE_API_KEY` is not set, and the walk starts with the key (AD-164 revised)
|
|
36
|
+
|
|
37
|
+
**While `TYPESAFE_API_KEY` is not in the environment the agent's commands run in, the Recommendations screen now
|
|
38
|
+
carries one optional item, `jev-connect`.** Its apply runs the read-only `/agent-workflow-kit jev` guide. Its
|
|
39
|
+
`recipe:` line is the decline: a preview of the ack writer that prints its `--apply` form; applied, it records
|
|
40
|
+
`jevConnectAck` in `docs/ai/acks.json`, and the item no longer shows for that project. With the key in the agent's
|
|
41
|
+
environment, no project shows it, and the key's value appears in no item, skip line or `--json` field.
|
|
42
|
+
|
|
43
|
+
- **The key comes first.** The walk now runs the key, the vendor skill, the first request. When `SHELL` is bash or
|
|
44
|
+
zsh, STEP 1 prints one line you run in a terminal of your own, outside the agent: it reads the key with no echo and
|
|
45
|
+
appends `export TYPESAFE_API_KEY=…` to `~/.bashrc` or `~/.zshrc`, so the key never passes through the agent or its
|
|
46
|
+
transcript. Any other shell gets a sentence to set it in an editor. Then restart the agent from a new terminal; if
|
|
47
|
+
the mark still reads `not set`, the mark's own text says to export the key in the file the agent's launcher reads,
|
|
48
|
+
then restart. `--json` gains `key.profile`.
|
|
49
|
+
- **The first request is one `curl` line.** STEP 3 prints the vendor's smallest documented request as a `curl`
|
|
50
|
+
command that reads the key from the environment, with its check (HTTP 200 and a department choice with a
|
|
51
|
+
confidence), then the three-ticket prompt. The mode doc tells the agent to run it only on your explicit yes in the
|
|
52
|
+
chat, since it sends the sample text to api.typesafe.ai, and never with a flag that prints request headers.
|
|
53
|
+
- **The guide itself is unchanged in kind:** it writes nothing, opens no connection and never prints the key's value.
|
|
54
|
+
|
|
7
55
|
## 14.16.0 — `/agent-workflow-kit jev` walks you to a first Jev request: what it is, what it is not, three steps and three prompts where it pays (AD-164)
|
|
8
56
|
|
|
9
57
|
**A new read-only command, `/agent-workflow-kit jev`, recommends Jev (TypeSafe), a decision model that returns a typed
|
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
|
|
238
|
+
| `/agent-workflow-kit jev` | any time | **read-only** guide to Jev (TypeSafe), a decision model: what it is, why it pays here, what it is not, the two steps to a connected key (the connect command, the optional vendor skill) and three prompts where it pays. Opens no connection and never prints the key. |
|
|
239
239
|
| `/agent-workflow-kit recipes` | any time | **read-only** orchestration advisor: presents five named recipes for composing the carriers of a step — the bridges and the full-tool executor subagent — into plan → execute → review — **Solo / Reviewed / Council / Delegated / Subagent** — plans + recommends one for your environment (degrading with a stated reason when a backend isn't ready, or when the executor vehicle is missing/unusable), and offers the choice. The activity/slot registry (six activities) is rendered by `procedures` and by `set-recipe --help`. The orchestrator runs it via the bridge skills or the executor vehicle and **always commits**; the kit never executes a recipe, never runs a subscription CLI, never commits. |
|
|
240
240
|
| `/agent-workflow-kit procedures <activity>` | any time | **read-only** activity-procedures advisor: prints a named activity's ordered steps (`plan-authoring` / `plan-execution` / `routine` / `feedback-triage` / `epic` / `task`) read **live** from the engine, plus the **resolved recipe per slot** from your `docs/ai/orchestration.json` (agent-writable via `set-recipe`, or hand-edited) + carrier readiness (`plan-authoring`: author, fold, review; default Reviewed when a backend is ready, Council on request; slot-aware incl. Delegated and Subagent) — and, for every dispatched backend, the **full driving contract at the point of use** (exact copy-pasteable invocation, grounding levers like agy's `--facts`/`--decided`, the round-2 `--continue` delta, guarded passthrough), verbatim from the bridge manifests (drift-guarded; each wrapper's `--help` prints the same) — except `feedback-triage`, whose record-bound block carries the check with `--excerpts`, the resolved member lines (ready bridge commands, the ready lens brief, or explicit skipped members), and either the record round-table command for a bridge obligation or the no-receipt fact otherwise: the record is the artifact. `--override <slot>=<value>` adjusts one slot per run. Composes with `recipes`; never writes, never commits, never runs a subscription CLI. |
|
|
241
241
|
| `/agent-workflow-kit set-recipe` | any time | **config writer** for `docs/ai/orchestration.json`: tell the agent your preference in plain language and it maps it to explicit `--set <activity>.<slot>=<value>` / `--unset` ops; the kit validates, **previews by default**, and writes only on `--write` (deployment-gated, atomic, symlink/TOCTOU-safe), resolving the effective recipe vs live readiness. Writes **only** that file — **never runs a backend or a subagent, never commits**; hand-editing stays fully supported. |
|
|
@@ -246,7 +246,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
|
|
|
246
246
|
| `/agent-workflow-kit core-evidence` | any time | **the ONE loop-evidence writer** (strip-the-kit) — every core evidence record lands in a single append-only JSONL store inside the git dir (never committable; versioned schema, latest-per-key supersession, byte-identical duplicates refused, malformed lines fail every reader closed). `red-proof "<file>#<pattern>"` declares an observed-red **BEFORE a bugfix** (N/N red runs + content custody + base + the pre-fix fingerprint; green/mixed/timeout are DISTINGUISHED refusals — nothing written); `degrade --backend --reason` is the ONLY escape for an unavailable review backend (per-tree, never all backends); `summary` renders the whole loop state statelessly (gate result · per-backend verdicts · red-proofs · degrades) — no ledger, no rounds, nothing remembered. Honest residual: records are forgeable — self-discipline, not a security boundary. Never commits, never runs a subscription CLI. |
|
|
247
247
|
| `/agent-workflow-kit coverage-check` | any time | **the final-run checker** (D3(c)+(d)) — **certifies coverage ONLY inside the `--final` run that owns the lcov** (ownership is exclusive by CONVENTION over the fixed path, not enforced — a concurrent writer to it is a stated residual, queued as LCOV-EXCLUSIVE-OWNERSHIP): an artifact on disk proves nothing about the tree it came from, so a standalone run prints its findings and states `attested=no` / `NO VERDICT` rather than a PASS (an lcov that predates an edit would otherwise certify a line the suite never executed). `attested=` claims a verdict was ISSUED, pass or fail — a run over uncovered lines still reads `attested=yes` and still exits 1, and a run whose coverage arm never executed (no lcov) reads `attested=no` even inside `--final`, because nothing was read and nothing is certified. The runner passes a nonce whose one-way commitment over `{nonce, fingerprint, base}` is the `final-start.attempt` it recorded; a context describing another tree, or matching no recorded attempt, is a REFUSAL, never a verdict. Findings are unchanged — reads the lcov the declared `unit-tests` gate produced at the FIXED git-dir path and fails on any uncovered CHANGED executable Node line (listed `file:line`; a changed file absent from the map is a file-level red; out-of-domain/unsupported files are LISTED — the claim narrowed honestly); VERIFIES every current-base red-proof declaration (bound test exists · custody hash unchanged · green N/N now · pre-fix fingerprint differs); prints `lcov-sha256=<hex|none>` of the exact bytes it consumed — the sha the `--final` receipt binds and re-hashes. An absent lcov is a LOUD `skipped-no-lcov`; a symlinked path is a refusal. `--check` is the gate exit code — declare it as the LAST gate (`run-gates --final` refuses otherwise). Read-only. |
|
|
248
248
|
| `/agent-workflow-kit commit-guard` | any time | **the read-only pre-commit guard** (D10) — makes the commit capture the whole current working tree, so «verified» and «about to be committed» are the same bytes (the receipt itself has a stated residual — see the mode doc). FIRST it refuses an **INDEX that lags the verified working tree** (the gates and the fingerprint describe the WORKING tree while `git commit` takes the INDEX alone, and the fingerprint cannot tell them apart — so a lagging index used to ship a strict subset of what was verified): unstaged tracked paths or reviewable untracked-not-ignored paths, named up to a bounded cap with the remainder stated, a dirty tracked **submodule** named separately with its own recovery, and fail-closed on an undecidable probe. This deliberately blocks a partial commit. Then it binds the LATEST completed `run-gates --final` receipt to the EXACT current tree: refuses on a missing/red/stale receipt, fingerprint drift under the run, a dangling later attempt, declaration content drift, evidence-hash or lcov drift, or unsatisfied review obligations (the same review-state decision, recomputed over a sanitized env — forged out-of-repo stores never satisfy). Re-runs NO gate or test. Wire it into `.git/hooks/pre-commit` (the installer writes the RESOLVED invocation). `git commit --no-verify` stays the stated residual. |
|
|
249
|
-
| `/agent-workflow-kit recommendations` | any time (every `upgrade` ends with it) | **read-only deployment advisor** (AD-044) — computes what in THIS deployment is configured sub-optimally (allowlist not seeded, autonomy render drifted, OS sandbox unavailable, gates undeclared, bridge friction, sandbox-mask clutter, an unacknowledged sandbox recipe, a feature-spec layer never adopted) and renders **verdict-first**: one composed verdict line (does anything need attention?), then each item as **{severity · what · one-line benefit · an optional `recipe:` line (the sandbox-lane live recipe, the worktrees-dir hand-apply-first grant advice, the agents hidden-mode reconcile follow-up,
|
|
249
|
+
| `/agent-workflow-kit recommendations` | any time (every `upgrade` ends with it) | **read-only deployment advisor** (AD-044) — computes what in THIS deployment is configured sub-optimally (allowlist not seeded, autonomy render drifted, OS sandbox unavailable, gates undeclared, bridge friction, sandbox-mask clutter, an unacknowledged sandbox recipe, a feature-spec layer never adopted) and renders **verdict-first**: one composed verdict line (does anything need attention?), then each item as **{severity · what · one-line benefit · an optional `recipe:` line (the sandbox-lane live recipe, the worktrees-dir hand-apply-first grant advice, the agents hidden-mode reconcile follow-up, the spec-adoption decline preview, or another offer's decline preview) · the exact consent-gated apply one-liner}**. The agent PRESENTS the section in the user's conversational language — every fact and count, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; raw tool block on request — and runs EXACTLY the rendered one-liners only on your yes, surfacing each item's posture note first. Renders **present-even-when-empty** (`no recommendations — flow optimal.`); a failed probe degrades to a stated skip line. Registry strings are fact-true frozen one-line data (posture/risk notes live in the mode doc at the consent moment); the kit never seeds `sandbox.network.allowedDomains` / `filesystem.allowWrite` (**HAND-APPLY** territory), and the sandbox-lane item's convergence is a neutral fingerprint acknowledgement recorded by a consent-gated ack writer into `docs/ai/acks.json` — never a security key (the recipe is documented per bridge in `capability.json` `networkHosts` + `writableDirs`). `--cwd` is required (the target project is explicit); never writes, never commits, never runs a subscription CLI. |
|
|
250
250
|
| `/agent-workflow-kit doc-parity` | any time | **read-only doc-parity lint** (AD-049) — kills the doc-drift class where a mode-contract doc silently lags a code constant (a `--check` doc still reading `300` after the diff cap moved to `400`): a **closed, exported registry** binds each live constant (review caps, schema versions, the ledger's own class/scope vocabulary, and the autonomy-doctor EXIT/status/trusted-dir contract) to the exact token its `references/modes/*.md` contract must carry, and asserts the CURRENT value renders into every bound file — a drifted doc, an unreadable file, or an absent token **fails closed**. The values are sourced from the live imports (never re-typed), so the lint can't itself go stale; adding a binding is adding a checked entry (closed-world, edit-safe). `--check` is the gate exit code a kit maintainer runs: the lint checks the kit's own package. Never writes, never commits, never runs a subscription CLI. |
|
|
251
251
|
| `/agent-workflow-kit dispatch` | any time | **the delegation engine** — makes "how much does delegating a sub-task actually buy?" a measured number instead of a feeling. `check <file>` validates a sub-task brief's contract block and exits 0/1 naming the first violated field — **form only**: whether the task is genuinely bounded, its design decided and its acceptance adequate stays your judgment, and a well-formed absurdity passes here by construction. `advise --step-class <c>` answers the question that comes BEFORE that one — which vehicle should carry this kind of sub-task, whether it is even present on this machine, and what the ledger has already recorded for threads of that class (finished, failed, closed without a fold, and still open — counted separately) — and it decides nothing: it refuses no dispatch, gates no verb, and a choice that diverges from the advice is noted rather than blocked. It reads the filesystem only: nothing is launched to find out what is installed, an absent ledger prints "no recorded history", an unreadable one prints the store's own words, and either way the advice still prints. The same block appears as a footer under a form-valid `check` — never under a refusal, so it can never hide one. `register` pre-registers an acceptance wave (step classes, pairing key, minimum observations per class, the mean and first-pass thresholds) so the bar can never be chosen after the results it judges — immutable per wave. `observe` records ONE hand-written observation (`solo-construction`, the baseline whose ratio is 1 by construction — except over a scope measuring zero bytes, which has no ratio at all and is recorded ineligible by name — or `self-reported`) — delegated numbers are never hand-written, they are derived. The writer verbs are where that derivation happens: `open` puts a delegated thread on the record with every mint-time field copied from the brief's own header, refusing a deadline the wrapper's cap plus its kill grace would not fit inside, and recording whether the tree it started from was clean against the thread's base — HEAD by default, or the tree `--checkpoint <oid>` names, so the second task of a story is measured over the first one's uncommitted work instead of opening dirty (a dirty one makes the result honestly unmeasurable rather than quietly counted), while `--task <brief>` also records which task brief the thread carries out and the files that brief lists, refusing, since the last commit, a task thread whose files overlap by path prefix those of an open task thread or of a folded thread of another brief (`files-overlap`), and a mix of the two kinds — an untasked open beside an open task thread or, for a `code` open of backend `codex`, after a task fold, and a task open beside an open untasked thread (`mixed-open`), refusing at open a Files path the snapshot scope can never hold (`task-files-scope`), and measuring a task thread over its files alone, so `return` and `fold` refuse a changed path that no open or folded task thread on the same base claims (`out-of-files`); `await` waits for that one dispatch to answer and writes nothing — only the finished receipt satisfies it, a run still holding the nonce means keep waiting, and a wait that runs out says so with its own exit code, names whether the deadline or your own timeout ended it, and authorizes nothing: a wait that ended without an answer is a question for you, never permission to dispatch again; `return` absorbs the receipt the wrapper minted — only a finished one, checked against the contract it actually ran, its deadline and its own report, and refused outright when the tree hides changes from git or moves while the return is being computed — and derives the bytes from git rather than from anyone's claim; `fold` is the acceptance, and it refuses if the tree moved since the return — precisely, it re-confirms **equality of the visible canonical payload**, which is a change detector rather than a cryptographic identity of the tree: under the default head base that payload's untracked section is unframed and carries no file mode, so a content or symlink target that imitates the marker opening the next entry can alias two trees, and making an untracked file executable moves nothing (both named as residuals in the mode doc, with the fix queued); under a checkpoint base it is one indexed diff that carries modes. A change set carrying a binary, a non-regular path, a submodule, or a symlink whose target is not valid UTF-8 is **refused** at `return` and `fold` — CONSERVATIVELY, rather than promise what the default head payload may not be able to follow — and the same refusals hold for a checkpoint thread. `degrade` closes a thread that never earned a fold, on the record and with its reason. `aggregate` prints one wave: the registered thresholds, every observation as context, and per step class the finished threads — a folded success with git-provable bytes contributes its ratio, one whose bytes are unprovable is excluded from the mean but still counts in the first-pass rate, and a failed thread, a degrade-closed one, or one folded after failing acceptance each count as a real zero. Below the registered minimum nothing is computed (insufficient); at or above it is computed and labeled PILOT evidence. It REFUSES rather than guessing: no pre-registration, an unfinished thread in scope, a recorded refusal-to-delegate that never opened a thread, or an ambiguous wave each stop the computation by name. `handoff-return --slug <s>` closes the worktree loop after `land --prepare`: it re-attests that main still holds exactly the prepared tree under the same HEAD (both recorded in the handoff at prepare time), prints the satellite handoff's user-owned content byte for byte with its boundaries, byte lengths and the main-owned destinations each part folds into, prints the handoff digest and both OIDs as its proof, states the after-the-fold order (a fold landed after the gates leaves them stale), and records ONE self-reported worktree-stream observation only when the whole prepared change set is measurable — a deletion, a rename, a symlink, a submodule or a mode-only change ends with a named NOT RECORDED instead, never a partial number. Appends only to its own ledger inside the git dir (never committable); it also writes `handoff-return`'s `write-tree` tree object and, for a checkpoint measurement, the leaf's OWN temporary index file and the blobs its forced adds store — never a ref, never the real index; never commits, never runs a subscription CLI. |
|
|
252
252
|
| `/agent-workflow-kit worktrees` | any time | **parallel feature worktrees** — run several features in DIFFERENT agent sessions on one repo, zero interference on working-tree files (the ONE exception is the dependency cache, below): `provision <slug> --plan <file>` creates a sibling git worktree on branch `aw/<slug>` and populates it (registry-derived footprint copy-if-missing — a tracked file is never overwritten; EXACTLY ONE seeded feature plan; the `handoff-<slug>.md` record from minute zero; `node_modules` symlinked where the link stays ignored — a shared MUTABLE dependency cache: writes through it hit MAIN's node_modules; for isolation run the printed isolated-install command (`--install` only PRINTS it; on `--resume` run the printed unlink-first recovery first — printed only for a link whose RAW TARGET is MAIN's `node_modules`, since a foreign or unreadable link is reported as such with no removal advised); absolute root-pinned gate commands rebased on untracked copies only, and only while their bytes equal the MAIN source or its rebased form — user-modified copies stay untouched); `list` is read-only (slug, branch, base, dirty, handoff); `prompt <slug>` is read-only too and re-prints the satellite's cold-start prompt — the same text `provision` ends its report with: where the worktree is, its ONE seeded plan, the handoff as the one channel back, MAIN's orientation (shared series index, landing) and, under its own heading because it is probed on the satellite, that checkout's install posture — every value derived LIVE rather than replayed from the frozen provision record, a recorded value that no longer matches NAMED as a divergence (with the cause its source makes likely) instead of printed as the runnable one, every offered command marked with WHO runs it (`MAIN $ …` for the landing, `HERE $ …` for this checkout's own install, a posture with nothing to run staying prose), and a control character in any rendered value a typed STOP rather than a forged prompt line; `land <slug> --prepare` locks the common git dir, fail-closes on divergence or incomplete satellite state, transfers the complete accepted satellite diff onto a CLEAN main, runs sync plus the declared gates, and reports HEAD/TRANSFER/PREPARED OIDs — the commit ALWAYS stays a dialogue ask; `cleanup <slug>` takes the same lock and removes a LANDED worktree only after live landed-verification against main HEAD, while `--abandon` is the ONE destructive arm (destroys unlanded work; **no preview step** on any writer). The parent dir is the `docs/ai/worktrees.json` `parentDir` setting (default: the repo's sibling parent); an unwritable parent degrades to printed maintainer-pasted commands, and the one-time host consent that makes it promptless surfaces via `recommendations`. Never commits, never pushes, never runs a subscription CLI. |
|
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.
|
|
6
|
+
version: '14.18.0'
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# agent-workflow-kit
|
package/capability.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-kit",
|
|
3
|
-
"version": "14.
|
|
3
|
+
"version": "14.18.0",
|
|
4
4
|
"description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
package/references/modes/jev.md
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
### Mode: jev
|
|
2
2
|
|
|
3
|
-
<!-- opt-in-capability:
|
|
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
|
|
5
|
+
Read-only. The Jev guide: what Jev (TypeSafe) is, why it pays in this workflow, what it is not, the two steps to a connected key and three prompts where it pays. The guide writes no file, opens no connection, never prints the key's value and never commits.
|
|
6
6
|
|
|
7
7
|
Run `node ${CLAUDE_SKILL_DIR}/tools/jev-guide.mjs --dir <project> --json`.
|
|
8
8
|
|
|
9
|
-
Render the returned envelope compactly in the user's conversational language. Never paste the JSON. Keep every path and every command verbatim, each command on its own line so it runs as printed; translate only the prose around them. Keep
|
|
9
|
+
Render the returned envelope compactly in the user's conversational language. Never paste the JSON. Keep every path and every command verbatim, each command on its own line so it runs as printed; translate only the prose around them. Keep the one vendor-quoted text verbatim and in quotes, never translated or reflowed: the not-a-replacement sentence.
|
|
10
10
|
|
|
11
|
-
|
|
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
|
+
|
|
13
|
+
Keep this order: the fixed text (WHAT, WHY HERE, NOT, SOURCES); then the two steps — the key with its connect line and its mark, the vendor skill with its install lines, its mark and its first prompt; then the three prompts where it pays.
|
|
12
14
|
|
|
13
15
|
Exit codes:
|
|
14
16
|
|
|
@@ -18,4 +20,4 @@ Exit codes:
|
|
|
18
20
|
| 1 | Nothing rendered: a stat of the `--dir` does not report a directory; the cause is printed on stderr. |
|
|
19
21
|
| 2 | Usage: an unknown argument, a `--dir` without a value, or `--help` / `-h` combined with anything. |
|
|
20
22
|
|
|
21
|
-
**Invariants:** read-only
|
|
23
|
+
**Invariants:** the guide is read-only and opens no connection · the connect line is run by the user in a terminal of their own, never by the agent · the key's value never printed · compact user-language render with paths, commands and vendor quotes verbatim · process.exitCode, never process.exit().
|
|
@@ -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) 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) 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,
|
|
@@ -17,7 +17,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root> [--
|
|
|
17
17
|
|
|
18
18
|
1. **`--cwd` is REQUIRED** — the target project is explicit, never inferred from the shell's current directory (a subdirectory invocation still advises on the named root).
|
|
19
19
|
2. **Verdict first (present-even-when-empty, opening at the `## Recommendations (agent-workflow)` header).** With everything optimal the body is exactly `no recommendations — flow optimal.` — the empty-state line ALONE is the verdict, zero added lines. Every other state opens the body with ONE verdict line composed from the frozen templates: `{K} item(s) need attention` leads when any item's severity is `attention` (a CONFIGURED declaration that is broken, drifted, degrading or invalid — the deployment needs review); `{N} optional recommendation(s), apply any you want` covers the `optional` class (offers to enable an unconfigured capability), led by `nothing is broken` ONLY when no item needs attention AND no probe check was skipped (a skipped probe could hide an attention-class problem — the claim never overreaches); `optimality NOT attested — {M} probe check(s) skipped` is appended last whenever probe checks were skipped. (The `(s)` invariant form IS the pinned pluralization — no singular/plural branching.) Items render attention-first, each tagged `needs attention:` / `optional:`. A failed probe renders as a stated `⚠ skipped item …` line — never a crash, never a fabricated item.
|
|
20
|
-
3. **The apply-through-agent lane — an explicit informed-consent checkpoint:** after presenting, OFFER to apply; the user selects items in plain language; for each selected item you **surface its posture note inline** where one exists (the per-item notes below cover exactly the risk-marked keys; for `sandbox-lane` the note INCLUDES the sandbox-lanes ladder — present the whole ladder inline at the consent moment, never as a bare pointer). The user **explicitly confirms**, and **only then** — **no command runs before confirmation** — do you run the rendered command. Do NOT infer safety from the presence or absence of an `--apply` flag: most items' rendered command IS the mutation and completes on that one run, **including a no-`--apply` mutation** such as `family-freshness`'s `npx … init`. Some items instead render a **dry-run preview** that changes nothing and prints an explicit follow-up `--apply`/mutating command to run NEXT (e.g. `sandbox-lane`'s ack-write — a NEUTRAL recipe fingerprint into the family-owned `docs/ai/acks.json`, never a security key — and the `gates-declaration` seeder): after the SAME confirmation you run that printed follow-up command — no second ask, no improvisation (each writer keeps its own consent semantics: previews, `--apply` flags, refusals). An item marked **HAND-APPLY** (e.g. the `read-lane` stale/missing arms) is **never run by you and never written by the kit** — hand the user its rendered line together with its posture note; HAND-APPLY is **maintainer territory wherever it renders** — the apply slot or the `recipe:` line. For `worktrees-dir` the two classes COMPOSE in one item and the ORDER is part of the contract: its `recipe:` line is the HAND-APPLY grant advice, its apply one-liner is the dry-run ack preview, and you **wait for the maintainer to confirm the grant is applied (or the terminal fallback chosen)** before running that preview and the exact `--apply` command it prints — the ack RECORDS a choice that must already have been made, and against a trusted host NO the item renders no ack at all.
|
|
20
|
+
3. **The apply-through-agent lane — an explicit informed-consent checkpoint:** after presenting, OFFER to apply; the user selects items in plain language; for each selected item you **surface its posture note inline** where one exists (the per-item notes below cover exactly the risk-marked keys; for `sandbox-lane` the note INCLUDES the sandbox-lanes ladder — present the whole ladder inline at the consent moment, never as a bare pointer). The user **explicitly confirms**, and **only then** — **no command runs before confirmation** — do you run the rendered command. Do NOT infer safety from the presence or absence of an `--apply` flag: most items' rendered command IS the mutation and completes on that one run, **including a no-`--apply` mutation** such as `family-freshness`'s `npx … init`. Some items instead render a **dry-run preview** that changes nothing and prints an explicit follow-up `--apply`/mutating command to run NEXT (e.g. `sandbox-lane`'s ack-write — a NEUTRAL recipe fingerprint into the family-owned `docs/ai/acks.json`, never a security key — and the `gates-declaration` seeder): after the SAME confirmation you run that printed follow-up command — no second ask, no improvisation (each writer keeps its own consent semantics: previews, `--apply` flags, refusals). An item marked **HAND-APPLY** (e.g. the `read-lane` stale/missing arms) is **never run by you and never written by the kit on your behalf** (a kit command the user runs in a terminal of their own is the user's own write) — hand the user its rendered line together with its posture note; HAND-APPLY is **maintainer territory wherever it renders** — the apply slot or the `recipe:` line. For `worktrees-dir` the two classes COMPOSE in one item and the ORDER is part of the contract: its `recipe:` line is the HAND-APPLY grant advice, its apply one-liner is the dry-run ack preview, and you **wait for the maintainer to confirm the grant is applied (or the terminal fallback chosen)** before running that preview and the exact `--apply` command it prints — the ack RECORDS a choice that must already have been made, and against a trusted host NO the item renders no ack at all.
|
|
21
21
|
4. Registry strings (benefits + item texts) are frozen tool data, fact-true, ONE line under the pinned shape cap — posture/risk detail lives in the notes below at the consent moment, never inline in the overview. The dual velocity+security wording (`safer — blast radius bounded by the OS sandbox, not human attention`) rides ONLY the items with a real security delta (the autonomy render, the sandbox provisioning); the bridge-wrappers item claims **velocity only**.
|
|
22
22
|
|
|
23
23
|
**The `executor-vehicle` item (the carrier the project already CONFIGURED — beside the `agents` offer, never the same item).** It fires on ONE pair of facts: a slot in `docs/ai/orchestration.json` names `subagent` while the executor vehicle (`.claude/agents/executor.md`) is `missing` or `unusable` — so every such slot silently degrades to solo. That is a configured declaration that is broken, hence **`attention`**, not an offer; its text names the state, its reason and how many configured slots are affected across the six activities (`plan-authoring`, `plan-execution`, `routine`, `feedback-triage`, `epic`, `task`), covering the subagent-capable slots `plan-authoring.author`, `plan-authoring.fold`, `plan-execution.execute`, `routine.carrier`, `epic.author`, `task.author` and `task.execute`. **`placed` and `customized` render NOTHING** (a working vehicle is not a recommendation), a config naming no `subagent` slot renders nothing, and a config the validated reader refuses is a stated **skip**, never an item computed over a shape nothing accepted. It is deliberately DISTINCT from its two neighbours, neither of which can observe this state: the `agents` offer converges the moment nothing is left to PLACE — which a present-but-unusable executor also satisfies — and the review-recipe probe skips a slot configured `subagent` by construction. Its apply is the agents writer's own `cheap-agents.mjs --apply` (the same writer the `agents` item previews; here the rendered line IS the mutation, so read the item's state before confirming), and its `recipe:` line is the hidden-mode reconcile follow-up (`hide-footprint --reconcile`), for a hidden deployment only. Honest limit on convergence: the apply places an ABSENT vehicle, so it clears `missing`; it STOPs on a symlink, so on `unusable` the item's apply line carries a `HAND-APPLY:` precondition naming each refusal the advisor observes; an executor whose frontmatter is invalid is re-derived by the apply itself. Risk profile: writes under `.claude/agents/` only — the executor's body re-derived from `docs/ai/vehicles.json`, every other vehicle create-only — never `settings.json`, never a commit.
|
|
@@ -38,6 +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
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.
|
|
42
43
|
|
|
43
44
|
**Sandbox lanes (what to DO with the `sandbox-lane` recipe, per host class):**
|
package/tools/ack-store.mjs
CHANGED
|
@@ -14,6 +14,7 @@ export const ACKS_WORKTREES_DIR_KEY = 'worktreesDirAck';
|
|
|
14
14
|
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
|
+
export const ACKS_JEV_CONNECT_KEY = 'jevConnectAck';
|
|
17
18
|
|
|
18
19
|
// The CLOSED-WORLD ack-lane registry: the lane name an advisor item renders on the writer's command line ->
|
|
19
20
|
// the store key that writer sets. A lane the registry does not name is a usage refusal at the writer.
|
|
@@ -23,6 +24,7 @@ export const ACK_LANES = Object.freeze({
|
|
|
23
24
|
'coverage-domain': ACKS_COVERAGE_DOMAIN_KEY,
|
|
24
25
|
'source-size-copy': ACKS_SOURCE_SIZE_COPY_KEY,
|
|
25
26
|
'spec-adoption': ACKS_SPEC_ADOPTION_KEY,
|
|
27
|
+
'jev-connect': ACKS_JEV_CONNECT_KEY,
|
|
26
28
|
});
|
|
27
29
|
|
|
28
30
|
export const FINGERPRINT_LENGTH = 16;
|
package/tools/commands.mjs
CHANGED
|
@@ -103,7 +103,7 @@ const CATALOG = [
|
|
|
103
103
|
invocation: invocationOf('jev'),
|
|
104
104
|
group: 'Inspect',
|
|
105
105
|
kind: READ_ONLY,
|
|
106
|
-
oneLine: 'Guide to Jev (TypeSafe), a decision model: what it is, why it pays here, the steps to a
|
|
106
|
+
oneLine: 'Guide to Jev (TypeSafe), a decision model: what it is, why it pays here, the two steps to a connected key and three prompts where it pays; the guide opens no connection.',
|
|
107
107
|
},
|
|
108
108
|
{
|
|
109
109
|
key: 'backends',
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// spec:jev-guide — docs/ai/specs/kit/jev-guide/jev-connect.md
|
|
3
|
+
// The one command that connects Jev (TypeSafe) on a host. The USER runs it in a terminal of their
|
|
4
|
+
// own: it reads the key with no echo, verifies it with one request, saves it as managed export lines
|
|
5
|
+
// in the shell's startup files and says to restart the agent. It never prints the key, never puts it in
|
|
6
|
+
// an argv, and refuses when stdin is not a terminal, so a non-interactive run (the agent's) cannot
|
|
7
|
+
// reach the prompt — the mode doc keeps the rule that the agent never runs it.
|
|
8
|
+
import { lstatSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { basename, dirname, isAbsolute, join, relative, sep } from 'node:path';
|
|
11
|
+
import { fail } from '../references/scripts/markdown-blocks.mjs';
|
|
12
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
13
|
+
import { JEV_ENDPOINT, JEV_ERROR_MEANINGS, JEV_SAMPLE_BODY, KEY_VARIABLE } from './jev-facts.mjs';
|
|
14
|
+
|
|
15
|
+
export const EXIT = Object.freeze({ saved: 0, notVerified: 1, usage: 2, noTerminal: 3, notSaved: 4 });
|
|
16
|
+
export const MARK = '# agent-workflow jev';
|
|
17
|
+
export const TIMEOUT_MS = 15000;
|
|
18
|
+
const LOGIN_FILES = ['.bash_profile', '.bash_login', '.profile'];
|
|
19
|
+
export const TERMINAL_SENTENCE = 'run this in a terminal of your own: it asks for the key there, so the agent never sees it';
|
|
20
|
+
export const CONNECTED = 'connected — quit the agent and start it again from a new terminal';
|
|
21
|
+
export const SAVED_IN_PART = 'saved in part — add the export line by hand where a line above says not saved, then restart the agent';
|
|
22
|
+
export const NOT_SAVED_SENTENCE = `not saved — set ${KEY_VARIABLE} to your key in your shell's startup file, in an editor and in that shell's own syntax, then restart the agent`;
|
|
23
|
+
export const EDITOR_SENTENCE = `verified, ${NOT_SAVED_SENTENCE}`;
|
|
24
|
+
const HELP = `jev-connect — connects Jev (TypeSafe) on this host; run it in a terminal of your own.
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
node jev-connect.mjs [--unverified]
|
|
28
|
+
|
|
29
|
+
Asks for ${KEY_VARIABLE} with no echo, verifies the key with one request to api.typesafe.ai, saves it
|
|
30
|
+
as managed export lines in your shell's startup files (bash: ~/.bashrc and the login file bash
|
|
31
|
+
reads; zsh: ~/.zshrc, under ZDOTDIR when set) and says to restart the agent. The key is never printed and never put in an argument.
|
|
32
|
+
|
|
33
|
+
--unverified save without the request (no network from here).
|
|
34
|
+
--help answered only when it is the whole invocation.
|
|
35
|
+
|
|
36
|
+
Exit codes: 0 saved; 1 not verified, nothing written; 2 usage; 3 no terminal, aborted or no key,
|
|
37
|
+
nothing written; 4 not saved (another shell, Windows, a linked startup file or directory) — the
|
|
38
|
+
last line says what to set by hand.`;
|
|
39
|
+
|
|
40
|
+
const parseArgv = (argv) => {
|
|
41
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
42
|
+
if (argv.length !== 1) throw fail(EXIT.usage, '--help is answered only when it is the whole invocation');
|
|
43
|
+
return { help: true };
|
|
44
|
+
}
|
|
45
|
+
return argv.reduce((parsed, arg) => {
|
|
46
|
+
if (arg === '--unverified') return { ...parsed, unverified: true };
|
|
47
|
+
throw fail(EXIT.usage, `unknown argument "${arg}" — run with --help`);
|
|
48
|
+
}, { help: false, unverified: false });
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// The no-echo read: raw mode on a terminal stdin, restored on every path. Resolves the first line
|
|
52
|
+
// (CR, LF or CRLF ends it; bytes after it are dropped), null on Ctrl-C / Ctrl-D.
|
|
53
|
+
export const readSecret = (prompt, { stdin, stdout }) => new Promise((resolve, reject) => {
|
|
54
|
+
let buffer = '';
|
|
55
|
+
const finish = (fn) => {
|
|
56
|
+
stdin.removeListener('data', onData);
|
|
57
|
+
try { stdin.setRawMode(false); } catch { /* the terminal is gone; the value still resolves */ }
|
|
58
|
+
stdin.pause();
|
|
59
|
+
stdout.write('\n');
|
|
60
|
+
fn();
|
|
61
|
+
};
|
|
62
|
+
const onData = (chunk) => {
|
|
63
|
+
for (const char of String(chunk)) {
|
|
64
|
+
if (char === '\u0003' || char === '\u0004') return finish(() => resolve(null));
|
|
65
|
+
if (char === '\r' || char === '\n') return finish(() => resolve(buffer.trim()));
|
|
66
|
+
if (char === '\u007f' || char === '\b') buffer = buffer.slice(0, -1);
|
|
67
|
+
else if (char >= ' ') buffer += char;
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
};
|
|
71
|
+
try {
|
|
72
|
+
stdout.write(prompt);
|
|
73
|
+
stdin.setRawMode(true);
|
|
74
|
+
stdin.setEncoding('utf8');
|
|
75
|
+
stdin.on('data', onData);
|
|
76
|
+
stdin.resume();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
finish(() => reject(error));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const scrub = (text, key) => String(text).split(key).join('<key>');
|
|
83
|
+
|
|
84
|
+
// One request: the vendor's sample body, the key in a header, no redirect, a bounded wait. The
|
|
85
|
+
// verdict is a line to print; `ok` says whether the save may follow. Nothing here writes.
|
|
86
|
+
export const verifyKey = async (key, io = {}) => {
|
|
87
|
+
const fetchImpl = io.fetch ?? globalThis.fetch;
|
|
88
|
+
const timeoutMs = io.timeoutMs ?? TIMEOUT_MS;
|
|
89
|
+
let response;
|
|
90
|
+
try {
|
|
91
|
+
response = await fetchImpl(JEV_ENDPOINT, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
|
|
94
|
+
body: JSON.stringify(JEV_SAMPLE_BODY),
|
|
95
|
+
redirect: 'error',
|
|
96
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const cause = error?.name === 'TimeoutError' ? `no answer within ${timeoutMs / 1000} s` : (error?.cause?.message ?? error?.message ?? String(error));
|
|
100
|
+
return { ok: false, line: `not verified: ${scrub(cause, key)}` };
|
|
101
|
+
}
|
|
102
|
+
if (response.status !== 200) {
|
|
103
|
+
const meaning = JEV_ERROR_MEANINGS[response.status];
|
|
104
|
+
return { ok: false, line: `not verified: HTTP ${response.status}${meaning ? ` — ${meaning}` : ''}` };
|
|
105
|
+
}
|
|
106
|
+
let body;
|
|
107
|
+
try { body = await response.json(); } catch { return { ok: false, line: 'not verified: HTTP 200 with an answer that is not JSON' }; }
|
|
108
|
+
const answer = body?.answers?.department;
|
|
109
|
+
if (typeof answer?.choice !== 'string' || typeof answer?.confidence !== 'number') {
|
|
110
|
+
return { ok: false, line: 'not verified: HTTP 200 without a department choice and a confidence' };
|
|
111
|
+
}
|
|
112
|
+
return { ok: true, line: `verified: HTTP 200 — department ${scrub(answer.choice, key)}, confidence ${answer.confidence}` };
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// The startup files the key goes to, by a closed rule over the shell: zsh reads ~/.zshrc (under
|
|
116
|
+
// ZDOTDIR when set); an interactive bash reads ~/.bashrc and a login bash the first of
|
|
117
|
+
// .bash_profile, .bash_login, .profile that exists — created as .bash_profile when none does. Both
|
|
118
|
+
// bash files always: no file content decides the set, so a login file's own older export is
|
|
119
|
+
// rewritten too. Any other shell, and Windows, gets no target.
|
|
120
|
+
export const targetsFor = ({ shell, home, zdotdir, platform = 'linux' }, deps = {}) => {
|
|
121
|
+
const exists = deps.exists ?? ((path) => { try { lstatSync(path); return true; } catch { return false; } });
|
|
122
|
+
if (platform === 'win32' || typeof home !== 'string' || home === '') return [];
|
|
123
|
+
const name = typeof shell === 'string' ? basename(shell) : '';
|
|
124
|
+
if (name === 'zsh') return [join(typeof zdotdir === 'string' && zdotdir !== '' ? zdotdir : home, '.zshrc')];
|
|
125
|
+
if (name !== 'bash') return [];
|
|
126
|
+
const login = LOGIN_FILES.map((file) => join(home, file)).find(exists) ?? join(home, LOGIN_FILES[0]);
|
|
127
|
+
return [join(home, '.bashrc'), login];
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const managedLine = (key) => `export ${KEY_VARIABLE}='${key.replace(/'/g, `'\\''`)}' ${MARK}`;
|
|
131
|
+
const isExportLine = (line) => new RegExp(`^\\s*export\\s+${KEY_VARIABLE}=`).test(line);
|
|
132
|
+
|
|
133
|
+
// Replace matching export lines in place, their indent and CR kept, so no if/case branch is
|
|
134
|
+
// emptied; append when none match.
|
|
135
|
+
export const upsertManagedLine = (text, line) => {
|
|
136
|
+
const lines = text.split('\n');
|
|
137
|
+
if (lines.some(isExportLine)) {
|
|
138
|
+
return lines.map((item) => (isExportLine(item)
|
|
139
|
+
? `${item.match(/^\s*/)[0]}${line}${item.endsWith('\r') ? '\r' : ''}` : item)).join('\n');
|
|
140
|
+
}
|
|
141
|
+
const body = text.length === 0 || text.endsWith('\n') ? text : `${text}\n`;
|
|
142
|
+
return `${body}${line}\n`;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// The directories between the home and the target, each lstat'ed: a link among them is never written
|
|
146
|
+
// through (a stow'd ZDOTDIR is a dotfiles repo); a target outside the home is not written at all.
|
|
147
|
+
const parentRefusal = (path, home, lstat) => {
|
|
148
|
+
const rel = relative(home, dirname(path));
|
|
149
|
+
if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return `${path} is outside the home`;
|
|
150
|
+
let dir = home;
|
|
151
|
+
for (const part of rel.split(sep).filter(Boolean)) {
|
|
152
|
+
dir = join(dir, part);
|
|
153
|
+
if (lstat(dir).isSymbolicLink()) return `${path} — ${dir} is a link`;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// One line per target: `saved: <path>` or `not saved: <path> — <why>`. A link — the target or a
|
|
159
|
+
// directory below the home — or a non-regular entry is never written through; a new file is created
|
|
160
|
+
// 0600; an existing file keeps its mode.
|
|
161
|
+
export const saveKey = (key, targets, home, deps = {}) => targets.map((path) => {
|
|
162
|
+
const lstat = deps.lstat ?? lstatSync;
|
|
163
|
+
const read = deps.read ?? ((at) => readFileSync(at, 'utf8'));
|
|
164
|
+
const write = deps.write ?? writeFileSync;
|
|
165
|
+
let stat = null;
|
|
166
|
+
try {
|
|
167
|
+
const refusal = parentRefusal(path, home, lstat);
|
|
168
|
+
if (refusal) return { path, saved: false, line: `not saved: ${refusal} — add the export line by hand` };
|
|
169
|
+
stat = lstat(path);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error?.code !== 'ENOENT') return { path, saved: false, line: `not saved: ${path} — ${error?.code ?? error?.message}` };
|
|
172
|
+
}
|
|
173
|
+
if (stat?.isSymbolicLink()) return { path, saved: false, line: `not saved: ${path} is a link — add the export line by hand` };
|
|
174
|
+
if (stat && !stat.isFile()) return { path, saved: false, line: `not saved: ${path} is not a regular file` };
|
|
175
|
+
try {
|
|
176
|
+
write(path, upsertManagedLine(stat ? read(path) : '', managedLine(key)), stat ? {} : { mode: 0o600 });
|
|
177
|
+
return { path, saved: true, line: `saved: ${path}` };
|
|
178
|
+
} catch (error) {
|
|
179
|
+
return { path, saved: false, line: `not saved: ${path} — ${error?.code ?? error?.message}` };
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const safeHome = (env) => {
|
|
184
|
+
if (typeof env.HOME === 'string' && env.HOME !== '') return env.HOME;
|
|
185
|
+
try { return homedir(); } catch { return ''; }
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export const main = async (argv, io = {}) => {
|
|
189
|
+
const log = io.log ?? console.log;
|
|
190
|
+
const error = io.error ?? console.error;
|
|
191
|
+
let parsed;
|
|
192
|
+
try { parsed = parseArgv(argv); } catch (err) { error(err.message); return EXIT.usage; }
|
|
193
|
+
if (parsed.help) { log(HELP); return EXIT.saved; }
|
|
194
|
+
const stdin = io.stdin ?? process.stdin;
|
|
195
|
+
const stdout = io.stdout ?? process.stdout;
|
|
196
|
+
if (!stdin.isTTY) { error(TERMINAL_SENTENCE); return EXIT.noTerminal; }
|
|
197
|
+
let key;
|
|
198
|
+
try { key = await readSecret(`${KEY_VARIABLE}: `, { stdin, stdout }); } catch (err) { error(`the terminal read failed: ${err?.message ?? err} — nothing written`); return EXIT.noTerminal; }
|
|
199
|
+
if (key === null) { error('aborted — nothing written'); return EXIT.noTerminal; }
|
|
200
|
+
if (key === '') { error('no key entered — nothing written'); return EXIT.noTerminal; }
|
|
201
|
+
if (parsed.unverified) log('not verified: skipped (--unverified)');
|
|
202
|
+
else {
|
|
203
|
+
const verdict = await verifyKey(key, io);
|
|
204
|
+
log(verdict.line);
|
|
205
|
+
if (!verdict.ok) { log('nothing written'); return EXIT.notVerified; }
|
|
206
|
+
}
|
|
207
|
+
const env = io.env ?? process.env;
|
|
208
|
+
const home = io.home ?? safeHome(env);
|
|
209
|
+
const targets = targetsFor({ shell: env.SHELL, home, zdotdir: env.ZDOTDIR, platform: io.platform ?? process.platform }, io);
|
|
210
|
+
const results = saveKey(key, targets, home, io);
|
|
211
|
+
for (const result of results) log(result.line);
|
|
212
|
+
const saved = results.filter((result) => result.saved).length;
|
|
213
|
+
if (results.length > 0 && saved === results.length) { log(CONNECTED); return EXIT.saved; }
|
|
214
|
+
if (saved > 0) log(SAVED_IN_PART);
|
|
215
|
+
else log(parsed.unverified ? NOT_SAVED_SENTENCE : EDITOR_SENTENCE);
|
|
216
|
+
return EXIT.notSaved;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
if (isDirectRun(import.meta.url)) process.exitCode = await main(process.argv.slice(2));
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// spec:jev-guide — docs/ai/specs/kit/jev-guide/index.md
|
|
2
|
+
// The one leaf the Jev surfaces share: the key rule, the connect line, and the vendor facts the
|
|
3
|
+
// connect command's one request uses. No side effect, no import beyond node:path — the read-only
|
|
4
|
+
// guide imports it under its closed import graph, the advisor and the connect command beside it.
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
export const KEY_VARIABLE = 'TYPESAFE_API_KEY';
|
|
8
|
+
|
|
9
|
+
// The ONE key rule: set exactly when the variable is a string that is not empty after trimming.
|
|
10
|
+
export const keySet = (env) => typeof env[KEY_VARIABLE] === 'string' && env[KEY_VARIABLE].trim() !== '';
|
|
11
|
+
|
|
12
|
+
// A minimal POSIX quoting for the one path the connect line carries: bare when every byte is safe,
|
|
13
|
+
// else single-quoted with the shell's own escape for a single quote.
|
|
14
|
+
const SAFE = /^[A-Za-z0-9_./:=+@%,-]+$/;
|
|
15
|
+
export const quoteArg = (value) => (SAFE.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`);
|
|
16
|
+
|
|
17
|
+
// The command the user runs in a terminal of their own; the guide prints it, the advisor's apply
|
|
18
|
+
// carries it. `toolsDir` is the kit's tools directory (the file beside this one).
|
|
19
|
+
export const connectLine = (toolsDir) => `node ${quoteArg(join(toolsDir, 'jev-connect.mjs'))}`;
|
|
20
|
+
|
|
21
|
+
// The vendor's smallest documented request (docs.typesafe.ai quickstart, read 2026-09-29) and the
|
|
22
|
+
// meanings of its documented error codes, quoted as the vendor prints them.
|
|
23
|
+
export const JEV_ENDPOINT = 'https://api.typesafe.ai/v1/systemone';
|
|
24
|
+
export const JEV_SAMPLE_BODY = Object.freeze({
|
|
25
|
+
state: 'Help! My payouts have been failing for 3 days.',
|
|
26
|
+
model: 'jev-latest',
|
|
27
|
+
questions: {
|
|
28
|
+
department: {
|
|
29
|
+
type: 'choice',
|
|
30
|
+
instructions: 'Which team should handle this?',
|
|
31
|
+
criteria: { billing: 'Payments, invoicing, refunds', technical: 'Bugs, outages, integrations', sales: 'Pricing, upgrades, new accounts' },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
export const JEV_ERROR_MEANINGS = Object.freeze({
|
|
36
|
+
401: 'Missing or invalid API key. Check the `Authorization` header.',
|
|
37
|
+
422: 'The request body failed validation — for example a missing required field or a malformed question. The body details the offending field.',
|
|
38
|
+
429: 'You have exceeded your rate limit. Back off and retry after a short delay.',
|
|
39
|
+
529: 'TypeSafe is temporarily overloaded. Retry after a short delay.',
|
|
40
|
+
});
|
package/tools/jev-guide.mjs
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
// spec:jev-guide — docs/ai/specs/kit/jev-guide/index.md
|
|
3
3
|
import { statSync } from 'node:fs';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
|
-
import { join, resolve } from 'node:path';
|
|
5
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import { fail } from '../references/scripts/markdown-blocks.mjs';
|
|
7
8
|
import { isDirectRun } from './direct-run.mjs';
|
|
9
|
+
import { KEY_VARIABLE, connectLine, keySet } from './jev-facts.mjs';
|
|
8
10
|
|
|
9
11
|
const SCHEMA = 1;
|
|
10
|
-
const
|
|
12
|
+
const TOOLS = dirname(fileURLToPath(import.meta.url));
|
|
11
13
|
const SKILL_FILE = join('typesafe-ai', 'SKILL.md');
|
|
12
|
-
const HELP = `jev — read-only: what Jev (TypeSafe) is, why it pays here, and the steps to a
|
|
14
|
+
const HELP = `jev — read-only: what Jev (TypeSafe) is, why it pays here, and the two steps to a connected key.
|
|
13
15
|
|
|
14
16
|
Usage:
|
|
15
17
|
node jev-guide.mjs [--dir PROJECT] [--json]
|
|
@@ -42,7 +44,13 @@ export const JEV_TEXT = Object.freeze([
|
|
|
42
44
|
'code.claude.com for claude plugin list, read 2026-09-29.',
|
|
43
45
|
]);
|
|
44
46
|
|
|
47
|
+
const KEY_GET = 'Get a key at console.typesafe.ai and set it on every host the agent runs on.';
|
|
48
|
+
const KEY_CONNECT = 'Connect it from a terminal of your own, never through the agent: the command asks for the key with no echo, checks it with one request and, for bash or zsh, saves it to your shell\'s startup files.';
|
|
49
|
+
const KEY_RESTART = 'Then quit the agent, start it again from a new terminal, and run /agent-workflow-kit jev again: the key mark should read set.';
|
|
50
|
+
const KEY_NEVER = 'Never paste the key into the chat or into a project file.';
|
|
51
|
+
const KEY_NOT_SET = `key: ${KEY_VARIABLE} not set. A host setting that filters the environment of the agent's commands also hides it. If it still reads not set after a restart and no such setting applies, the agent may not have been started from a shell that reads its startup file: export the variable in the file the agent's launcher reads, then restart.`;
|
|
45
52
|
const INSTALL = [
|
|
53
|
+
'Optional, for your own code and prompts — the vendor skill:',
|
|
46
54
|
'Claude Code:',
|
|
47
55
|
'claude plugin marketplace add typesafe-ai/skills',
|
|
48
56
|
'claude plugin install typesafe@typesafe-ai',
|
|
@@ -55,27 +63,9 @@ const NOT_SEEN = [
|
|
|
55
63
|
'in a Claude Code session (the vendor); ask any other agent to use the TypeSafe skill (the vendor); or run Claude Code\'s own',
|
|
56
64
|
'claude plugin list',
|
|
57
65
|
];
|
|
58
|
-
const
|
|
59
|
-
'
|
|
60
|
-
|
|
61
|
-
'Never paste the key into the chat or into a project file.',
|
|
62
|
-
];
|
|
63
|
-
const KEY_NOT_SET = `key: ${KEY_VARIABLE} not set. A host setting that filters the environment of the agent's commands also hides it.`;
|
|
64
|
-
const FIRST_USE = [
|
|
65
|
-
'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?".',
|
|
66
|
-
'The vendor\'s smallest documented request:',
|
|
67
|
-
'POST https://api.typesafe.ai/v1/systemone',
|
|
68
|
-
'Authorization: Bearer <API_KEY>',
|
|
69
|
-
'{"state":"Help! My payouts have been failing for 3 days.","model":"jev-latest","questions":{"department":{"type":"choice","instructions":"Which team should handle this?","criteria":{"billing":"Payments, invoicing, refunds","technical":"Bugs, outages, integrations","sales":"Pricing, upgrades, new accounts"}}}}',
|
|
70
|
-
'Its documented response:',
|
|
71
|
-
'{"model":"jev-1.13.0","answers":{"department":{"type":"choice","choice":"billing","probabilities":{"billing":0.88,"technical":0.12,"sales":0.0},"confidence":0.81}},"usage":{"input_tokens":318,"output_tokens":34}}',
|
|
72
|
-
'choice: "The highest-probability option."',
|
|
73
|
-
'probabilities: "Every option mapped to its probability (floats that sum to 1)."',
|
|
74
|
-
'confidence: "How certain the model is, derived from probabilities."',
|
|
75
|
-
'401: "Missing or invalid API key. Check the `Authorization` header."',
|
|
76
|
-
'422: "The request body failed validation — for example a missing required field or a malformed question. The body details the offending field."',
|
|
77
|
-
'429: "You have exceeded your rate limit. Back off and retry after a short delay."',
|
|
78
|
-
'529: "TypeSafe is temporarily overloaded. Retry after a short delay."',
|
|
66
|
+
const AFTER_INSTALL = [
|
|
67
|
+
'After an install, quit the agent and start it again from a new terminal.',
|
|
68
|
+
'Then give the agent this prompt: using the TypeSafe skill, route these three tickets to billing, technical or sales, one request each, and print each choice and confidence — "I was charged twice this month.", "The app crashes when I upload a file.", "Is there a discount for a yearly plan?".',
|
|
79
69
|
'Success: three choices, each with a confidence.',
|
|
80
70
|
];
|
|
81
71
|
const LIMITS = 'Each request stays within the vendor limits: 255 options, 64k tokens, 32k for the state plus the longest question.';
|
|
@@ -107,16 +97,18 @@ const parseArgv = (argv) => {
|
|
|
107
97
|
const statIs = (path, kind) => {
|
|
108
98
|
try { return kind === 'file' ? statSync(path).isFile() : statSync(path).isDirectory(); } catch { return false; }
|
|
109
99
|
};
|
|
100
|
+
const homeOf = (io) => {
|
|
101
|
+
if (io.home !== undefined) return io.home;
|
|
102
|
+
try { return homedir(); } catch { return ''; }
|
|
103
|
+
};
|
|
110
104
|
const skillPaths = (dir, home) => [...new Set([join(dir, '.claude', 'skills'), join(dir, '.agents', 'skills'),
|
|
111
|
-
join(home, '.claude', 'skills'), join(home, '.codex', 'skills'), join(home, '.cursor', 'skills')]
|
|
105
|
+
...(home === '' ? [] : [join(home, '.claude', 'skills'), join(home, '.codex', 'skills'), join(home, '.cursor', 'skills')])]
|
|
112
106
|
.map((root) => join(root, SKILL_FILE)))];
|
|
113
|
-
const keySet = (env) => typeof env[KEY_VARIABLE] === 'string' && env[KEY_VARIABLE].trim() !== '';
|
|
114
107
|
|
|
115
|
-
const renderSteps = (found, set) => [
|
|
116
|
-
['STEP 1 — the
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
['STEP 3 — the first use', ...FIRST_USE],
|
|
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],
|
|
110
|
+
['STEP 2 — the vendor skill', ...INSTALL,
|
|
111
|
+
...(found.length ? found.map((path) => `skill: found at ${path}`) : NOT_SEEN), ...AFTER_INSTALL],
|
|
120
112
|
];
|
|
121
113
|
|
|
122
114
|
export const main = (argv, io = {}) => {
|
|
@@ -129,12 +121,13 @@ export const main = (argv, io = {}) => {
|
|
|
129
121
|
if (parsed.help) { log(HELP); return 0; }
|
|
130
122
|
const dir = resolve(io.cwd ?? process.cwd(), parsed.dir);
|
|
131
123
|
if (!statIs(dir, 'directory')) { error(`${dir} is not a directory`); return 1; }
|
|
132
|
-
const found = skillPaths(dir, io
|
|
133
|
-
const
|
|
134
|
-
const
|
|
124
|
+
const found = skillPaths(dir, homeOf(io)).filter((path) => statIs(path, 'file'));
|
|
125
|
+
const env = io.env ?? process.env;
|
|
126
|
+
const set = keySet(env);
|
|
127
|
+
const connect = connectLine(io.toolsDir ?? TOOLS);
|
|
128
|
+
const steps = renderSteps(found, set, connect);
|
|
135
129
|
log(parsed.json
|
|
136
|
-
? JSON.stringify({ schema: SCHEMA, command: 'jev', dir, text: JEV_TEXT, skill: { found }, key: { set },
|
|
137
|
-
steps, prompts: PROMPTS }, null, 2)
|
|
130
|
+
? JSON.stringify({ schema: SCHEMA, command: 'jev', dir, text: JEV_TEXT, skill: { found }, key: { set }, connect, steps, prompts: PROMPTS }, null, 2)
|
|
138
131
|
: [...JEV_TEXT, ...steps.flat(), 'WHERE IT PAYS IN THIS WORKFLOW', ...PROMPTS].join('\n'));
|
|
139
132
|
return 0;
|
|
140
133
|
};
|
|
@@ -94,6 +94,7 @@ import {
|
|
|
94
94
|
ACKS_WORKTREES_DIR_KEY,
|
|
95
95
|
ACKS_COVERAGE_DOMAIN_KEY,
|
|
96
96
|
ACKS_SOURCE_SIZE_COPY_KEY,
|
|
97
|
+
ACKS_JEV_CONNECT_KEY,
|
|
97
98
|
ACK_LANES,
|
|
98
99
|
factFingerprint,
|
|
99
100
|
readAckValue,
|
|
@@ -101,9 +102,12 @@ import {
|
|
|
101
102
|
import { ADOPTION, STORE_DIR_REL as SPEC_STORE_DIR_REL, SPEC_ADOPTION_LANE, declineFingerprint, readDeclineAck, surveySpecAdoption } from './spec-adoption.mjs';
|
|
102
103
|
import { ENSURE_OPS } from './ensure-vocabulary.mjs';
|
|
103
104
|
import { composeProfileGapScreen } from './profile-gap-screen.mjs';
|
|
105
|
+
import { connectLine, keySet } from './jev-facts.mjs';
|
|
104
106
|
|
|
105
107
|
// The upgrade ensure that seeds the spec store — the not-adopted item's apply; pinned to the vocabulary.
|
|
106
108
|
const SPEC_LAYER_ENSURE = ENSURE_OPS.includes('specs') ? 'specs' : null;
|
|
109
|
+
const JEV_CONNECT_DECLINED = 'jev-connect:declined';
|
|
110
|
+
const JEV_CONNECT_DECLINE = factFingerprint(JEV_CONNECT_DECLINED);
|
|
107
111
|
|
|
108
112
|
export { ACKS_FILE, ACKS_LANE_KEY, ACKS_WORKTREES_DIR_KEY, ACKS_COVERAGE_DOMAIN_KEY, ACKS_SOURCE_SIZE_COPY_KEY, ACK_LANES, factFingerprint };
|
|
109
113
|
|
|
@@ -201,6 +205,7 @@ export const SEVERITIES = Object.freeze({
|
|
|
201
205
|
// the decline. Neither arm can leave the flow-optimal line standing — an offer is still an item.
|
|
202
206
|
'spec-adoption': SEVERITY_OPTIONAL,
|
|
203
207
|
'profile-gap': SEVERITY_OPTIONAL,
|
|
208
|
+
'jev-connect': SEVERITY_OPTIONAL,
|
|
204
209
|
'spec-adoption.adopting': SEVERITY_OPTIONAL,
|
|
205
210
|
});
|
|
206
211
|
// The per-item render tags (frozen presentation data, same language contract as the templates).
|
|
@@ -278,6 +283,7 @@ export const WHATS = Object.freeze({
|
|
|
278
283
|
'worktrees-dir': 'write access to the worktrees parent dir {dir} is not confirmed — provision may still stop',
|
|
279
284
|
'spec-adoption': 'feature-spec store absent (docs/ai/specs) — no feature contract can govern a plan here yet; seed the store, or record the decline',
|
|
280
285
|
'profile-gap': '{what}',
|
|
286
|
+
'jev-connect': 'Jev (TypeSafe) is not connected: TYPESAFE_API_KEY is not in the environment the agent\'s commands run in',
|
|
281
287
|
'spec-adoption.adopting': 'feature-spec store: {n} draft spec(s), no live contract — nothing governs a plan through it yet; land a live contract, or record the decline',
|
|
282
288
|
});
|
|
283
289
|
|
|
@@ -339,6 +345,7 @@ export const BENEFITS = Object.freeze({
|
|
|
339
345
|
'worktrees-dir': 'parallel features — the host-specific write allowance or terminal fallback is surfaced before provision',
|
|
340
346
|
'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',
|
|
341
347
|
'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
|
+
'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',
|
|
342
349
|
});
|
|
343
350
|
|
|
344
351
|
// ── the CLOSED opt-in capability registry (OPT-IN-SHIPS-INVISIBLE) ──────────────────────────────
|
|
@@ -386,6 +393,7 @@ export const OPT_IN_CAPABILITIES = Object.freeze([
|
|
|
386
393
|
// its adoption state is declared where the store is seeded.
|
|
387
394
|
{ id: 'spec-adoption', mode: 'upgrade', advisorKey: 'spec-adoption' },
|
|
388
395
|
{ id: 'full-flow-profile', mode: 'upgrade', advisorKey: 'profile-gap' },
|
|
396
|
+
{ id: 'jev-connect', mode: 'jev', advisorKey: 'jev-connect' },
|
|
389
397
|
{ id: 'adr-store-migration', mode: 'migrate-adr-store', advisorKey: 'adr-store-migration' },
|
|
390
398
|
{ id: 'review-recipe', mode: 'set-recipe', advisorKey: 'review-recipe' },
|
|
391
399
|
// The execute slot is a DISTINCT opt-in from the review slot, and the same probe reports both —
|
|
@@ -1305,7 +1313,7 @@ const readReadLaneToggle = (root, deps) => {
|
|
|
1305
1313
|
// D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
|
|
1306
1314
|
// at the consent moment; the static contract test asserts EXACT bidirectional coverage
|
|
1307
1315
|
// (risk-marked keys == mode-doc note keys — a dropped note goes red, not silent).
|
|
1308
|
-
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']);
|
|
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']);
|
|
1309
1317
|
|
|
1310
1318
|
// The feature-spec layer's adoption state (contract: kit/spec-adoption). The canon lets a plan cite
|
|
1311
1319
|
// zero governing specs while a project adopts the layer, and nothing ever said whether adoption had
|
|
@@ -1600,6 +1608,20 @@ const probeMcpChannel = ({ root, deps, add, skip }) => {
|
|
|
1600
1608
|
}
|
|
1601
1609
|
};
|
|
1602
1610
|
|
|
1611
|
+
// The Jev offer (contract: kit/jev-guide, part jev-offer). The key is judged first, through the facts leaf's one rule over
|
|
1612
|
+
// the injected environment; the apply is the connect command the USER runs in a terminal of their own (HAND-APPLY), never a guide.
|
|
1613
|
+
export const probeJevConnect = ({ root, deps, add, skip }) => {
|
|
1614
|
+
try {
|
|
1615
|
+
if (keySet(deps.getenv ?? {})) return;
|
|
1616
|
+
if (readAckValue(root, deps, ACKS_JEV_CONNECT_KEY) === JEV_CONNECT_DECLINE) return;
|
|
1617
|
+
const decline = `node ${q(toolPath('ack-write.mjs'))} --lane jev-connect --fingerprint ${JEV_CONNECT_DECLINE} --cwd ${q(root)}`;
|
|
1618
|
+
add('jev-connect', WHATS['jev-connect'], `HAND-APPLY: ${connectLine(HERE)}`, 'jev-connect',
|
|
1619
|
+
`HAND-APPLY alternative (instead of the apply, never after it): decline the offer by recording it — ${decline}`);
|
|
1620
|
+
} catch (err) {
|
|
1621
|
+
skip('jev-connect', err);
|
|
1622
|
+
}
|
|
1623
|
+
};
|
|
1624
|
+
|
|
1603
1625
|
export const probeProfileGaps = ({ root, deps, add, skip }) => {
|
|
1604
1626
|
try {
|
|
1605
1627
|
const { gaps, skips } = composeProfileGapScreen({ root, deps });
|
|
@@ -1632,6 +1654,7 @@ const PROBES = Object.freeze([
|
|
|
1632
1654
|
probeWorktreesDir,
|
|
1633
1655
|
probeMcpChannel,
|
|
1634
1656
|
probeSpecAdoption,
|
|
1657
|
+
probeJevConnect,
|
|
1635
1658
|
probeProfileGaps,
|
|
1636
1659
|
]);
|
|
1637
1660
|
|
|
@@ -1758,7 +1781,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
1758
1781
|
}
|
|
1759
1782
|
})();
|
|
1760
1783
|
if (st == null || !st.isDirectory()) throw Object.assign(new Error(`--cwd is not a directory: ${cwd}`), { exitCode: 1 });
|
|
1761
|
-
const result = buildRecommendations({ cwd, deps: ctx.deps ?? {} });
|
|
1784
|
+
const result = buildRecommendations({ cwd, deps: ctx.deps ?? { getenv: process.env } });
|
|
1762
1785
|
if (json) return { code: 0, stdout: JSON.stringify(result, null, 2), stderr: '' };
|
|
1763
1786
|
return { code: 0, stdout: formatRecommendations(result), stderr: '' };
|
|
1764
1787
|
} catch (err) {
|