@sabaiway/agent-workflow-kit 1.44.0 → 1.45.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 +52 -0
- package/README.md +6 -3
- package/SKILL.md +9 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +87 -16
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +3 -0
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +4 -2
- package/bridges/antigravity-cli-bridge/capability.json +3 -2
- package/bridges/codex-cli-bridge/SKILL.md +1 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +3 -0
- package/bridges/codex-cli-bridge/bin/codex-review.sh +90 -19
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +4 -0
- package/bridges/codex-cli-bridge/capability.json +3 -2
- package/bridges/codex-cli-bridge/references/driving-codex.md +3 -2
- package/bridges/codex-cli-bridge/references/sandbox-and-flags.md +3 -2
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/bootstrap.md +1 -1
- package/references/modes/grounding.md +8 -7
- package/references/modes/recommendations.md +14 -0
- package/references/modes/review-state.md +1 -1
- package/references/modes/sandbox-masks.md +15 -0
- package/references/modes/upgrade.md +10 -7
- package/references/modes/velocity.md +13 -2
- package/references/shared/composition-handoff.md +21 -16
- package/references/shared/report-footer.md +5 -1
- package/references/templates/AGENTS.md +2 -1
- package/references/templates/autonomy.json +3 -0
- package/tools/autonomy-config.mjs +13 -3
- package/tools/commands.mjs +15 -1
- package/tools/delegation.mjs +9 -5
- package/tools/detect-backends.mjs +2 -2
- package/tools/doc-parity.mjs +8 -0
- package/tools/engine-source.mjs +6 -0
- package/tools/family-registry.mjs +21 -7
- package/tools/fold-completeness-run.mjs +5 -0
- package/tools/grounding.mjs +139 -22
- package/tools/inject-methodology.mjs +117 -28
- package/tools/manifest/schema.md +20 -0
- package/tools/manifest/validate.mjs +26 -0
- package/tools/procedures.mjs +61 -8
- package/tools/recipes.mjs +94 -15
- package/tools/recommendations.mjs +469 -0
- package/tools/review-ledger-write.mjs +14 -0
- package/tools/review-ledger.mjs +3 -2
- package/tools/review-state.mjs +101 -24
- package/tools/run-gates.mjs +3 -0
- package/tools/sandbox-masks.mjs +370 -0
- package/tools/set-recipe.mjs +13 -1
- package/tools/velocity-profile.mjs +228 -22
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Marker-slot injection + reconciliation — the composition root's only mutation of a deployed
|
|
3
|
-
// AGENTS.md. A deployed entry point carries
|
|
3
|
+
// AGENTS.md. A deployed entry point carries THREE reconciled marker slots, each filled LIVE from the
|
|
4
4
|
// installed agent-workflow-engine (the family's one source of truth, no bundled mirror):
|
|
5
5
|
// 1. workflow:methodology — the plan→execute→review pointer (references/methodology-slot.md).
|
|
6
6
|
// 2. workflow:orchestration — the recipe-vocabulary pointer (references/orchestration-slot.md).
|
|
7
|
+
// 3. workflow:autonomy — the autonomy-policy read contract (references/autonomy-slot.md).
|
|
7
8
|
//
|
|
8
|
-
//
|
|
9
|
+
// All share ONE generic marker engine (a slot DESCRIPTOR parameterizes the markers / anchor /
|
|
9
10
|
// empty-slot / leading-blank). The methodology exports (findSlot / injectMethodology / ensureSlot /
|
|
10
11
|
// reconcileSlot / slotNeedsFill / extractSlot) delegate to it byte-for-byte, so the methodology
|
|
11
|
-
// contract is unchanged; the
|
|
12
|
+
// contract is unchanged; the later slots are the SAME engine with different descriptors.
|
|
12
13
|
//
|
|
13
14
|
// Contract per slot, strictly enforced:
|
|
14
15
|
// exactly one ordered start→end pair → replace only the bytes between them;
|
|
@@ -31,6 +32,8 @@ export const START_MARKER = '<!-- workflow:methodology:start -->';
|
|
|
31
32
|
export const END_MARKER = '<!-- workflow:methodology:end -->';
|
|
32
33
|
export const ORCH_START_MARKER = '<!-- workflow:orchestration:start -->';
|
|
33
34
|
export const ORCH_END_MARKER = '<!-- workflow:orchestration:end -->';
|
|
35
|
+
export const AUTONOMY_START_MARKER = '<!-- workflow:autonomy:start -->';
|
|
36
|
+
export const AUTONOMY_END_MARKER = '<!-- workflow:autonomy:end -->';
|
|
34
37
|
export const AGENTS_MD_CAP = 100; // the deployed AGENTS.md line budget (its own footer rule)
|
|
35
38
|
|
|
36
39
|
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -59,11 +62,16 @@ export const METHODOLOGY_ANCHOR = /^.*Read it before any code change\..*$/m;
|
|
|
59
62
|
// The orchestration pair lands right BELOW the methodology end marker — so its anchor is that line.
|
|
60
63
|
// A well-formed entry point carries exactly one methodology end marker → exactly one anchor.
|
|
61
64
|
export const ORCH_ANCHOR = new RegExp(`^.*${escapeRegExp(END_MARKER)}.*$`, 'm');
|
|
65
|
+
// The autonomy pair chains right below the orchestration pair — its anchor is the orchestration end
|
|
66
|
+
// marker line. The anchor can legitimately be ABSENT (a legacy file whose orchestration pair was
|
|
67
|
+
// itself skipped) — the chained CLI classifies that as a soft skip, never a hard STOP (D4).
|
|
68
|
+
export const AUTONOMY_ANCHOR = new RegExp(`^.*${escapeRegExp(ORCH_END_MARKER)}.*$`, 'm');
|
|
62
69
|
|
|
63
70
|
// The canonical empty slots (an ordered start→end pair, nothing between) — what a fresh template
|
|
64
71
|
// ships and what ensureSlot inserts. LF form; ensureSlot rewrites the newline to match the document.
|
|
65
72
|
export const EMPTY_SLOT = `${START_MARKER}\n${END_MARKER}`;
|
|
66
73
|
export const ORCH_EMPTY_SLOT = `${ORCH_START_MARKER}\n${ORCH_END_MARKER}`;
|
|
74
|
+
export const AUTONOMY_EMPTY_SLOT = `${AUTONOMY_START_MARKER}\n${AUTONOMY_END_MARKER}`;
|
|
67
75
|
|
|
68
76
|
// ── known-prior canonical fragments (drift-guarded, APPEND-ONLY per slot) ───────
|
|
69
77
|
// The EXACT content a PREVIOUS release's engine fragment shipped (what a filled slot would carry today).
|
|
@@ -113,6 +121,21 @@ export const ORCHESTRATION_DESCRIPTOR = {
|
|
|
113
121
|
upgradeAdvice:
|
|
114
122
|
'the orchestration-recipes pointer predates the read-at-start clause — add "read `docs/ai/orchestration.json` at session start; set it with /agent-workflow-kit set-recipe", or set your preference now with /agent-workflow-kit set-recipe.',
|
|
115
123
|
};
|
|
124
|
+
// The autonomy-policy slot (AD-044 Plan 3) — first canon, so no known priors yet: any future
|
|
125
|
+
// fragment change must append the outgoing content to a KNOWN_PRIOR_AUTONOMY_SLOT store first.
|
|
126
|
+
export const AUTONOMY_DESCRIPTOR = {
|
|
127
|
+
startMarker: AUTONOMY_START_MARKER,
|
|
128
|
+
endMarker: AUTONOMY_END_MARKER,
|
|
129
|
+
anchor: AUTONOMY_ANCHOR,
|
|
130
|
+
emptySlot: AUTONOMY_EMPTY_SLOT,
|
|
131
|
+
leadingBlank: false,
|
|
132
|
+
markerName: 'autonomy',
|
|
133
|
+
anchorLabel: 'autonomy anchor (the orchestration end-marker line)',
|
|
134
|
+
knownPriorCanonicals: [],
|
|
135
|
+
upgradeSignature: 'docs/ai/autonomy.json',
|
|
136
|
+
upgradeAdvice:
|
|
137
|
+
'the autonomy pointer does not name the per-project policy file — add "read `docs/ai/autonomy.json` at session start; set it with /agent-workflow-kit set-autonomy", or refresh the slot to the current canon.',
|
|
138
|
+
};
|
|
116
139
|
|
|
117
140
|
// ── generic marker-slot engine (descriptor-parameterized) ──────────────────────
|
|
118
141
|
|
|
@@ -283,15 +306,22 @@ export const markerSlotUpgradeHint = (text, descriptor) => {
|
|
|
283
306
|
// stable "(cap N)" substring both cap messages carry, so the dual-slot reconcile can skip the
|
|
284
307
|
// orchestration pointer (loud) while keeping the methodology fill, instead of aborting both.
|
|
285
308
|
const isCapRefusal = (errorMessage) => typeof errorMessage === 'string' && errorMessage.includes('(cap ');
|
|
309
|
+
// The autonomy lane distinguishes the TWO cap messages "(cap " conflates (D4 cap-lane honesty):
|
|
310
|
+
// only a FILL-overflow from an in-cap input (injectIntoSlot's "injection would push …") is the soft
|
|
311
|
+
// skip — the pointer was genuinely withheld. An ALREADY-over-cap custom file (reconcileMarkerSlot's
|
|
312
|
+
// "AGENTS.md is N lines … trim the file") keeps its distinct over-cap report as a hard refusal,
|
|
313
|
+
// never a mislabeled "skipped". Exported so the split is pinned against the real messages.
|
|
314
|
+
export const isFillCapRefusal = (errorMessage) =>
|
|
315
|
+
isCapRefusal(errorMessage) && errorMessage.includes('injection would push');
|
|
286
316
|
|
|
287
317
|
const main = async (argv) => {
|
|
288
318
|
const { readFile, writeFile, rename, rm } = await import('node:fs/promises');
|
|
289
319
|
const { dirname, basename, join, resolve } = await import('node:path');
|
|
290
320
|
const { homedir } = await import('node:os');
|
|
291
|
-
const { resolveEngineDir, readEngineFragment, detectEngine, ENGINE_FRAGMENT_REL, ORCHESTRATION_FRAGMENT_REL } = await import('./engine-source.mjs');
|
|
321
|
+
const { resolveEngineDir, readEngineFragment, detectEngine, ENGINE_FRAGMENT_REL, ORCHESTRATION_FRAGMENT_REL, AUTONOMY_FRAGMENT_REL } = await import('./engine-source.mjs');
|
|
292
322
|
|
|
293
323
|
// `reconcile <AGENTS.md> [fragment.md]` = ensure-slot + inject-if-empty + cap (bootstrap/upgrade) for
|
|
294
|
-
//
|
|
324
|
+
// ALL THREE slots; `<AGENTS.md> [fragment.md]` = the legacy inject-into-existing-(methodology)-slot mode.
|
|
295
325
|
const mode = argv[0] === 'reconcile' ? 'reconcile' : 'inject';
|
|
296
326
|
const rest = mode === 'reconcile' ? argv.slice(1) : argv;
|
|
297
327
|
const agentsPath = rest[0];
|
|
@@ -323,36 +353,32 @@ const main = async (argv) => {
|
|
|
323
353
|
}
|
|
324
354
|
};
|
|
325
355
|
|
|
326
|
-
// The orchestration
|
|
327
|
-
// two failures keyed off the engine's own detection (not message text): an
|
|
328
|
-
// simply TOO OLD to ship
|
|
329
|
-
//
|
|
330
|
-
// absent/invalid engine (it cannot supply
|
|
331
|
-
//
|
|
332
|
-
|
|
356
|
+
// The orchestration + autonomy fragments are the CHAINED (less-critical) pointers. Source each
|
|
357
|
+
// lazily, but DISTINGUISH two failures keyed off the engine's own detection (not message text): an
|
|
358
|
+
// engine that is VALID but simply TOO OLD to ship the requested fragment is a SOFT skip — the prior
|
|
359
|
+
// fills are kept and the pointer is reported as withheld (parallel to the cap-skip); only a FULLY
|
|
360
|
+
// absent/invalid engine (it cannot supply ANY pointer) is a hard STOP. A read error on a PRESENT
|
|
361
|
+
// fragment is a real corruption STOP, NEVER a "too old" skip (don't mislabel a current engine with
|
|
362
|
+
// an unreadable fragment). Returns { fragment } on success, { skip } for the soft case, or
|
|
363
|
+
// process.exit(1) for the hard STOP.
|
|
364
|
+
const sourceChainedFragment = async (rel) => {
|
|
333
365
|
const { dir, source } = resolveEngineDir({ env: process.env, home: homedir() });
|
|
334
366
|
const stop = (err) => {
|
|
335
367
|
console.error(`[inject-methodology] reconcile STOP — ${err.message}`);
|
|
336
368
|
process.exit(1);
|
|
337
369
|
};
|
|
338
|
-
|
|
339
|
-
// PRESENT fragment is a real corruption STOP, NEVER a "too old" skip (don't mislabel a current
|
|
340
|
-
// engine with an unreadable fragment).
|
|
341
|
-
if (detectEngine(dir, { source, rel: ORCHESTRATION_FRAGMENT_REL }).ok) {
|
|
370
|
+
if (detectEngine(dir, { source, rel }).ok) {
|
|
342
371
|
try {
|
|
343
|
-
return { fragment: readEngineFragment(dir, { source, rel
|
|
372
|
+
return { fragment: readEngineFragment(dir, { source, rel }) };
|
|
344
373
|
} catch (err) {
|
|
345
374
|
stop(err);
|
|
346
375
|
}
|
|
347
376
|
}
|
|
348
|
-
// The orchestration fragment is NOT a usable file. SOFT-skip ONLY when the engine is otherwise
|
|
349
|
-
// valid (it can still supply the methodology fragment) — a genuine too-old (<1.2.0) engine. A
|
|
350
|
-
// fully absent/invalid engine cannot supply EITHER fragment → hard STOP with the install command.
|
|
351
377
|
if (detectEngine(dir, { source }).ok) {
|
|
352
378
|
return { skip: 'the installed engine is too old to supply it — refresh with `npx @sabaiway/agent-workflow-engine@latest init`' };
|
|
353
379
|
}
|
|
354
380
|
try {
|
|
355
|
-
readEngineFragment(dir, { source, rel
|
|
381
|
+
readEngineFragment(dir, { source, rel }); // throws the canonical install-me error
|
|
356
382
|
} catch (err) {
|
|
357
383
|
stop(err);
|
|
358
384
|
}
|
|
@@ -398,7 +424,7 @@ const main = async (argv) => {
|
|
|
398
424
|
for (const n of notes) console.log(`[inject-methodology] note: ${n}`);
|
|
399
425
|
};
|
|
400
426
|
|
|
401
|
-
// ── Explicit [fragment.md] binds methodology ONLY → skip the orchestration
|
|
427
|
+
// ── Explicit [fragment.md] binds methodology ONLY → skip the orchestration + autonomy reconciles ──
|
|
402
428
|
if (explicitFragmentArg) {
|
|
403
429
|
if (afterMeth === text) {
|
|
404
430
|
console.log('[inject-methodology] methodology slot already present and filled — nothing to do (zero-diff).');
|
|
@@ -416,7 +442,7 @@ const main = async (argv) => {
|
|
|
416
442
|
let orchSkipped = false;
|
|
417
443
|
|
|
418
444
|
const orchSource = markerSlotNeedsFill(afterMeth, ORCHESTRATION_DESCRIPTOR)
|
|
419
|
-
? await
|
|
445
|
+
? await sourceChainedFragment(ORCHESTRATION_FRAGMENT_REL)
|
|
420
446
|
: { fragment: '' };
|
|
421
447
|
if (orchSource.skip) {
|
|
422
448
|
// Engine too old to supply the recipes pointer → SOFT skip, parallel to the cap-skip: the
|
|
@@ -453,16 +479,79 @@ const main = async (argv) => {
|
|
|
453
479
|
}
|
|
454
480
|
}
|
|
455
481
|
|
|
456
|
-
// ──
|
|
482
|
+
// ── Slot 3: autonomy, chained on the orchestration-reconciled text (same combined ≤100 guard).
|
|
483
|
+
// D4 invariant: every failure lane SPECIFIC to this slot is a LOUD soft skip that preserves
|
|
484
|
+
// the prior fills and exits 0 — (i) a too-old engine (no autonomy fragment), (ii) a
|
|
485
|
+
// fill-overflow cap refusal, (iii) an ABSENT anchor (the orchestration pair is absent or was
|
|
486
|
+
// itself skipped above — a 0-anchors ensure error must not discard the methodology fill).
|
|
487
|
+
// Malformed autonomy markers, an already-over-cap custom file, a present-but-unreadable
|
|
488
|
+
// fragment, and a fully absent/invalid engine stay hard STOPs. ──
|
|
489
|
+
let describeAut;
|
|
490
|
+
let autSkipped = false;
|
|
491
|
+
const autSlot = findMarkerSlot(finalText, AUTONOMY_DESCRIPTOR);
|
|
492
|
+
if (autSlot.state === 'malformed') {
|
|
493
|
+
// Malformed markers are a hard STOP on EVERY lane — validated before the soft-skip
|
|
494
|
+
// short-circuits so a duplicate/reversed autonomy pair can never ride out as a "skip"
|
|
495
|
+
// alongside a partial (methodology) write.
|
|
496
|
+
console.error(`[inject-methodology] reconcile refused (autonomy) — ${autSlot.reason}`);
|
|
497
|
+
process.exit(1);
|
|
498
|
+
}
|
|
499
|
+
if (orchSkipped) {
|
|
500
|
+
// The chain is CAUSAL, not merely positional: a pointer never lands in a run that withheld
|
|
501
|
+
// the pointer it chains below (cap or too-old) — otherwise a partially-shipped engine could
|
|
502
|
+
// fill autonomy under an unfilled recipes pointer, and a near-cap file could spend its last
|
|
503
|
+
// lines on the less-critical pointer. LOUD soft skip. This branch also subsumes the D4 (iii)
|
|
504
|
+
// anchor-absent lane: after a non-skipped orchestration reconcile the orchestration pair —
|
|
505
|
+
// the autonomy anchor — is present by construction, so orchSkipped is the ONLY route to a
|
|
506
|
+
// missing anchor (a separate anchor check would be dead code; coverage proved it).
|
|
507
|
+
autSkipped = true;
|
|
508
|
+
describeAut =
|
|
509
|
+
'autonomy pointer skipped — chained below the orchestration pointer, which was itself skipped this run; re-run once it lands';
|
|
510
|
+
} else {
|
|
511
|
+
const autSource = markerSlotNeedsFill(finalText, AUTONOMY_DESCRIPTOR)
|
|
512
|
+
? await sourceChainedFragment(AUTONOMY_FRAGMENT_REL)
|
|
513
|
+
: { fragment: '' };
|
|
514
|
+
if (autSource.skip) {
|
|
515
|
+
autSkipped = true;
|
|
516
|
+
describeAut = `autonomy pointer skipped — ${autSource.skip}`;
|
|
517
|
+
} else {
|
|
518
|
+
const autResult = reconcileMarkerSlot(finalText, AUTONOMY_DESCRIPTOR, autSource.fragment, { maxLines: AGENTS_MD_CAP });
|
|
519
|
+
if (autResult.status === 'error') {
|
|
520
|
+
if (isFillCapRefusal(autResult.error)) {
|
|
521
|
+
// SOFT skip — only the genuine fill-overflow withholds the pointer; an already-over-cap
|
|
522
|
+
// custom file keeps its distinct over-cap report via the hard-STOP branch below.
|
|
523
|
+
autSkipped = true;
|
|
524
|
+
describeAut = `autonomy pointer skipped — ${autResult.error}`;
|
|
525
|
+
} else {
|
|
526
|
+
console.error(`[inject-methodology] reconcile refused (autonomy) — ${autResult.error}`);
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
finalText = autResult.text;
|
|
531
|
+
describeAut = {
|
|
532
|
+
'reconciled-inserted': 'inserted the autonomy pointer below it and filled it',
|
|
533
|
+
'reconciled-filled': 'filled the empty autonomy pointer',
|
|
534
|
+
'reconciled-refreshed': 'refreshed the autonomy pointer to the current canon',
|
|
535
|
+
'present-filled': 'autonomy pointer already present',
|
|
536
|
+
}[autResult.status];
|
|
537
|
+
if (autResult.status === 'present-filled') {
|
|
538
|
+
const u = markerSlotUpgradeHint(finalText, AUTONOMY_DESCRIPTOR);
|
|
539
|
+
if (u) notes.push(u);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// ── One atomic write of the final (three-slot) text ──
|
|
457
546
|
if (finalText === text) {
|
|
458
|
-
// Byte-unchanged. Still report a
|
|
459
|
-
if (orchSkipped) console.log(`[inject-methodology] reconcile: ${describeMeth}; ${describeOrch}.`);
|
|
460
|
-
else console.log('[inject-methodology] reconcile:
|
|
547
|
+
// Byte-unchanged. Still report a skip (it is not "nothing to do" — a pointer was withheld).
|
|
548
|
+
if (orchSkipped || autSkipped) console.log(`[inject-methodology] reconcile: ${describeMeth}; ${describeOrch}; ${describeAut}.`);
|
|
549
|
+
else console.log('[inject-methodology] reconcile: all three pointers already present and filled — nothing to do (zero-diff).');
|
|
461
550
|
reportNotes();
|
|
462
551
|
return;
|
|
463
552
|
}
|
|
464
553
|
await writeAtomic(finalText);
|
|
465
|
-
console.log(`[inject-methodology] reconcile: ${describeMeth}; ${describeOrch}.`);
|
|
554
|
+
console.log(`[inject-methodology] reconcile: ${describeMeth}; ${describeOrch}; ${describeAut}.`);
|
|
466
555
|
reportNotes();
|
|
467
556
|
return;
|
|
468
557
|
}
|
package/tools/manifest/schema.md
CHANGED
|
@@ -18,6 +18,7 @@ expansion), never via the shell. The validator is [`validate.mjs`](./validate.mj
|
|
|
18
18
|
| `roles` | object | yes | keys ⊆ `provides`; see *Roles* |
|
|
19
19
|
| `detect` | object | no | `installed` (skill on the machine) + `deployed` (substrate set up in cwd) |
|
|
20
20
|
| `settings` | array | no | the bridge's **settings-file surface** (typed; see *Settings*). Unlike `contract`, a malformed entry **fails** `--strict`. |
|
|
21
|
+
| `networkHosts` | string[] | no | the backend CLI's **observed egress host families** (see *Network hosts*). A malformed list **fails** `--strict`. |
|
|
21
22
|
| `install` / `uninstall` | object | no | `install.npm` is a package name, not a path |
|
|
22
23
|
| `cost` / `quota` / `provenance` | misc | no | informational |
|
|
23
24
|
| `available` | boolean | no | `false` = a declared-but-not-installed stub; skips fs/version checks |
|
|
@@ -85,6 +86,25 @@ Wrappers never parse JSON at run time: each carries its own shell registry/valid
|
|
|
85
86
|
drift-guarded set-equal to this block by the bridge `bin/*.test.mjs` suites (help section keys,
|
|
86
87
|
`aw_settings_known` registry, `AW_SETTINGS_APPLIED` subset, `aw_settings_valid` typed arms).
|
|
87
88
|
|
|
89
|
+
## Network hosts (AD-044 Plan 4, consult-locked)
|
|
90
|
+
|
|
91
|
+
`networkHosts` declares the hosts the backend CLI is **observed** to contact (synthetic examples:
|
|
92
|
+
`*.api.backend.example`, `accounts.backend.example`; the real observed lists live in each bridge's
|
|
93
|
+
`capability.json`) — the **single documentation
|
|
94
|
+
source** for a hand-applied sandbox/network allowlist (session sandbox config, or a hand-pasted
|
|
95
|
+
`sandbox.network.allowedDomains` entry). Rules:
|
|
96
|
+
|
|
97
|
+
- Each entry is a bare dotted hostname or a `*.family` wildcard — never a scheme/path/port.
|
|
98
|
+
Entries must be unique. A malformed list **fails** `--strict` (the entries are pasted verbatim
|
|
99
|
+
into allowlist lines by the Recommendations advisor).
|
|
100
|
+
- **The kit never seeds these into settings** (bridge council 2026-07-11, both backends concur):
|
|
101
|
+
a network pre-allow widens egress for **every** sandboxed command, so running the wrappers
|
|
102
|
+
**outside** the sandbox (`sandbox.excludedCommands`, the `--bridge-tier` wiring) stays the
|
|
103
|
+
primary lane; the hosts list exists for the **hand-apply** fallback under harness-managed
|
|
104
|
+
sandboxes where settings-level exclusions are inert.
|
|
105
|
+
- Observed-minimal, honestly incomplete: a blocked host names itself at run time — extend the
|
|
106
|
+
hand-applied list by hand; the manifest list records what was actually observed.
|
|
107
|
+
|
|
88
108
|
## Path-field rules (Windows-safe, traversal-safe)
|
|
89
109
|
|
|
90
110
|
- **No absolute paths**, **no `..` traversal**, **no unresolved placeholders** (`{{…}}` / `${…}`)
|
|
@@ -63,6 +63,10 @@ export const settingValueValid = (entry, value) => {
|
|
|
63
63
|
const hasTraversal = (p) => p.split(/[\\/]/).includes('..');
|
|
64
64
|
const isUnresolved = (s) => /\{\{|\}\}|\$\{/.test(s);
|
|
65
65
|
|
|
66
|
+
// A `networkHosts` entry: a bare dotted hostname, optionally a `*.` family wildcard — never a
|
|
67
|
+
// scheme, a path, a port, or whitespace (the entry is pasted verbatim into an allowlist line).
|
|
68
|
+
export const NETWORK_HOST_RE = /^(\*\.)?[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i;
|
|
69
|
+
|
|
66
70
|
// Cross-platform "absolute-like": POSIX root (/x), Windows drive (C:\ or C:/), UNC (\\host),
|
|
67
71
|
// or a leading backslash. node:path.isAbsolute() alone is platform-dependent, so a Windows path
|
|
68
72
|
// would slip through unchecked on a POSIX CI runner — Windows-safety requires rejecting both.
|
|
@@ -313,6 +317,28 @@ export const validateManifest = (skillDir) => {
|
|
|
313
317
|
}
|
|
314
318
|
}
|
|
315
319
|
|
|
320
|
+
// `networkHosts` (AD-044 Plan 4, consult-locked): the backend CLI's OBSERVED egress host
|
|
321
|
+
// families — a DOCUMENTATION source for hand-applied sandbox/network allowlists. The kit never
|
|
322
|
+
// seeds these into settings (exclusion stays primary; a network pre-allow widens egress for
|
|
323
|
+
// EVERY sandboxed command), but a malformed list FAILS --strict like `settings`: the
|
|
324
|
+
// Recommendations advisor renders it verbatim, so a bad entry would corrupt a pasted line.
|
|
325
|
+
const networkHosts = manifest.networkHosts;
|
|
326
|
+
if (networkHosts != null) {
|
|
327
|
+
if (!Array.isArray(networkHosts) || networkHosts.length === 0) {
|
|
328
|
+
errors.push('`networkHosts` must be a non-empty array of host strings');
|
|
329
|
+
} else {
|
|
330
|
+
const seenHosts = new Set();
|
|
331
|
+
networkHosts.forEach((host, i) => {
|
|
332
|
+
if (typeof host !== 'string' || !NETWORK_HOST_RE.test(host)) {
|
|
333
|
+
errors.push(`\`networkHosts[${i}]\` must be a bare hostname or a *.family wildcard (${JSON.stringify(host)})`);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (seenHosts.has(host)) errors.push(`duplicate networkHosts entry "${host}" (\`networkHosts[${i}]\`)`);
|
|
337
|
+
else seenHosts.add(host);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
316
342
|
if (!isStub) {
|
|
317
343
|
const auth = readAuthoritativeVersion(skillDir);
|
|
318
344
|
if (auth.version == null) errors.push(`could not resolve an authoritative version (${auth.from})`);
|
package/tools/procedures.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import { plansInFlight, PLANS_REL } from './review-state.mjs';
|
|
|
36
36
|
// (an import-split test pins it). CONFIG_REL is RE-EXPORTED so existing importers (procedures.test.mjs,
|
|
37
37
|
// historically) keep their import site working.
|
|
38
38
|
import { CONFIG_REL, fail, loadConfig, assertSlotRecipe } from './orchestration-config.mjs';
|
|
39
|
+
import { AUTONOMY_REL, loadAutonomy, resolveAutonomy, isSparseSeedConfig } from './autonomy-config.mjs';
|
|
39
40
|
export { CONFIG_REL };
|
|
40
41
|
|
|
41
42
|
// ── argument + override parsing (usage errors → exit 2) ─────────────────────────────
|
|
@@ -224,7 +225,10 @@ const reviewLoopAdvice = (slots, activity) =>
|
|
|
224
225
|
// (the review-state plan-in-flight detector): exactly ONE plan in flight → render it populated;
|
|
225
226
|
// zero or several → the explicit `--plan <path>` placeholder + a one-line discovery caveat. The
|
|
226
227
|
// suggested --out lives OUTSIDE the repo (/tmp) — grounding.mjs refuses a non-scratch destination.
|
|
227
|
-
|
|
228
|
+
// Exported for the bridge-tier byte-parity pin (AD-044 Plan 4): the velocity tier seeds the
|
|
229
|
+
// grounding rule in EXACTLY this rendered spelling — `node "${GROUNDING_TOOL}"` — so seeded and
|
|
230
|
+
// rendered forms can never drift apart.
|
|
231
|
+
export const GROUNDING_TOOL = join(dirname(fileURLToPath(import.meta.url)), 'grounding.mjs');
|
|
228
232
|
const GROUNDING_FACTS_OUT = '/tmp/review-facts.md';
|
|
229
233
|
const groundingPreStepAdvice = (activity, slots, plans) => {
|
|
230
234
|
if (!slots.some((s) => (s.backends ?? []).includes('agy-review'))) return [];
|
|
@@ -242,17 +246,42 @@ const groundingPreStepAdvice = (activity, slots, plans) => {
|
|
|
242
246
|
// Path arguments are double-quoted — a skill dir or plan name with a space must stay copy-pasteable.
|
|
243
247
|
const lines = [
|
|
244
248
|
'Grounding pre-step (agy is dispatched — assemble the verified facts BEFORE the review; grounding.mjs slices verbatim, judgment additions stay yours):',
|
|
245
|
-
` run: node "${GROUNDING_TOOL}" --constraints ${planArg} --out ${GROUNDING_FACTS_OUT}`,
|
|
249
|
+
` run: node "${GROUNDING_TOOL}" --constraints --autonomy ${planArg} --out ${GROUNDING_FACTS_OUT}`,
|
|
246
250
|
` then: ${reviewForm} --facts @${GROUNDING_FACTS_OUT}`,
|
|
247
251
|
];
|
|
248
252
|
if (plans.length === 0) {
|
|
249
|
-
lines.push(` ↳ plan discovery: no plan in flight under ${PLANS_REL} — substitute the plan file you are reviewing against, or drop --plan for constraints
|
|
253
|
+
lines.push(` ↳ plan discovery: no plan in flight under ${PLANS_REL} — substitute the plan file you are reviewing against, or drop --plan for constraints+autonomy facts.`);
|
|
250
254
|
} else if (plans.length > 1) {
|
|
251
255
|
lines.push(` ↳ plan discovery: ${plans.length} plans in flight under ${PLANS_REL} (${plans.join(', ')}) — populate --plan with the one under review.`);
|
|
252
256
|
}
|
|
253
257
|
return lines;
|
|
254
258
|
};
|
|
255
259
|
|
|
260
|
+
// The per-activity autonomy block (AD-044 Plan 4): the resolved level for THIS activity + what it
|
|
261
|
+
// implies, rendered from resolveAutonomy — never retyped constants. Read at session start beside
|
|
262
|
+
// the resolved recipes (the AGENTS.md autonomy pointer's read contract). A malformed policy
|
|
263
|
+
// surfaces LOUDLY here (the session-start read is exactly where "malformed → STOP, never guess"
|
|
264
|
+
// must be visible); an absent file states the computed-defaults origin honestly.
|
|
265
|
+
const autonomyAdvice = (activity, facts) => {
|
|
266
|
+
if (facts == null) return [];
|
|
267
|
+
if (facts.error) return [`Autonomy (${AUTONOMY_REL}): MALFORMED — ${facts.error} — STOP and fix the policy file, never guess around it.`];
|
|
268
|
+
const level = facts.activities?.[activity]?.autonomy;
|
|
269
|
+
if (!level) return [];
|
|
270
|
+
const origin = facts.source === 'none'
|
|
271
|
+
? `computed defaults — no ${AUTONOMY_REL}`
|
|
272
|
+
: facts.defaultsEquivalent
|
|
273
|
+
? `computed defaults — ${AUTONOMY_REL} is the sparse defaults-equivalent seed`
|
|
274
|
+
: `from ${facts.source}`;
|
|
275
|
+
const redlines = Object.entries(facts.redlines).map(([k, v]) => `${k}=${v}`).join(', ');
|
|
276
|
+
const implies = level === 'sandbox'
|
|
277
|
+
? 'the OS sandbox confines and auto-allows confined commands — work runs to the next checkpoint without per-command prompts'
|
|
278
|
+
: 'every non-allowlisted command prompts (the sandbox, where enabled, still confines)';
|
|
279
|
+
return [
|
|
280
|
+
`Autonomy for "${activity}" (${origin}): ${level} — ${implies}.`,
|
|
281
|
+
` red-lines (always): ${redlines} — commit/push/publish keep their maintainer asks regardless of level.`,
|
|
282
|
+
];
|
|
283
|
+
};
|
|
284
|
+
|
|
256
285
|
// The cost-lane advisory block (cost-tiered execution — orchestration.md §5 canon, paraphrased
|
|
257
286
|
// at the point of use like reviewLoopAdvice paraphrases §9/§4). Rendered UNCONDITIONALLY for
|
|
258
287
|
// every activity — the lanes route EVERY step, review-backed or not (unlike reviewLoopAdvice,
|
|
@@ -267,6 +296,7 @@ const costLanesAdvice = () => [
|
|
|
267
296
|
' • L2 subscription bridge (codex / agy) — reviews per the resolved recipe above, on frontier bridge models (quality-first).',
|
|
268
297
|
' • L3 frontier — judgment: plan/fold/synthesis, ADR/handover/changelog-entry wording, persuasive copy, go/no-go, real code.',
|
|
269
298
|
' • A step with no named guardrail does not move down a lane; red lines never move down (council review models · real code · memory/copy wording · the maintainer approval asks).',
|
|
299
|
+
' • Sandbox lanes (under an OS sandbox): the L0 surfaces are sandbox-safe — gates/ledger/state/fold checks, git reads, plain no-network tests; the bridge wrappers are genuinely unsandboxed (network); npm-cache-touching commands are COMMAND-SHAPE dependent — first try the sandbox-safe shape (cache under $TMPDIR, offline/notifier off). Move ONLY the failing command out of the sandbox, never its class; BATCH consecutive unsandboxed calls.',
|
|
270
300
|
];
|
|
271
301
|
|
|
272
302
|
// The verbatim per-backend DRIVING CONTRACT block (M-contract): the exact invocation descriptor(s),
|
|
@@ -297,7 +327,7 @@ const contractLines = ({ cmd, contract, settings }) => {
|
|
|
297
327
|
return lines;
|
|
298
328
|
};
|
|
299
329
|
|
|
300
|
-
const formatHuman = ({ activity, section, slots, warnings, plans }) => {
|
|
330
|
+
const formatHuman = ({ activity, section, slots, warnings, plans, autonomy }) => {
|
|
301
331
|
const lines = [
|
|
302
332
|
section,
|
|
303
333
|
'',
|
|
@@ -309,6 +339,8 @@ const formatHuman = ({ activity, section, slots, warnings, plans }) => {
|
|
|
309
339
|
if (s.reason) lines.push(` ↳ ${s.reason}`);
|
|
310
340
|
for (const c of s.contracts ?? []) lines.push(...contractLines(c));
|
|
311
341
|
}
|
|
342
|
+
const autonomyBlock = autonomyAdvice(activity, autonomy);
|
|
343
|
+
if (autonomyBlock.length) lines.push('', ...autonomyBlock);
|
|
312
344
|
const grounding = groundingPreStepAdvice(activity, slots, plans);
|
|
313
345
|
if (grounding.length) lines.push('', ...grounding);
|
|
314
346
|
const advice = reviewLoopAdvice(slots, activity);
|
|
@@ -321,7 +353,7 @@ const formatHuman = ({ activity, section, slots, warnings, plans }) => {
|
|
|
321
353
|
return lines.join('\n');
|
|
322
354
|
};
|
|
323
355
|
|
|
324
|
-
const buildJson = ({ activity, section, slots, configSource, warnings, plans }) => ({
|
|
356
|
+
const buildJson = ({ activity, section, slots, configSource, warnings, plans, autonomy }) => ({
|
|
325
357
|
activity,
|
|
326
358
|
section,
|
|
327
359
|
slots: Object.fromEntries(
|
|
@@ -334,6 +366,8 @@ const buildJson = ({ activity, section, slots, configSource, warnings, plans })
|
|
|
334
366
|
groundingPreStep: groundingPreStepAdvice(activity, slots, plans),
|
|
335
367
|
// ADDITIVE (cost-tiered execution): the unconditional cost-lane advisory, structured.
|
|
336
368
|
costLanes: costLanesAdvice(),
|
|
369
|
+
// ADDITIVE (AD-044 Plan 4): the per-activity autonomy block, structured (empty when unresolvable).
|
|
370
|
+
autonomy: autonomyAdvice(activity, autonomy),
|
|
337
371
|
configSource,
|
|
338
372
|
warnings,
|
|
339
373
|
});
|
|
@@ -354,7 +388,8 @@ ${CONFIG_REL} + the read-only backend detector, and prints both. A per-run
|
|
|
354
388
|
Read-only: never writes, never commits, never runs a subscription CLI.
|
|
355
389
|
|
|
356
390
|
Exit codes: 0 success (an unsatisfiable override degrades loudly, still 0);
|
|
357
|
-
2 usage (unknown activity / bad --override); 1 config or engine error
|
|
391
|
+
2 usage (unknown activity / bad --override); 1 config or engine error
|
|
392
|
+
(incl. a malformed ${AUTONOMY_REL} — the advisory still renders, the exit flips).`;
|
|
358
393
|
|
|
359
394
|
// ── main ───────────────────────────────────────────────────────────────────────────
|
|
360
395
|
|
|
@@ -387,9 +422,27 @@ export const main = (argv, ctx = {}) => {
|
|
|
387
422
|
const slots = resolveAllSlots({ activity, config, detection, overrides });
|
|
388
423
|
const warnings = [...detectWarnings, ...collectWarnings(slots)];
|
|
389
424
|
const plans = plansInFlight(cwd);
|
|
425
|
+
// The autonomy facts (AD-044 Plan 4): resolved levels + red-lines from the policy file. A
|
|
426
|
+
// malformed policy renders LOUDLY in the block AND flips the exit to 1 (config error) — the
|
|
427
|
+
// recipes and contracts still print, only the code changes.
|
|
428
|
+
const autonomy = (() => {
|
|
429
|
+
try {
|
|
430
|
+
const { config: autonomyConfig, source } = loadAutonomy(cwd, readFile, lstat);
|
|
431
|
+
const resolved = resolveAutonomy(autonomyConfig);
|
|
432
|
+
// Structural seed detection (shared predicate) — a declared-defaults policy reads as
|
|
433
|
+
// "from docs/ai/autonomy.json", never as the seed (codex, Segment B closing).
|
|
434
|
+
const defaultsEquivalent = source !== 'none' && isSparseSeedConfig(autonomyConfig);
|
|
435
|
+
return { source, defaultsEquivalent, ...resolved };
|
|
436
|
+
} catch (err) {
|
|
437
|
+
return { error: (err && err.message) || String(err) };
|
|
438
|
+
}
|
|
439
|
+
})();
|
|
390
440
|
const stdout = json
|
|
391
|
-
? JSON.stringify(buildJson({ activity, section, slots, configSource, warnings, plans }), null, 2)
|
|
392
|
-
: formatHuman({ activity, section, slots, warnings, plans });
|
|
441
|
+
? JSON.stringify(buildJson({ activity, section, slots, configSource, warnings, plans, autonomy }), null, 2)
|
|
442
|
+
: formatHuman({ activity, section, slots, warnings, plans, autonomy });
|
|
443
|
+
if (autonomy?.error) {
|
|
444
|
+
return { code: 1, stdout, stderr: `procedures: malformed ${AUTONOMY_REL} — ${autonomy.error}` };
|
|
445
|
+
}
|
|
393
446
|
return { code: 0, stdout, stderr: '' };
|
|
394
447
|
} catch (err) {
|
|
395
448
|
return { code: err.exitCode ?? 1, stdout: '', stderr: `procedures: ${err.message}` };
|