@sabaiway/agent-workflow-kit 14.32.0 → 14.33.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 +42 -0
- package/README.md +3 -3
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/recommendations.md +4 -4
- package/references/modes/velocity.md +7 -5
- package/references/modes/worktrees.md +10 -9
- package/tools/apply-danger-check.mjs +23 -6
- package/tools/init-project.mjs +30 -5
- package/tools/recommendations.mjs +20 -33
- package/tools/velocity-profile.mjs +118 -10
- package/tools/worktrees.mjs +28 -10
- package/tools/write-lanes.mjs +16 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,48 @@ 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.33.0 — the worktrees grant gets a writer: the agent writes it into this copy's local settings on the chat yes, and the worktrees stops name that route (AD-185)
|
|
8
|
+
|
|
9
|
+
**The `worktrees-dir` upgrade item, which used to hand the user a settings line to paste, is now a harness item the
|
|
10
|
+
agent applies on the chat yes, after the danger check.** Before, the item printed `HAND-APPLY FIRST:` advice to add the
|
|
11
|
+
dir to `sandbox.filesystem.allowWrite` in `.claude/settings.json`, and a sandboxed `worktrees` provision, land or
|
|
12
|
+
cleanup that could not write the parent stopped with `.claude/settings.json → sandbox.filesystem.allowWrite += "<dir>"`
|
|
13
|
+
for you to paste.
|
|
14
|
+
|
|
15
|
+
- **`velocity-profile --worktrees`, the writer.** `node <kit>/tools/velocity-profile.mjs --worktrees --cwd <root>`
|
|
16
|
+
previews; `--apply` writes. It adds ONE `sandbox.filesystem.allowWrite` entry, the worktrees parent
|
|
17
|
+
(`docs/ai/worktrees.json` `parentDir`, else the repository's own parent; `~/…` under the home), to this copy's
|
|
18
|
+
`.claude/settings.local.json` only, never the committed `.claude/settings.json`, merging with what is there. It
|
|
19
|
+
creates the parent and each missing ancestor first (`would create: <dir>` per dir), whether or not an entry already
|
|
20
|
+
covers it, and writes no settings byte when one does. It seeds no read-only core and no ask rule. A parent that is the home or holds it is refused
|
|
21
|
+
(`VELOCITY_WORKTREES_HOME`); so is a malformed `sandbox` key in the local file and a linked or non-regular local file;
|
|
22
|
+
the flag beside `--autonomy`, `--kit-tools`, `--bridge-tier` or `--accept-edits` is a usage error (exit 2). Every run
|
|
23
|
+
prints what the entry widens: every sandboxed command of this copy, and of every worktree provisioned from it, can
|
|
24
|
+
write the whole parent dir, sibling repositories under it included. Choose a dedicated `parentDir` before the first
|
|
25
|
+
apply: no apply removes an entry.
|
|
26
|
+
- **The advisor item.** `worktrees-dir` now carries the danger check line (`apply-danger-check.mjs --variant
|
|
27
|
+
worktrees-dir --cwd <root>`) and its apply; its message names the parent itself, where it used to name the nearest
|
|
28
|
+
existing ancestor. The acknowledgement stays as the `recipe:` line's fallback, run only on your word that this host
|
|
29
|
+
ignores the settings key, instead of the grant. A parent that is the home or holds it renders the new
|
|
30
|
+
`worktrees-dir.home` arm instead: your own step, pointing `parentDir` at a dedicated directory. `init` previews the
|
|
31
|
+
grant with its check and applies it on its y before the other items, running the check again just before: when the
|
|
32
|
+
result is no longer the one shown (a link planted on the parent's path, say), the grant and every later item are not
|
|
33
|
+
applied and `init` exits 1.
|
|
34
|
+
- **The danger check covers it.** Variant `worktrees-dir` shows the one local entry, `writes: <dir>/ created` for each
|
|
35
|
+
dir the apply makes, and judges the local settings file; the yes is bound to that result.
|
|
36
|
+
- **The worktrees stops name the route.** The provision, land and cleanup writability stops keep their first line and
|
|
37
|
+
their plain-terminal fallback; in place of the settings line to paste they print one line: the kit's `worktrees-dir`
|
|
38
|
+
item grants the configured parent on your yes, the agent runs its check line, then the apply, then the command again,
|
|
39
|
+
and the terminal fallback applies when that check refuses, prints nothing to change, or grants a dir that does not
|
|
40
|
+
hold the command's target.
|
|
41
|
+
- **Shipped text.** `recommendations.md` names the writer, the local file and the ack fallback; `worktrees.md` the
|
|
42
|
+
check line, the local file and the ack fallback; `velocity.md` and the README's velocity row document the
|
|
43
|
+
`--worktrees` writer and the local file; the README's worktrees row names the chat yes after the check, the local
|
|
44
|
+
file and the ack fallback, and its advisor row the ack fallback. No line of the item says `HAND-APPLY`.
|
|
45
|
+
- **Stated limits.** A session under `permissions.blockReadsOutsideWorkingDirectories` or an admin-required sandbox
|
|
46
|
+
ignores the local `allowWrite`, and a host that ignores the key gains nothing: the ack and the terminal fallback cover
|
|
47
|
+
both. A root reached through a symlink re-offers an existing acknowledgement once.
|
|
48
|
+
|
|
7
49
|
## 14.32.0 — the git-hook items get a writer: the enforcement scripts and the commit guard are applied on the chat yes, and the project's own installer decides the hook (AD-184)
|
|
8
50
|
|
|
9
51
|
**Two `/agent-workflow-kit upgrade` items that used to hand the user a shell line are now harness items the agent
|
package/README.md
CHANGED
|
@@ -256,12 +256,12 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
|
|
|
256
256
|
| `/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. |
|
|
257
257
|
| `/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. |
|
|
258
258
|
| `/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. |
|
|
259
|
-
| `/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, 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 bridge-tier item's missing-entries line — each missing entry by kind: allow rules, exclusions, hosts, state dirs, then what the execution wrapper codex-exec needs, then the host condition; the worktrees-dir
|
|
259
|
+
| `/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, 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 bridge-tier item's missing-entries line — each missing entry by kind: allow rules, exclusions, hosts, state dirs, then what the execution wrapper codex-exec needs, then the host condition; the worktrees-dir ack fallback for a host that ignores the settings key, 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); `velocity --bridge-tier` seeds the used bridges' review allow rules, `<wrapper> *` exclusions (`codex-exec *` where an execute slot delegates, keeping its permission prompt), declared hosts into `sandbox.network.allowedDomains`, and home-relative state dirs into `sandbox.filesystem.allowWrite`; hosts and dirs are declared per bridge in `capability.json` `networkHosts` + `writableDirs`, and the advisor's `bridge-tier` item counts and names each missing one. `--cwd` is required (the target project is explicit); writes nothing, never commits, never runs a subscription CLI. |
|
|
260
260
|
| `/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. |
|
|
261
261
|
| `/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. |
|
|
262
|
-
| `/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
|
|
262
|
+
| `/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 prints the `worktrees-dir` check line: the agent applies the grant on your chat yes after the check, one entry in this copy's `.claude/settings.local.json`, never the committed project settings (`init` is the route you may take instead, and the ack is the fallback for a host that ignores the key), else the printed terminal command runs it. Never commits, never pushes, never runs a subscription CLI. |
|
|
263
263
|
| `/agent-workflow-kit uninstall` | opt-in, any time | **guarded teardown** — the inverse of `init` / `setup`. Removes only what's **provably ours** (managed skill dirs + bridge wrappers; in a project, the hidden-mode git-ignore block it added + the pre-commit hook it installed); **never deletes** your `docs/ai` / `AGENTS.md` (prints the exact `rm` to run by hand) or your `.claude/settings.json` (prints an **edit** — remove the attribution key, review any velocity `permissions.*` — never an `rm`). Always `--dry-run` first; preflight-then-mutate; never commits. |
|
|
264
|
-
| `/agent-workflow-kit velocity` | Claude Code · opt-in | **onboarding velocity profile** — seeds a fixed, audited **read-only** allowlist into `.claude/settings.json` so routine read-only commands stop idling on approval prompts while you're away; every allowlist apply (flagless, `--kit-tools`, `--bridge-tier`) also seeds one `permissions.ask` rule, so all work can stay in auto mode: without it auto mode refuses a harness apply (the `apply:` line of each harness item the upgrade advisor renders) and you must leave auto mode for it; with it the harness shows its usual dialog — the rule allows nothing; opt-in `acceptEdits`; plus a **read-only advisory** of likely project gate commands to add by hand. Writes
|
|
264
|
+
| `/agent-workflow-kit velocity` | Claude Code · opt-in | **onboarding velocity profile** — seeds a fixed, audited **read-only** allowlist into `.claude/settings.json` so routine read-only commands stop idling on approval prompts while you're away; every allowlist apply (flagless, `--kit-tools`, `--bridge-tier`) also seeds one `permissions.ask` rule, so all work can stay in auto mode: without it auto mode refuses a harness apply (the `apply:` line of each harness item the upgrade advisor renders) and you must leave auto mode for it; with it the harness shows its usual dialog — the rule allows nothing; opt-in `acceptEdits`; plus a **read-only advisory** of likely project gate commands to add by hand. Writes `.claude/settings.json`; a fourth opt-in, `--worktrees`, writes the worktrees grant into the local settings, `.claude/settings.local.json`, alone — **never** allowlists commit/push/publish, never commits. A seeded entry is a **trust posture, not a sandbox** (a runtime residual remains at the settings level — its guard ships as the opt-in `hook` command); a direct commit/push/publish still asks. `--dry-run` first. |
|
|
265
265
|
| `/agent-workflow-kit agents` | Claude Code · opt-in | **subagent vehicles** — places the five bundled subagent definitions into `.claude/agents/`: **four read-only vehicles + one full-tool `executor`**. **No read-only vehicle gets `Bash`**, and that is the load-bearing property: a read-only fan-out on a full-tool subagent shells out for facts it could have read, and every shelled command is an approval prompt you never needed to see. Three of the four ride a **cheap model** (haiku, low effort) for mechanical work — extraction sweeps, changelog fact-skeletons, gate-failure triage; the fourth, **`review-lens`**, is an ADDITIONAL independent read-only review opinion on code your configured backends have already seen (never a replacement for your plan or diff review recipe, advisory like every review; an epic is reviewed by the lens alone, whatever `epic.review` resolves to). The fifth, **`executor`** (all tools; model and effort from `docs/ai/vehicles.json`), is the ONE vehicle with a shell and the instrument behind the **Subagent** recipe: dispatched only for a bounded, file-disjoint execution / authoring / routine slice you verify afterwards — never for read-only work, never as a review backend, and it never commits. Its state (`placed` / `customized` / `unusable` / `missing`) is what a `subagent` slot's readiness resolves from. Writing code and running gates stay off the four read-only vehicles; the executor carries a bounded slice you re-verify on your main lane, and every vehicle's output is verified. Preview by default (`--apply` writes); an existing customized read-only vehicle or lens is **preserved, never overwritten**; the executor's body is re-derived from `docs/ai/vehicles.json` (a hand edit is replaced); never touches `settings*.json`, never commits. |
|
|
266
266
|
| `/agent-workflow-kit hook` | Claude Code · opt-in | **gate-approval hook** — places a self-contained PreToolUse hook (`.claude/hooks/`) and wires it into `.claude/settings.json`: a Bash command **byte-identical** to a gate you declared in `docs/ai/gates.json` (run from the project root) is auto-approved — no prompt, no idle; a seeded read-only command carrying a runtime residual (output redirection, command substitution, `--output` writes) now **asks** even where an allow rule would have silently passed it (proven live — hook `ask` overrides an allow rule). Detection is a conservative string scan, so it over-asks on a byte that is ordinary text — searching for `=>`, or a read wearing `2>/dev/null`, prompts. That is a documented open limit: 4.1.0 built three mechanisms to narrow it and removed all three in review, each counterexample kept as a test (see Mode: hook). Exact matches only, never patterns; never `deny`; a broken `gates.json` only disables auto-approval, never the guard. An opt-in **read-lane** (`--read-lane` → `docs/ai/lanes.json`, a **separate** file from `gates.json`) additionally auto-approves *compounds* of seeded read-only commands carrying no shell metaprogramming; `--apply --read-lane` verifies the placed hook is current first; a stale or diverged placed hook is replaced by the reseed route (`--apply --reseed`, the bundled bytes in place). Preview by default; never writes `settings.local.json`; never commits. |
|
|
267
267
|
| `/agent-workflow-kit state-block-guard` | Claude Code · opt-in | **closing-block detector** — a `Stop` hook that reads the turn's final assistant message and warns when the closing state block is defective: the «what I need from you» slot answering *nothing* (a turn that ENDS always needs a resume, so the answer is false, not merely unhelpful), or a first-person promise of imminent work in a turn that is over. Judged on the slot's first clause, so a real ask followed by a clause break and "nothing else" passes, and a promise gated on something named passes; matching is word-bounded and Unicode-aware, quoted and fenced examples are stripped, and the last STARTED block decides — an incomplete tail counts as no block rather than falling back to an earlier one. A message carrying no block at all is silent unless you pass `--require-block` — this kit does not mandate the block, and a hook that runs every turn must not warn every turn. The judgement is lexical and the mode doc names every residual it leaves. **Detection, never prevention** — a `Stop` hook cannot un-send the message it judges; what it buys is that a silent recurrence becomes a visible one. Warns via `systemMessage` and **exits 0 on every path**: never `deny`, never blocks the stop, never re-enters the model. Reads nothing but the message, makes no network call, approves nothing. Its writer is `tools/state-block-hook.mjs`: it places the self-contained hook and merges its `Stop` entry into `.claude/settings.json` by the three merge cases, keeping every foreign hook — the agent applies it on the chat yes after the danger check. |
|
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.33.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.33.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",
|
|
@@ -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, a configured `subagent` carrier whose executor vehicle cannot carry it, a difference from the reference profile the project has not declined, Jev not connected on this host, the Jev skill not installed for every agent) and renders **verdict-first**: one composed verdict line, then each item as **{severity · what is sub-optimal · the benefit in ONE plain line · `lane: chat`, `lane: harness` or `lane: user` · an optional `recipe:` line (the `bridge-tier` missing-entries line, the `worktrees-dir`
|
|
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, a configured `subagent` carrier whose executor vehicle cannot carry it, a difference from the reference profile the project has not declined, Jev not connected on this host, the Jev skill not installed for every agent) and renders **verdict-first**: one composed verdict line, then each item as **{severity · what is sub-optimal · the benefit in ONE plain line · `lane: chat`, `lane: harness` or `lane: user` · an optional `recipe:` line (the `bridge-tier` missing-entries line, the `worktrees-dir` ack fallback, or the `agents` / `executor-vehicle` preconditions and hidden-mode reconcile follow-up) · a harness item's `check:` line before its apply slot · the apply slot: a chat one-liner, a harness item's apply line or a user item's own step}**. 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:** the agent applies chat and harness items on the chat yes, harness items by the consent route below. A chat item is never retried outside the sandbox and every failure of a chat step is a STOP. A user item, and any apply slot that opens with HAND-APPLY whatever its lane, is the user's own step, handed over as worded. 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 a chat item: 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 chat items' rendered command IS the mutation and completes on that one run, **including a no-`--apply` mutation** such as `spec-adoption`'s `ensure-configs --reconcile --only specs` (behind an edited file it prints a replace line that takes its own yes). 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. 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. `source-size.id-squatter`) is **the user's own step** — 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`
|
|
20
|
+
3. **The apply-through-agent lane — an explicit informed-consent checkpoint:** the agent applies chat and harness items on the chat yes, harness items by the consent route below. A chat item is never retried outside the sandbox and every failure of a chat step is a STOP. A user item, and any apply slot that opens with HAND-APPLY whatever its lane, is the user's own step, handed over as worded. 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 a chat item: 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 chat items' rendered command IS the mutation and completes on that one run, **including a no-`--apply` mutation** such as `spec-adoption`'s `ensure-configs --reconcile --only specs` (behind an edited file it prints a replace line that takes its own yes). 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. 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. `source-size.id-squatter`) is **the user's own step** — 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` you apply the grant on the chat yes after its check, like every harness item, with no hand step to wait for; its `recipe:` line's `ack-write` preview is the ack fallback, run only on the user's word that the host ignores the settings key, instead of the grant, and against a trusted host NO no ack renders.
|
|
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 **consent route** for a harness item: run its `check:` line in the sandbox and show its result. On the check's `masked:` line, say so and run that same check line once more outside the sandbox, so it re-judges in the apply's own environment; the yes is taken on the outside result. Take the user's yes on that result and run its `apply:` line with that result's digest once, outside the sandbox at once, so the harness asks at the point of action (with the apply ask rule seeded, its own dialog); never another line, never unseen. When velocity-core, kit-tools-tier or bridge-tier is among the user's picks (velocity-core preferred), check, confirm and apply it, or leave it pending, before any other pick's check runs, so its apply seeds the ask rule before the other harness applies. When the harness's auto mode refuses the check or the apply, say so and give the one plain step: leave auto mode for this one action (Shift+Tab in the terminal, the mode indicator in an editor) and say yes, or run `npx @sabaiway/agent-workflow-kit@latest init` in a terminal of your own. On the user's word, run the same line once more when they left auto mode, never another, never unseen; when they ran `init` instead, run the advisor again and offer only what is still pending, each through this route from its check (after a restart `init` asked for, the next session's upgrade does it). When that refusal hits the velocity-core, kit-tools-tier or bridge-tier pick taken first, run no other pick's check until the user answers the plain step. A check that refuses (exit 1) leaves the item unapplied and pending, its result shown; a changed digest (exit 5) applies nothing, shows the new result and asks a new yes. A no, a harness refusal and any other failure of a harness apply leave the item pending with its output shown, and the agent continues. `init`, run in a console from the project's folder, is the route the user may take instead.
|
|
@@ -31,7 +31,7 @@ A variant's posture note, where its key has one, is printed by `init` from this
|
|
|
31
31
|
- `bridge-tier` — the lane is harness and env-dependent: the agent applies the item on the chat yes after the check, writing the project `.claude/settings.json`; `init` previews `velocity-profile --bridge-tier` and applies it on the terminal y as the route the user may take instead. It wires the USED bridges from the project's effective recipes and host readiness: each used review wrapper's `code`-mode allow rule (a wrapper then runs UNATTENDED: it reads any repo file it is pointed at and sends the assembled payload to its subscription backend), the quoted grounding pre-step rule when `agy-review` is used, `codex-review *` / `agy-review *` in `sandbox.excludedCommands`, and `codex-exec *` where an execute slot delegates (no execution allow rule: it keeps its prompt). It also seeds the used bridges' declared hosts (`networkHosts`) in `sandbox.network.allowedDomains` and resolved state dirs (`writableDirs`), home-relative where under home, in `sandbox.filesystem.allowWrite`. Where the host honours the settings sandbox keys, an excluded wrapper runs outside the sandbox, every sandboxed command of the project can reach the seeded hosts, and every sandboxed command can write the seeded state dirs. The item fires while an allow rule, a `*` exclusion, a host or a state dir of a used bridge is missing, so a project wired before this revision is offered the one re-run. Its `recipe:` line names each missing entry by kind (allow rules, exclusions, hosts, state dirs), then what the execution wrapper `codex-exec` needs where an execute slot delegates, then, when the project settings enable the sandbox, "once added, wired where the host honors the settings sandbox keys", or "host condition unchecked" with the reason.
|
|
32
32
|
|
|
33
33
|
- `read-lane` — enabling the opt-in read-only compound lane auto-approves *compounds* (and singles) of the seeded read-only core that carry ZERO shell metaprogramming: an UNATTENDED trust extension, bounded by the audited read-only core (never a command outside it; prompt-bypass only, never a sandbox bypass) and applied regardless of which of those core commands you seeded as individual settings rules. It is a PROJECT-PERSISTENT declaration in `docs/ai/lanes.json` — every future session, subagents' Bash too where the host fires hooks on subagent Bash, and (committed) every checkout. The apply depends on state: when the lane is OFF, it is the `gate-hook --read-lane` preview (whose own currency check refuses a stale hook — a pre-1.48 hook never reads `lanes.json`); when the placed hook is MISSING, the item instead surfaces the re-place offer; when it is STALE (an enabled lane over an old hook), the **stale** arm is an attention item on the harness lane: its writer is `gate-hook --apply --reseed`, which replaces the placed hook with the bundled bytes in place, and the agent applies it on the chat yes after the check, whose result names the replaced file with both digests; `init` applies it on the terminal y as the route the user may take instead. Risk profile: a bounded read-only trust-posture extension — no write/exec exposure beyond the audited core.
|
|
34
|
-
- `worktrees-dir` —
|
|
34
|
+
- `worktrees-dir` — the agent applies the item on the chat yes after the check: one `sandbox.filesystem.allowWrite` entry naming the worktrees parent itself, written to this copy's `.claude/settings.local.json` only, never the committed project settings, the parent created when missing. `init` applies it on the batch terminal yes as the route the user may take instead. Where the host honors the key, the entry widens the OS-sandbox WRITE surface to the whole parent dir: every sibling path under it (other repositories included) becomes agent-writable, for every later session and every worktree provisioned afterwards. Narrow it BEFORE the first apply: point `docs/ai/worktrees.json` `parentDir` at a dedicated dir — no apply removes an entry, so an earlier one stays until the user deletes it. A parent that is the home or holds it renders `worktrees-dir.home` instead, the user's own step. **Convergence** is two-path: a declared `sandbox.filesystem.allowWrite` entry covering the probed dir (either settings scope — `~` and `~/…` resolve against home, and coverage is path-segment-aware, so a grant on a sibling or on a child never counts), or the dir-bound acknowledgement `worktreesDirAck` in the family-owned `docs/ai/acks.json`, recorded by the `ack-write --lane worktrees-dir` line the `recipe:` line carries — the ack fallback for a host that ignores the key, run only on the user's word, instead of the grant; against a trusted host NO no ack renders. Neither is proof of write CAPABILITY: the provision preflight's real create+delete probe stays the runtime truth, and the ack is bound to the **resolved probe dir**, so the item re-fires only when that resolved dir changes. Risk profile: a real write-surface widening where honored — scope it deliberately.
|
|
35
35
|
|
|
36
36
|
- `gates-inert` — the gate matrix is DECLARED but verifies nothing, and the two causes have different remedies, so the item renders a different apply for each. **Cause A** — a canonical `coverage-check` gate with no producer gate declared BEFORE it (none at all, or one declared after it, which writes the lcov too late): the run certifies no coverage of its own — `coverage=not-run` when nothing wrote an lcov, or a verdict over STALE bytes an earlier run left in the git dir, which is worse because it reads as `coverage=certified`. This item is what surfaces that state at upgrade instead of leaving it to be noticed. The remedy needs the producer to run BEFORE the checker — declared or MOVED there — and what the fill can do about that is exactly what the apply says. When the checker is the LAST declared gate and the project's own scripts yield an offerable producer, the fill PLACES that producer before it, so the apply is the ordinary `gates-init` dry-run preview. Otherwise — no offerable producer, or a checker that is not last — this arm stays **HAND-APPLY**: the maintainer's own step is to edit `docs/ai/gates.json` (the exact producer form lives in `${CLAUDE_SKILL_DIR}/references/modes/gates.md`), because the fill never reorders entries it did not write. **Cause B** — every declared gate is one of the kit's own canonical checkers, so the matrix runs no project-verification command at all: the apply is the `gates-init` dry-run preview, the same consent-gated seeder the `gates-declaration` item renders, and after the SAME confirmation you run the `--apply` line it prints. Neither cause changes any gate result, exit code or receipt — the item adds no enforcement, only the offer. Risk profile: `docs/ai/gates.json` is a PRIVILEGED file (a declared gate is auto-approvable by the wired hook), so both causes land in the same place — where the fill can help, it declares a project-controlled command on your explicit yes; where it cannot, the edit stays maintainer-only.
|
|
37
37
|
|
|
@@ -56,6 +56,6 @@ A first session may meet ordinary prompts until the autonomy render lands; a cha
|
|
|
56
56
|
- **Harness-managed sandbox** (an IDE/session-imposed sandbox that ignores the settings keys) — a NARROWEST-SCOPE ladder: (1) prefer wrapper/command-SCOPED sandbox rules where the harness offers them (only the review wrappers gain the egress + state-dir writes); (2) a SESSION-scoped allowance (the manifest hosts + the resolved state dirs for the whole session) is an INFORMED WIDENING — it carries the same blast-radius class the `--bridge-tier` keys carry where a host honours them (every sandboxed command in the session gains that egress and write access to those state dirs), acceptable only as the maintainer's deliberate choice for a review-heavy session. (3) Else the per-run consented bypass. The kit writes no shape for this sandbox — the harness's own controls (its scoped rules, a session allowance, its consent at the point of action) govern there; an IDE-managed session sandbox was live-observed (2026-07-11/12) ignoring the settings security keys in BOTH scopes, and whether a session's sandbox honours them is not knowable from the advisor.
|
|
57
57
|
- Either way the recipe is **observed-minimal, honestly incomplete** — a blocked host names itself at run time; the manifests record what was observed.
|
|
58
58
|
|
|
59
|
-
**Invariants:** read-only (never writes, never commits, never runs a subscription CLI) · `--cwd` required · present-even-when-empty · verdict-first from frozen templates · probe failures degrade to stated skip lines · chat apply one-liners are cwd-independent (absolute tool paths + a pinned `--cwd`; the ONE exception is the `set-autonomy` item, a conversational skill invocation explicitly labeled *run IN the target project*) · the bare `sandbox-provision` item is the user's own step, pinned via a `cd <root> &&` prefix — the doctor reads its cwd · harness items carry a check and an apply bound to its digest; user items, including `family-freshness`, carry the user's own step · the used bridges' `sandbox.filesystem.allowWrite` and `sandbox.network.allowedDomains` are seeded only by the consented `velocity-profile --bridge-tier`; the worktrees grant
|
|
59
|
+
**Invariants:** read-only (never writes, never commits, never runs a subscription CLI) · `--cwd` required · present-even-when-empty · verdict-first from frozen templates · probe failures degrade to stated skip lines · chat apply one-liners are cwd-independent (absolute tool paths + a pinned `--cwd`; the ONE exception is the `set-autonomy` item, a conversational skill invocation explicitly labeled *run IN the target project*) · the bare `sandbox-provision` item is the user's own step, pinned via a `cd <root> &&` prefix — the doctor reads its cwd · harness items carry a check and an apply bound to its digest; user items, including `family-freshness`, carry the user's own step · the used bridges' `sandbox.filesystem.allowWrite` and `sandbox.network.allowedDomains` are seeded only by the consented `velocity-profile --bridge-tier`; the worktrees grant is seeded into the local settings by the consented `velocity-profile --worktrees` · the `worktrees-dir` item converges on a declared `allowWrite` entry covering its probed dir in either scope, or on its `worktreesDirAck` in `docs/ai/acks.json`, recorded by the `ack-write` line its `recipe:` line carries, not by its apply line.
|
|
60
60
|
|
|
61
61
|
**Exit codes:** `0` report rendered (items or the empty state); `1` error (e.g. `--cwd` is not a directory); `2` usage.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
Requires: ${CLAUDE_SKILL_DIR}/references/shared/command-shapes.md
|
|
10
10
|
|
|
11
|
-
The opt-in onboarding **velocity profile** — it seeds a fixed, audited **read-only** Claude Code allowlist into `.claude/settings.json` so an agent stops idling on approval prompts for routine read-only commands while the maintainer is away. It is the family's **first programmatic `.claude/settings.json` writer** (attribution stayed an agent-driven prose merge). **In-agent, opt-in, writes
|
|
11
|
+
The opt-in onboarding **velocity profile** — it seeds a fixed, audited **read-only** Claude Code allowlist into `.claude/settings.json` so an agent stops idling on approval prompts for routine read-only commands while the maintainer is away. It is the family's **first programmatic `.claude/settings.json` writer** (attribution stayed an agent-driven prose merge). **In-agent, opt-in, writes `.claude/settings.json`** — its `--worktrees` mode alone writes `.claude/settings.local.json` plus the created parent — on one hard rule: **it never allowlists `commit`/`push`/`publish`** — so a direct commit/push/publish still ASKs; the only caveat is the trust-posture residual (below) — its closure is **shipped, opt-in: `${CLAUDE_SKILL_DIR}/references/modes/hook.md`**.
|
|
12
12
|
|
|
13
13
|
**Version-status routing (like the other writer modes):** read `docs/ai/.workflow-version` first — not-deployed → bootstrap; stamp < `4.0.0` → `upgrade`; stamp > head / unparseable → STOP. The tool enforces this in code too (`--apply` STOPs unless the stamp is the lineage head).
|
|
14
14
|
|
|
@@ -17,8 +17,8 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/velocity-profile.mjs [--dry-run | --apply] [
|
|
|
17
17
|
1. **`--dry-run` first, always** (the default — changes nothing). It prints: the fixed read-only core it would add; the apply ask rule it would add (step 3); a **read-only advisory** that lists your `package.json` `scripts` as **unaudited candidates you may add BY HAND** (inspect each first) to `.claude/settings.json` / `settings.local.json` — the tool **never** writes them and flags obviously-mutating names as "do not add"; any **pre-existing non-read-only `Bash(...)` entries** to consider removing by hand; and the honest residual notice (below). It STOPs (zero writes) on a symlinked `.claude` / non-regular `settings.json`, malformed settings JSON, or an unsafe `permissions.defaultMode` — `bypassPermissions` or anything outside `default`/`acceptEdits`/`plan`, present in **either** `settings.json` or `settings.local.json`.
|
|
18
18
|
2. **Ask the opt-ins** via **`AskUserQuestion` where supported** — two separate choices, the safe option FIRST in each. First the **`--kit-tools` tier** (separate from `acceptEdits`; **"core only" remains the default answer** — seed the tier only on an explicit yes, then add `--kit-tools` to the `--apply` re-run). Then the `acceptEdits` opt-in:
|
|
19
19
|
- **"Keep per-edit approval prompts (recommended)"** — seed only the read-only allowlist; file edits still prompt.
|
|
20
|
-
- **"Auto-accept file edits (`defaultMode: acceptEdits`)"** — present the honest FULL posture: it auto-applies Edit/Write AND auto-runs `mkdir`/`touch`/`mv`/`cp` in the working dir, is paired with the read-only allowlist, and — stated plainly — a settings-level allow rule is a **trust posture, not a sandbox**: a read-only entry can still write a file via output redirection, and (Claude Code's allow rules do not inspect command substitution) could in principle run another command via `cmd $(…)`. velocity **never adds `commit`/`push`/`publish` as allow rules** — so a direct `git push` still ASKs — and the residual guard for the seeded core ships as the opt-in PreToolUse hook (`${CLAUDE_SKILL_DIR}/references/modes/hook.md`; probe-proven on the `--output` family — current engine builds already intercept redirection/substitution upstream). The approval **floor** (never auto-approved, with or without the tier): every writer `--apply`/`--write`/`--yes` still prompts; clobber-protection STOPs still stop; the three release asks (commit/push/publish) stay maintainer-owned. Note also that a `defaultMode` in `settings.local.json` would override this project-level write (local > project), since velocity writes
|
|
21
|
-
3. **Only on an explicit yes**, re-run with `--apply` (add `--accept-edits` only if they chose the second option). It merges-don't-clobber (preserves `includeCoAuthoredBy`, every key, and existing allow entries) and writes **only** `.claude/settings.json
|
|
20
|
+
- **"Auto-accept file edits (`defaultMode: acceptEdits`)"** — present the honest FULL posture: it auto-applies Edit/Write AND auto-runs `mkdir`/`touch`/`mv`/`cp` in the working dir, is paired with the read-only allowlist, and — stated plainly — a settings-level allow rule is a **trust posture, not a sandbox**: a read-only entry can still write a file via output redirection, and (Claude Code's allow rules do not inspect command substitution) could in principle run another command via `cmd $(…)`. velocity **never adds `commit`/`push`/`publish` as allow rules** — so a direct `git push` still ASKs — and the residual guard for the seeded core ships as the opt-in PreToolUse hook (`${CLAUDE_SKILL_DIR}/references/modes/hook.md`; probe-proven on the `--output` family — current engine builds already intercept redirection/substitution upstream). The approval **floor** (never auto-approved, with or without the tier): every writer `--apply`/`--write`/`--yes` still prompts; clobber-protection STOPs still stop; the three release asks (commit/push/publish) stay maintainer-owned. Note also that a `defaultMode` in `settings.local.json` would override this project-level write (local > project), since velocity writes `.claude/settings.json` here — its `--worktrees` mode alone writes `.claude/settings.local.json`, and only its one grant entry.
|
|
21
|
+
3. **Only on an explicit yes**, re-run with `--apply` (add `--accept-edits` only if they chose the second option). It merges-don't-clobber (preserves `includeCoAuthoredBy`, every key, and existing allow entries) and writes **only** `.claude/settings.json` (the `--worktrees` mode below is the one exception). Every apply — flagless, `--kit-tools`, `--bridge-tier` — also appends one ask rule, `Bash(node *apply-danger-check.mjs*--apply*)`, to `permissions.ask` once, beside any ask entries already there: the rule exists so all work can stay in auto mode: without it auto mode refuses a harness apply (the `apply:` line of each harness item the advisor renders) and the user must leave auto mode for it; with it the harness shows its usual dialog. The rule allows nothing, and `--autonomy` leaves it in place. When you name the rule to the user, lead with this purpose.
|
|
22
22
|
4. **Surface delegation-readiness, read-only.** If they want a step run Delegated, set it with `/agent-workflow-kit set-recipe --set plan-execution.execute=delegated` (`${CLAUDE_SKILL_DIR}/references/modes/set-recipe.md`) or by hand-editing `docs/ai/orchestration.json`; **velocity itself never writes the orchestration config.**
|
|
23
23
|
|
|
24
24
|
**The `--kit-tools` tier (opt-in).** On top of the read-only core, `--kit-tools` seeds an audited kit-tool tier — every path **resolved absolute from the running skill at seed time**, so the posture is **fail-safe**: a moved or reinstalled skill leaves a stale rule that simply **prompts again** (re-run velocity to reseed; nothing widens silently). A seeded entry equals its covered dispatch line below with `${CLAUDE_SKILL_DIR}` replaced by the resolved skill dir and `${PROJECT_ROOT}` by this project's resolved root, **UNQUOTED** — a quoted path is a different byte string and will still prompt (a dead rule, not a hole). The covered dispatch lines (this list IS the documented-invocation source):
|
|
@@ -43,7 +43,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/velocity-profile.mjs [--dry-run | --apply] [
|
|
|
43
43
|
|
|
44
44
|
Honesty notes: tier entries get **NO PreToolUse-hook residual coverage — with TWO deliberate exceptions, `repo-search.mjs` and `path-inventory.mjs`**, whose prefixes are in the hook's scanned list because they take caller-supplied argument bytes: a redirection or substitution on THEIR invocation raises the ask, whose reason names that tool's out-of-band lane (`--pattern-file` / `--paths-file`) — context for the HUMAN deciding, not a message to the caller. Neither is in the seeded core, so neither inherits the read-lane allow. Every other tier entry: the opt-in hook's residual ask-net guards only the seeded read-only CORE prefixes, so the tier rides the same settings-level residual posture as the core (redirection / command substitution are not inspectable at the settings layer; see the residual notice). A skill or project path that cannot survive UNQUOTED in a byte-exact rule (spaces, metacharacters, non-POSIX) **STOPs the tier up front with a clear error** — nothing is seeded. Anything you want covered beyond the tier — such paths, this repo's own relative-path spellings, other tools — stays a **BY-HAND add** to your settings, with the path your project actually reaches the kit by. Pre-existing `node …` allow entries that do NOT match the seeded tier byte-forms stay flagged by the advisory for hand review.
|
|
45
45
|
|
|
46
|
-
**Invariants:** creates `.claude/` if absent and writes
|
|
46
|
+
**Invariants:** creates `.claude/` if absent and writes `.claude/settings.json`, and with `--worktrees` alone `.claude/settings.local.json` plus the created parent (no other file); **never** allowlists commit/push/publish; never commits; opt-in `acceptEdits`, never silent.
|
|
47
47
|
|
|
48
48
|
**Every settings-derived RUNTIME claim on this mode's surfaces is host-conditional.** Whether a host applies the `sandbox.*` settings keys is **not knowable from here** — a settings-native host honors them; an IDE/session-imposed sandbox was observed ignoring `sandbox.network.allowedDomains` / `sandbox.filesystem.allowWrite` in BOTH scopes. So every statement about what a settings key DOES at run time is stated **`where the host honors the settings sandbox keys`**, never promised — the bridge tier's exclusion routing; the autonomy render's `allowedDomains` / `allowWrite` / `allowUnsandboxedCommands` / `excludedCommands` degrade lines over FOREIGN keys; and equally the render's claims about the keys it writes ITSELF (the `sandbox:` line, the `fs_outside_repo` confinement note, the network prompt-on-egress and credentials-coverage degrades), which is where a clean deployment reads a promise. The qualifier rides every such line, and one notice naming the unknown closes **every** `--autonomy` render, clean settings included. A malformed `allowWrite` (not an array, or an entry that is not a non-empty string) is reported as unreadable rather than assumed empty — as UNVERIFIABLE, never as a weakening: nothing may be claimed about what a value the tool cannot read would widen.
|
|
49
49
|
|
|
@@ -61,7 +61,9 @@ Honesty notes: tier entries get **NO PreToolUse-hook residual coverage — with
|
|
|
61
61
|
|
|
62
62
|
**Read-side invocation shape (why routine reads prompt, and the fix).** The same rule binds EVERY prefix allow entry, seeded read-only core included: a **compound chain** (`grep … && …`, `;`/`|`-joined reads), an **env-var prefix** (`FOO=bar grep …`), a **`$`/`${…}`/`$(…)` expansion**, or an **inline `node -e '…'`** never matches a prefix rule — so read *compounds* keep prompting at the settings layer even when each command is individually seeded. The mechanism that clears them is the opt-in **read-lane** in the PreToolUse hook (`${CLAUDE_SKILL_DIR}/references/modes/hook.md`): it auto-approves a compound whose every separator-split segment is a plain seeded read-only core command with **zero shell metaprogramming**. A scripted probe stays allow-listable by being a **WRITTEN scratch file run as `node <file>`** (a byte-exact declared gate, or a kit-tool tier entry) — **never** `node -e '…'`, which is unclassifiable and always prompts.
|
|
63
63
|
|
|
64
|
-
**
|
|
64
|
+
**The `--worktrees` grant (own opt-in, its own mode).** Run `node ${CLAUDE_SKILL_DIR}/tools/velocity-profile.mjs --worktrees [--apply] [--cwd <dir>]`. It writes ONE `sandbox.filesystem.allowWrite` entry, the worktrees parent itself (`docs/ai/worktrees.json` `parentDir`, else the repository's own parent; home-relative under the home), into this copy's `.claude/settings.local.json` alone, and creates the parent when it is missing. It never writes `.claude/settings.json`, seeds no core and no ask rule, and cannot be combined with `--autonomy`, `--kit-tools`, `--bridge-tier` or `--accept-edits`. A parent that is the home or holds it is refused (`VELOCITY_WORKTREES_HOME`), nothing written. **Where the host honors the settings sandbox keys**, every sandboxed command of this copy, and of every worktree provisioned from it, can then write the whole parent dir, sibling repositories under it included, for every later session. Narrow it BEFORE the first apply: point `parentDir` at a dedicated dir — no apply removes an entry, so an earlier one stays until you delete it. The advisor's `worktrees-dir` item runs it through the danger check on the chat yes.
|
|
65
|
+
|
|
66
|
+
**Exit codes:** `0` done / dry-run; `1` a precondition STOP (stamp not current, unsafe mode, malformed settings, symlinked `.claude` / non-regular target, a worktrees parent that is the home or holds it); `2` bad arguments.
|
|
65
67
|
|
|
66
68
|
---
|
|
67
69
|
|
|
@@ -213,16 +213,17 @@ stated skip), and the ancestor chain is verified even when the leaf is ABSENT (a
|
|
|
213
213
|
`docs/` or `docs/ai` never reads as plain absence). `--dir` overrides per invocation.
|
|
214
214
|
|
|
215
215
|
**Host-specific consent (zero-prompt only where the host honors it):** every sibling-dir mutation
|
|
216
|
-
runs a REAL create+delete writability probe first. An unwritable parent prints
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
216
|
+
runs a REAL create+delete writability probe first. An unwritable parent prints the `worktrees-dir`
|
|
217
|
+
check line and the full terminal command. The agent runs that check line and, on the chat yes on
|
|
218
|
+
its result, its apply line by the consent route: one `sandbox.filesystem.allowWrite` entry naming
|
|
219
|
+
the parent, written to this copy's `.claude/settings.local.json` only; where the host honors the
|
|
220
|
+
key, later provision/cleanup then run promptless. When the check refuses, prints nothing to
|
|
221
|
+
change, or grants a dir that does not hold the command's target, use the printed terminal command
|
|
222
|
+
for each operation. The `recommendations` mode surfaces this lane and converges it two ways:
|
|
222
223
|
a declared `sandbox.filesystem.allowWrite` entry covering the probed parent (either settings
|
|
223
|
-
scope), or — on a host that ignores that key — the neutral dir-bound acknowledgement
|
|
224
|
-
|
|
225
|
-
|
|
224
|
+
scope), or — on a host that ignores that key — the neutral dir-bound acknowledgement
|
|
225
|
+
`worktreesDirAck` in `docs/ai/acks.json`, recorded by the `ack-write` line the item's `recipe:`
|
|
226
|
+
line carries, the fallback instead of the grant. Neither is proof of write
|
|
226
227
|
capability: the create+delete probe above stays the runtime truth, and the ack binds to the
|
|
227
228
|
resolved probe dir, so the item re-fires only when that resolved dir changes.
|
|
228
229
|
|
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
writeAutonomyProfile, writeVelocityProfile,
|
|
13
13
|
} from './velocity-profile.mjs';
|
|
14
14
|
import { KIT_GROUNDING_TOOL, bundledSandboxRecipe, usedBridges } from './bridge-sandbox-recipe.mjs';
|
|
15
|
-
import { mergeAllowWrite, stateDirsOf } from './bridge-state-dirs.mjs';
|
|
15
|
+
import { homeRelative, mergeAllowWrite, stateDirsOf } from './bridge-state-dirs.mjs';
|
|
16
|
+
import { nearestExistingDir, worktreesGrantDir, worktreesGrantHoldsHome } from './worktrees.mjs';
|
|
16
17
|
import { loadConfig } from './orchestration-config.mjs';
|
|
17
18
|
import { HOOK_FILE_REL, HOOKS_DIR, buildHookSettingsEntry, mergeHookEntry, writeGateHook } from './gate-hook.mjs';
|
|
18
19
|
import { STATE_BLOCK_HOOK_FILE_REL, buildStopHookEntry, mergeStopEntry, writeStateBlockHook } from './state-block-hook.mjs';
|
|
@@ -36,8 +37,9 @@ const STOP = 'state-block';
|
|
|
36
37
|
const MCP_VARIANTS = ['mcp-channel', 'mcp-channel.differing'];
|
|
37
38
|
const AGENT_VARIANTS = ['agents', 'executor-vehicle'];
|
|
38
39
|
const GIT_VARIANTS = ['enforcement', 'commit-guard'];
|
|
40
|
+
const WORKTREES = 'worktrees-dir';
|
|
39
41
|
const VARIANTS = [...VELOCITY_VARIANTS, 'autonomy-render', ...HOOK_VARIANTS, STOP, ...MCP_VARIANTS, ...AGENT_VARIANTS,
|
|
40
|
-
...SKILL_VARIANTS, ...GIT_VARIANTS];
|
|
42
|
+
...SKILL_VARIANTS, ...GIT_VARIANTS, WORKTREES];
|
|
41
43
|
const HOOK_KEYS = ['hooks.PreToolUse', 'hooks.Stop'];
|
|
42
44
|
const CREDENTIALS = 'sandbox.credentials.envVars';
|
|
43
45
|
const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -181,7 +183,12 @@ export const scopeOf = (variant, facts, deps = {}) => {
|
|
|
181
183
|
set('permissions.ask', RENDER_OWNED_REDLINE_RULES);
|
|
182
184
|
set('permissions.deny', RENDER_OWNED_REDLINE_RULES);
|
|
183
185
|
allowed[CREDENTIALS] = (value) => value.mode === 'deny';
|
|
184
|
-
}
|
|
186
|
+
}
|
|
187
|
+
const grantDir = variant === WORKTREES ? worktreesGrantDir(facts.root, facts) : null;
|
|
188
|
+
if (grantDir !== null && !worktreesGrantHoldsHome(grantDir, facts.home, facts)) {
|
|
189
|
+
set('sandbox.filesystem.allowWrite', [homeRelative(grantDir, facts.home)]);
|
|
190
|
+
}
|
|
191
|
+
if (HOOK_VARIANTS.includes(variant)) set('hooks.PreToolUse', [buildHookSettingsEntry()]);
|
|
185
192
|
else if (variant === STOP) set('hooks.Stop', [buildStopHookEntry()]);
|
|
186
193
|
else if (MCP_VARIANTS.includes(variant)) {
|
|
187
194
|
set(`${SERVERS_KEY}.${SERVER_NAME}`, [buildServerEntry(join(facts.toolsDir, 'mcp-server.mjs'))]);
|
|
@@ -195,6 +202,8 @@ export const scopeOf = (variant, facts, deps = {}) => {
|
|
|
195
202
|
const directories = SKILL_VARIANTS.includes(variant) ? [] : [join(facts.root, CLAUDE_DIR)];
|
|
196
203
|
if (HOOK_VARIANTS.includes(variant) || variant === STOP) directories.push(join(facts.root, HOOKS_DIR));
|
|
197
204
|
if (AGENT_VARIANTS.includes(variant)) directories.push(join(facts.root, AGENTS_DIR));
|
|
205
|
+
const existingAncestor = grantDir === null ? null : nearestExistingDir(grantDir, facts);
|
|
206
|
+
for (let at = grantDir; at !== existingAncestor; at = dirname(at)) directories.push(at);
|
|
198
207
|
for (const target of targets) {
|
|
199
208
|
for (let at = target.root; at !== target.base && covers(target.base, at); at = dirname(at)) directories.push(at);
|
|
200
209
|
filePaths.push(target.path);
|
|
@@ -209,8 +218,9 @@ export const scopeOf = (variant, facts, deps = {}) => {
|
|
|
209
218
|
return {
|
|
210
219
|
admitsRun: (argv) => runs !== null && equal(argv, runs),
|
|
211
220
|
admits: ({ path, key, change, value }) => {
|
|
212
|
-
const correctFile = key.startsWith(`${SERVERS_KEY}.`) ? path === MCP_JSON_REL
|
|
213
|
-
|
|
221
|
+
const correctFile = key.startsWith(`${SERVERS_KEY}.`) ? path === MCP_JSON_REL
|
|
222
|
+
: path === (variant === WORKTREES ? SETTINGS_LOCAL_FILE : SETTINGS_FILE);
|
|
223
|
+
const gains = variant === 'bridge-tier' || variant === WORKTREES || HOOK_KEYS.includes(key) || key === ENABLED_KEY || key === CREDENTIALS
|
|
214
224
|
|| (VELOCITY_VARIANTS.includes(variant) && key === 'permissions.ask');
|
|
215
225
|
return correctFile && (!gains || change === 'added') && Boolean(allowed[key]?.(value));
|
|
216
226
|
},
|
|
@@ -223,7 +233,7 @@ const jsonAt = (path) => existsSync(path) ? JSON.parse(readFileSync(path, 'utf8'
|
|
|
223
233
|
const halted = (lines) => Object.assign(new Error(lines.join('\n')), { code: 'CHECK_HALTED', lines });
|
|
224
234
|
const maskedFiles = (variant, facts) => {
|
|
225
235
|
const paths = VELOCITY_VARIANTS.includes(variant) || variant === 'autonomy-render' || HOOK_VARIANTS.includes(variant)
|
|
226
|
-
|| variant === STOP ? [SETTINGS_FILE, SETTINGS_LOCAL_FILE] : MCP_VARIANTS.includes(variant) ? [MCP_JSON_REL, SETTINGS_FILE] : [];
|
|
236
|
+
|| variant === STOP || variant === WORKTREES ? [SETTINGS_FILE, SETTINGS_LOCAL_FILE] : MCP_VARIANTS.includes(variant) ? [MCP_JSON_REL, SETTINGS_FILE] : [];
|
|
227
237
|
return paths.flatMap((path) => lstatSync(join(facts.root, path), { throwIfNoEntry: false })?.isCharacterDevice()
|
|
228
238
|
? [`masked: ${path}`] : []);
|
|
229
239
|
};
|
|
@@ -231,6 +241,7 @@ export const plannedChange = (variant, facts, deps = {}) => {
|
|
|
231
241
|
const files = [];
|
|
232
242
|
const writes = [];
|
|
233
243
|
const temporaries = [];
|
|
244
|
+
const created = [];
|
|
234
245
|
let runs = null;
|
|
235
246
|
const writerDeps = writerFacts(facts, deps);
|
|
236
247
|
const addFile = (path, before, after) => {
|
|
@@ -295,6 +306,11 @@ export const plannedChange = (variant, facts, deps = {}) => {
|
|
|
295
306
|
const plan = (deps.planGitHooks ?? planGitHooks)({ root: facts.root, variant, toolsDir: facts.toolsDir });
|
|
296
307
|
for (const copy of plan.copies.filter(({ action }) => action === 'created')) writes.push({ path: copy.path, bytes: copy.bytes });
|
|
297
308
|
runs = { argv: plan.runs.shown, line: formatPlan(plan).at(-1) };
|
|
309
|
+
} else if (variant === WORKTREES) {
|
|
310
|
+
const plan = writeVelocityProfile({ cwd: facts.root, dryRun: true, worktrees: true }, writerDeps);
|
|
311
|
+
const before = plan.localSettings.data ?? null;
|
|
312
|
+
addFile(SETTINGS_LOCAL_FILE, before, mergeAllowWrite(before ?? {}, plan.worktreesDirToAdd));
|
|
313
|
+
created.push(...plan.worktreesDirsToCreate);
|
|
298
314
|
} else throw new Error(`unsupported variant: ${variant}`);
|
|
299
315
|
} catch (error) {
|
|
300
316
|
throw error.code === 'CHECK_HALTED' ? error : halted([error.message]);
|
|
@@ -304,6 +320,7 @@ export const plannedChange = (variant, facts, deps = {}) => {
|
|
|
304
320
|
for (const path of [...changedFiles.map(({ path }) => join(facts.root, path)), ...writes.map(({ path }) => path)]) {
|
|
305
321
|
for (let parent = dirname(path); !existsSync(parent); parent = dirname(parent)) directories.add(parent);
|
|
306
322
|
}
|
|
323
|
+
for (const path of created) directories.add(path);
|
|
307
324
|
return { files, writes, temporaries, directories: [...directories], runs };
|
|
308
325
|
};
|
|
309
326
|
|
package/tools/init-project.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { preflightInit } from './init-preflight.mjs';
|
|
|
13
13
|
import { buildRecommendations, hostFacts } from './recommendations.mjs';
|
|
14
14
|
import { compareSemver } from './semver-lite.mjs';
|
|
15
15
|
import { EXPECTED_WORKFLOW_VERSION } from './velocity-profile.mjs';
|
|
16
|
-
import { LANE_HARNESS, VARIANT_LANES, consoleArgv, isEnvDependent, isProtectedPath, laneOf, writesOf } from './write-lanes.mjs';
|
|
16
|
+
import { LANE_HARNESS, VARIANT_LANES, consoleArgv, isEnvDependent, isProtectedPath, isRejudged, laneOf, writesOf } from './write-lanes.mjs';
|
|
17
17
|
|
|
18
18
|
const INSIDE_LINE = 'you are inside an agent; run this in your own terminal';
|
|
19
19
|
const APPLY_PROMPT = 'apply? y/N: ';
|
|
@@ -217,17 +217,20 @@ const previewQueue = async (context, queue) => {
|
|
|
217
217
|
context.print(result.stdout);
|
|
218
218
|
const tuple = result.stdout.match(/--apply\s+(\S+)/)?.[1];
|
|
219
219
|
const facts = { ...context.facts, tuple };
|
|
220
|
+
let checkDigest = null;
|
|
220
221
|
if (laneOf(item.variant) === LANE_HARNESS) {
|
|
221
222
|
try {
|
|
222
|
-
const { lines, refused } = context.checkApply(item.variant, facts, {});
|
|
223
|
+
const { lines, refused, digest } = context.checkApply(item.variant, facts, {});
|
|
223
224
|
for (const line of lines) context.print(line);
|
|
224
225
|
if (refused.length > 0) throw new Error(refused.map(({ key }) => key).join(', '));
|
|
226
|
+
checkDigest = digest;
|
|
225
227
|
} catch (error) {
|
|
226
228
|
context.print(`not applied: ${item.variant} — the danger check refused: ${error.message}`);
|
|
227
229
|
continue;
|
|
228
230
|
}
|
|
229
231
|
}
|
|
230
|
-
batch.push({ ...item,
|
|
232
|
+
batch.push({ ...item, checkFacts: facts, checkDigest, applyArgv: consoleArgv(item.variant, facts).apply,
|
|
233
|
+
writes: writesOf(item.variant, facts) });
|
|
231
234
|
printNote(context, item.key);
|
|
232
235
|
}
|
|
233
236
|
for (const item of queue.filter(({ variant }) => consoleArgv(variant, context.facts) === null)) {
|
|
@@ -254,11 +257,24 @@ const takeRecord = (context, queue) => {
|
|
|
254
257
|
}
|
|
255
258
|
};
|
|
256
259
|
|
|
260
|
+
// A rejudged item applies only while its check, run again now, answers the digest its preview showed; else the reason.
|
|
261
|
+
const rejudgeFailure = (context, item) => {
|
|
262
|
+
try {
|
|
263
|
+
const { refused, digest } = context.checkApply(item.variant, item.checkFacts, {});
|
|
264
|
+
if (refused.length > 0) return `it refuses ${refused.map(({ key }) => key).join(', ')}`;
|
|
265
|
+
return digest === item.checkDigest ? null : 'its digest moved';
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return String(error?.message || error);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
257
271
|
const runPass = async (context, queue) => {
|
|
258
272
|
const record = takeRecord(context, queue);
|
|
259
273
|
if (record === null) return { code: 1, successes: 0, variants: [] };
|
|
260
|
-
const
|
|
261
|
-
if (
|
|
274
|
+
const previewed = await previewQueue(context, queue);
|
|
275
|
+
if (previewed.length === 0) return { code: 0, successes: 0, variants: [] };
|
|
276
|
+
// Rejudged items go first: an earlier writer of the batch (one creating .claude/) would move their check.
|
|
277
|
+
const batch = [...previewed.filter(({ variant }) => isRejudged(variant)), ...previewed.filter(({ variant }) => !isRejudged(variant))];
|
|
262
278
|
const before = new Map(record);
|
|
263
279
|
const outcome = { code: 0, successes: 0, variants: batch.map((item) => item.variant) };
|
|
264
280
|
context.print(RUN_NONE);
|
|
@@ -271,6 +287,15 @@ const runPass = async (context, queue) => {
|
|
|
271
287
|
outcome.code = 1;
|
|
272
288
|
break;
|
|
273
289
|
}
|
|
290
|
+
const failure = isRejudged(item.variant) ? rejudgeFailure(context, item) : null;
|
|
291
|
+
if (failure !== null) {
|
|
292
|
+
context.print(`not applied: ${item.variant} — its danger check no longer answers the previewed result: ${failure}`);
|
|
293
|
+
for (const later of batch.slice(index + 1)) {
|
|
294
|
+
context.print(`not applied: ${later.variant} — an earlier item's danger check no longer answers the previewed result`);
|
|
295
|
+
}
|
|
296
|
+
outcome.code = 1;
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
274
299
|
const result = await launch(context, item.applyArgv, ['inherit', 'pipe', 'inherit']);
|
|
275
300
|
if (result.stdout) context.print(result.stdout);
|
|
276
301
|
const success = result.valid && (result.status === 0 || (result.status === 5 && result.stdout.includes('install completed')));
|
|
@@ -80,7 +80,7 @@ import { loadConfig } from './orchestration-config.mjs';
|
|
|
80
80
|
import { homeRelative } from './bridge-state-dirs.mjs';
|
|
81
81
|
import { sandboxEnabled, HOST_HONORS_QUALIFIER } from './bridge-wiring.mjs';
|
|
82
82
|
import { assertContainedRealPath } from './fs-safe.mjs';
|
|
83
|
-
import {
|
|
83
|
+
import { nearestExistingDir, worktreesGrantDir, worktreesGrantHoldsHome } from './worktrees.mjs';
|
|
84
84
|
import { preflightCheapAgents, EXECUTOR_VEHICLE } from './cheap-agents.mjs';
|
|
85
85
|
import { NODE_EVIDENCE, NODE_EVIDENCE_SCRIPTS, probeNodeEvidence } from './node-evidence.mjs';
|
|
86
86
|
// The vehicle READINESS comes from the read-only half — the same survey `recipes` and `status` read.
|
|
@@ -199,6 +199,7 @@ export const SEVERITIES = Object.freeze({
|
|
|
199
199
|
'sandbox-masks': SEVERITY_OPTIONAL,
|
|
200
200
|
'sandbox-masks.unfenced-mount': SEVERITY_ATTENTION,
|
|
201
201
|
'worktrees-dir': SEVERITY_OPTIONAL,
|
|
202
|
+
'worktrees-dir.home': SEVERITY_OPTIONAL,
|
|
202
203
|
// The layer is opt-in, so both arms are OFFERS under the frozen registry (attention is a CONFIGURED
|
|
203
204
|
// declaration that is broken): an absent store offers the seed, a store with no live contract offers
|
|
204
205
|
// the decline. Neither arm can leave the flow-optimal line standing — an offer is still an item.
|
|
@@ -282,6 +283,7 @@ export const WHATS = Object.freeze({
|
|
|
282
283
|
'sandbox-masks': '{n} sandbox mask(s) clutter git status — the managed exclude block is absent or stale',
|
|
283
284
|
'sandbox-masks.stale-real': '{n} sandbox mask(s) clutter git status — the exclude block is stale; {m} fenced entr(ies) are REAL paths (a fresh apply drops them)',
|
|
284
285
|
'worktrees-dir': 'write access to the worktrees parent dir {dir} is not confirmed — provision may still stop',
|
|
286
|
+
'worktrees-dir.home': 'the worktrees parent dir {dir} is the home or holds it — the kit grants no write surface there',
|
|
285
287
|
'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',
|
|
286
288
|
'profile-gap': '{what}',
|
|
287
289
|
'jev-connect': 'Jev (TypeSafe) is not connected: TYPESAFE_API_KEY is not in the environment the agent\'s commands run in',
|
|
@@ -1404,46 +1406,31 @@ const declaresWritableDir = (root, probeDir, deps) => {
|
|
|
1404
1406
|
// existing ancestor resolve to the same dir and keep the same ack.
|
|
1405
1407
|
export const worktreesDirFingerprint = (probeDir, home) => recipeFingerprint({ hosts: [], dirs: [probeDir], home });
|
|
1406
1408
|
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
//
|
|
1410
|
-
//
|
|
1411
|
-
// production, so the item fired forever even once its own advice had been applied.
|
|
1409
|
+
const WORKTREES_ACK_FALLBACK = 'ack fallback, only on your word that this host ignores the settings key, instead of the grant: ';
|
|
1410
|
+
|
|
1411
|
+
// The worktrees-dir item, by the table of spec bridges/velocity-profile, part worktrees-grant: the grant
|
|
1412
|
+
// names the parent itself; the probe dir (its nearest existing ancestor) serves the convergence alone.
|
|
1412
1413
|
const probeWorktreesDir = ({ root, deps, add, skip }) => {
|
|
1413
1414
|
try {
|
|
1414
|
-
const
|
|
1415
|
-
const
|
|
1416
|
-
// the SAME canonical derivation the provision preflight probes (worktrees.mjs) — an absent
|
|
1417
|
-
// configured dir resolves to its nearest existing ancestor, never a false denial
|
|
1418
|
-
const probeDir = resolveProbeDir(parent, deps);
|
|
1419
|
-
// A supplied host signal is runtime truth and OVERRIDES both declaration lanes in EITHER
|
|
1420
|
-
// direction: a trusted yes converges, a trusted no fires however the project is configured.
|
|
1415
|
+
const grantDir = worktreesGrantDir(root, deps);
|
|
1416
|
+
const probeDir = nearestExistingDir(grantDir, deps);
|
|
1421
1417
|
const hostSignal = deps.canWriteDir?.(probeDir);
|
|
1422
1418
|
if (hostSignal === true) return;
|
|
1423
1419
|
const home = deps.home ?? homedir();
|
|
1420
|
+
if (worktreesGrantHoldsHome(grantDir, home, deps)) {
|
|
1421
|
+
const dir = truncatedTo(oneLineOf(grantDir), templateBudget(WHATS['worktrees-dir.home']));
|
|
1422
|
+
add('worktrees-dir', fillTemplate(WHATS['worktrees-dir.home'], { dir }), '', 'worktrees-dir.home');
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1424
1425
|
const fingerprint = worktreesDirFingerprint(probeDir, home);
|
|
1425
1426
|
if (hostSignal !== false) {
|
|
1426
1427
|
if (declaresWritableDir(root, probeDir, deps)) return;
|
|
1427
1428
|
if (readAckValue(root, deps, ACKS_WORKTREES_DIR_KEY) === fingerprint) return;
|
|
1428
1429
|
}
|
|
1429
|
-
const dir = truncatedTo(oneLineOf(
|
|
1430
|
-
const
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
// the apply (dry-run; it prints the exact --apply the flow then runs) and the grant advice
|
|
1434
|
-
// rides the detail, labeled as the first step. Against a trusted NO the ack could never
|
|
1435
|
-
// converge, so the grant advice is the apply and no ack surfaces.
|
|
1436
|
-
if (hostSignal === false) {
|
|
1437
|
-
add('worktrees-dir', fillTemplate(WHATS['worktrees-dir'], { dir }), `HAND-APPLY: ${grantAdvice}`);
|
|
1438
|
-
} else {
|
|
1439
|
-
add(
|
|
1440
|
-
'worktrees-dir',
|
|
1441
|
-
fillTemplate(WHATS['worktrees-dir'], { dir }),
|
|
1442
|
-
`node ${q(toolPath('ack-write.mjs'))} --lane worktrees-dir --fingerprint ${fingerprint} --cwd ${q(root)}`,
|
|
1443
|
-
'worktrees-dir',
|
|
1444
|
-
`HAND-APPLY FIRST: ${grantAdvice}; THEN this item's apply one-liner previews the dir-bound ack and prints the exact --apply that records it`,
|
|
1445
|
-
);
|
|
1446
|
-
}
|
|
1430
|
+
const dir = truncatedTo(oneLineOf(grantDir), templateBudget(WHATS['worktrees-dir']));
|
|
1431
|
+
const ack = `node ${q(toolPath('ack-write.mjs'))} --lane worktrees-dir --fingerprint ${fingerprint} --cwd ${q(root)}`;
|
|
1432
|
+
add('worktrees-dir', fillTemplate(WHATS['worktrees-dir'], { dir }), ack, 'worktrees-dir',
|
|
1433
|
+
hostSignal === false ? null : `${WORKTREES_ACK_FALLBACK}${ack}`);
|
|
1447
1434
|
} catch (err) {
|
|
1448
1435
|
skip('worktrees-dir', err);
|
|
1449
1436
|
}
|
|
@@ -1619,7 +1606,7 @@ export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
|
|
|
1619
1606
|
// pattern-matching prose that is free to be reworded.
|
|
1620
1607
|
// `detail` (optional) is an extra rendered `recipe:` line — factual context that is TOO LONG for
|
|
1621
1608
|
// the capped WHAT and does NOT belong in the pure-command apply (the bridge-tier missing-entries
|
|
1622
|
-
// line; the worktrees-dir
|
|
1609
|
+
// line; the worktrees-dir ack fallback; the
|
|
1623
1610
|
// agents hidden-mode reconcile follow-up). Single-line like apply; absent for every other item.
|
|
1624
1611
|
// Returns whether the item really RENDERED. One probe's disposition depends on another's having
|
|
1625
1612
|
// spoken (the marker-stale ⟷ read-lane.stale precedence), and "the conditions still look right"
|
|
@@ -1711,7 +1698,7 @@ Usage:
|
|
|
1711
1698
|
Computes the deterministic Recommendations section every kit upgrade ends with — VERDICT-FIRST:
|
|
1712
1699
|
one composed verdict line opens every non-optimal render, then per item {severity · what is
|
|
1713
1700
|
sub-optimal · the benefit in one plain line · the lane · an optional \`recipe:\` line (the bridge-tier
|
|
1714
|
-
missing-entries line, the worktrees-dir
|
|
1701
|
+
missing-entries line, the worktrees-dir ack fallback, or the agents hidden-mode reconcile
|
|
1715
1702
|
follow-up) · the harness check line · the exact consent-gated apply one-liner}. --cwd is
|
|
1716
1703
|
REQUIRED (the target project is explicit, never inferred from the shell's current directory). The
|
|
1717
1704
|
section renders present-even-when-empty ("${RECOMMENDATIONS_EMPTY_LINE}"); a probe failure is a
|
|
@@ -13,8 +13,9 @@ import { SHELL_METACHARACTERS, hasShellMetacharacter, isSeedablePathToken } from
|
|
|
13
13
|
import { isDirectRun } from './direct-run.mjs';
|
|
14
14
|
import { compareSemver } from './semver-lite.mjs';
|
|
15
15
|
import { BRIDGE_REVIEW_WRAPPERS, BRIDGE_REVIEW_MODE, KIT_GROUNDING_TOOL, usedBridges, bundledSandboxRecipe, exclusionOf } from './bridge-sandbox-recipe.mjs';
|
|
16
|
-
import { stateDirsOf, mergeAllowWrite } from './bridge-state-dirs.mjs';
|
|
16
|
+
import { stateDirsOf, mergeAllowWrite, homeRelative } from './bridge-state-dirs.mjs';
|
|
17
17
|
import { HOST_HONORS_QUALIFIER, sandboxSurfaceDelta } from './bridge-wiring.mjs';
|
|
18
|
+
import { nearestExistingDir, worktreesGrantDir, worktreesGrantHoldsHome } from './worktrees.mjs';
|
|
18
19
|
import { loadConfig } from './orchestration-config.mjs';
|
|
19
20
|
import { validateManifest } from './manifest/validate.mjs';
|
|
20
21
|
export { BRIDGE_REVIEW_WRAPPERS, BRIDGE_REVIEW_MODE, KIT_GROUNDING_TOOL };
|
|
@@ -269,6 +270,7 @@ export const VELOCITY_STAMP = 'VELOCITY_STAMP';
|
|
|
269
270
|
export const VELOCITY_UNSAFE_MODE = 'VELOCITY_UNSAFE_MODE';
|
|
270
271
|
export const VELOCITY_MALFORMED = 'VELOCITY_MALFORMED';
|
|
271
272
|
export const VELOCITY_SYMLINK = 'VELOCITY_SYMLINK';
|
|
273
|
+
export const VELOCITY_WORKTREES_HOME = 'VELOCITY_WORKTREES_HOME';
|
|
272
274
|
// The --autonomy render refuses an absent policy file (a writer renders only a declared policy).
|
|
273
275
|
export const VELOCITY_NO_POLICY = 'VELOCITY_NO_POLICY';
|
|
274
276
|
|
|
@@ -299,6 +301,7 @@ const FLAG_ACCEPT_EDITS = '--accept-edits';
|
|
|
299
301
|
const FLAG_KIT_TOOLS = '--kit-tools';
|
|
300
302
|
const FLAG_BRIDGE_TIER = '--bridge-tier';
|
|
301
303
|
const FLAG_AUTONOMY = '--autonomy';
|
|
304
|
+
const FLAG_WORKTREES = '--worktrees';
|
|
302
305
|
const FLAG_CHECK = '--check';
|
|
303
306
|
const FLAG_CWD = '--cwd';
|
|
304
307
|
const FLAG_HELP = '--help';
|
|
@@ -329,6 +332,7 @@ const RESIDUAL_NOTICE =
|
|
|
329
332
|
const USAGE = `usage: velocity-profile [--dry-run | --apply] [--kit-tools] [--bridge-tier] [--accept-edits] [--cwd <dir>] [--help]
|
|
330
333
|
velocity-profile --autonomy [--apply] [--cwd <dir>] (render the autonomy policy)
|
|
331
334
|
velocity-profile --autonomy --check [--cwd <dir>] (read-only drift gate)
|
|
335
|
+
velocity-profile --worktrees [--apply] [--cwd <dir>] (grant the worktrees parent)
|
|
332
336
|
|
|
333
337
|
Allowlist mode (default): seeds the fixed read-only Claude Code allowlist into .claude/settings.json.
|
|
334
338
|
Default is --dry-run. --apply writes; --accept-edits only sets defaultMode when applying.
|
|
@@ -337,7 +341,7 @@ absolute path (args wildcard), run-gates.mjs as ONE exact project-root-pinned by
|
|
|
337
341
|
(project-exec - it runs YOUR declared gates.json), source-size-check.mjs as ONE exact --check
|
|
338
342
|
byte-string (its read-only mode only - its writers keep prompting), and the writers' exact arg-free
|
|
339
343
|
dry-run preview byte-strings. Never touches settings.local.json.
|
|
340
|
-
Every apply also appends the apply ask rule ${APPLY_ASK_RULE} to permissions.ask once, so all work
|
|
344
|
+
Every flagless, --kit-tools and --bridge-tier apply also appends the apply ask rule ${APPLY_ASK_RULE} to permissions.ask once, so all work
|
|
341
345
|
can stay in auto mode: without it auto mode refuses a harness apply and you must leave auto mode for it; with it the harness shows its usual dialog. It allows nothing.
|
|
342
346
|
--bridge-tier (own consent) derives each entry for a used bridge from the effective project recipes:
|
|
343
347
|
review wrappers' CODE mode (codex-review code, agy-review code), the quoted grounding pre-step rule,
|
|
@@ -354,7 +358,16 @@ permissions.ask/deny red-lines + permissions.defaultMode. POLICY-ONLY: never see
|
|
|
354
358
|
leaves permissions.allow untouched. Preview by default; --apply writes; --check is a read-only drift
|
|
355
359
|
gate (exit 1 on drift). --autonomy cannot combine with --accept-edits, --kit-tools, or --bridge-tier
|
|
356
360
|
(allowlist-mode flags). Refuses an absent policy (seed one first with set-autonomy). Never touches
|
|
357
|
-
settings.local.json
|
|
361
|
+
settings.local.json.
|
|
362
|
+
|
|
363
|
+
--worktrees (own consent, its own mode) writes ONE sandbox.filesystem.allowWrite entry, the worktrees
|
|
364
|
+
parent (docs/ai/worktrees.json parentDir, else the repository's own parent), into this copy's
|
|
365
|
+
.claude/settings.local.json only, never .claude/settings.json, and creates the parent when it is
|
|
366
|
+
missing; it seeds no core and no ask rule.
|
|
367
|
+
It widens, ${HOST_HONORS_QUALIFIER}: every sandboxed command of this copy
|
|
368
|
+
and of every worktree provisioned from it can then write the whole parent dir, sibling repositories
|
|
369
|
+
under it included. A parent that is the home or holds it is refused. It cannot combine with
|
|
370
|
+
--autonomy, --kit-tools, --bridge-tier or --accept-edits.`;
|
|
358
371
|
|
|
359
372
|
const fail = (exitCode, message) => Object.assign(new Error(message), { exitCode });
|
|
360
373
|
|
|
@@ -578,13 +591,13 @@ const collectPreExistingNonReadonly = (sources, derivedTier = [], derivedBridgeA
|
|
|
578
591
|
.map((entry) => ({ source, entry })),
|
|
579
592
|
);
|
|
580
593
|
|
|
581
|
-
const assertTargetWritable = (absPath, deps = {}) => {
|
|
594
|
+
const assertTargetWritable = (absPath, deps = {}, relPath = SETTINGS_FILE) => {
|
|
582
595
|
const fs = fsDeps(deps);
|
|
583
596
|
const stat = lstatNoFollow(absPath, fs);
|
|
584
597
|
if (stat !== null && !stat.isFile()) {
|
|
585
598
|
throw makeVelocityProfileError(
|
|
586
599
|
VELOCITY_SYMLINK,
|
|
587
|
-
`${
|
|
600
|
+
`${relPath} exists but is not a regular file - refusing to clobber it`,
|
|
588
601
|
);
|
|
589
602
|
}
|
|
590
603
|
};
|
|
@@ -689,6 +702,19 @@ const formatBridgeTier = (result) =>
|
|
|
689
702
|
]
|
|
690
703
|
: [];
|
|
691
704
|
|
|
705
|
+
export const KIT_WORKTREES_GRANT_NOTICE =
|
|
706
|
+
`worktrees grant: ${HOST_HONORS_QUALIFIER}, every sandboxed command of this copy, and of every worktree provisioned from it afterwards (provision copies the local settings), can write the whole worktrees parent dir, sibling repositories under it included, for every later session. ` +
|
|
707
|
+
'The entry lands in this copy\'s .claude/settings.local.json only, never the committed project settings. A host that ignores the key, a session under permissions.blockReadsOutsideWorkingDirectories and an admin-required sandbox are unchanged by it. ' +
|
|
708
|
+
'Narrow it: choose a dedicated docs/ai/worktrees.json parentDir BEFORE the first apply — no apply removes an entry, so a parent changed later leaves the earlier entry in settings.local.json until you delete it, your own step.';
|
|
709
|
+
|
|
710
|
+
const formatWorktreesGrant = (result) => [
|
|
711
|
+
`${result.wrote ? 'added' : 'would add'} worktrees grant (sandbox.filesystem.allowWrite in ${SETTINGS_LOCAL_FILE}) entries: ${result.worktreesDirToAdd.length}`,
|
|
712
|
+
...formatEntryList(result.worktreesDirToAdd),
|
|
713
|
+
`already present (worktrees grant): ${result.worktreesDirAlreadyPresent.length}`,
|
|
714
|
+
...result.worktreesDirsToCreate.map((dir) => `${result.wrote ? 'created' : 'would create'}: ${dir}`),
|
|
715
|
+
KIT_WORKTREES_GRANT_NOTICE,
|
|
716
|
+
];
|
|
717
|
+
|
|
692
718
|
const formatGateAdvisory = (gateCandidates) => [
|
|
693
719
|
'gate advisory: candidates you may add BY HAND to .claude/settings.json or settings.local.json; this tool will NOT add them',
|
|
694
720
|
...(gateCandidates.length
|
|
@@ -710,7 +736,10 @@ const formatDefaultMode = (result) =>
|
|
|
710
736
|
: `defaultMode: ${result.setsDefaultMode ? `would set to ${ACCEPT_EDITS_MODE}` : 'would not be set by this run'}`;
|
|
711
737
|
|
|
712
738
|
const formatVelocityProfileResult = (result) =>
|
|
713
|
-
[
|
|
739
|
+
result.worktrees ? [
|
|
740
|
+
result.dryRun ? 'agent-workflow velocity profile - DRY RUN (no changes)' : 'agent-workflow velocity profile - APPLY',
|
|
741
|
+
...formatWorktreesGrant(result),
|
|
742
|
+
].join(LF) : [
|
|
714
743
|
result.dryRun ? 'agent-workflow velocity profile - DRY RUN (no changes)' : 'agent-workflow velocity profile - APPLY',
|
|
715
744
|
...formatAllowlist(result),
|
|
716
745
|
...formatKitTier(result),
|
|
@@ -986,7 +1015,65 @@ const deriveUsedBridgeTier = (root, deps = {}) => {
|
|
|
986
1015
|
return { used, recipe, dirs, env, home };
|
|
987
1016
|
};
|
|
988
1017
|
|
|
1018
|
+
const NO_WORKTREES_GRANT = Object.freeze({
|
|
1019
|
+
worktrees: false, worktreesDir: null, worktreesDirToAdd: [], worktreesDirAlreadyPresent: [], worktreesDirsToCreate: [],
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
const assertLocalSandboxMergeable = (data) => {
|
|
1023
|
+
const malformed = (key, shape) => makeVelocityProfileError(VELOCITY_MALFORMED, `${SETTINGS_LOCAL_FILE}: ${key} must be ${shape}`);
|
|
1024
|
+
const sandbox = data?.sandbox;
|
|
1025
|
+
if (sandbox === undefined) return {};
|
|
1026
|
+
if (!isJsonObject(sandbox)) throw malformed('sandbox', 'a JSON object');
|
|
1027
|
+
for (const key of ['filesystem', 'network']) {
|
|
1028
|
+
if (sandbox[key] !== undefined && !isJsonObject(sandbox[key])) throw malformed(`sandbox.${key}`, 'a JSON object');
|
|
1029
|
+
}
|
|
1030
|
+
if (sandbox.excludedCommands !== undefined && !Array.isArray(sandbox.excludedCommands)) throw malformed('sandbox.excludedCommands', 'an array');
|
|
1031
|
+
if (sandbox.network?.allowedDomains !== undefined && !Array.isArray(sandbox.network.allowedDomains)) {
|
|
1032
|
+
throw malformed('sandbox.network.allowedDomains', 'an array');
|
|
1033
|
+
}
|
|
1034
|
+
const allowWrite = sandbox.filesystem?.allowWrite;
|
|
1035
|
+
if (allowWrite !== undefined && !(Array.isArray(allowWrite) && allowWrite.every(isResolvableDeclaredEntry))) {
|
|
1036
|
+
throw malformed('sandbox.filesystem.allowWrite', 'an array of non-blank strings');
|
|
1037
|
+
}
|
|
1038
|
+
return sandbox;
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
const planWorktreesGrant = (preflight, opts) => {
|
|
1042
|
+
const home = opts.home ?? homedir();
|
|
1043
|
+
const root = preflight.cwd;
|
|
1044
|
+
const dir = (() => {
|
|
1045
|
+
try {
|
|
1046
|
+
return worktreesGrantDir(root, opts);
|
|
1047
|
+
} catch (err) {
|
|
1048
|
+
throw makeVelocityProfileError(VELOCITY_MALFORMED, String(err?.message ?? err).replace(/^\[agent-workflow-kit\] /u, ''));
|
|
1049
|
+
}
|
|
1050
|
+
})();
|
|
1051
|
+
if (worktreesGrantHoldsHome(dir, home, opts)) {
|
|
1052
|
+
throw makeVelocityProfileError(VELOCITY_WORKTREES_HOME,
|
|
1053
|
+
`the worktrees parent dir ${dir} is the home or holds it - the kit grants no write surface there; point docs/ai/worktrees.json parentDir at a dedicated directory`);
|
|
1054
|
+
}
|
|
1055
|
+
const sandbox = assertLocalSandboxMergeable(preflight.localSettings?.data);
|
|
1056
|
+
const entry = homeRelative(dir, home);
|
|
1057
|
+
const { dirs: worktreesDirToAdd } = sandboxSurfaceDelta({ excludedCommands: [], hosts: [], dirs: [entry] }, sandbox, { home, root });
|
|
1058
|
+
const existing = nearestExistingDir(dir, opts);
|
|
1059
|
+
const worktreesDirsToCreate = [];
|
|
1060
|
+
for (let at = dir; at !== existing; at = dirname(at)) worktreesDirsToCreate.unshift(at);
|
|
1061
|
+
return {
|
|
1062
|
+
...planVelocityProfile(preflight, {}),
|
|
1063
|
+
toAdd: [],
|
|
1064
|
+
alreadyPresent: [],
|
|
1065
|
+
askToAdd: [],
|
|
1066
|
+
askAlreadyPresent: [],
|
|
1067
|
+
worktrees: true,
|
|
1068
|
+
worktreesDir: entry,
|
|
1069
|
+
worktreesDirToAdd,
|
|
1070
|
+
worktreesDirAlreadyPresent: worktreesDirToAdd.length ? [] : [entry],
|
|
1071
|
+
worktreesDirsToCreate,
|
|
1072
|
+
};
|
|
1073
|
+
};
|
|
1074
|
+
|
|
989
1075
|
export const planVelocityProfile = (preflight, opts = {}) => {
|
|
1076
|
+
if (opts.worktrees === true) return planWorktreesGrant(preflight, opts);
|
|
990
1077
|
const { acceptEdits, kitTools, bridgeTier } = opts;
|
|
991
1078
|
const projectAllow = getAllowEntries(preflight.projectSettings?.data);
|
|
992
1079
|
const projectAsk = getPermissions(preflight.projectSettings?.data).ask ?? [];
|
|
@@ -1041,17 +1128,32 @@ export const planVelocityProfile = (preflight, opts = {}) => {
|
|
|
1041
1128
|
dirsAlreadyPresent,
|
|
1042
1129
|
executeToAdd,
|
|
1043
1130
|
setsDefaultMode: acceptEdits === true,
|
|
1131
|
+
...NO_WORKTREES_GRANT,
|
|
1044
1132
|
};
|
|
1045
1133
|
};
|
|
1046
1134
|
|
|
1047
|
-
|
|
1135
|
+
// The parent chain goes first, outermost first, so the entry never names a missing directory.
|
|
1136
|
+
const applyWorktreesGrant = (projectDir, preflight, plan, resultBase, deps) => {
|
|
1137
|
+
const fs = fsDeps(deps);
|
|
1138
|
+
for (const dir of plan.worktreesDirsToCreate) fs.mkdir(dir);
|
|
1139
|
+
if (plan.worktreesDirToAdd.length === 0) return { wrote: true, dryRun: false, settingsPath: null, ...resultBase };
|
|
1140
|
+
if (preflight.claudeDirAbsent) fs.mkdir(join(projectDir, CLAUDE_DIR), { recursive: true });
|
|
1141
|
+
const settingsPath = join(projectDir, SETTINGS_LOCAL_FILE);
|
|
1142
|
+
const merged = mergeAllowWrite(preflight.localSettings.data ?? {}, plan.worktreesDirToAdd);
|
|
1143
|
+
fs.writeFile(settingsPath, formatJson(merged, preflight.localSettings.eol ?? LF), UTF8);
|
|
1144
|
+
return { wrote: true, dryRun: false, settingsPath, ...resultBase };
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
export const writeVelocityProfile = ({ cwd, acceptEdits = false, dryRun = true, kitTools = false, bridgeTier = false, worktrees = false } = {}, deps = {}) => {
|
|
1048
1148
|
const projectDir = cwd ?? deps.cwd ?? process.cwd();
|
|
1149
|
+
if (worktrees === true) assertTargetWritable(join(projectDir, SETTINGS_LOCAL_FILE), deps, SETTINGS_LOCAL_FILE);
|
|
1049
1150
|
const preflight = preflightVelocityProfile({ cwd: projectDir }, deps);
|
|
1050
1151
|
const plan = planVelocityProfile(preflight, {
|
|
1051
1152
|
...deps,
|
|
1052
1153
|
acceptEdits,
|
|
1053
1154
|
kitTools,
|
|
1054
1155
|
bridgeTier,
|
|
1156
|
+
worktrees,
|
|
1055
1157
|
});
|
|
1056
1158
|
// Drift guard runs on BOTH dry-run and apply (so a dry-run faithfully predicts the apply) and
|
|
1057
1159
|
// validates the FULL audited core, not just the to-add delta — a drifted core entry is caught even
|
|
@@ -1079,6 +1181,7 @@ export const writeVelocityProfile = ({ cwd, acceptEdits = false, dryRun = true,
|
|
|
1079
1181
|
`not a deployed agent-workflow project at lineage ${EXPECTED_WORKFLOW_VERSION} (found ${preflight.stamp ?? 'none'}) - run init/upgrade first`,
|
|
1080
1182
|
);
|
|
1081
1183
|
}
|
|
1184
|
+
if (worktrees === true) return applyWorktreesGrant(projectDir, preflight, plan, resultBase, deps);
|
|
1082
1185
|
|
|
1083
1186
|
const fs = fsDeps(deps);
|
|
1084
1187
|
const settingsPath = join(projectDir, SETTINGS_FILE);
|
|
@@ -1697,7 +1800,7 @@ export const formatAutonomyResult = (r) => {
|
|
|
1697
1800
|
// boundary is deliberate: permissions.* precedence is the harness's own documented permission
|
|
1698
1801
|
// model (the layer this whole mode rests on), so those masks stay flat statements.
|
|
1699
1802
|
const scoped = m.key.startsWith(`${SANDBOX_KEY}.`) ? ` ${HOST_HONORS_QUALIFIER}` : '';
|
|
1700
|
-
lines.push(` ⚠ ${SETTINGS_LOCAL_FILE} sets ${m.key}=${JSON.stringify(m.local)}, which MASKS this render's ${m.key}=${JSON.stringify(m.rendered)} (local > project) — the local value wins${scoped}, so the render is not effective for that key; the local file is the maintainer's and
|
|
1803
|
+
lines.push(` ⚠ ${SETTINGS_LOCAL_FILE} sets ${m.key}=${JSON.stringify(m.local)}, which MASKS this render's ${m.key}=${JSON.stringify(m.rendered)} (local > project) — the local value wins${scoped}, so the render is not effective for that key; the local file is the maintainer's and this render never writes it.`);
|
|
1701
1804
|
}
|
|
1702
1805
|
for (const b of r.redlineBypass ?? []) {
|
|
1703
1806
|
lines.push(` ⚠ DEGRADE: ${b.source} has a pre-existing allow entry ${b.entry} that would BYPASS the rendered red-line(s) ${b.redlines.join('/')} (a matching allow rule AUTO-APPROVES the command, defeating ask/deny) — remove it by hand; this render never touches permissions.allow.`);
|
|
@@ -1881,6 +1984,7 @@ export const parseArgs = (argv) => {
|
|
|
1881
1984
|
if (arg === FLAG_KIT_TOOLS) return { ...state, kitTools: true };
|
|
1882
1985
|
if (arg === FLAG_BRIDGE_TIER) return { ...state, bridgeTier: true };
|
|
1883
1986
|
if (arg === FLAG_AUTONOMY) return { ...state, autonomy: true };
|
|
1987
|
+
if (arg === FLAG_WORKTREES) return { ...state, worktrees: true };
|
|
1884
1988
|
if (arg === FLAG_CHECK) return { ...state, check: true };
|
|
1885
1989
|
if (arg === FLAG_CWD) {
|
|
1886
1990
|
const next = allArgs[index + 1];
|
|
@@ -1890,8 +1994,11 @@ export const parseArgs = (argv) => {
|
|
|
1890
1994
|
if (arg.startsWith('-')) throw fail(EXIT_USAGE, `unknown flag: ${arg}`);
|
|
1891
1995
|
throw fail(EXIT_USAGE, `unexpected argument: ${arg}`);
|
|
1892
1996
|
},
|
|
1893
|
-
{ help: false, dryRunFlag: false, apply: false, acceptEdits: false, kitTools: false, bridgeTier: false, autonomy: false, check: false, cwd: undefined, skipNext: false },
|
|
1997
|
+
{ help: false, dryRunFlag: false, apply: false, acceptEdits: false, kitTools: false, bridgeTier: false, autonomy: false, worktrees: false, check: false, cwd: undefined, skipNext: false },
|
|
1894
1998
|
);
|
|
1999
|
+
const worktreesConflict = [[parsed.autonomy, FLAG_AUTONOMY], [parsed.kitTools, FLAG_KIT_TOOLS],
|
|
2000
|
+
[parsed.bridgeTier, FLAG_BRIDGE_TIER], [parsed.acceptEdits, FLAG_ACCEPT_EDITS]].find(([set]) => parsed.worktrees && set);
|
|
2001
|
+
if (worktreesConflict) throw fail(EXIT_USAGE, `${FLAG_WORKTREES} is its own mode writing ${SETTINGS_LOCAL_FILE} alone — it cannot be combined with ${worktreesConflict[1]}`);
|
|
1895
2002
|
|
|
1896
2003
|
if (parsed.dryRunFlag && parsed.apply) throw fail(EXIT_USAGE, `${FLAG_DRY_RUN} and ${FLAG_APPLY} cannot be used together`);
|
|
1897
2004
|
// The --autonomy render is a SEPARATE mode from the allowlist seeding: the allowlist-only flags
|
|
@@ -1911,6 +2018,7 @@ export const parseArgs = (argv) => {
|
|
|
1911
2018
|
kitTools: parsed.kitTools,
|
|
1912
2019
|
bridgeTier: parsed.bridgeTier,
|
|
1913
2020
|
autonomy: parsed.autonomy,
|
|
2021
|
+
worktrees: parsed.worktrees,
|
|
1914
2022
|
check: parsed.check,
|
|
1915
2023
|
cwd: parsed.cwd,
|
|
1916
2024
|
};
|
|
@@ -1939,7 +2047,7 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
|
1939
2047
|
return EXIT_OK;
|
|
1940
2048
|
}
|
|
1941
2049
|
const result = writeVelocityProfile(
|
|
1942
|
-
{ cwd, acceptEdits: args.acceptEdits, dryRun: args.dryRun, kitTools: args.kitTools, bridgeTier: args.bridgeTier },
|
|
2050
|
+
{ cwd, acceptEdits: args.acceptEdits, dryRun: args.dryRun, kitTools: args.kitTools, bridgeTier: args.bridgeTier, worktrees: args.worktrees },
|
|
1943
2051
|
deps,
|
|
1944
2052
|
);
|
|
1945
2053
|
log(formatVelocityProfileResult(result));
|
package/tools/worktrees.mjs
CHANGED
|
@@ -23,11 +23,12 @@ import {
|
|
|
23
23
|
patternToProbe,
|
|
24
24
|
} from './known-footprint.mjs';
|
|
25
25
|
import { isDirectRun } from './direct-run.mjs';
|
|
26
|
-
import { isScratchPlanName, plansInFlight, PLANS_REL
|
|
26
|
+
import { isScratchPlanName, plansInFlight, PLANS_REL } from './plan-files.mjs';
|
|
27
|
+
import { shellQuoteArg } from './repo-lex.mjs';
|
|
27
28
|
import { writeContainedFileAtomic } from './atomic-write.mjs';
|
|
28
29
|
import { assertContainedRealPath } from './fs-safe.mjs';
|
|
29
30
|
import { GIT_MAX_BUFFER } from './git-env.mjs';
|
|
30
|
-
import { isFinalCapableDeclaration } from './
|
|
31
|
+
import { isFinalCapableDeclaration } from './gates-declaration.mjs';
|
|
31
32
|
import {
|
|
32
33
|
WORKTREES_STOP, stop, EXIT, handoffBasename, recordValue, hasControlByte, displayValue,
|
|
33
34
|
composeProvisionRecordSection, composeLandingValue, composeHandoffStub,
|
|
@@ -38,6 +39,8 @@ import {
|
|
|
38
39
|
readSatelliteIdentity,
|
|
39
40
|
} from './satellite-locator.mjs';
|
|
40
41
|
import { composeSatellitePrompt, resolveSeededPlan } from './worktree-prompt.mjs';
|
|
42
|
+
import { applySlot } from './write-lanes.mjs';
|
|
43
|
+
import { dirCovers } from './declared-paths.mjs';
|
|
41
44
|
|
|
42
45
|
// The record format and the satellite locator now live in leaves the dispatch side reads too — but
|
|
43
46
|
// every name they took away is re-exported here, so no import site and no asserted error `code`
|
|
@@ -577,11 +580,25 @@ export const nearestExistingDir = (path, deps = {}) => {
|
|
|
577
580
|
return walk(resolve(path));
|
|
578
581
|
};
|
|
579
582
|
|
|
580
|
-
// The
|
|
581
|
-
//
|
|
583
|
+
// The provision preflight's probe-dir derivation: resolve through the existing-parent realpath, then
|
|
584
|
+
// walk to the nearest existing dir (the advisor walks worktreesGrantDir's answer the same way).
|
|
582
585
|
export const resolveProbeDir = (path, deps = {}) =>
|
|
583
586
|
nearestExistingDir(realpathThroughExistingParent(path, deps), deps);
|
|
584
587
|
|
|
588
|
+
// The worktrees write grant names the parent ITSELF (spec bridges/velocity-profile, part worktrees-grant):
|
|
589
|
+
// the writer, the advisor and the danger check read this one derivation, never resolveProbeDir's ancestor.
|
|
590
|
+
export const worktreesGrantDir = (root, deps = {}) => {
|
|
591
|
+
const realRoot = realpathThroughExistingParent(root, deps);
|
|
592
|
+
const { parentDir } = loadWorktreesConfig(realRoot, deps);
|
|
593
|
+
const dir = realpathThroughExistingParent(parentDir == null ? dirname(realRoot) : resolve(realRoot, parentDir), deps);
|
|
594
|
+
const st = lstatNoFollow(fsOf(deps).lstat, dir);
|
|
595
|
+
if (st !== null && !st.isDirectory()) throw stop(`${CONFIG_REL}: parentDir names an existing non-directory: ${dir}`);
|
|
596
|
+
return dir;
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
export const worktreesGrantHoldsHome = (dir, home, deps = {}) =>
|
|
600
|
+
dirCovers(dir, resolve(home)) || dirCovers(dir, realpathThroughExistingParent(resolve(home), deps));
|
|
601
|
+
|
|
585
602
|
export const probeParentWritable = (parentDir, deps = {}) => {
|
|
586
603
|
const { mkdirPlain, rmdir, rand } = fsOf(deps);
|
|
587
604
|
const probe = join(parentDir, `.aw-write-probe-${rand()}`);
|
|
@@ -620,10 +637,13 @@ const quoteOwnTool = () => `'${WORKTREES_TOOL_ABS.replace(/'/g, `'\\''`)}'`;
|
|
|
620
637
|
const composeOwnToolPrefix = (root) =>
|
|
621
638
|
`cd ${shellQuoteArg(root)} && node ${quoteOwnTool()}`;
|
|
622
639
|
|
|
640
|
+
const composeGrantRoute = (root) => `the kit's worktrees-dir item grants the configured worktrees parent on your yes: the agent runs ${
|
|
641
|
+
applySlot('worktrees-dir', { toolsDir: dirname(WORKTREES_TOOL_ABS), root }).check
|
|
642
|
+
} and, on your yes on its result, its apply line by the consent route, then runs this command again; when that check refuses, prints nothing to change, or grants a dir that does not hold this command's target, use the terminal fallback below`;
|
|
643
|
+
|
|
623
644
|
const landWritabilityStop = ({ parentDir, root, slug }) => stop([
|
|
624
645
|
`the worktrees parent dir is not writable from this session: ${parentDir}`,
|
|
625
|
-
|
|
626
|
-
` .claude/settings.json → sandbox.filesystem.allowWrite += ${JSON.stringify(parentDir)}`,
|
|
646
|
+
composeGrantRoute(root),
|
|
627
647
|
'Or run the full command yourself in a plain terminal:',
|
|
628
648
|
` ${composeOwnToolPrefix(root)} land ${shellQuoteArg(slug)} --prepare`,
|
|
629
649
|
].join('\n'));
|
|
@@ -638,8 +658,7 @@ const writabilityStop = ({ parentDir, root, slug, flags }) => {
|
|
|
638
658
|
return stop(
|
|
639
659
|
[
|
|
640
660
|
`the worktrees parent dir is not writable from this session: ${parentDir}`,
|
|
641
|
-
|
|
642
|
-
` .claude/settings.json → sandbox.filesystem.allowWrite += ${JSON.stringify(parentDir)}`,
|
|
661
|
+
composeGrantRoute(root),
|
|
643
662
|
'Or run the full command yourself in a plain terminal:',
|
|
644
663
|
` ${composeProvisionArgv({ root, slug, flags })}`,
|
|
645
664
|
`(from the target repo root, when that checkout carries the kit at agent-workflow-kit/: node agent-workflow-kit/tools/worktrees.mjs provision ${q(slug)} ${provisionFlagsTail(flags, q).join(' ')})`,
|
|
@@ -3150,8 +3169,7 @@ const destructiveRecovery = ({ slug, branch }) =>
|
|
|
3150
3169
|
|
|
3151
3170
|
const cleanupWritabilityStop = ({ parentDir, root, slug, branch, abandon }) => stop([
|
|
3152
3171
|
`the worktrees parent dir is not writable from this session: ${parentDir}`,
|
|
3153
|
-
|
|
3154
|
-
` .claude/settings.json → sandbox.filesystem.allowWrite += ${JSON.stringify(parentDir)}`,
|
|
3172
|
+
composeGrantRoute(root),
|
|
3155
3173
|
'Or run the full command yourself in a plain terminal:',
|
|
3156
3174
|
` ${composeOwnToolPrefix(root)} ${composeCleanupCommand({ slug, branch, abandon })}`,
|
|
3157
3175
|
].join('\n'));
|
package/tools/write-lanes.mjs
CHANGED
|
@@ -42,7 +42,8 @@ export const VARIANT_LANES = Object.freeze({
|
|
|
42
42
|
'adr-store-migration': LANE_CHAT,
|
|
43
43
|
'sandbox-masks': LANE_CHAT,
|
|
44
44
|
'sandbox-masks.unfenced-mount': LANE_CHAT,
|
|
45
|
-
'worktrees-dir':
|
|
45
|
+
'worktrees-dir': LANE_HARNESS,
|
|
46
|
+
'worktrees-dir.home': LANE_USER,
|
|
46
47
|
'spec-adoption': LANE_CHAT,
|
|
47
48
|
'profile-gap': LANE_CHAT,
|
|
48
49
|
'jev-connect': LANE_USER,
|
|
@@ -80,6 +81,7 @@ export const isProtectedPath = (abs, { root, home, hooksDir }) => {
|
|
|
80
81
|
};
|
|
81
82
|
|
|
82
83
|
const SETTINGS_WRITES = [SETTINGS_PATH];
|
|
84
|
+
const LOCAL_SETTINGS_WRITES = [join('.claude', 'settings.local.json')];
|
|
83
85
|
const HOOK_WRITES = [join('.claude', 'hooks', 'agent-workflow-gates.mjs'), ...SETTINGS_WRITES];
|
|
84
86
|
const STOP_HOOK_WRITES = [join('.claude', 'hooks', 'state-block-guard.mjs'), ...SETTINGS_WRITES];
|
|
85
87
|
const MCP_WRITES = ['.mcp.json', ...SETTINGS_WRITES];
|
|
@@ -105,13 +107,15 @@ const getGuardPaths = (facts) => [preCommitOf(facts), join(facts.root, INSTALLER
|
|
|
105
107
|
const formatCommand = (argv) => argv.map(quoteArg).join(' ');
|
|
106
108
|
const TERMINAL_STEP = 'run in a terminal of your own: ';
|
|
107
109
|
|
|
108
|
-
|
|
110
|
+
// rejudged: the written value derives from state init's record does not watch, so init re-runs its check before the apply.
|
|
111
|
+
const createForm = (file, preview, apply, writes, envDependent = false, userStep = null, rejudged = false) => Object.freeze({
|
|
109
112
|
file,
|
|
110
113
|
preview,
|
|
111
114
|
apply,
|
|
112
115
|
writes,
|
|
113
116
|
envDependent,
|
|
114
117
|
userStep,
|
|
118
|
+
rejudged,
|
|
115
119
|
});
|
|
116
120
|
|
|
117
121
|
const CORE_FORM = createForm('velocity-profile.mjs', ['--cwd', '<root>'], ['--apply', '--cwd', '<root>'], SETTINGS_WRITES);
|
|
@@ -157,6 +161,11 @@ const FORMS = Object.freeze({
|
|
|
157
161
|
['--enforcement', '--cwd', '<root>'], ['--apply', '--enforcement', '--cwd', '<root>'], getEnforcementPaths),
|
|
158
162
|
'commit-guard': createForm('git-hooks.mjs',
|
|
159
163
|
['--commit-guard', '--cwd', '<root>'], ['--apply', '--commit-guard', '--cwd', '<root>'], getGuardPaths),
|
|
164
|
+
'worktrees-dir': createForm('velocity-profile.mjs',
|
|
165
|
+
['--worktrees', '--cwd', '<root>'], ['--apply', '--worktrees', '--cwd', '<root>'], LOCAL_SETTINGS_WRITES, false, null, true),
|
|
166
|
+
'worktrees-dir.home': Object.freeze({
|
|
167
|
+
userStep: () => 'your own step — point docs/ai/worktrees.json parentDir at a dedicated directory, then run /agent-workflow-kit recommendations',
|
|
168
|
+
}),
|
|
160
169
|
});
|
|
161
170
|
|
|
162
171
|
const resolveArgument = (arg, facts) => {
|
|
@@ -190,6 +199,11 @@ export const isEnvDependent = (variant) => {
|
|
|
190
199
|
return FORMS[variant]?.envDependent === true;
|
|
191
200
|
};
|
|
192
201
|
|
|
202
|
+
export const isRejudged = (variant) => {
|
|
203
|
+
laneOf(variant);
|
|
204
|
+
return FORMS[variant]?.rejudged === true;
|
|
205
|
+
};
|
|
206
|
+
|
|
193
207
|
export const applySlot = (variant, facts) => {
|
|
194
208
|
const lane = laneOf(variant);
|
|
195
209
|
const form = FORMS[variant];
|