@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.
- package/CHANGELOG.md +39 -0
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/migrations/1.2.0-agent-attribution.md +10 -8
- package/migrations/3.0.0-hardened-core-loop.md +9 -13
- package/package.json +1 -1
- package/references/contracts.md +2 -2
- package/references/modes/bootstrap.md +5 -3
- package/references/modes/recommendations.md +3 -1
- package/references/modes/upgrade.md +5 -5
- package/references/shared/composition-handoff.md +3 -3
- package/references/shared/deploy-tail.md +2 -2
- package/references/templates/vehicles.json +1 -1
- package/tools/apply-danger-check.mjs +37 -12
- package/tools/attribution.mjs +133 -0
- package/tools/cheap-agents-read.mjs +12 -0
- package/tools/commit-guard.mjs +3 -16
- package/tools/ensure-configs.mjs +6 -6
- package/tools/ensure-executor.mjs +13 -30
- package/tools/ensure-vocabulary.mjs +4 -4
- package/tools/git-hooks.mjs +41 -3
- package/tools/hide-footprint.mjs +87 -61
- package/tools/init-project.mjs +1 -1
- package/tools/recommendations.mjs +92 -32
- package/tools/upgrade-runlist.mjs +1 -1
- package/tools/write-lanes.mjs +7 -0
package/tools/hide-footprint.mjs
CHANGED
|
@@ -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 —
|
|
18
|
-
//
|
|
19
|
-
// host's
|
|
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
|
-
//
|
|
94
|
-
const
|
|
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
|
|
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,
|
|
148
|
-
writeFile: deps.writeFile ?? ((p, c) => writeFileSync(p, c,
|
|
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
|
-
//
|
|
369
|
-
//
|
|
370
|
-
export const
|
|
371
|
-
const
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
let
|
|
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
|
-
|
|
396
|
+
before = fsOf(deps).readFile(source, 'latin1');
|
|
377
397
|
} catch (err) {
|
|
378
|
-
if (err && err.code === 'ENOENT') return
|
|
379
|
-
throw stop(`cannot read global excludes ${
|
|
398
|
+
if (err && err.code === 'ENOENT') return null;
|
|
399
|
+
throw stop(`cannot read global excludes ${source} (${err.code ?? 'fs error'})`);
|
|
380
400
|
}
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
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
|
|
473
|
+
// ── unhide (D12): drop our fence; report the residual global block ──────
|
|
456
474
|
if (opts.unhide) {
|
|
457
|
-
const global = migrateFromGlobal(deps, dir, { home
|
|
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
|
|
524
|
-
const global = migrateFromGlobal(deps, dir, { home
|
|
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>]... [--
|
|
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
|
-
--
|
|
554
|
-
--remove
|
|
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
|
|
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,
|
|
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.
|
|
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
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
|
|
628
|
-
{ dir: args.dir, dryRun: args.dryRun
|
|
629
|
-
|
|
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) {
|
package/tools/init-project.mjs
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
894
|
-
const
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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 } =
|
|
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 (
|
|
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('; ')}; ` : ''}
|
|
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
|
};
|
package/tools/write-lanes.mjs
CHANGED
|
@@ -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)}`),
|