@sabaiway/agent-workflow-kit 14.29.0 → 14.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,31 @@ 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.29.1 — an upgrade keeps the sandbox-masks block byte for byte and names every non-blank line it takes out (AD-181)
8
+
9
+ **On a hidden deployment, the footprint step of `/agent-workflow-kit upgrade` (`hide-footprint --reconcile`) now leaves
10
+ the sandbox-masks block in `.git/info/exclude` byte-identical.** Before, the step read every line outside its own
11
+ managed block as its own: it could take `/.claude/skills` out of the masks block and re-encode the block's line
12
+ endings while it reported `+0/−0 — the hidden set is unchanged`, and the masks lane then read its block as drifted.
13
+
14
+ - **Another kit lane's block is kept whole.** A block opened by a `# >>> agent-workflow…` line and closed by its own
15
+ lane's `# <<< agent-workflow… <<<` line keeps every line, its order and its own line endings, a last line with no
16
+ line ending included; none of its lines is folded into the hidden-mode block. A hidden-mode block the run creates
17
+ goes at the end of the file, or right before such a block when that block ends the file with no line ending.
18
+ - **A malformed marker stops the step with nothing written.** A damaged hidden-mode marker, a block opened inside
19
+ another, a closer with no open block, a closer of another block, a block never closed or a repeated marker refuse
20
+ with exit 1 and `refusing to edit a malformed managed block in <file>: …`, naming the line at fault; the exclude
21
+ file is unchanged, on a dry run and under `--unhide` alike.
22
+ - **Every non-blank line taken out is named.** The report lists each non-blank line the run takes out outside the hidden-mode block,
23
+ verbatim and in file order, one per line: `[hide-footprint] removed=<line>` after an apply,
24
+ `[hide-footprint] would-remove=<line>` on a dry run. A run that takes out nothing prints none.
25
+ - **The apply ask rule is named by its purpose.** The velocity help, `references/modes/velocity.md`, the README
26
+ velocity row and the advisor's `velocity-core` texts now say why the rule exists: so all work can stay in auto mode.
27
+ Without it auto mode refuses a harness apply and you must leave auto mode for it; with it the harness shows its
28
+ usual dialog. The rule and what it seeds are unchanged.
29
+ - Known limits: the file is read and written as UTF-8, so a byte that is not UTF-8 is not kept, as before; a stop
30
+ after the file has changed can leave a removed line unreported.
31
+
7
32
  ## 14.29.0 — in auto mode the chat yes applies a harness item through the harness's own dialog: one seeded ask rule (AD-179)
8
33
 
9
34
  **In a session the harness runs in its auto mode, once the apply ask rule is seeded (every velocity-core, kit-tools-tier or bridge-tier apply seeds it), the
package/README.md CHANGED
@@ -261,7 +261,7 @@ file), or run the guarded `/agent-workflow-kit uninstall`.
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
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 degrades to printed maintainer-pasted commands, and the one-time host consent that makes it promptless surfaces via `recommendations`. Never commits, never pushes, never runs a subscription CLI. |
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 the harness asks before every harness apply (the `apply:` line of each harness item the upgrade advisor renders) instead of judging it in its auto mode — a question added, it removes no prompt; opt-in `acceptEdits`; plus a **read-only advisory** of likely project gate commands to add by hand. Writes **only** `.claude/settings.json` — **never** allowlists commit/push/publish, never writes `settings.local.json`, 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. |
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 **only** `.claude/settings.json` — **never** allowlists commit/push/publish, never writes `settings.local.json`, 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 (delete-to-reseed otherwise). 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. This mode is **read-only and has no writer** — it ships the self-contained hook and a paste-ready wiring block you merge into `.claude/settings.json` yourself. |
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.29.0'
6
+ version: '14.29.1'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-kit",
5
5
  "kind": "composition-root",
6
- "version": "14.29.0",
6
+ "version": "14.29.1",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-kit",
3
- "version": "14.29.0",
3
+ "version": "14.29.1",
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",
@@ -18,7 +18,7 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/velocity-profile.mjs [--dry-run | --apply] [
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
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 only `.claude/settings.json`.
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`. 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 harness then asks before every harness apply (the `apply:` line of each harness item the advisor renders) instead of judging it in its auto mode. It is a question added, never an allow, and it removes no prompt; `--autonomy` leaves it in place.
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`. 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):
@@ -35,6 +35,11 @@ import {
35
35
 
36
36
  export const START_MARKER = '# >>> agent-workflow-kit hidden mode (managed; do not edit between markers) >>>';
37
37
  export const END_MARKER = '# <<< agent-workflow-kit hidden mode <<<';
38
+ // Any other line opening or closing with these prefixes (a damaged hidden-mode marker aside: it
39
+ // refuses) marks ANOTHER kit lane's block (the sandbox-masks lane's): this writer never absorbs,
40
+ // moves or re-encodes a byte of it, a byte that is not UTF-8 aside (the file is read as UTF-8).
41
+ const FOREIGN_OPEN = '# >>> agent-workflow';
42
+ const FOREIGN_CLOSE = '# <<< agent-workflow';
38
43
 
39
44
  // Recognized legacy forms we ABSORB (local) or MIGRATE (global). Matched by prefix so a minor wording
40
45
  // drift can't strand a line (D7/D8).
@@ -101,11 +106,18 @@ const detectEol = (content) => {
101
106
  };
102
107
  // Split into logical lines; `lines.join(eol)` reproduces the structure (CRLF normalized to `eol`).
103
108
  const splitLines = (content) => (content === '' ? [] : content.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n'));
104
- // Re-join, dropping trailing blank lines and ending with exactly one EOL (canonical, idempotent).
109
+ // Each logical line's own ending ('' on an unterminated last line), index-aligned with splitLines.
110
+ const lineEnds = (content) => {
111
+ if (content === '') return [];
112
+ const parts = content.split(/(\r\n|\n|\r)/);
113
+ return parts.filter((_, i) => i % 2 === 0).map((_, i) => parts[2 * i + 1] ?? '');
114
+ };
115
+ // Re-join, dropping trailing blank lines (canonical, idempotent): a plain line takes `eol`; a line
116
+ // given as { text, end } (a foreign block's) keeps its own ending, an absent one on the last line included.
105
117
  const joinLines = (lines, eol) => {
106
118
  const out = [...lines];
107
119
  while (out.length && out[out.length - 1] === '') out.pop();
108
- return out.length ? `${out.join(eol)}${eol}` : '';
120
+ return out.map((l, i) => (typeof l === 'string' ? `${l}${eol}` : `${l.text}${l.end || (i < out.length - 1 ? eol : '')}`)).join('');
109
121
  };
110
122
 
111
123
  // A raw exclude line → an anchored pattern (leading "/"), or null when blank/comment.
@@ -261,15 +273,44 @@ const assembleCandidates = (deps, dir, fsx, forcedPreserve) => {
261
273
 
262
274
  export const buildBlock = (patterns) => [...new Set(patterns)].sort();
263
275
 
264
- // Find the managed fence in a line array. { state:'none' } | { state:'ok', startIdx, endIdx } |
265
- // { state:'malformed', reason }. Malformed → caller STOPs with the file byte-for-byte unchanged.
276
+ // Scan every kit marker line (judged trimmed, as the masks lane reads its own): our fence and each
277
+ // foreign block. { state:'none'|'ok', startIdx, endIdx, foreign:Set<lineIdx>, blockStart:Map<closer
278
+ // idx, opener idx> } | { state:'malformed', reason }. A closer `# <<< <lane> <<<` closes only an
279
+ // opener whose lane (its name up to ' — ' or ' >>>') is <lane>; malformed → caller STOPs, file unchanged.
280
+ const DAMAGED_OWN = ['# >>> agent-workflow-kit hidden mode', '# <<< agent-workflow-kit hidden mode'];
266
281
  const findFence = (lines) => {
267
- const starts = lines.map((l, i) => (l === START_MARKER ? i : -1)).filter((i) => i >= 0);
268
- const ends = lines.map((l, i) => (l === END_MARKER ? i : -1)).filter((i) => i >= 0);
269
- if (starts.length === 0 && ends.length === 0) return { state: 'none' };
270
- if (starts.length !== 1 || ends.length !== 1) return { state: 'malformed', reason: `expected exactly one managed marker pair, found ${starts.length} start / ${ends.length} end` };
271
- if (ends[0] < starts[0]) return { state: 'malformed', reason: 'end marker precedes start marker' };
272
- return { state: 'ok', startIdx: starts[0], endIdx: ends[0] };
282
+ const bad = (reason) => ({ state: 'malformed', reason });
283
+ const foreign = new Set();
284
+ const blockStart = new Map();
285
+ const seen = new Set();
286
+ let open = null;
287
+ let own = null;
288
+ for (const [i, raw] of lines.entries()) {
289
+ const t = raw.trim();
290
+ const opens = t.startsWith(FOREIGN_OPEN);
291
+ if (!opens && !t.startsWith(FOREIGN_CLOSE)) continue;
292
+ const mine = t === START_MARKER || t === END_MARKER;
293
+ if (!mine && DAMAGED_OWN.some((p) => t.startsWith(p))) return bad(`line ${i + 1} is a damaged hidden-mode marker`);
294
+ if (opens) {
295
+ if (open) return bad(`line ${i + 1} opens a block inside the one opened at line ${open.i + 1}`);
296
+ open = { i, mine, name: t.slice(6) };
297
+ } else {
298
+ if (!open) return bad(`line ${i + 1} closes a block that was never opened`);
299
+ const lane = t.endsWith(' <<<') ? t.slice(6, -4) : null;
300
+ const closes = mine ? open.mine : !open.mine && lane !== null && open.name.split(/ — | >>>/)[0] === lane;
301
+ if (!closes) return bad(`line ${i + 1} closes another block than the one opened at line ${open.i + 1}`);
302
+ }
303
+ if (seen.has(t)) return bad(`line ${i + 1} repeats the marker of an earlier block`);
304
+ seen.add(t);
305
+ if (!opens) {
306
+ if (mine) own = { startIdx: open.i, endIdx: i };
307
+ else for (let j = open.i; j <= i; j += 1) foreign.add(j);
308
+ if (!mine) blockStart.set(i, open.i);
309
+ open = null;
310
+ }
311
+ }
312
+ if (open) return bad(`the block opened at line ${open.i + 1} is never closed`);
313
+ return { state: own ? 'ok' : 'none', ...own, foreign, blockStart };
273
314
  };
274
315
 
275
316
  // ── absorb (local pre-existing recognized lines → folded into the fence) ──────────
@@ -280,21 +321,24 @@ const findFence = (lines) => {
280
321
  // block. Low-risk rules fold regardless of presence (D8 preserve). A HIGH-RISK rule folds as consent
281
322
  // only while its file is still PRESENT; if its file is absent it is left exactly as-is (a deliberate
282
323
  // pre-emptive user hide is never silently removed, and there is no present-file report mismatch).
324
+ // A foreign block's line arrives as { text, end } and is kept whole; `removed` lists, in file order,
325
+ // every line taken out (the report names each one).
283
326
  const absorbOutside = (outsideLines, isPresent) => {
284
327
  const kept = [];
285
328
  const absorbed = [];
329
+ const removed = [];
286
330
  for (const raw of outsideLines) {
331
+ if (typeof raw !== 'string') { kept.push(raw); continue; }
287
332
  const t = raw.trim();
288
- if (isAd013Comment(t) || isLegacyGlobalHeader(t)) continue; // orphan header for an absorbed rule
289
333
  const pat = lineToPattern(raw);
290
334
  const canon = pat ? recognizeHideRule(pat) : null;
291
- if (canon && (LOWRISK_SET.has(canon) || isPresent(canon))) {
335
+ if (isAd013Comment(t) || isLegacyGlobalHeader(t)) removed.push(raw); // orphan header for an absorbed rule
336
+ else if (canon && (LOWRISK_SET.has(canon) || isPresent(canon))) {
292
337
  absorbed.push(canon);
293
- continue;
294
- }
295
- kept.push(raw); // unrecognized, or a high-risk recognized rule whose file is absent → leave it
338
+ removed.push(raw);
339
+ } else kept.push(raw); // unrecognized, or a high-risk recognized rule whose file is absent → leave it
296
340
  }
297
- return { kept, absorbed };
341
+ return { kept, absorbed, removed };
298
342
  };
299
343
 
300
344
  // ── verify (D5 — over the WROTE set; "hidden" ≡ untracked ∧ ignored-by-OUR-source) ─
@@ -396,9 +440,13 @@ export const hideFootprint = (opts = {}, deps = {}) => {
396
440
  }
397
441
  })();
398
442
  const eol = detectEol(content);
399
- const lines = splitLines(content);
400
- const fence = findFence(lines);
443
+ const ends = lineEnds(content);
444
+ const texts = splitLines(content);
445
+ const fence = findFence(texts);
401
446
  if (fence.state === 'malformed') throw stop(`refusing to edit a malformed managed block in ${excludeFile}: ${fence.reason} (file left unchanged)`);
447
+ const lines = texts.map((l, i) => (fence.foreign.has(i) ? { text: l, end: ends[i] } : l));
448
+ // A created fence goes before a foreign block that ends the file unterminated, so no byte of it moves.
449
+ const tailBlock = ends.at(-1) === '' ? lines[fence.blockStart.get(texts.length - 1)] : undefined;
402
450
 
403
451
  const before = fence.state === 'ok' ? lines.slice(0, fence.startIdx) : lines;
404
452
  const fenceBodyLines = fence.state === 'ok' ? lines.slice(fence.startIdx + 1, fence.endIdx) : [];
@@ -416,8 +464,8 @@ export const hideFootprint = (opts = {}, deps = {}) => {
416
464
 
417
465
  // ── absorb pre-existing recognized lines, assemble + classify candidates ───────
418
466
  const isPresentCanon = (canon) => presentOnDisk(canon, fsx, dir);
419
- const { kept: cleanBefore, absorbed: absorbedBefore } = absorbOutside(before, isPresentCanon);
420
- const { kept: cleanAfter, absorbed: absorbedAfter } = absorbOutside(after, isPresentCanon);
467
+ const { kept: cleanBefore, absorbed: absorbedBefore, removed: removedBefore } = absorbOutside(before, isPresentCanon);
468
+ const { kept: cleanAfter, absorbed: absorbedAfter, removed: removedAfter } = absorbOutside(after, isPresentCanon);
421
469
  const fenceBodyPatterns = fenceBodyLines.map((l) => recognizeHideRule(lineToPattern(l) ?? '')).filter(Boolean);
422
470
  // Every pre-existing recognized hide rule (current fence body + absorbed loose lines, glob children
423
471
  // included) is prior consent. LOW-RISK rules are force-preserved as candidates regardless of presence
@@ -455,8 +503,9 @@ export const hideFootprint = (opts = {}, deps = {}) => {
455
503
 
456
504
  // ── build the new file (splice the fence; preserve outside lines) ──────────────
457
505
  const fenceLines = writtenPatterns.length ? [START_MARKER, ...writtenPatterns, END_MARKER] : [];
506
+ const at = fence.state === 'none' && tailBlock ? cleanBefore.indexOf(tailBlock) : cleanBefore.length;
458
507
  const newLines = writtenPatterns.length
459
- ? [...cleanBefore, ...fenceLines, ...cleanAfter]
508
+ ? [...cleanBefore.slice(0, at), ...fenceLines, ...cleanBefore.slice(at), ...cleanAfter]
460
509
  : [...cleanBefore, ...cleanAfter];
461
510
  const newContent = joinLines(newLines, eol);
462
511
  const changed = newContent !== content;
@@ -482,6 +531,7 @@ export const hideFootprint = (opts = {}, deps = {}) => {
482
531
  wrote: writtenPatterns,
483
532
  added,
484
533
  removed,
534
+ removedOutside: [...removedBefore, ...removedAfter],
485
535
  asks: asks.filter((a) => !includedAsks.some((i) => i.pattern === a.pattern)).map((a) => ({ path: a.pattern, reason: a.reason, owner: a.owner })),
486
536
  needsUntrack: needsUntrack.map((a) => {
487
537
  const target = patternToProbe(a.pattern).replace(/\/$/, '');
@@ -558,6 +608,8 @@ export const formatReport = (r, dryRun) => {
558
608
  if (r.removed.length) lines.push(` • −${r.removed.length} removed: ${r.removed.join(', ')}`);
559
609
  if (!r.added.length && !r.removed.length) lines.push(' • +0/−0 — the hidden set is unchanged');
560
610
  }
611
+ // Each line taken out outside the fence, verbatim, on the whole-line machine channel (exempt by grammar).
612
+ for (const l of r.removedOutside ?? []) lines.push(` [hide-footprint] ${dryRun ? 'would-remove' : 'removed'}=${l}`);
561
613
  for (const a of r.asks) lines.push(` • ASK ${a.path} — ${a.reason}`);
562
614
  for (const n of r.needsUntrack) lines.push(` • tracked, NOT hidden: ${n.path} — run \`${n.command}\` to un-track (kept on disk)`);
563
615
  if (r.dropped.length) lines.push(` • skipped ${r.dropped.length} already-ignored (tracked .gitignore)`);
@@ -244,8 +244,8 @@ export const composeVerdict = ({ attention, optional, skipped }) => {
244
244
  // WHAT is never truncated: the leaf's suite pins every composed WHAT under the cap.
245
245
  export const WHATS = Object.freeze({
246
246
  'velocity-core': 'routine read-only commands still prompt — {n} audited read-only allowlist entr(ies) not seeded',
247
- 'velocity-core.rule': 'in auto mode a harness apply is judged instead of asked — the apply ask rule is not seeded',
248
- 'velocity-core.both': 'routine read-only commands still prompt, and in auto mode a harness apply is judged instead of asked — {n} entr(ies) not seeded',
247
+ 'velocity-core.rule': 'auto mode refuses a harness apply, so work cannot stay in auto mode — the apply ask rule is not seeded',
248
+ 'velocity-core.both': 'routine read-only commands still prompt, and auto mode refuses a harness apply — {n} entr(ies) not seeded',
249
249
  'kit-tools-tier': "the kit's own read-only tools still prompt — {n} kit-tools tier entr(ies) not seeded",
250
250
  'bridge-tier': 'bridge-wrappers tier incomplete — {n} entr(ies) missing (allow rules, exclusions, hosts or state dirs of used bridges)',
251
251
  'autonomy-policy': 'no {path} — the computed defaults apply implicitly (red-lines ask/deny; every activity floors at prompt)',
@@ -319,7 +319,7 @@ const templateBudget = (template) => ITEM_LINE_CAP - template.replace(/\{[A-Za-z
319
319
 
320
320
  // ── the frozen benefit registry (fact-true; pinned by tests) ────────────────────────────────────
321
321
  export const BENEFITS = Object.freeze({
322
- 'velocity-core': 'velocity — routine read-only commands stop prompting while the maintainer is away; the harness asks before each harness apply',
322
+ 'velocity-core': 'velocity — routine read-only commands stop prompting while the maintainer is away; all work stays in auto mode',
323
323
  'kit-tools-tier': "velocity — the kit's own read-only tools run promptless (audited, resolved-absolute tier)",
324
324
  'bridge-tier':
325
325
  'velocity — used bridges get <wrapper> * exclusions (codex-exec * if delegated), hosts, state dirs (allowWrite); reviews: code-mode allows',
@@ -39,7 +39,7 @@ export const WORKFLOW_STAMP = 'docs/ai/.workflow-version';
39
39
  export const SAFE_DEFAULT_MODES = Object.freeze(['default', 'acceptEdits', 'plan']);
40
40
  export const UNSAFE_BYPASS_MODE = 'bypassPermissions';
41
41
  export const ACCEPT_EDITS_MODE = 'acceptEdits';
42
- // The harness asks before every harness apply; an ask rule allows nothing.
42
+ // Lets all work stay in auto mode: a harness apply meets the harness dialog, not a refusal; an ask rule allows nothing.
43
43
  export const APPLY_ASK_RULE = 'Bash(node *apply-danger-check.mjs*--apply*)';
44
44
 
45
45
  // The audited read-only core. Every entry is a Claude Code Bash allow pattern whose command runs NO
@@ -337,8 +337,8 @@ absolute path (args wildcard), run-gates.mjs as ONE exact project-root-pinned by
337
337
  (project-exec - it runs YOUR declared gates.json), source-size-check.mjs as ONE exact --check
338
338
  byte-string (its read-only mode only - its writers keep prompting), and the writers' exact arg-free
339
339
  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
341
- once, so the harness asks before every harness apply; it allows nothing.
340
+ Every apply also appends the apply ask rule ${APPLY_ASK_RULE} to permissions.ask once, so all work
341
+ 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
342
  --bridge-tier (own consent) derives each entry for a used bridge from the effective project recipes:
343
343
  review wrappers' CODE mode (codex-review code, agy-review code), the quoted grounding pre-step rule,
344
344
  and <wrapper> * exclusions in sandbox.excludedCommands. An execute slot delegating to codex adds