@sabaiway/agent-workflow-kit 14.33.0 → 14.34.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.
@@ -14,9 +14,9 @@
14
14
  // Decisions (plan D1–D16): one managed fence, re-derived wholesale → re-run is byte-identical (zero
15
15
  // diff); tracked → ASK; present-but-untracked high-risk (generic names) → ASK; everything else → HIDE;
16
16
  // asks are excluded from the block unless opted in via `--include` (an asks-only, traversal/glob-free
17
- // path). Add-local-first, then DETECT + REPORT the residual legacy global block — removal is the
18
- // explicit, ASK-gated `--remove-global` (prints a restorable backup), never a default (an arbitrary
19
- // host's OTHER hidden repo may rely on the same root-anchored global lines). Windows is supported (text
17
+ // path). Add-local-first, then DETECT + REPORT the residual legacy global block — the hide never writes
18
+ // the global file; removal is the `--remove-global` mode alone, the advisor's footprint-global item
19
+ // (prints a restorable backup; another of the host's hidden repos may rely on the same lines). Windows is supported (text
20
20
  // edit; forward-slash patterns; CRLF preserved via EOL detection). Dependency-free, Node >= 22.
21
21
 
22
22
  import {
@@ -90,10 +90,13 @@ const canonicalize = (pattern) => {
90
90
  // canonicalize alone misses glob children, so the consented line would be dropped → a silent un-hide.
91
91
  const recognizeHideRule = (pattern) => canonicalize(pattern) ?? (matchesKnownGlob(pattern) ? normalizeSlashes(pattern) : null);
92
92
 
93
- // Slash-tolerant membership in the HISTORICAL global set.
94
- const historicalMatch = (pattern) => {
93
+ // The historical lines any repository's own ignore may carry: a removal plan needs one kit-specific line besides them.
94
+ const GENERIC_GLOBAL_PATHS = new Set(['/AGENTS.md', '/CLAUDE.md', '/docs/ai/', '/docs/plans/', '/.claude/skills/']);
95
+
96
+ // Slash-tolerant membership in a global path set (the HISTORICAL one by default).
97
+ const historicalMatch = (pattern, set = HISTORICAL_GLOBAL_PATHS) => {
95
98
  const p = normalizeSlashes(pattern);
96
- return HISTORICAL_GLOBAL_PATHS.has(p) || HISTORICAL_GLOBAL_PATHS.has(`${p}/`) || (p.endsWith('/') && HISTORICAL_GLOBAL_PATHS.has(p.slice(0, -1)));
99
+ return set.has(p) || set.has(`${p}/`) || (p.endsWith('/') && set.has(p.slice(0, -1)));
97
100
  };
98
101
 
99
102
  // ── EOL + line model ────────────────────────────────────────────────────────────
@@ -144,8 +147,8 @@ const defaultGit = (args, { cwd, env }) => {
144
147
  const runGit = (deps, dir, args) => (deps.git ?? defaultGit)(args, { cwd: dir, env: deps.env ?? process.env });
145
148
 
146
149
  const fsOf = (deps = {}) => ({
147
- readFile: deps.readFile ?? ((p) => readFileSync(p, 'utf8')),
148
- writeFile: deps.writeFile ?? ((p, c) => writeFileSync(p, c, 'utf8')),
150
+ readFile: deps.readFile ?? ((p, encoding = 'utf8') => readFileSync(p, encoding)),
151
+ writeFile: deps.writeFile ?? ((p, c, encoding = 'utf8') => writeFileSync(p, c, encoding)),
149
152
  stat: deps.stat ?? statSync,
150
153
  readdir: deps.readdir ?? readdirSync,
151
154
  });
@@ -362,45 +365,60 @@ const globalExcludesPath = (deps, dir, home) => {
362
365
  if (r.status === 1) return null; // unset = NORMAL "nothing to migrate" (not a STOP)
363
366
  if (r.status !== 0) throw stop(`git config --get core.excludesFile failed (exit ${r.status}): ${r.stderr.trim()}`);
364
367
  const p = r.stdout.trim();
365
- return p ? expandTilde(p, home) : null;
368
+ return p ? resolve(dir, expandTilde(p, home)) : null;
366
369
  };
367
370
 
368
- // Detect + REPORT a residual legacy global block; remove ONLY contiguous recognized runs (header +
369
- // historical paths) and ONLY when removeGlobal — printing the removed lines as a restorable backup.
370
- export const migrateFromGlobal = (deps, dir, { home, removeGlobal, dryRun }) => {
371
- const fsx = fsOf(deps);
372
- const gp = globalExcludesPath(deps, dir, home);
373
- if (!gp) return { found: false, source: null, removedLines: [], action: 'none' };
374
- let content;
371
+ // The pure core over the global file read as latin1: each recognised legacy line goes with its own
372
+ // terminator and every other byte stays; null without one kit-specific line among them.
373
+ export const globalRemovalOf = (text) => {
374
+ const parts = text.split(/(\r\n|\n|\r)/);
375
+ const removedLines = [];
376
+ let after = '';
377
+ let specific = false;
378
+ for (let i = 0; i < parts.length; i += 2) {
379
+ const t = parts[i].trim();
380
+ const pattern = lineToPattern(parts[i]);
381
+ const marker = isLegacyGlobalHeader(t) || isAd013Comment(t);
382
+ if (marker || (pattern && historicalMatch(pattern))) {
383
+ removedLines.push(parts[i]);
384
+ specific ||= marker || !historicalMatch(pattern, GENERIC_GLOBAL_PATHS);
385
+ } else after += parts[i] + (parts[i + 1] ?? '');
386
+ }
387
+ return specific ? { removedLines, after } : null;
388
+ };
389
+
390
+ // The one answer the hide's kept line, the advisor's probe, the danger check and --remove-global read.
391
+ export const planGlobalRemoval = (deps, root, { home }) => {
392
+ const source = globalExcludesPath(deps, root, home);
393
+ if (!source) return null;
394
+ let before;
375
395
  try {
376
- content = fsx.readFile(gp);
396
+ before = fsOf(deps).readFile(source, 'latin1');
377
397
  } catch (err) {
378
- if (err && err.code === 'ENOENT') return { found: false, source: gp, removedLines: [], action: 'none' };
379
- throw stop(`cannot read global excludes ${gp} (${err.code ?? 'fs error'})`);
398
+ if (err && err.code === 'ENOENT') return null;
399
+ throw stop(`cannot read global excludes ${source} (${err.code ?? 'fs error'})`);
380
400
  }
381
- const eol = detectEol(content);
382
- const lines = splitLines(content);
383
- const recognized = (raw) => {
384
- const t = raw.trim();
385
- if (isLegacyGlobalHeader(t) || isAd013Comment(t)) return true;
386
- const pat = lineToPattern(raw);
387
- return pat ? historicalMatch(pat) : false;
388
- };
389
- const kept = [];
390
- const removed = [];
391
- let i = 0;
392
- while (i < lines.length) {
393
- if (recognized(lines[i])) {
394
- while (i < lines.length && recognized(lines[i])) { removed.push(lines[i]); i += 1; }
395
- } else {
396
- kept.push(lines[i]);
397
- i += 1;
398
- }
401
+ const removal = globalRemovalOf(before);
402
+ return removal && { source, before, ...removal };
403
+ };
404
+
405
+ // Detect + REPORT a residual legacy global block; the hide never writes the global file.
406
+ export const migrateFromGlobal = (deps, dir, { home }) => {
407
+ const plan = planGlobalRemoval(deps, dir, { home });
408
+ if (!plan) return { found: false, source: null, removedLines: [], action: 'none' };
409
+ return { found: true, source: plan.source, removedLines: plan.removedLines, action: 'kept' };
410
+ };
411
+
412
+ // The --remove-global mode: no exclude file read, no local block written; a refused write is the lane's STOP.
413
+ const removeGlobal = (opts, deps) => {
414
+ const plan = planGlobalRemoval(deps, resolve(opts.dir ?? process.cwd()), { home: deps.home ?? os.homedir() });
415
+ if (!plan || opts.dryRun) return { plan, action: plan ? 'would-remove' : 'none' };
416
+ try {
417
+ fsOf(deps).writeFile(plan.source, plan.after, 'latin1');
418
+ } catch (err) {
419
+ throw stop(`cannot write the global excludes ${plan.source} (${err?.code ?? err?.message ?? err}) — the block stays`);
399
420
  }
400
- if (removed.length === 0) return { found: false, source: gp, removedLines: [], action: 'none' };
401
- if (!removeGlobal) return { found: true, source: gp, removedLines: removed, action: 'kept' };
402
- if (!dryRun) fsx.writeFile(gp, joinLines(kept, eol));
403
- return { found: true, source: gp, removedLines: removed, action: 'removed', backup: removed.join('\n') };
421
+ return { plan, action: 'removed' };
404
422
  };
405
423
 
406
424
  // ── --include validation (D4 — only an asks[] path; no traversal/glob) ───────────
@@ -452,9 +470,9 @@ export const hideFootprint = (opts = {}, deps = {}) => {
452
470
  const fenceBodyLines = fence.state === 'ok' ? lines.slice(fence.startIdx + 1, fence.endIdx) : [];
453
471
  const after = fence.state === 'ok' ? lines.slice(fence.endIdx + 1) : [];
454
472
 
455
- // ── unhide (D12): drop our fence; report/remove the residual global block ──────
473
+ // ── unhide (D12): drop our fence; report the residual global block ──────
456
474
  if (opts.unhide) {
457
- const global = migrateFromGlobal(deps, dir, { home, removeGlobal: opts.removeGlobal, dryRun });
475
+ const global = migrateFromGlobal(deps, dir, { home });
458
476
  const newLines = [...before, ...after];
459
477
  const newContent = joinLines(newLines, eol);
460
478
  const changed = newContent !== content;
@@ -520,8 +538,8 @@ export const hideFootprint = (opts = {}, deps = {}) => {
520
538
  throw stop(`hide verification failed — these were written but are not hidden by the project-local exclude: ${notHidden.map((v) => v.path).join(', ')}`);
521
539
  }
522
540
 
523
- // ── then detect/report (or remove) the residual legacy global block (D6) ───────
524
- const global = migrateFromGlobal(deps, dir, { home, removeGlobal: opts.removeGlobal, dryRun });
541
+ // ── then detect + report the residual legacy global block (D6) ─────────────────
542
+ const global = migrateFromGlobal(deps, dir, { home });
525
543
 
526
544
  const action = !changed ? 'noop' : fence.state === 'ok' ? 'updated' : 'created';
527
545
  return {
@@ -545,29 +563,29 @@ export const hideFootprint = (opts = {}, deps = {}) => {
545
563
 
546
564
  // ── CLI ───────────────────────────────────────────────────────────────────────────
547
565
 
548
- const USAGE = `usage: hide-footprint [--dir <project>] [--reconcile] [--include=<path>]... [--keep-global | --remove-global] [--unhide] [--dry-run] [--help]
566
+ const USAGE = `usage: hide-footprint [--dir <project>] [--reconcile] [--include=<path>]... [--unhide] [--dry-run] [--help]
567
+ hide-footprint --remove-global [--dir <project>] [--dry-run]
549
568
 
550
569
  --dir <project> the target project (default: cwd)
551
570
  --reconcile upgrade mode — infer visibility first; write zero bytes when VISIBLE, ASK when AMBIGUOUS
552
571
  --include=<path> opt a surfaced ASK path into the hidden set (only an asks[] path; no glob/traversal)
553
- --keep-global keep + report the residual legacy machine-global block (DEFAULT)
554
- --remove-global remove the recognized legacy machine-global block (prints a restorable backup)
555
- --unhide remove the project-local managed block (residual global only with --remove-global)
572
+ --remove-global its own mode: remove the recognized legacy machine-global block (prints a restorable backup)
573
+ --unhide remove the project-local managed block
556
574
  --dry-run print the plan; change nothing
557
575
  --help, -h this help
558
576
 
559
- Writes ONE managed block in the PROJECT-LOCAL .git/info/exclude (never the machine-global excludes).
577
+ Writes ONE managed block in the PROJECT-LOCAL .git/info/exclude; a residual legacy machine-global block is
578
+ kept and reported, and only --remove-global (the advisor's footprint-global item) removes it.
560
579
  Tracked files are never silently un-tracked — the tool prints the \`git rm --cached\` it will not run.`;
561
580
 
562
581
  const parseArgs = (argv) => {
563
- const out = { dir: undefined, reconcile: false, include: [], removeGlobal: false, keepGlobal: false, unhide: false, dryRun: false, help: false, bad: null };
582
+ const out = { dir: undefined, reconcile: false, include: [], removeGlobal: false, unhide: false, dryRun: false, help: false, bad: null };
564
583
  for (let i = 0; i < argv.length; i += 1) {
565
584
  const a = argv[i];
566
585
  if (a === '--help' || a === '-h') out.help = true;
567
586
  else if (a === '--dry-run') out.dryRun = true;
568
587
  else if (a === '--reconcile') out.reconcile = true;
569
588
  else if (a === '--unhide') out.unhide = true;
570
- else if (a === '--keep-global') out.keepGlobal = true;
571
589
  else if (a === '--remove-global') out.removeGlobal = true;
572
590
  else if (a === '--dir') {
573
591
  const next = argv[i + 1];
@@ -580,15 +598,22 @@ const parseArgs = (argv) => {
580
598
  else { out.include.push(next); i += 1; }
581
599
  } else out.bad = `unknown argument: ${a}`;
582
600
  }
583
- if (out.keepGlobal && out.removeGlobal) out.bad = '--keep-global and --remove-global are mutually exclusive';
601
+ if (out.removeGlobal && !out.bad && (out.reconcile || out.unhide || out.include.length)) out.bad = '--remove-global is its own mode — it cannot be combined with --reconcile, --unhide or --include';
584
602
  return out;
585
603
  };
586
604
 
587
605
  const fmtGlobal = (g) => {
588
- if (!g || g.action === 'none' || g.action === 'skipped') return [];
589
- if (g.action === 'kept') return [` • residual legacy machine-global block in ${g.source} (${g.removedLines.filter((l) => l.trim() && !l.trim().startsWith('#')).length} path line(s)) — KEPT + reported; pass --remove-global to remove (with a printed backup)`];
590
- if (g.action === 'removed') return [` • removed the legacy machine-global block from ${g.source} (backup of ${g.removedLines.length} line(s) printed below)`, ...g.removedLines.map((l) => ` | ${l}`)];
591
- return [];
606
+ if (!g || g.action !== 'kept') return [];
607
+ return [` • residual legacy machine-global block in ${g.source} (${g.removedLines.filter((l) => l.trim() && !l.trim().startsWith('#')).length} path line(s)) — KEPT + reported; the advisor's footprint-global item removes it (with a printed backup)`];
608
+ };
609
+
610
+ const formatRemoval = ({ plan, action }, dryRun) => {
611
+ const lines = [dryRun ? 'hide-footprint — DRY RUN (no changes)' : 'hide-footprint'];
612
+ if (action === 'none') return [...lines, ' • no legacy machine-global block to remove'].join('\n');
613
+ const n = plan.removedLines.length;
614
+ lines.push(action === 'removed' ? ` • removed the legacy machine-global block from ${plan.source} (backup of ${n} line(s) printed below)`
615
+ : ` • would remove the legacy machine-global block from ${plan.source} (${n} line(s)):`);
616
+ return [...lines, ...plan.removedLines.map((l) => ` | ${l}`)].join('\n');
592
617
  };
593
618
 
594
619
  export const formatReport = (r, dryRun) => {
@@ -624,10 +649,11 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
624
649
  if (args.help) { log(USAGE); return 0; }
625
650
  if (args.bad) { errlog(args.bad); errlog(USAGE); return 2; }
626
651
  try {
627
- const result = hideFootprint(
628
- { dir: args.dir, dryRun: args.dryRun, reconcile: args.reconcile, include: args.include, removeGlobal: args.removeGlobal, unhide: args.unhide },
629
- deps,
630
- );
652
+ if (args.removeGlobal) {
653
+ log(formatRemoval(removeGlobal({ dir: args.dir, dryRun: args.dryRun }, deps), args.dryRun));
654
+ return 0;
655
+ }
656
+ const result = hideFootprint({ dir: args.dir, dryRun: args.dryRun, reconcile: args.reconcile, include: args.include, unhide: args.unhide }, deps);
631
657
  log(formatReport(result, args.dryRun));
632
658
  return 0;
633
659
  } catch (err) {
@@ -216,7 +216,7 @@ const previewQueue = async (context, queue) => {
216
216
  }
217
217
  context.print(result.stdout);
218
218
  const tuple = result.stdout.match(/--apply\s+(\S+)/)?.[1];
219
- const facts = { ...context.facts, tuple };
219
+ const facts = { ...context.facts, env: context.spawnEnv, tuple };
220
220
  let checkDigest = null;
221
221
  if (laneOf(item.variant) === LANE_HARNESS) {
222
222
  try {
@@ -75,16 +75,19 @@ import { CHECKER_CLAIM, INITIAL_ADOPTION_REASON, SOURCE_SIZE_GATE_ID, classifySo
75
75
  // The declared-path resolution + segment containment this item's convergence lane shares with the
76
76
  // autonomy render's allowWrite degrade — ONE leaf, so the two answers cannot drift.
77
77
  import { resolveDeclaredDir, dirCovers, isResolvableDeclaredEntry } from './declared-paths.mjs';
78
- import { resolveGitHooksPath } from './commit-guard.mjs';
78
+ import { readPreCommitState } from './git-hooks.mjs';
79
+ export { resolveGitHooksPath, readPreCommitState, canonicalGuardLineRe, INSTALLER_MARKER_RE } from './git-hooks.mjs';
79
80
  import { loadConfig } from './orchestration-config.mjs';
80
81
  import { homeRelative } from './bridge-state-dirs.mjs';
81
82
  import { sandboxEnabled, HOST_HONORS_QUALIFIER } from './bridge-wiring.mjs';
82
83
  import { assertContainedRealPath } from './fs-safe.mjs';
83
84
  import { nearestExistingDir, worktreesGrantDir, worktreesGrantHoldsHome } from './worktrees.mjs';
84
- import { preflightCheapAgents, EXECUTOR_VEHICLE } from './cheap-agents.mjs';
85
+ import { preflightCheapAgents, writeCheapAgents, EXECUTOR_VEHICLE } from './cheap-agents.mjs';
86
+ import { ATTRIBUTION_KEY, LEGACY_ATTRIBUTION_KEY, attributionHidesText, effectiveAttribution, readAttributionChoice, wrongTypedKey } from './attribution.mjs';
87
+ import { planGlobalRemoval } from './hide-footprint.mjs';
85
88
  import { NODE_EVIDENCE, NODE_EVIDENCE_SCRIPTS, probeNodeEvidence } from './node-evidence.mjs';
86
89
  // The vehicle READINESS comes from the read-only half — the same survey `recipes` and `status` read.
87
- import { surveyExecutorVehicle, readExecutorPosture, executorTemplate, readStamp, readFsDeps, WORKFLOW_STAMP, EXPECTED_WORKFLOW_VERSION } from './cheap-agents-read.mjs';
90
+ import { surveyExecutorVehicle, readExecutorPosture, executorBodyDiffers, executorTemplate, readStamp, readFsDeps, WORKFLOW_STAMP, EXPECTED_WORKFLOW_VERSION } from './cheap-agents-read.mjs';
88
91
  // The ack store's path, keys, lane registry, fingerprint and guarded reader live in their own leaf
89
92
  // (contract: kit/ack-store) — `status` reads the same store, and a second copy is what drifts.
90
93
  import {
@@ -194,6 +197,12 @@ export const SEVERITIES = Object.freeze({
194
197
  // names the subagent carrier over a vehicle that cannot carry it — a configured declaration that
195
198
  // is broken, which is what attention means.
196
199
  'executor-vehicle': SEVERITY_ATTENTION,
200
+ // A hand edit of the placed executor is not a broken declaration — the vehicle still carries — so
201
+ // re-deriving it is an offer.
202
+ 'executor-vehicle.differing': SEVERITY_OPTIONAL,
203
+ // The AGENTS.md block is a configured choice the harness does not honour.
204
+ attribution: SEVERITY_ATTENTION,
205
+ 'footprint-global': SEVERITY_OPTIONAL,
197
206
  'family-freshness': SEVERITY_ATTENTION,
198
207
  'adr-store-migration': SEVERITY_ATTENTION,
199
208
  'sandbox-masks': SEVERITY_OPTIONAL,
@@ -278,6 +287,9 @@ export const WHATS = Object.freeze({
278
287
  'mcp-channel.differing': 'an "{server}" MCP entry is already declared here and DIFFERS from the registration this kit copy would write',
279
288
  agents: '{n} bundled subagent vehicle(s) not placed (Claude Code) — {ro} read-only, {ex} the full-tool executor; the apply PREVIEWS first',
280
289
  'executor-vehicle': '{n} slot(s) configured subagent but the executor vehicle is {state}{reason} — every such slot runs solo until it is usable',
290
+ 'executor-vehicle.differing': 'the placed executor vehicle differs from the body derived from docs/ai/vehicles.json — a hand edit stands until it is re-derived',
291
+ attribution: 'AGENTS.md says attribution off but .claude/settings.json does not hide the harness trailer and PR text',
292
+ 'footprint-global': 'legacy kit ignore line(s) sit in {file} — they ignore kit paths in every repository here',
281
293
  'family-freshness': '{parts}',
282
294
  'adr-store-migration': 'still on the retired 3-tier ADR layout — {shape}',
283
295
  'sandbox-masks': '{n} sandbox mask(s) clutter git status — the managed exclude block is absent or stale',
@@ -343,6 +355,8 @@ export const BENEFITS = Object.freeze({
343
355
  'mcp-channel': 'velocity — path facts and literal searches arrive as typed tool calls whose arguments are JSON fields, never a shell string',
344
356
  agents: 'cost and quiet — cheap-model mechanical work, no shell on a read-only vehicle (no prompt flood); the executor carries slices you verify',
345
357
  'executor-vehicle': 'carrier readiness — a slot you configured subagent dispatches the subagent it names instead of silently running solo',
358
+ attribution: 'attribution — the harness adds no Co-Authored-By trailer, PR text or session link, as your AGENTS.md block requires',
359
+ 'footprint-global': 'hygiene — the legacy machine-global excludes stop ignoring kit paths in every other repository on this host',
346
360
  'family-freshness': 'currency — placed family members carry the latest shipped fixes and features',
347
361
  'adr-store-migration': 'durability — every decision becomes its own file with a generated navigator, instead of one hand-rotated pile',
348
362
  'sandbox-masks': 'zero clutter — a mask git ignores through the block leaves git status; a mount-only one leaves git add -A and the review fingerprint too',
@@ -352,6 +366,10 @@ export const BENEFITS = Object.freeze({
352
366
  'jev-connect': 'decisions — a typed choice with a confidence in under a second for your code and the agent\'s scripts, once for every project here',
353
367
  'jev-skill': 'the TypeSafe skill for Claude Code, Codex and Antigravity CLI, one pinned verified copy per agent root, from one command you run',
354
368
  });
369
+ // The one arm whose gain differs from its base item's (BENEFITS stays keyed by item key).
370
+ export const VARIANT_BENEFITS = Object.freeze({
371
+ 'executor-vehicle.differing': 'carrier fidelity — the placed executor runs the model and effort docs/ai/vehicles.json names, not a hand edit',
372
+ });
355
373
 
356
374
  // ── the CLOSED opt-in capability registry (OPT-IN-SHIPS-INVISIBLE) ──────────────────────────────
357
375
  // 3.14.0 shipped a capability with every surface an AGENT reads and NO advisor entry, so `upgrade`
@@ -388,6 +406,8 @@ export const OPT_IN_CAPABILITIES = Object.freeze([
388
406
  { id: 'read-lane', mode: 'hook', advisorKey: 'read-lane' },
389
407
  { id: 'commit-guard', mode: 'commit-guard', advisorKey: 'commit-guard' },
390
408
  { id: 'enforcement', mode: 'bootstrap', advisorKey: 'enforcement' },
409
+ { id: 'attribution', mode: 'bootstrap', advisorKey: 'attribution' },
410
+ { id: 'footprint-global', mode: 'bootstrap', advisorKey: 'footprint-global' },
391
411
  { id: 'state-block', mode: 'state-block-guard', advisorKey: 'state-block' },
392
412
  { id: 'sandbox-masks', mode: 'sandbox-masks', advisorKey: 'sandbox-masks' },
393
413
  { id: 'mcp-channel', mode: 'mcp', advisorKey: 'mcp-channel' },
@@ -890,27 +910,11 @@ const probeSourceSize = ({ root, deps, add, skip }) => {
890
910
  // alike; an UNMANAGED hook is a loud skip naming the user's own step (never an overwrite offer); a
891
911
  // non-final-capable declaration gets NO offer (an armed guard over a matrix that cannot mint a
892
912
  // receipt would block every commit).
893
- const INSTALLER_MARKER_RE = /:install-git-hooks\.mjs$/m;
894
- const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
895
- // Convergence needs the EXACT canonical armed line the installer writes — full-line anchored on
896
- // the kit's OWN resolved guard path. A commented-out line or a lookalike path (fake-commit-guard)
897
- // must never read as armed (that would suppress the offer and leave commits unguarded).
898
- const canonicalGuardLineRe = () => new RegExp(`^node "${escapeRe(toolPath('commit-guard.mjs'))}" --check$`, 'm');
899
- // The one reader of git's pre-commit: whose hook it is, and whether the canonical guard line arms it.
900
- export const readPreCommitState = (root, deps = {}) => {
901
- const hooksPath = (deps.gitHooksPath ?? resolveGitHooksPath)(root);
902
- if (hooksPath === null) return { state: 'no-hooks-dir', armed: false };
903
- const path = join(hooksPath, 'pre-commit');
904
- const hook = (() => {
905
- try {
906
- if (readsAbsent(deps, path)) return null;
907
- return String((deps.readFile ?? readFileSync)(path, 'utf8'));
908
- } catch {
909
- return null;
910
- }
911
- })();
912
- if (hook === null) return { state: 'absent', armed: false };
913
- return { state: INSTALLER_MARKER_RE.test(hook) ? 'managed' : 'unmanaged', armed: canonicalGuardLineRe().test(hook) };
913
+ // The advisor asks git under its own process env; only the commit-guard apply passes the installer's.
914
+ const readHookState = (root, deps) => {
915
+ const hook = readPreCommitState(root, { ...deps, env: undefined });
916
+ if (hook.state === 'unreadable') throw new Error(`${hook.path} is unreadable (${hook.code}) — the pre-commit hook cannot be judged`);
917
+ return hook;
914
918
  };
915
919
  const formsApply = (variant, root) => consoleArgv(variant, { toolsDir: HERE, root }).apply.map(q).join(' ');
916
920
  const probeCommitGuard = ({ root, deps, add, skip }) => {
@@ -931,7 +935,7 @@ const probeCommitGuard = ({ root, deps, add, skip }) => {
931
935
  skip('commit-guard', new Error(enforcementAdmits(root, deps) ? COMMIT_GUARD_INSTALLER_VIA_ITEM : COMMIT_GUARD_INSTALLER_OWN_STEP));
932
936
  return;
933
937
  }
934
- const { state, armed } = readPreCommitState(root, deps);
938
+ const { state, armed } = readHookState(root, deps);
935
939
  if (armed || state === 'no-hooks-dir') return; // armed (managed or not), or not a git tree — converged
936
940
  if (state === 'unmanaged') {
937
941
  skip('commit-guard', new Error(COMMIT_GUARD_UNMANAGED_HOOK));
@@ -977,7 +981,7 @@ const enforcementOffer = (root, deps) => {
977
981
  if (readEnforcementNode(destination, lstat) === null) continue;
978
982
  throw new Error(`${destination} already exists; the enforcement copy would clobber it`);
979
983
  }
980
- if (readPreCommitState(root, deps).state === 'unmanaged') throw new Error(ENFORCEMENT_UNMANAGED_HOOK);
984
+ if (readHookState(root, deps).state === 'unmanaged') throw new Error(ENFORCEMENT_UNMANAGED_HOOK);
981
985
  return true;
982
986
  };
983
987
 
@@ -1125,16 +1129,31 @@ const writerBlock = (root, deps) => {
1125
1129
  }
1126
1130
  };
1127
1131
 
1132
+ const HIDDEN_AGENTS_FOLLOW_UP = (root) => `hidden-mode deployments only: after the apply, run node ${q(toolPath('hide-footprint.mjs'))} --dir ${q(root)} --reconcile so the placed .claude/agents/ stays invisible to git status`;
1133
+
1128
1134
  const probeExecutorVehicle = ({ root, deps, add, skip }) => {
1135
+ if (renderBrokenExecutor({ root, deps, add, skip })) return;
1136
+ try {
1137
+ if (!executorBodyDiffers(root, deps)) return;
1138
+ const plan = writeCheapAgents({ cwd: root, dryRun: true }, deps);
1139
+ if (!plan.stampOk) throw new Error(`run /agent-workflow-kit upgrade first (deployment stamp ${plan.stamp ?? 'none'}, expected ${EXPECTED_WORKFLOW_VERSION})`);
1140
+ add('executor-vehicle', WHATS['executor-vehicle.differing'], formsApply('executor-vehicle.differing', root),
1141
+ 'executor-vehicle.differing', HIDDEN_AGENTS_FOLLOW_UP(root));
1142
+ } catch (err) {
1143
+ skip('executor-vehicle', err);
1144
+ }
1145
+ };
1146
+
1147
+ const renderBrokenExecutor = ({ root, deps, add, skip }) => {
1129
1148
  try {
1130
1149
  // The VALIDATED reader, as probeReviewRecipe uses it: a schema-invalid config is a stated skip,
1131
1150
  // never an item computed over a shape nothing accepted.
1132
1151
  const { config } = loadConfig(root, deps.readFile ?? readFileSync, deps.lstat ?? lstatSync);
1133
1152
  const configured = Object.entries(ACTIVITIES).flatMap(([activity, def]) =>
1134
1153
  Object.keys(def.slots).filter((slot) => config?.[activity]?.[slot] === 'subagent'));
1135
- if (configured.length === 0) return;
1154
+ if (configured.length === 0) return false;
1136
1155
  const survey = (deps.surveyVehicle ?? surveyExecutorVehicle)(root, deps);
1137
- if (!VEHICLE_BROKEN_STATES.includes(survey.state)) return;
1156
+ if (!VEHICLE_BROKEN_STATES.includes(survey.state)) return false;
1138
1157
  const reason = safeLine(survey.reason ?? '');
1139
1158
  const { posture, reason: settingsReason } = readExecutorPosture(root, deps);
1140
1159
  const stamp = readStamp(join(root, WORKFLOW_STAMP), readFsDeps(deps));
@@ -1147,7 +1166,7 @@ const probeExecutorVehicle = ({ root, deps, add, skip }) => {
1147
1166
  const blocked = writerBlock(root, deps);
1148
1167
  if (blocked) preconditions.push(`${blocked} — fix that`);
1149
1168
  const room = templateBudget(WHATS['executor-vehicle']) - String(configured.length).length - survey.state.length - 2;
1150
- add(
1169
+ return add(
1151
1170
  'executor-vehicle',
1152
1171
  fillTemplate(WHATS['executor-vehicle'], {
1153
1172
  n: configured.length,
@@ -1158,10 +1177,11 @@ const probeExecutorVehicle = ({ root, deps, add, skip }) => {
1158
1177
  // frontmatter or read-only tools included), so those states need no precondition; every precondition above is a refusal the apply would meet.
1159
1178
  `node ${q(toolPath('cheap-agents.mjs'))} --apply --cwd ${q(root)}`,
1160
1179
  'executor-vehicle',
1161
- `${preconditions.length ? `precondition: ${preconditions.join('; ')}; ` : ''}hidden-mode deployments only: after the apply, run node ${q(toolPath('hide-footprint.mjs'))} --dir ${q(root)} --reconcile so the placed .claude/agents/ stays invisible to git status`,
1180
+ `${preconditions.length ? `precondition: ${preconditions.join('; ')}; ` : ''}${HIDDEN_AGENTS_FOLLOW_UP(root)}`,
1162
1181
  );
1163
1182
  } catch (err) {
1164
1183
  skip('executor-vehicle', err);
1184
+ return false;
1165
1185
  }
1166
1186
  };
1167
1187
 
@@ -1314,7 +1334,7 @@ const readReadLaneToggle = (root, deps) => {
1314
1334
  // D3: the risk-marked keys — every key here has a per-item posture note in the mode doc, surfaced
1315
1335
  // at the consent moment; the static contract test asserts EXACT bidirectional coverage
1316
1336
  // (risk-marked keys == mode-doc note keys — a dropped note goes red, not silent).
1317
- export const RISK_NOTED_KEYS = Object.freeze(['bridge-tier', 'read-lane', 'worktrees-dir', 'adr-store-migration', 'gates-inert', 'source-size', 'gate-hook', 'mcp-channel', 'spec-adoption', 'enforcement', 'profile-gap', 'jev-connect', 'jev-skill']);
1337
+ export const RISK_NOTED_KEYS = Object.freeze(['bridge-tier', 'read-lane', 'worktrees-dir', 'adr-store-migration', 'gates-inert', 'source-size', 'gate-hook', 'mcp-channel', 'spec-adoption', 'enforcement', 'profile-gap', 'jev-connect', 'jev-skill', 'attribution', 'footprint-global']);
1318
1338
 
1319
1339
  // The feature-spec layer's adoption state (contract: kit/spec-adoption). The canon lets a plan cite
1320
1340
  // zero governing specs while a project adopts the layer, and nothing ever said whether adoption had
@@ -1564,9 +1584,48 @@ export const probeProfileGaps = ({ root, deps, add, skip }) => {
1564
1584
  }
1565
1585
  };
1566
1586
 
1587
+ // The attribution item by its table (spec init-project, part in-session-writes): a choice of off the
1588
+ // effective settings do not honour; the writer's preflight runs before it renders, so no rendered check always exits 3.
1589
+ const probeAttribution = ({ root, deps, add, skip }) => {
1590
+ try {
1591
+ if (readAttributionChoice(root, deps) !== 'off') return;
1592
+ const project = readSettingsFile(join(root, SETTINGS_FILE), { ...deps, cwd: root }).data;
1593
+ const local = readSettingsFile(join(root, SETTINGS_LOCAL_FILE), { ...deps, cwd: root }).data;
1594
+ const effective = effectiveAttribution(project, local);
1595
+ if (attributionHidesText(effective)) return;
1596
+ preflightVelocityProfile({ cwd: root }, deps);
1597
+ for (const [file, data] of [[SETTINGS_FILE, project], [SETTINGS_LOCAL_FILE, local]]) {
1598
+ const wrong = wrongTypedKey(data);
1599
+ if (wrong) throw new Error(`${file}: ${wrong} — the attribution item cannot judge it`);
1600
+ }
1601
+ const setsText = (value) => value !== null && typeof value === 'object' && ['commit', 'pr'].some((key) => Object.hasOwn(value, key));
1602
+ const localKey = Object.hasOwn(local ?? {}, ATTRIBUTION_KEY) && !attributionHidesText({ [ATTRIBUTION_KEY]: local[ATTRIBUTION_KEY] }) ? ATTRIBUTION_KEY
1603
+ : local?.[LEGACY_ATTRIBUTION_KEY] === true && !setsText(effective[ATTRIBUTION_KEY]) ? LEGACY_ATTRIBUTION_KEY : null;
1604
+ if (localKey) throw new Error(`${SETTINGS_LOCAL_FILE} sets ${localKey} so the harness adds its text — a local-scope override is your own edit`);
1605
+ add('attribution', WHATS.attribution, formsApply('attribution', root));
1606
+ } catch (err) {
1607
+ skip('attribution', err);
1608
+ }
1609
+ };
1610
+
1611
+ // The leftover machine-global block: the one plan the hide reports and the --remove-global mode removes,
1612
+ // asked of git under the home the advisor was given.
1613
+ const probeFootprintGlobal = ({ root, deps, add, skip }) => {
1614
+ try {
1615
+ const home = deps.home ?? deps.jevHost?.home ?? homedir();
1616
+ const plan = (deps.planGlobalRemoval ?? planGlobalRemoval)({ env: { ...process.env, HOME: home }, home }, root, { home });
1617
+ if (plan === null) return;
1618
+ const file = truncatedTo(plan.source, templateBudget(WHATS['footprint-global']));
1619
+ add('footprint-global', fillTemplate(WHATS['footprint-global'], { file }), formsApply('footprint-global', root));
1620
+ } catch (err) {
1621
+ skip('footprint-global', err);
1622
+ }
1623
+ };
1624
+
1567
1625
  // ── assembly (frozen presentation order) ─────────────────────────────────────────────────────────
1568
1626
  const PROBES = Object.freeze([
1569
1627
  probeVelocityItems,
1628
+ probeAttribution,
1570
1629
  probeAutonomyItems,
1571
1630
  probeSandboxProvision,
1572
1631
  probeReviewRecipe,
@@ -1582,6 +1641,7 @@ const PROBES = Object.freeze([
1582
1641
  probeFamilyFreshness,
1583
1642
  probeAdrStore,
1584
1643
  probeMasksItem,
1644
+ probeFootprintGlobal,
1585
1645
  probeWorktreesDir,
1586
1646
  probeMcpChannel,
1587
1647
  probeSpecAdoption,
@@ -1629,7 +1689,7 @@ export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
1629
1689
  home: deps.jevHost?.home ?? deps.home ?? '',
1630
1690
  platform: deps.jevHost?.platform ?? deps.platform,
1631
1691
  });
1632
- items.push({ key, variant, severity: SEVERITIES[variant], lane: laneOf(variant), what, benefit: BENEFITS[key],
1692
+ items.push({ key, variant, severity: SEVERITIES[variant], lane: laneOf(variant), what, benefit: VARIANT_BENEFITS[variant] ?? BENEFITS[key],
1633
1693
  check: slot?.check ?? null, apply: slot?.apply ?? apply, detail });
1634
1694
  return true;
1635
1695
  };
@@ -40,7 +40,7 @@ export const UPGRADE_RUNLIST = Object.freeze([
40
40
  'note-refreshed',
41
41
  'refreshed',
42
42
  'regenerated',
43
- 're-derived',
43
+ 'pending',
44
44
  'already-current',
45
45
  'customized-preserved',
46
46
  'malformed-preserved',
@@ -38,6 +38,9 @@ export const VARIANT_LANES = Object.freeze({
38
38
  'mcp-channel.differing': LANE_HARNESS,
39
39
  agents: LANE_HARNESS,
40
40
  'executor-vehicle': LANE_HARNESS,
41
+ 'executor-vehicle.differing': LANE_HARNESS,
42
+ attribution: LANE_HARNESS,
43
+ 'footprint-global': LANE_HARNESS,
41
44
  'family-freshness': LANE_USER,
42
45
  'adr-store-migration': LANE_CHAT,
43
46
  'sandbox-masks': LANE_CHAT,
@@ -142,6 +145,10 @@ const FORMS = Object.freeze({
142
145
  ['--replace', '--cwd', '<root>'], ['--replace', '--apply', '--cwd', '<root>'], MCP_WRITES),
143
146
  agents: AGENT_FORM,
144
147
  'executor-vehicle': AGENT_FORM,
148
+ 'executor-vehicle.differing': AGENT_FORM,
149
+ attribution: createForm('attribution.mjs', ['--cwd', '<root>'], ['--apply', '--cwd', '<root>'], SETTINGS_WRITES),
150
+ 'footprint-global': createForm('hide-footprint.mjs',
151
+ ['--remove-global', '--dry-run', '--dir', '<root>'], ['--remove-global', '--dir', '<root>'], [], false, null, true),
145
152
  'sandbox-provision.installable': createForm('autonomy-doctor.mjs', [], ['--apply', '<tuple>'], [], true,
146
153
  (facts, argv) => 'your own step (a sudo install; the doctor prints the --apply line to run next) — '
147
154
  + `${TERMINAL_STEP}cd ${quoteArg(facts.root)} && ${formatCommand(argv.preview)}`),