@sabaiway/agent-workflow-kit 2.1.0 → 3.1.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 +94 -1
- package/README.md +7 -5
- package/SKILL.md +16 -8
- package/bin/install.mjs +11 -2
- package/bridges/antigravity-cli-bridge/SKILL.md +8 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +163 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +76 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +80 -15
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +171 -22
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +59 -41
- package/bridges/antigravity-cli-bridge/capability.json +3 -2
- package/bridges/codex-cli-bridge/SKILL.md +8 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +67 -22
- package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +156 -0
- package/bridges/codex-cli-bridge/bin/codex-review.sh +71 -10
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +158 -27
- package/bridges/codex-cli-bridge/capability.json +3 -2
- package/capability.json +1 -1
- package/migrations/3.0.0-hardened-core-loop.md +44 -0
- package/migrations/README.md +1 -1
- package/package.json +2 -2
- package/references/hooks/gate-approve.mjs +1 -1
- package/references/modes/bootstrap.md +3 -3
- package/references/modes/commit-guard.md +19 -0
- package/references/modes/core-evidence.md +15 -0
- package/references/modes/coverage-check.md +12 -0
- package/references/modes/doc-parity.md +7 -6
- package/references/modes/gates.md +7 -6
- package/references/modes/grounding.md +1 -2
- package/references/modes/hook.md +1 -1
- package/references/modes/recommendations.md +1 -0
- package/references/modes/review-state.md +3 -3
- package/references/modes/upgrade.md +6 -6
- package/references/modes/velocity.md +2 -2
- package/references/modes/worktrees.md +120 -0
- package/references/scripts/archive-decisions.mjs +3 -3
- package/references/scripts/archive-decisions.test.mjs +5 -5
- package/references/scripts/check-docs-size-cli.test.mjs +41 -0
- package/references/scripts/check-docs-size.mjs +46 -29
- package/references/scripts/install-git-hooks-repo-exec.test.mjs +82 -0
- package/references/scripts/install-git-hooks.mjs +90 -18
- package/references/scripts/install-git-hooks.test.mjs +102 -0
- package/references/scripts/migrate-gates-branches.test.mjs +157 -0
- package/references/scripts/migrate-gates.mjs +395 -0
- package/references/scripts/migrate-gates.test.mjs +284 -0
- package/references/templates/agent_rules.md +2 -2
- package/tools/ack-write.mjs +1 -1
- package/tools/atomic-write.mjs +2 -2
- package/tools/autonomy-config.mjs +1 -1
- package/tools/autonomy-doctor.mjs +2 -2
- package/tools/autonomy-write.mjs +1 -1
- package/tools/bridge-settings-read.mjs +1 -1
- package/tools/bridge-settings.mjs +1 -1
- package/tools/changed-surface.mjs +16 -77
- package/tools/cheap-agents.mjs +2 -2
- package/tools/commands.mjs +26 -10
- package/tools/commit-guard.mjs +167 -0
- package/tools/core-evidence.mjs +914 -0
- package/tools/coverage-check.mjs +260 -0
- package/tools/delegation.mjs +1 -1
- package/tools/detect-backends.mjs +3 -3
- package/tools/doc-parity.mjs +11 -27
- package/tools/engine-source.mjs +1 -1
- package/tools/family-members.mjs +1 -1
- package/tools/family-registry.mjs +1 -1
- package/tools/fs-safe.mjs +1 -1
- package/tools/gate-hook.mjs +1 -1
- package/tools/{seed-gates.mjs → gates-init.mjs} +58 -52
- package/tools/grounding.mjs +19 -96
- package/tools/hide-footprint.mjs +1 -1
- package/tools/inject-methodology.mjs +72 -41
- package/tools/known-footprint.mjs +1 -1
- package/tools/labels.mjs +1 -1
- package/tools/lcov.mjs +6 -10
- package/tools/lens-region.mjs +1 -1
- package/tools/manifest/validate.mjs +19 -0
- package/tools/migrate-adr-store.mjs +5 -5
- package/tools/orchestration-config.mjs +1 -1
- package/tools/orchestration-write.mjs +1 -1
- package/tools/presentation.mjs +1 -1
- package/tools/procedures.mjs +3 -3
- package/tools/recipes.mjs +62 -11
- package/tools/recommendations.mjs +97 -14
- package/tools/release-scan.mjs +10 -3
- package/tools/renderers.mjs +1 -1
- package/tools/review-state.mjs +291 -343
- package/tools/run-gates.mjs +216 -92
- package/tools/sandbox-masks.mjs +14 -14
- package/tools/semver-lite.mjs +1 -1
- package/tools/set-autonomy.mjs +1 -1
- package/tools/set-recipe.mjs +2 -2
- package/tools/setup-backends.mjs +1 -1
- package/tools/surface.mjs +1 -1
- package/tools/uninstall.mjs +1 -1
- package/tools/velocity-profile.mjs +9 -9
- package/tools/view-model.mjs +1 -1
- package/tools/worktrees.mjs +2292 -0
- package/references/modes/fold-completeness.md +0 -30
- package/references/modes/review-ledger.md +0 -34
- package/references/templates/verification-profile.json +0 -10
- package/tools/fold-completeness-run.mjs +0 -1120
- package/tools/fold-completeness.mjs +0 -672
- package/tools/review-ledger-core.mjs +0 -428
- package/tools/review-ledger-write.mjs +0 -647
- package/tools/review-ledger.mjs +0 -630
- package/tools/sarif.mjs +0 -52
- package/tools/verification-profile.mjs +0 -219
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// The live read is lazy + fail-loud: resolve+read the engine ONLY when a fill is actually needed, and
|
|
19
19
|
// STOP loudly (never a silent fallback) when the engine is needed but absent/invalid.
|
|
20
20
|
//
|
|
21
|
-
// Pure string functions (testable with byte-preservation fixtures); dependency-free, Node >=
|
|
21
|
+
// Pure string functions (testable with byte-preservation fixtures); dependency-free, Node >= 22.
|
|
22
22
|
//
|
|
23
23
|
// Canonical-refresh (AD-025): a slot filled BEFORE a clause existed is normally preserved verbatim
|
|
24
24
|
// (reconcile only fills an EMPTY slot). To push a NEW canonical clause to the EXISTING filled base
|
|
@@ -314,11 +314,31 @@ const isCapRefusal = (errorMessage) => typeof errorMessage === 'string' && error
|
|
|
314
314
|
export const isFillCapRefusal = (errorMessage) =>
|
|
315
315
|
isCapRefusal(errorMessage) && errorMessage.includes('injection would push');
|
|
316
316
|
|
|
317
|
-
const
|
|
317
|
+
const EXIT = Symbol('inject-methodology.exit');
|
|
318
|
+
|
|
319
|
+
// The return-code entry point (5.1): argv[] + injected env/home → { code, stdout, stderr }, no
|
|
320
|
+
// process.argv / process.exit / console inside — an in-process caller is as hermetic as a spawned
|
|
321
|
+
// one. The thin shell at the bottom is the only process-coupled code.
|
|
322
|
+
export const runCli = async (argv, deps = {}) => {
|
|
318
323
|
const { readFile, writeFile, rename, rm } = await import('node:fs/promises');
|
|
319
324
|
const { dirname, basename, join, resolve } = await import('node:path');
|
|
320
325
|
const { homedir } = await import('node:os');
|
|
321
326
|
const { resolveEngineDir, readEngineFragment, detectEngine, ENGINE_FRAGMENT_REL, ORCHESTRATION_FRAGMENT_REL, AUTONOMY_FRAGMENT_REL } = await import('./engine-source.mjs');
|
|
327
|
+
const env = deps.env ?? process.env;
|
|
328
|
+
const home = deps.home ?? homedir();
|
|
329
|
+
|
|
330
|
+
const stdoutLines = [];
|
|
331
|
+
const stderrLines = [];
|
|
332
|
+
const log = (line) => stdoutLines.push(line);
|
|
333
|
+
const logError = (line) => stderrLines.push(line);
|
|
334
|
+
const result = (code) => ({
|
|
335
|
+
code,
|
|
336
|
+
stdout: stdoutLines.length > 0 ? `${stdoutLines.join('\n')}\n` : '',
|
|
337
|
+
stderr: stderrLines.length > 0 ? `${stderrLines.join('\n')}\n` : '',
|
|
338
|
+
});
|
|
339
|
+
// Nested sourcing helpers end the run from arbitrary depth — a tagged throw the catch below
|
|
340
|
+
// translates into the result code (the process.exit of the pre-entry-point CLI).
|
|
341
|
+
const stop = (code) => Object.assign(new Error(`exit ${code}`), { [EXIT]: code });
|
|
322
342
|
|
|
323
343
|
// `reconcile <AGENTS.md> [fragment.md]` = ensure-slot + inject-if-empty + cap (bootstrap/upgrade) for
|
|
324
344
|
// ALL THREE slots; `<AGENTS.md> [fragment.md]` = the legacy inject-into-existing-(methodology)-slot mode.
|
|
@@ -326,10 +346,11 @@ const main = async (argv) => {
|
|
|
326
346
|
const rest = mode === 'reconcile' ? argv.slice(1) : argv;
|
|
327
347
|
const agentsPath = rest[0];
|
|
328
348
|
if (!agentsPath) {
|
|
329
|
-
|
|
330
|
-
|
|
349
|
+
logError('usage: inject-methodology.mjs [reconcile] <path/to/AGENTS.md> [fragment.md]');
|
|
350
|
+
return result(2);
|
|
331
351
|
}
|
|
332
352
|
const explicitFragmentArg = rest[1];
|
|
353
|
+
try {
|
|
333
354
|
const text = await readFile(resolve(agentsPath), 'utf8');
|
|
334
355
|
|
|
335
356
|
// Source a bounded fragment LAZILY, per slot. An explicit [fragment.md] arg (tests + manual) wins and
|
|
@@ -339,7 +360,7 @@ const main = async (argv) => {
|
|
|
339
360
|
// the install command. The caller only invokes this when a fill is actually needed (the laziness).
|
|
340
361
|
const sourceFragment = async (rel) => {
|
|
341
362
|
if (explicitFragmentArg) return readFile(resolve(explicitFragmentArg), 'utf8');
|
|
342
|
-
const { dir, source } = resolveEngineDir({ env
|
|
363
|
+
const { dir, source } = resolveEngineDir({ env, home });
|
|
343
364
|
return readEngineFragment(dir, { source, rel }); // sync; throws loudly when the engine is absent/invalid
|
|
344
365
|
};
|
|
345
366
|
const sourceFragmentOrStop = async (label, rel) => {
|
|
@@ -348,8 +369,8 @@ const main = async (argv) => {
|
|
|
348
369
|
} catch (err) {
|
|
349
370
|
// Engine needed-but-absent → a hard STOP, distinct from the soft cap-skip. The "methodology
|
|
350
371
|
// engine not found/invalid" prefix lets the agent classify this exit (SKILL.md).
|
|
351
|
-
|
|
352
|
-
|
|
372
|
+
logError(`[inject-methodology] ${label} — ${err.message}`);
|
|
373
|
+
throw stop(1);
|
|
353
374
|
}
|
|
354
375
|
};
|
|
355
376
|
|
|
@@ -362,16 +383,16 @@ const main = async (argv) => {
|
|
|
362
383
|
// an unreadable fragment). Returns { fragment } on success, { skip } for the soft case, or
|
|
363
384
|
// process.exit(1) for the hard STOP.
|
|
364
385
|
const sourceChainedFragment = async (rel) => {
|
|
365
|
-
const { dir, source } = resolveEngineDir({ env
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
386
|
+
const { dir, source } = resolveEngineDir({ env, home });
|
|
387
|
+
const chainedStop = (err) => {
|
|
388
|
+
logError(`[inject-methodology] reconcile STOP — ${err.message}`);
|
|
389
|
+
throw stop(1);
|
|
369
390
|
};
|
|
370
391
|
if (detectEngine(dir, { source, rel }).ok) {
|
|
371
392
|
try {
|
|
372
393
|
return { fragment: readEngineFragment(dir, { source, rel }) };
|
|
373
394
|
} catch (err) {
|
|
374
|
-
|
|
395
|
+
chainedStop(err);
|
|
375
396
|
}
|
|
376
397
|
}
|
|
377
398
|
if (detectEngine(dir, { source }).ok) {
|
|
@@ -380,7 +401,7 @@ const main = async (argv) => {
|
|
|
380
401
|
try {
|
|
381
402
|
readEngineFragment(dir, { source, rel }); // throws the canonical install-me error
|
|
382
403
|
} catch (err) {
|
|
383
|
-
|
|
404
|
+
chainedStop(err);
|
|
384
405
|
}
|
|
385
406
|
};
|
|
386
407
|
|
|
@@ -402,8 +423,8 @@ const main = async (argv) => {
|
|
|
402
423
|
if (methResult.status === 'error') {
|
|
403
424
|
// cap-refusal OR malformed/anchor STOP — preserve the single-slot classification (SKILL.md
|
|
404
425
|
// distinguishes by the message); the file is byte-for-byte unchanged either way.
|
|
405
|
-
|
|
406
|
-
|
|
426
|
+
logError(`[inject-methodology] reconcile refused — ${methResult.error}`);
|
|
427
|
+
return result(1);
|
|
407
428
|
}
|
|
408
429
|
const afterMeth = methResult.text; // === text when the methodology slot was already filled (custom)
|
|
409
430
|
const describeMeth = {
|
|
@@ -421,18 +442,18 @@ const main = async (argv) => {
|
|
|
421
442
|
const u = markerSlotUpgradeHint(afterMeth, METHODOLOGY_DESCRIPTOR); if (u) notes.push(u);
|
|
422
443
|
}
|
|
423
444
|
const reportNotes = () => {
|
|
424
|
-
for (const n of notes)
|
|
445
|
+
for (const n of notes) log(`[inject-methodology] note: ${n}`);
|
|
425
446
|
};
|
|
426
447
|
|
|
427
448
|
// ── Explicit [fragment.md] binds methodology ONLY → skip the orchestration + autonomy reconciles ──
|
|
428
449
|
if (explicitFragmentArg) {
|
|
429
450
|
if (afterMeth === text) {
|
|
430
|
-
|
|
431
|
-
return;
|
|
451
|
+
log('[inject-methodology] methodology slot already present and filled — nothing to do (zero-diff).');
|
|
452
|
+
return result(0);
|
|
432
453
|
}
|
|
433
454
|
await writeAtomic(afterMeth);
|
|
434
|
-
|
|
435
|
-
return;
|
|
455
|
+
log(`[inject-methodology] reconcile: ${describeMeth}.`);
|
|
456
|
+
return result(0);
|
|
436
457
|
}
|
|
437
458
|
|
|
438
459
|
// ── Slot 2: orchestration, reconciled on the methodology-reconciled text (the cap-check then guards
|
|
@@ -459,8 +480,8 @@ const main = async (argv) => {
|
|
|
459
480
|
describeOrch = `orchestration-recipes pointer skipped — ${orchResult.error}`;
|
|
460
481
|
} else {
|
|
461
482
|
// Malformed orchestration slot/anchor → a hard STOP. No partial write.
|
|
462
|
-
|
|
463
|
-
|
|
483
|
+
logError(`[inject-methodology] reconcile refused (orchestration) — ${orchResult.error}`);
|
|
484
|
+
return result(1);
|
|
464
485
|
}
|
|
465
486
|
} else {
|
|
466
487
|
finalText = orchResult.text;
|
|
@@ -493,8 +514,8 @@ const main = async (argv) => {
|
|
|
493
514
|
// Malformed markers are a hard STOP on EVERY lane — validated before the soft-skip
|
|
494
515
|
// short-circuits so a duplicate/reversed autonomy pair can never ride out as a "skip"
|
|
495
516
|
// alongside a partial (methodology) write.
|
|
496
|
-
|
|
497
|
-
|
|
517
|
+
logError(`[inject-methodology] reconcile refused (autonomy) — ${autSlot.reason}`);
|
|
518
|
+
return result(1);
|
|
498
519
|
}
|
|
499
520
|
if (orchSkipped) {
|
|
500
521
|
// The chain is CAUSAL, not merely positional: a pointer never lands in a run that withheld
|
|
@@ -523,8 +544,8 @@ const main = async (argv) => {
|
|
|
523
544
|
autSkipped = true;
|
|
524
545
|
describeAut = `autonomy pointer skipped — ${autResult.error}`;
|
|
525
546
|
} else {
|
|
526
|
-
|
|
527
|
-
|
|
547
|
+
logError(`[inject-methodology] reconcile refused (autonomy) — ${autResult.error}`);
|
|
548
|
+
return result(1);
|
|
528
549
|
}
|
|
529
550
|
} else {
|
|
530
551
|
finalText = autResult.text;
|
|
@@ -545,34 +566,44 @@ const main = async (argv) => {
|
|
|
545
566
|
// ── One atomic write of the final (three-slot) text ──
|
|
546
567
|
if (finalText === text) {
|
|
547
568
|
// Byte-unchanged. Still report a skip (it is not "nothing to do" — a pointer was withheld).
|
|
548
|
-
if (orchSkipped || autSkipped)
|
|
549
|
-
else
|
|
569
|
+
if (orchSkipped || autSkipped) log(`[inject-methodology] reconcile: ${describeMeth}; ${describeOrch}; ${describeAut}.`);
|
|
570
|
+
else log('[inject-methodology] reconcile: all three pointers already present and filled — nothing to do (zero-diff).');
|
|
550
571
|
reportNotes();
|
|
551
|
-
return;
|
|
572
|
+
return result(0);
|
|
552
573
|
}
|
|
553
574
|
await writeAtomic(finalText);
|
|
554
|
-
|
|
575
|
+
log(`[inject-methodology] reconcile: ${describeMeth}; ${describeOrch}; ${describeAut}.`);
|
|
555
576
|
reportNotes();
|
|
556
|
-
return;
|
|
577
|
+
return result(0);
|
|
557
578
|
}
|
|
558
579
|
|
|
559
580
|
// Legacy inject-into-existing-slot mode (METHODOLOGY only). injectMethodology no-ops on absent markers
|
|
560
581
|
// and errors on a malformed slot WITHOUT reading the fragment, so resolve+read the engine only when
|
|
561
582
|
// there is a present (ok) slot to fill — a markerless legacy AGENTS.md stays a no-op without the engine.
|
|
562
583
|
const fragment = findSlot(text).state === 'ok' ? await sourceFragmentOrStop('STOP', ENGINE_FRAGMENT_REL) : '';
|
|
563
|
-
const
|
|
564
|
-
if (
|
|
565
|
-
|
|
566
|
-
|
|
584
|
+
const injected = injectMethodology(text, fragment, { maxLines: AGENTS_MD_CAP });
|
|
585
|
+
if (injected.status === 'error') {
|
|
586
|
+
logError(`[inject-methodology] malformed slot — refusing to edit: ${injected.error}`);
|
|
587
|
+
return result(1);
|
|
588
|
+
}
|
|
589
|
+
if (injected.status === 'noop-absent') {
|
|
590
|
+
log('[inject-methodology] no methodology markers found — nothing to inject (legacy AGENTS.md).');
|
|
591
|
+
return result(0);
|
|
567
592
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
593
|
+
await writeAtomic(injected.text);
|
|
594
|
+
log('[inject-methodology] injected the bounded methodology fragment into the slot.');
|
|
595
|
+
return result(0);
|
|
596
|
+
} catch (err) {
|
|
597
|
+
if (err[EXIT] !== undefined) return result(err[EXIT]);
|
|
598
|
+
throw err;
|
|
571
599
|
}
|
|
572
|
-
await writeAtomic(result.text);
|
|
573
|
-
console.log('[inject-methodology] injected the bounded methodology fragment into the slot.');
|
|
574
600
|
};
|
|
575
601
|
|
|
576
602
|
const { pathToFileURL } = await import('node:url');
|
|
577
603
|
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
578
|
-
if (isDirectRun)
|
|
604
|
+
if (isDirectRun) {
|
|
605
|
+
const { code, stdout, stderr } = await runCli(process.argv.slice(2));
|
|
606
|
+
if (stdout) process.stdout.write(stdout);
|
|
607
|
+
if (stderr) process.stderr.write(stderr);
|
|
608
|
+
process.exitCode = code;
|
|
609
|
+
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// count sentinel + intrinsic invariants (anchoring, uniqueness, disjointness, no subsumption) — and
|
|
15
15
|
// references/contracts.md carries the human-readable mirror table, kept in sync by review.
|
|
16
16
|
//
|
|
17
|
-
// Pure, dependency-free, Node >=
|
|
17
|
+
// Pure, dependency-free, Node >= 22. The only fs touch is expandGlob (readdir/stat of a glob parent),
|
|
18
18
|
// injected so the registry stays unit-testable without the real filesystem.
|
|
19
19
|
|
|
20
20
|
import { readdirSync, statSync } from 'node:fs';
|
package/tools/labels.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// imports family-registry for vocabulary, and family-registry imports + re-exports the public subset
|
|
8
8
|
// it exported before (the 7 state constants + DISPLAY_NAMES), so every existing importer stays green.
|
|
9
9
|
//
|
|
10
|
-
// Pure data, no side effects, Node >=
|
|
10
|
+
// Pure data, no side effects, Node >= 22. Every export is frozen — this is a contract, not a mutable.
|
|
11
11
|
|
|
12
12
|
// ── manifestState values (internal; the detect-backends precedence, generalized to any member) ──────
|
|
13
13
|
// These are INTERNAL — they must never reach a user surface verbatim (mapped through STATE_PUBLIC
|
package/tools/lcov.mjs
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// lcov.mjs — a dependency-free LCOV parser for the
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// (the diff-cover / c8 --reporter=lcov pattern) and this module reads it into a per-file
|
|
6
|
-
// line→hits map, keyed by the SAME canonical absolute path the V8 coverage map uses — so the
|
|
7
|
-
// runner's ONE uncovered-changed loop consumes either source unchanged (computeUncoveredLines /
|
|
8
|
-
// effectiveCount stay the V8-only path, D10).
|
|
2
|
+
// lcov.mjs — a dependency-free LCOV parser for the coverage-check final-run checker (D3(d)).
|
|
3
|
+
// The declared unit-tests gate leaves an LCOV file at the fixed git-dir path and this module reads
|
|
4
|
+
// it into a per-file line→hits map, keyed by a canonical absolute path.
|
|
9
5
|
//
|
|
10
6
|
// FAIL-CLOSED posture (the whole reason the fold gate exists): a malformed record can never mark a
|
|
11
7
|
// line COVERED — a DA with a valid line but a non-integer HIT count reads the line UNCOVERED (hits 0,
|
|
@@ -16,7 +12,7 @@
|
|
|
16
12
|
// changed executable line with NO DA entry reads non-executable — LCOV carries no separate
|
|
17
13
|
// executability signal; the V8 path, which reads an absent line as uncovered, differs by format.)
|
|
18
14
|
//
|
|
19
|
-
// Dependency-free, Node >=
|
|
15
|
+
// Dependency-free, Node >= 22. No side effects on import.
|
|
20
16
|
|
|
21
17
|
import { join, resolve, isAbsolute, sep } from 'node:path';
|
|
22
18
|
import { realpathSync } from 'node:fs';
|
|
@@ -80,8 +76,8 @@ const defaultCanon = (p) => {
|
|
|
80
76
|
};
|
|
81
77
|
|
|
82
78
|
// Segment-safe containment ('/a' never contains '/ab'); correct at the filesystem root (mirrors
|
|
83
|
-
//
|
|
84
|
-
//
|
|
79
|
+
// core-evidence.mjs containsPath). Defined locally so lcov.mjs imports no family module (its
|
|
80
|
+
// consumers import THIS module).
|
|
85
81
|
const containsPath = (realRoot, realAbs) =>
|
|
86
82
|
realAbs === realRoot || realAbs.startsWith(realRoot.endsWith(sep) ? realRoot : realRoot + sep);
|
|
87
83
|
|
package/tools/lens-region.mjs
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// never a silent truncate; a file without frontmatter/`maxLines` skips the guard with a stated
|
|
23
23
|
// note (such a file is outside the docs cap gate anyway).
|
|
24
24
|
//
|
|
25
|
-
// Pure string functions (fs only in the CLI); dependency-free, Node >=
|
|
25
|
+
// Pure string functions (fs only in the CLI); dependency-free, Node >= 22.
|
|
26
26
|
|
|
27
27
|
import { normalizeCanonical } from './orchestration-config.mjs';
|
|
28
28
|
|
|
@@ -704,6 +704,25 @@ export const validateManifest = (skillDir) => {
|
|
|
704
704
|
else validateModeCatalog(modeCatalog, roles, errors);
|
|
705
705
|
}
|
|
706
706
|
|
|
707
|
+
// `posture` (strip-the-kit D5): the DEFAULT dispatch posture pins the wrapper ships — the
|
|
708
|
+
// checkable source the kit renders the configured posture from (before D5 these lived only in
|
|
709
|
+
// shell defaults + prose). ADDITIVE-OPTIONAL like modeCatalog (absence = a bridge predating
|
|
710
|
+
// posture labeling); a PRESENT block is typed-validated with the SAME shape rule the receipt
|
|
711
|
+
// predicate applies (isValidReceiptPosture's manifest twin): `model` a non-empty string;
|
|
712
|
+
// `effort` (when present) a non-empty string; `tier` (when present) a non-empty string or null.
|
|
713
|
+
if (Object.hasOwn(manifest, 'posture')) {
|
|
714
|
+
const posture = manifest.posture;
|
|
715
|
+
if (posture === null || typeof posture !== 'object' || Array.isArray(posture)) {
|
|
716
|
+
errors.push('`posture` must be an object of dispatch-posture pins');
|
|
717
|
+
} else {
|
|
718
|
+
if (typeof posture.model !== 'string' || posture.model.length === 0) errors.push('`posture.model` must be a non-empty string');
|
|
719
|
+
if (Object.hasOwn(posture, 'effort') && (typeof posture.effort !== 'string' || posture.effort.length === 0)) errors.push('`posture.effort` (when declared) must be a non-empty string');
|
|
720
|
+
if (Object.hasOwn(posture, 'tier') && posture.tier !== null && (typeof posture.tier !== 'string' || posture.tier.length === 0)) errors.push('`posture.tier` (when declared) must be a non-empty string or null');
|
|
721
|
+
const unknown = Object.keys(posture).filter((k) => !['model', 'effort', 'tier'].includes(k));
|
|
722
|
+
if (unknown.length > 0) errors.push(`\`posture\` carries unknown key(s): ${unknown.join(', ')} (closed vocabulary: model, effort, tier)`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
707
726
|
if (!isStub) {
|
|
708
727
|
const auth = readAuthoritativeVersion(skillDir);
|
|
709
728
|
if (auth.version == null) errors.push(`could not resolve an authoritative version (${auth.from})`);
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
//
|
|
24
24
|
// Write discipline: preview (--dry-run) is the DEFAULT and writes NOTHING; --apply performs the
|
|
25
25
|
// migration. It NEVER commits. Exit codes: 0 done / dry-run / no-op; 1 precondition STOP (no
|
|
26
|
-
// deployment, no writable snapshot base, a failed migration); 2 usage. Dependency-free, Node >=
|
|
26
|
+
// deployment, no writable snapshot base, a failed migration); 2 usage. Dependency-free, Node >= 22.
|
|
27
27
|
// No side effects on import.
|
|
28
28
|
|
|
29
29
|
import { existsSync, readFileSync, readdirSync, statSync, mkdirSync, writeFileSync, chmodSync } from 'node:fs';
|
|
@@ -65,7 +65,7 @@ conservation proof) and writes NOTHING. --apply snapshots, force-refreshes the e
|
|
|
65
65
|
then runs the conservation-checked migration. It NEVER commits — review the tree and commit yourself.`;
|
|
66
66
|
|
|
67
67
|
// The mutually-exclusive dry-run/apply parse (a consent-gated writer never lets a later flag silently
|
|
68
|
-
// decide whether it mutates) + --cwd
|
|
68
|
+
// decide whether it mutates) + --cwd — the family's shared consented-writer contract.
|
|
69
69
|
export const parseArgs = (argv) => {
|
|
70
70
|
const parsed = argv.reduce(
|
|
71
71
|
(acc, a, i) => {
|
|
@@ -125,7 +125,7 @@ const isUnder = (child, parent) => {
|
|
|
125
125
|
|
|
126
126
|
// The ordered snapshot bases that are provably NOT stageable: the git dir first (always safe), then the
|
|
127
127
|
// fallback base ONLY when its snapshot dir lands OUTSIDE cwd (else it is in the work tree and could be
|
|
128
|
-
// committed — reject it
|
|
128
|
+
// committed — reject it). Returns [{ base, dir, viaGitDir }] (possibly empty).
|
|
129
129
|
const snapshotBases = (cwd, stamp, gitDir, fallbackBase) => {
|
|
130
130
|
const bases = [];
|
|
131
131
|
if (gitDir) bases.push({ base: gitDir, dir: resolve(gitDir, `${SNAPSHOT_PREFIX}-${stamp}`), viaGitDir: true });
|
|
@@ -226,14 +226,14 @@ export const main = (argv = process.argv.slice(2), deps = {}) => {
|
|
|
226
226
|
log(` snapshot → ${preview.dir ? `${preview.dir} (${preview.viaGitDir ? 'git dir' : 'out-of-tree fallback'})` : 'NONE — no out-of-tree location; run inside a git repo (apply would refuse otherwise)'}`);
|
|
227
227
|
log(` refresh ${refresh.length} enforcement script(s) to this kit's version${drifted.length ? ` (${drifted.length} locally differ: ${drifted.map((r) => r.name).join(', ')})` : ''}`);
|
|
228
228
|
log(' then the conservation-checked rotation:');
|
|
229
|
-
// Surface the rotation's own exit code
|
|
229
|
+
// Surface the rotation's own exit code: a failed dry-run must NOT print the
|
|
230
230
|
// "run with --apply" go-ahead nor exit 0 — it would send the user to --apply on an unsafe tree.
|
|
231
231
|
const code = runMigrate(['--migrate'], { root: cwd, log: (m) => log(` ${m}`), logError: (m) => error(` ${m}`) });
|
|
232
232
|
if (code !== EXIT_OK) {
|
|
233
233
|
throw stop(`the dry-run rotation would not conserve every ADR (exit ${code}) — NOT safe to --apply; fix the reported problem, then re-run.`);
|
|
234
234
|
}
|
|
235
235
|
// A null preview means --apply would refuse (no out-of-tree snapshot base) — never green-light it
|
|
236
|
-
//
|
|
236
|
+
// A dry-run go-ahead must not send the user to an apply that will STOP.
|
|
237
237
|
if (preview.dir === null) {
|
|
238
238
|
throw stop('no out-of-tree snapshot location — --apply would refuse; run inside a git repo (or point the fallback outside the project), then re-run.');
|
|
239
239
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
//
|
|
17
17
|
// This module performs NO filesystem WRITES — only reads (loadConfig). The single fs-writer lives in
|
|
18
18
|
// orchestration-write.mjs, which procedures.mjs never imports, so "procedures never reaches a writer"
|
|
19
|
-
// is structurally true. Pure-where-possible (fs injectable), dependency-free, Node >=
|
|
19
|
+
// is structurally true. Pure-where-possible (fs injectable), dependency-free, Node >= 22. No side
|
|
20
20
|
// effects on import.
|
|
21
21
|
|
|
22
22
|
import { readFileSync, lstatSync } from 'node:fs';
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// consumed here with this module's own STOP identity, so the public API + error contract are
|
|
11
11
|
// unchanged (this file's tests pin them, characterize-first).
|
|
12
12
|
//
|
|
13
|
-
// Dependency-free, Node >=
|
|
13
|
+
// Dependency-free, Node >= 22. Every fs primitive is injectable (deps.*) so the guards are unit-testable.
|
|
14
14
|
|
|
15
15
|
import { CONFIG_REL, serializeConfig } from './orchestration-config.mjs';
|
|
16
16
|
import { writeDocsAiFileAtomic } from './atomic-write.mjs';
|
package/tools/presentation.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// in the user's language per SKILL.md §4.4 — it never reads this file. Keeping the vocabulary here (a
|
|
7
7
|
// frozen leaf) means the renderers carry no inline strings and the phrasing is pinned by tests.
|
|
8
8
|
//
|
|
9
|
-
// Pure data, no side effects, Node >=
|
|
9
|
+
// Pure data, no side effects, Node >= 22. Every export frozen.
|
|
10
10
|
|
|
11
11
|
// public state token (STATE_PUBLIC value) → the direct-CLI English phrase. `installed` is null because
|
|
12
12
|
// an installed member shows its VERSION, not a phrase (the renderer special-cases it). Mirrors the
|
package/tools/procedures.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
//
|
|
10
10
|
// Invariants (mirror recipes.mjs): pure-where-possible, READ-ONLY (never writes, never commits, never
|
|
11
11
|
// runs a subscription CLI). The deterministic resolution lives in the kit (resolveActivityRecipe), not
|
|
12
|
-
// in the agent. Dependency-free, Node >=
|
|
12
|
+
// in the agent. Dependency-free, Node >= 22.
|
|
13
13
|
//
|
|
14
14
|
// Exit codes: 0 success (an unsatisfiable explicit override degrades LOUDLY but still exits 0 — it is a
|
|
15
15
|
// valid request that gracefully degraded); 2 usage (unknown <activity> / bad --override syntax);
|
|
@@ -213,7 +213,7 @@ const reviewLoopAdvice = (slots, activity) =>
|
|
|
213
213
|
' • At the cap, classify every surviving blocking finding: fixable-bug (fold ONCE as a red→green test, re-review) / inherent-layer-residual (document + raise to an acceptance criterion) / escalate (the maintainer decides); a minor never forces triage.',
|
|
214
214
|
...(activity === 'plan-execution'
|
|
215
215
|
? [
|
|
216
|
-
' • The computed instrument for THIS loop:
|
|
216
|
+
' • The computed instrument for THIS loop: declare each bugfix red BEFORE the fix (core-evidence red-proof — observed N/N red, custody-hashed); an unavailable backend gets an explicit core-evidence degrade record, never a silent skip; then stage everything, run the reviews on the STAGED tree, and mint the ONE receipt with run-gates --final (coverage + red-proof verification ride the final run); the commit is gated by commit-guard --check (the D13 ordering: any edit after the final run re-stales it); read the loop state statelessly with core-evidence summary.',
|
|
217
217
|
]
|
|
218
218
|
: []),
|
|
219
219
|
]
|
|
@@ -234,7 +234,7 @@ const groundingPreStepAdvice = (activity, slots, plans) => {
|
|
|
234
234
|
if (!slots.some((s) => (s.backends ?? []).includes('agy-review'))) return [];
|
|
235
235
|
const planArg = plans.length === 1 ? `--plan "${PLANS_REL}/${plans[0]}"` : '--plan <path>';
|
|
236
236
|
// plan-authoring reviews the plan FILE — when exactly one plan is in flight, the review command
|
|
237
|
-
// is populated with the same discovered path (
|
|
237
|
+
// is populated with the same discovered path (a known path never renders a placeholder).
|
|
238
238
|
const reviewForm =
|
|
239
239
|
activity === 'plan-authoring'
|
|
240
240
|
? plans.length === 1
|
package/tools/recipes.mjs
CHANGED
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
// Invariants (the backends/status posture): pure (no fs/network/CLI in planRecipe/recommendRecipe),
|
|
13
13
|
// read-only, NEVER runs a subscription CLI. The kit only surfaces/selects/plans a recipe — the
|
|
14
14
|
// orchestrator (the main agent) executes it via the bridge skills and always makes the single commit;
|
|
15
|
-
// a backend is advisory or delegated, never autonomous. Dependency-free, Node >=
|
|
15
|
+
// a backend is advisory or delegated, never autonomous. Dependency-free, Node >= 22.
|
|
16
16
|
|
|
17
|
-
import { existsSync } from 'node:fs';
|
|
17
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
18
18
|
import { dirname, join, resolve } from 'node:path';
|
|
19
19
|
import { pathToFileURL } from 'node:url';
|
|
20
20
|
// The host-level bridge-settings snapshot (fact-only, best-effort). READ-ONLY core only — never the
|
|
21
21
|
// writer — so this read-only advisor never pulls in the atomic-write core.
|
|
22
|
-
import { settingsSnapshot } from './bridge-settings-read.mjs';
|
|
22
|
+
import { settingsSnapshot, DEFAULT_BUNDLE_ROOT } from './bridge-settings-read.mjs';
|
|
23
23
|
import {
|
|
24
24
|
detectBackends,
|
|
25
25
|
wrapperCmdFor,
|
|
@@ -298,7 +298,50 @@ export const resolveActivityRecipe = ({ config = {}, readiness = [], activity, s
|
|
|
298
298
|
// DISPLAY_ALIASES — the ONE alias table the recommendation clause already uses; ordering is the
|
|
299
299
|
// deterministic BACKEND_PRIORITY (codex before agy), independent of detection emission order.
|
|
300
300
|
// Always exactly one line: no part may carry a newline (pinned by tests).
|
|
301
|
-
|
|
301
|
+
// composeConfiguredPosture(ctx) → the CONFIGURED dispatch posture (strip-the-kit D5), rendered
|
|
302
|
+
// from the bundled manifests' VALIDATED posture pins overlaid with the ACTIVE bridge-settings —
|
|
303
|
+
// today only the codex tier knob (bridge-settings stays model/effort-free by design, so this
|
|
304
|
+
// surface can never carry a settings-file model claim). Returns null when NO bundled bridge
|
|
305
|
+
// declares a posture block (a pre-D5 bundle keeps every surface byte-identical). Best-effort:
|
|
306
|
+
// a corrupt bundle degrades to null — the posture segment is a footer fact, never a gate.
|
|
307
|
+
export const composeConfiguredPosture = (ctx = {}) => {
|
|
308
|
+
try {
|
|
309
|
+
const bundleRoot = ctx.bundleRoot ?? DEFAULT_BUNDLE_ROOT;
|
|
310
|
+
const read = ctx.readFile ?? readFileSync;
|
|
311
|
+
const parts = [];
|
|
312
|
+
for (const name of BACKEND_PRIORITY) {
|
|
313
|
+
let manifest;
|
|
314
|
+
try {
|
|
315
|
+
manifest = JSON.parse(String(read(join(bundleRoot, name, 'capability.json'), 'utf8')));
|
|
316
|
+
} catch {
|
|
317
|
+
return null; // an unreadable/corrupt manifest is CORRUPTION — a partial posture would lie
|
|
318
|
+
}
|
|
319
|
+
if (!Object.hasOwn(manifest, 'posture')) continue; // a pre-D5 bridge — legitimate absence
|
|
320
|
+
const posture = manifest.posture;
|
|
321
|
+
// The FULL closed shape (the validator/receipt-predicate twin): a present-but-invalid
|
|
322
|
+
// block nulls the WHOLE render — fail closed to silence, never a partial/mangled line.
|
|
323
|
+
const invalid =
|
|
324
|
+
posture === null || typeof posture !== 'object' || Array.isArray(posture) ||
|
|
325
|
+
typeof posture.model !== 'string' || posture.model.length === 0 ||
|
|
326
|
+
(Object.hasOwn(posture, 'effort') && (typeof posture.effort !== 'string' || posture.effort.length === 0)) ||
|
|
327
|
+
(Object.hasOwn(posture, 'tier') && posture.tier !== null && (typeof posture.tier !== 'string' || posture.tier.length === 0)) ||
|
|
328
|
+
Object.keys(posture).some((k) => !['model', 'effort', 'tier'].includes(k));
|
|
329
|
+
if (invalid) return null;
|
|
330
|
+
const seg = [`model=${posture.model}`];
|
|
331
|
+
if (Object.hasOwn(posture, 'effort')) seg.push(`effort=${posture.effort}`);
|
|
332
|
+
if (Object.hasOwn(posture, 'tier')) {
|
|
333
|
+
const knob = (ctx.settings?.active ?? []).find((s) => s.key === 'CODEX_SERVICE_TIER' && s.bridge === name);
|
|
334
|
+
seg.push(knob ? `tier=${knob.value} (bridge-settings)` : `tier=${posture.tier ?? 'standard'}`);
|
|
335
|
+
}
|
|
336
|
+
parts.push(`${DISPLAY_ALIASES[name] ?? name} ${seg.join(' ')}`);
|
|
337
|
+
}
|
|
338
|
+
return parts.length ? parts.join(' · ') : null;
|
|
339
|
+
} catch {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
export const composeStatusLine = (detection, recommendation, settings = null, autonomy = null, posture = null) => {
|
|
302
345
|
const backends = [...detection]
|
|
303
346
|
.sort((a, b) => priorityIndex(a.name) - priorityIndex(b.name))
|
|
304
347
|
.map((b) => `${DISPLAY_ALIASES[b.name] ?? b.name} ${b.readiness === READY ? '✓' : '✗'} ${b.readiness}`)
|
|
@@ -315,7 +358,10 @@ export const composeStatusLine = (detection, recommendation, settings = null, au
|
|
|
315
358
|
// suffix precedent). Fact-only: effective per-activity levels + the render-sync state; an absent
|
|
316
359
|
// policy says "computed defaults" honestly; a malformed policy surfaces LOUDLY, never omitted.
|
|
317
360
|
const autonomySegment = autonomy == null ? '' : ` · autonomy: ${oneLine(formatAutonomySegment(autonomy))}`;
|
|
318
|
-
|
|
361
|
+
// The D5 posture segment: rendered ONLY when the caller supplies the composed pins-derived
|
|
362
|
+
// posture (composeConfiguredPosture) — an omitted/null param keeps the line byte-identical.
|
|
363
|
+
const postureSegment = posture == null ? '' : ` · posture: ${oneLine(posture)}`;
|
|
364
|
+
return base + suffix + autonomySegment + postureSegment;
|
|
319
365
|
};
|
|
320
366
|
|
|
321
367
|
// The one-segment autonomy renderer behind composeStatusLine's 4th param.
|
|
@@ -414,7 +460,7 @@ export const composeActiveRecipeLine = ({ config, source } = {}, detection, auto
|
|
|
414
460
|
const rec = recommendRecipe(detection);
|
|
415
461
|
const origin = source === 'none' || config == null ? 'no config file — computed defaults apply' : `from ${source}`;
|
|
416
462
|
// A MALFORMED policy must surface LOUDLY on this paste surface too — silently rendering cells
|
|
417
|
-
// without levels would hide the required STOP signal (
|
|
463
|
+
// without levels would hide the required STOP signal (Segment B). One line always.
|
|
418
464
|
const malformed = autonomy?.error
|
|
419
465
|
? ` · autonomy: MALFORMED policy — ${String(autonomy.error).replace(/[\s]+/g, ' ').trim()}`
|
|
420
466
|
: '';
|
|
@@ -425,7 +471,7 @@ export const composeActiveRecipeLine = ({ config, source } = {}, detection, auto
|
|
|
425
471
|
|
|
426
472
|
// The structured report behind `--json` — the recipes, the recommendation, a plan per recipe, and
|
|
427
473
|
// (additive) the pasteable one-line backend status composed from the same detection.
|
|
428
|
-
export const buildReport = (detection, settings = null, autonomy = null) => {
|
|
474
|
+
export const buildReport = (detection, settings = null, autonomy = null, posture = null) => {
|
|
429
475
|
const recommendation = recommendRecipe(detection);
|
|
430
476
|
return {
|
|
431
477
|
recipes: RECIPES.map(({ id, title, role, minBackends, degradesTo, summary }) => ({
|
|
@@ -439,8 +485,8 @@ export const buildReport = (detection, settings = null, autonomy = null) => {
|
|
|
439
485
|
recommendation,
|
|
440
486
|
plans: RECIPES.map((r) => planRecipe(r.id, detection)),
|
|
441
487
|
// The SAME autonomy facts the --status-line surface renders — the --json envelope must never
|
|
442
|
-
// expose a stale machine-composed status line (
|
|
443
|
-
statusLine: composeStatusLine(detection, recommendation, settings, autonomy),
|
|
488
|
+
// expose a stale machine-composed status line (Segment B).
|
|
489
|
+
statusLine: composeStatusLine(detection, recommendation, settings, autonomy, posture),
|
|
444
490
|
};
|
|
445
491
|
};
|
|
446
492
|
|
|
@@ -512,8 +558,13 @@ exclusive. Detection only — never writes, never commits, never runs a subscrip
|
|
|
512
558
|
console.error(`[agent-workflow-kit] ${err.message}`);
|
|
513
559
|
return err.exitCode ?? 1;
|
|
514
560
|
}
|
|
515
|
-
} else if (argv.includes('--status-line'))
|
|
516
|
-
|
|
561
|
+
} else if (argv.includes('--status-line')) {
|
|
562
|
+
const snapshot = settingsSnapshot();
|
|
563
|
+
console.log(composeStatusLine(detection, recommendRecipe(detection), snapshot, await composeAutonomyFacts(process.cwd()), composeConfiguredPosture({ settings: snapshot })));
|
|
564
|
+
} else if (argv.includes('--json')) {
|
|
565
|
+
const snapshot = settingsSnapshot();
|
|
566
|
+
console.log(JSON.stringify(buildReport(detection, snapshot, await composeAutonomyFacts(process.cwd()), composeConfiguredPosture({ settings: snapshot })), null, 2));
|
|
567
|
+
}
|
|
517
568
|
else console.log(formatRecipes(detection));
|
|
518
569
|
return 0;
|
|
519
570
|
};
|