@sabaiway/agent-workflow-kit 1.24.0 → 1.26.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 +69 -0
- package/README.md +12 -8
- package/SKILL.md +53 -25
- package/bin/install.mjs +74 -29
- package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +35 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +186 -0
- package/bridges/antigravity-cli-bridge/bin/agy.sh +23 -0
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +58 -2
- package/bridges/antigravity-cli-bridge/capability.json +25 -2
- package/bridges/codex-cli-bridge/SKILL.md +2 -2
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +34 -0
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +160 -1
- package/bridges/codex-cli-bridge/bin/codex-review.sh +28 -0
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +127 -0
- package/bridges/codex-cli-bridge/capability.json +36 -3
- package/bridges/codex-cli-bridge/references/driving-codex.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/tools/commands.mjs +1 -1
- package/tools/detect-backends.mjs +59 -0
- package/tools/family-members.mjs +2 -1
- package/tools/family-registry.mjs +111 -13
- package/tools/labels.mjs +13 -0
- package/tools/manifest/schema.md +15 -0
- package/tools/procedures.mjs +37 -6
- package/tools/recipes.mjs +56 -17
- package/tools/renderers.mjs +12 -1
- package/tools/semver-lite.mjs +23 -0
- package/tools/setup-backends.mjs +143 -6
- package/tools/view-model.mjs +11 -1
|
@@ -540,3 +540,130 @@ describe('codex-review.sh — assemble & output edge cases', () => {
|
|
|
540
540
|
assert.match(r.stdout, /FAKE_FINAL_MESSAGE/);
|
|
541
541
|
});
|
|
542
542
|
});
|
|
543
|
+
|
|
544
|
+
// ── driving contract: --help ⟷ manifest ⟷ real arg-parsing (drift-guarded) ─────
|
|
545
|
+
// The manifest roles.review.contract is the single machine-readable source of the
|
|
546
|
+
// driving contract; these suites pin (a) --help renders it verbatim (set-EQUALITY,
|
|
547
|
+
// both directions), (b) the wrapper's REAL parser arms equal the declared sets
|
|
548
|
+
// (source-level reverse guard), (c) each declared mode is really accepted.
|
|
549
|
+
// Helpers are inline — each bridge test file stays standalone (mirror byte-equality).
|
|
550
|
+
|
|
551
|
+
const MANIFEST = JSON.parse(readFileSync(join(HERE, '..', 'capability.json'), 'utf8'));
|
|
552
|
+
const REVIEW_CONTRACT = MANIFEST.roles.review.contract;
|
|
553
|
+
const norm = (s) => s.replace(/\s+/g, ' ').trim();
|
|
554
|
+
const setEq = (got, want, msg) => assert.deepEqual([...got].sort(), [...want].sort(), msg);
|
|
555
|
+
|
|
556
|
+
// Run `--help`/-h with PATH stripped of codex/agy/git, from a non-git cwd with no
|
|
557
|
+
// AGENTS.md — proving the short-circuit fires BEFORE every preflight.
|
|
558
|
+
const runHelp = (arg) => {
|
|
559
|
+
const root = mkdtempSync(join(tmpdir(), 'codex-review-help-'));
|
|
560
|
+
const nongit = join(root, 'nongit');
|
|
561
|
+
mkdirSync(nongit, { recursive: true });
|
|
562
|
+
const path = makePathWithout(root, ['codex', 'agy', 'git']);
|
|
563
|
+
const r = spawnSync('bash', [WRAPPER, arg], {
|
|
564
|
+
cwd: nongit, encoding: 'utf8', timeout: 15000, env: { HOME: root, PATH: path },
|
|
565
|
+
});
|
|
566
|
+
rmSync(root, { recursive: true, force: true });
|
|
567
|
+
return r;
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
// The lines of a labelled --help section (header line → the next blank line).
|
|
571
|
+
const helpSection = (text, header) => {
|
|
572
|
+
const lines = text.split('\n');
|
|
573
|
+
const i = lines.findIndex((l) => l.trim() === header);
|
|
574
|
+
assert.notEqual(i, -1, `--help must carry a "${header}" section`);
|
|
575
|
+
const out = [];
|
|
576
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
577
|
+
if (lines[j].trim() === '') break;
|
|
578
|
+
out.push(lines[j].trim());
|
|
579
|
+
}
|
|
580
|
+
return out;
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
// Source-level parser-arm extractor — the reverse drift guard. Scans ONLY `case`
|
|
584
|
+
// statements whose SUBJECT is a CLI-argument variable (allowlisted), skipping
|
|
585
|
+
// heredoc bodies (a heredoc may carry non-CLI `case` arms — e.g. codex-exec's
|
|
586
|
+
// git-shim). Returns Map(subject → [raw arm label, …]) in source order.
|
|
587
|
+
const ARG_SUBJECTS = new Set(['"$mode"', '"${1:-}"', '"$1"', '"$_arg"']);
|
|
588
|
+
const extractArgCaseArms = (source) => {
|
|
589
|
+
const arms = new Map();
|
|
590
|
+
const stack = [];
|
|
591
|
+
let heredoc = null;
|
|
592
|
+
for (const raw of source.split('\n')) {
|
|
593
|
+
if (heredoc) {
|
|
594
|
+
if (raw.trim() === heredoc) heredoc = null;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (raw.trimStart().startsWith('#')) continue; // a comment line may carry a stray ')'
|
|
598
|
+
const hd = raw.match(/<<-?\s*['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?/);
|
|
599
|
+
if (hd) { heredoc = hd[1]; continue; }
|
|
600
|
+
const cs = raw.match(/^\s*case\s+(\S+)\s+in\b/);
|
|
601
|
+
if (cs) { stack.push(cs[1]); continue; }
|
|
602
|
+
if (/^\s*esac\b/.test(raw)) { stack.pop(); continue; }
|
|
603
|
+
if (stack.length && ARG_SUBJECTS.has(stack[stack.length - 1])) {
|
|
604
|
+
const arm = raw.match(/^\s*([^)(\s][^)(]*)\)/);
|
|
605
|
+
if (arm) {
|
|
606
|
+
const subject = stack[stack.length - 1];
|
|
607
|
+
if (!arms.has(subject)) arms.set(subject, []);
|
|
608
|
+
arms.get(subject).push(arm[1].trim());
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return arms;
|
|
613
|
+
};
|
|
614
|
+
const splitArms = (labels) => (labels ?? []).flatMap((l) => l.split('|'));
|
|
615
|
+
|
|
616
|
+
describe('codex-review.sh — --help contract (manifest-pinned)', () => {
|
|
617
|
+
it('--help and -h exit 0 pre-preflight (no codex, no git, no AGENTS.md)', () => {
|
|
618
|
+
for (const arg of ['--help', '-h']) {
|
|
619
|
+
const r = runHelp(arg);
|
|
620
|
+
assert.equal(r.status, 0, `${arg}: ${r.stderr}`);
|
|
621
|
+
assert.match(r.stdout, /Usage:/, `${arg} prints the contract to stdout`);
|
|
622
|
+
assert.equal(r.stderr, '', `${arg} prints nothing to stderr`);
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it('Usage set-EQUALS the manifest invocation descriptors (both directions)', () => {
|
|
627
|
+
const help = runHelp('--help').stdout;
|
|
628
|
+
const got = helpSection(help, 'Usage:').filter((l) => l.startsWith('codex-review')).map(norm);
|
|
629
|
+
assert.ok(REVIEW_CONTRACT.invocations.length > 0, 'manifest invocations must be non-empty');
|
|
630
|
+
setEq(got, REVIEW_CONTRACT.invocations.map(norm), 'help Usage ⟷ manifest invocations');
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('Grounding renders the manifest grounding note verbatim', () => {
|
|
634
|
+
const help = runHelp('--help').stdout;
|
|
635
|
+
assert.equal(norm(helpSection(help, 'Grounding:').join(' ')), norm(REVIEW_CONTRACT.grounding));
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it('Round-2 / resume set-EQUALS the manifest continue descriptors (empty — one-shot)', () => {
|
|
639
|
+
const help = runHelp('--help').stdout;
|
|
640
|
+
const got = helpSection(help, 'Round-2 / resume:').filter((l) => l.startsWith('codex-review')).map(norm);
|
|
641
|
+
setEq(got, (REVIEW_CONTRACT.continue ?? []).map(norm), 'help continue ⟷ manifest continue');
|
|
642
|
+
assert.deepEqual(REVIEW_CONTRACT.continue, [], 'codex-review is one-shot — no continue descriptor');
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
describe('codex-review.sh — source-level reverse guard (parser arms ⟷ manifest)', () => {
|
|
647
|
+
const arms = extractArgCaseArms(readFileSync(WRAPPER, 'utf8'));
|
|
648
|
+
|
|
649
|
+
it('the real mode arms equal the manifest modes (adding a mode without the manifest fails here)', () => {
|
|
650
|
+
const modes = splitArms(arms.get('"$mode"')).filter((a) => a !== '*');
|
|
651
|
+
assert.ok(MANIFEST.roles.review.modes.length > 0, 'manifest modes must be non-empty');
|
|
652
|
+
setEq(new Set(modes), MANIFEST.roles.review.modes, 'parser mode arms ⟷ manifest modes');
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it('the first-arg entrypoints are exactly --help/-h (no undeclared resume/flag entrypoint)', () => {
|
|
656
|
+
setEq(new Set(splitArms(arms.get('"${1:-}"'))), ['--help', '-h']);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it('every manifest mode is really accepted (forward guard)', () => {
|
|
660
|
+
const drive = { plan: ['plan', 'plan.md'], code: ['code'] };
|
|
661
|
+
for (const mode of MANIFEST.roles.review.modes) {
|
|
662
|
+
assert.ok(drive[mode], `no test drive for manifest mode "${mode}" — add one`);
|
|
663
|
+
const sb = makeSandbox();
|
|
664
|
+
const r = run(sb, { args: drive[mode] });
|
|
665
|
+
rmSync(sb.root, { recursive: true, force: true });
|
|
666
|
+
assert.equal(r.status, 0, `mode ${mode}: ${r.stderr}`);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
});
|
|
@@ -3,11 +3,44 @@
|
|
|
3
3
|
"schema": 1,
|
|
4
4
|
"name": "codex-cli-bridge",
|
|
5
5
|
"kind": "execution-backend",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.1.0",
|
|
7
7
|
"provides": ["execute", "review"],
|
|
8
8
|
"roles": {
|
|
9
|
-
"execute": {
|
|
10
|
-
|
|
9
|
+
"execute": {
|
|
10
|
+
"cmd": "codex-exec",
|
|
11
|
+
"source": "bin/codex-exec.sh",
|
|
12
|
+
"output": "diff",
|
|
13
|
+
"contract": {
|
|
14
|
+
"invocations": [
|
|
15
|
+
"codex-exec <plan-file|->",
|
|
16
|
+
"codex-exec <plan-file|-> -- <extra codex flags...>"
|
|
17
|
+
],
|
|
18
|
+
"grounding": "automatic — the root AGENTS.md (Hard Constraints) is auto-merged into codex's context and the wrapper prepends the orchestrator execution contract; no grounding flags",
|
|
19
|
+
"continue": [
|
|
20
|
+
"codex-exec --resume-last <plan-file|->",
|
|
21
|
+
"codex-exec --resume <session-id> <plan-file|->"
|
|
22
|
+
],
|
|
23
|
+
"passthrough": {
|
|
24
|
+
"policy": "guarded",
|
|
25
|
+
"blocked": ["-c*", "--config*", "-s*", "--sandbox*", "--dangerously-bypass-approvals-and-sandbox", "--dangerously-bypass-hook-trust", "--full-auto", "--oss", "--local-provider*", "-p*", "--profile*", "-m*", "--model*", "-o*", "--output-last-message*", "--json*", "--color*", "--output-schema*", "--ephemeral*"],
|
|
26
|
+
"probeRelaxed": ["--add-dir*", "-C*", "--cd*", "--skip-git-repo-check", "--ignore-rules", "--enable*", "--disable*"]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"review": {
|
|
31
|
+
"cmd": "codex-review",
|
|
32
|
+
"source": "bin/codex-review.sh",
|
|
33
|
+
"modes": ["plan", "code"],
|
|
34
|
+
"output": "advisory",
|
|
35
|
+
"contract": {
|
|
36
|
+
"invocations": [
|
|
37
|
+
"codex-review plan <plan-file>",
|
|
38
|
+
"codex-review code [extra focus...]"
|
|
39
|
+
],
|
|
40
|
+
"grounding": "automatic — the wrapper precomputes the full working-tree change set (repo map, status, diffs, untracked contents) and codex auto-merges the root AGENTS.md; no grounding flags",
|
|
41
|
+
"continue": []
|
|
42
|
+
}
|
|
43
|
+
}
|
|
11
44
|
},
|
|
12
45
|
"detect": {
|
|
13
46
|
"installed": {
|
|
@@ -50,7 +50,7 @@ object (raw-text fallback on failure).
|
|
|
50
50
|
```bash
|
|
51
51
|
codex-exec docs/plans/<slug>.md # drive a plan file
|
|
52
52
|
echo "apply review fix: tighten the guard in X, keep tests green" | codex-exec -
|
|
53
|
-
codex-exec <file|-> -- <
|
|
53
|
+
codex-exec <file|-> -- <extra codex flags> # GUARDED passthrough AFTER `--` (policy/model/capture flags rejected; some relaxed only under CODEX_PROBE=1)
|
|
54
54
|
|
|
55
55
|
codex-exec --resume-last docs/plans/<slug>.md # continue the last session, no re-send
|
|
56
56
|
echo "now do step 2" | codex-exec --resume <id> -
|
package/capability.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.26.0",
|
|
4
4
|
"description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
package/tools/commands.mjs
CHANGED
|
@@ -113,7 +113,7 @@ const CATALOG = [
|
|
|
113
113
|
invocation: invocationOf('procedures'),
|
|
114
114
|
group: 'Orchestrate',
|
|
115
115
|
kind: READ_ONLY,
|
|
116
|
-
oneLine: 'Show a named activity’s steps
|
|
116
|
+
oneLine: 'Show a named activity’s steps, the recipe per stage, and each dispatched backend’s exact driving contract (invocation + grounding + round-2 delta).',
|
|
117
117
|
},
|
|
118
118
|
{
|
|
119
119
|
key: 'set-recipe',
|
|
@@ -60,6 +60,36 @@ const RAW_BACKENDS = [
|
|
|
60
60
|
name: 'codex-cli-bridge',
|
|
61
61
|
installed: { env: 'CODEX_CLI_BRIDGE_DIR', default: '~/.claude/skills/codex-cli-bridge', file: 'SKILL.md' },
|
|
62
62
|
roleCmds: { execute: 'codex-exec', review: 'codex-review' },
|
|
63
|
+
// The per-role DRIVING CONTRACT (exact invocation descriptors + grounding + round-2 continue),
|
|
64
|
+
// mirroring the bridge manifest roles[role].contract byte-for-byte — drift-guarded like roleCmds.
|
|
65
|
+
// Scope = dispatchable recipe roles ONLY (review, execute): the probe role is never dispatched by
|
|
66
|
+
// an activity slot, so it carries NO contract here (wrapperContractFor(_, 'probe') → null).
|
|
67
|
+
roleContracts: {
|
|
68
|
+
execute: {
|
|
69
|
+
invocations: [
|
|
70
|
+
'codex-exec <plan-file|->',
|
|
71
|
+
'codex-exec <plan-file|-> -- <extra codex flags...>',
|
|
72
|
+
],
|
|
73
|
+
grounding: "automatic — the root AGENTS.md (Hard Constraints) is auto-merged into codex's context and the wrapper prepends the orchestrator execution contract; no grounding flags",
|
|
74
|
+
continue: [
|
|
75
|
+
'codex-exec --resume-last <plan-file|->',
|
|
76
|
+
'codex-exec --resume <session-id> <plan-file|->',
|
|
77
|
+
],
|
|
78
|
+
passthrough: {
|
|
79
|
+
policy: 'guarded',
|
|
80
|
+
blocked: ['-c*', '--config*', '-s*', '--sandbox*', '--dangerously-bypass-approvals-and-sandbox', '--dangerously-bypass-hook-trust', '--full-auto', '--oss', '--local-provider*', '-p*', '--profile*', '-m*', '--model*', '-o*', '--output-last-message*', '--json*', '--color*', '--output-schema*', '--ephemeral*'],
|
|
81
|
+
probeRelaxed: ['--add-dir*', '-C*', '--cd*', '--skip-git-repo-check', '--ignore-rules', '--enable*', '--disable*'],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
review: {
|
|
85
|
+
invocations: [
|
|
86
|
+
'codex-review plan <plan-file>',
|
|
87
|
+
'codex-review code [extra focus...]',
|
|
88
|
+
],
|
|
89
|
+
grounding: 'automatic — the wrapper precomputes the full working-tree change set (repo map, status, diffs, untracked contents) and codex auto-merges the root AGENTS.md; no grounding flags',
|
|
90
|
+
continue: [],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
63
93
|
bin: 'codex',
|
|
64
94
|
credential: { env: 'CODEX_HOME', default: '~/.codex', file: 'auth.json' },
|
|
65
95
|
setupUrl: 'https://github.com/sabaiway/agent-workflow/blob/main/codex-cli-bridge/setup/README.md',
|
|
@@ -72,6 +102,26 @@ const RAW_BACKENDS = [
|
|
|
72
102
|
name: 'antigravity-cli-bridge',
|
|
73
103
|
installed: { env: 'ANTIGRAVITY_CLI_BRIDGE_DIR', default: '~/.claude/skills/antigravity-cli-bridge', file: 'SKILL.md' },
|
|
74
104
|
roleCmds: { review: 'agy-review', probe: 'agy-run' },
|
|
105
|
+
// Mirror of the manifest roles.review.contract (see the codex entry note). probe: NO contract.
|
|
106
|
+
roleContracts: {
|
|
107
|
+
review: {
|
|
108
|
+
invocations: [
|
|
109
|
+
'agy-review code [--facts @f] [--decided @f] [--focus "…"] [extra focus…]',
|
|
110
|
+
'agy-review plan <plan-file> [--facts @f] [--decided @f] [--focus "…"]',
|
|
111
|
+
'agy-review diff <diff-file> [--facts @f] [--decided @f] [--focus "…"]',
|
|
112
|
+
],
|
|
113
|
+
grounding: 'grounded review — agy reads NOTHING by default, an ungrounded review GUESSES: --facts @f = the verified facts to review AGAINST; --decided @f = decisions already made, do NOT re-raise (anti-circling)',
|
|
114
|
+
flags: [
|
|
115
|
+
'--facts @f — verified facts the review runs AGAINST (omit ⇒ loud ungrounded-review warning)',
|
|
116
|
+
'--decided @f — already-decided / already-addressed list; do NOT re-raise (anti-circling; the round-2 payload)',
|
|
117
|
+
'--focus "…" — extra focus (repeatable; code mode also takes trailing focus words)',
|
|
118
|
+
],
|
|
119
|
+
continue: [
|
|
120
|
+
'agy-review --continue [--decided @f] [--focus "…"]',
|
|
121
|
+
'agy-review --conversation <id> [--decided @f] [--focus "…"]',
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
75
125
|
bin: 'agy',
|
|
76
126
|
credential: { env: null, default: '~/.gemini/antigravity-cli', file: 'antigravity-oauth-token' },
|
|
77
127
|
setupUrl: 'https://github.com/sabaiway/agent-workflow/blob/main/antigravity-cli-bridge/setup/README.md',
|
|
@@ -91,6 +141,15 @@ export const KNOWN_BACKENDS = RAW_BACKENDS.map((entry) => ({ ...entry, wrapperCm
|
|
|
91
141
|
export const wrapperCmdFor = (backendName, role) =>
|
|
92
142
|
KNOWN_BACKENDS.find((b) => b.name === backendName)?.roleCmds?.[role] ?? null;
|
|
93
143
|
|
|
144
|
+
// Resolve a dispatched (backend, role) to its structured DRIVING CONTRACT — the registry mirror of
|
|
145
|
+
// the bridge manifest roles[role].contract: exact invocation descriptor(s), the grounding note, the
|
|
146
|
+
// closed flag set (when the wrapper's grammar is closed), the round-2/continue descriptor(s), and
|
|
147
|
+
// the guarded passthrough tiers (codex-exec). The point-of-use advisor (procedures.mjs) renders this
|
|
148
|
+
// VERBATIM so a driving agent never re-derives the contract from wrapper source. `null` when the
|
|
149
|
+
// pair is unknown or the role carries no contract (probe — never dispatched by an activity slot).
|
|
150
|
+
export const wrapperContractFor = (backendName, role) =>
|
|
151
|
+
KNOWN_BACKENDS.find((b) => b.name === backendName)?.roleContracts?.[role] ?? null;
|
|
152
|
+
|
|
94
153
|
// ── pure helpers ─────────────────────────────────────────────────────────────
|
|
95
154
|
|
|
96
155
|
// Expand a leading "~" / "~/x" against home; absolute and relative paths pass through untouched.
|
package/tools/family-members.mjs
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
// ── the unified registry ───────────────────────────────────────────────────────
|
|
14
14
|
// One entry per family member. `installed` is the detect.installed spec (env + home-relative default
|
|
15
15
|
// + marker file); `deployed` is the project-relative stamp a deploy writes (kit + memory only);
|
|
16
|
-
// `npm` is the install package (null for the bridges
|
|
16
|
+
// `npm` is the install package (null for the bridges — placed by `setup`, and once placed refreshed
|
|
17
|
+
// by `init`/`upgrade` from the kit's bundled copies, never npm);
|
|
17
18
|
// `wrapperCmds` is the deduped roles[].cmd set the `setup` linker creates on PATH (bridges only).
|
|
18
19
|
// Kept in lockstep with the 5 in-repo capability.json by the drift-guard test. The two release skills
|
|
19
20
|
// (release-engineering / release-marketing) are deliberately NOT here — they are not family members
|
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
// side effects on import (the isDirectRun idiom) — tests import the helpers with nothing run.
|
|
18
18
|
|
|
19
19
|
import { existsSync, statSync, readFileSync, lstatSync } from 'node:fs';
|
|
20
|
-
import { join, resolve } from 'node:path';
|
|
21
|
-
import { pathToFileURL } from 'node:url';
|
|
20
|
+
import { join, resolve, dirname } from 'node:path';
|
|
21
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
22
22
|
import os from 'node:os';
|
|
23
23
|
import { resolveDir, detectBackends, findOnPath } from './detect-backends.mjs';
|
|
24
|
+
// The ONE dependency-free semver (shared with bin/install.mjs) — the bridge freshness probe compares
|
|
25
|
+
// the placed version against the kit-bundled mirror; null-on-unparseable maps to 'unknown' (INV-B).
|
|
26
|
+
import { parseSemver, compareSemver } from './semver-lite.mjs';
|
|
24
27
|
import { validateManifest, readAuthoritativeVersion, UNSUPPORTED, INVALID } from './manifest/validate.mjs';
|
|
25
28
|
import { START_MARKER, excludePath, inferVisibility } from './hide-footprint.mjs';
|
|
26
29
|
import { readEngineFragment, ORCHESTRATION_FRAGMENT_REL, PROCEDURES_FRAGMENT_REL } from './engine-source.mjs';
|
|
@@ -57,6 +60,10 @@ import {
|
|
|
57
60
|
VISIBILITY_PUBLIC,
|
|
58
61
|
DISPLAY_NAMES,
|
|
59
62
|
displayOf,
|
|
63
|
+
FRESH_CURRENT,
|
|
64
|
+
FRESH_BEHIND,
|
|
65
|
+
FRESH_UNKNOWN,
|
|
66
|
+
FRESH_NOT_CHECKED,
|
|
60
67
|
} from './labels.mjs';
|
|
61
68
|
// The capability-adaptive direct-CLI presenter (Plan §4.2/§4.5): the surface detector, the
|
|
62
69
|
// envelope→ViewModel transform, and the plain/ansi renderers. main() composes them; the agent-mediated
|
|
@@ -135,7 +142,18 @@ export const classifyMember = (member, deps = {}) => {
|
|
|
135
142
|
const report = markerPresent ? validate(skillDir) : { result: NOT_INSTALLED };
|
|
136
143
|
const manifestState = classifyState(markerPresent, report, member);
|
|
137
144
|
const installed = manifestState !== NOT_INSTALLED;
|
|
138
|
-
|
|
145
|
+
// Crash-safe version read: readAuthoritativeVersion THROWS on a present-but-unreadable SKILL.md
|
|
146
|
+
// (stat needs no read permission, readFileSync does — a TOCTOU/EACCES window after validate).
|
|
147
|
+
// The read-only survey must never crash on it: degrade to null, which the bridge freshness probe
|
|
148
|
+
// then reports as 'unknown' (INV-B), never as a throw and never as a silent claim.
|
|
149
|
+
const version = (() => {
|
|
150
|
+
if (manifestState !== OK) return null;
|
|
151
|
+
try {
|
|
152
|
+
return readVersion(skillDir).version ?? null;
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
})();
|
|
139
157
|
|
|
140
158
|
return { name: member.name, kind: member.kind, installed, skillDir: installed ? skillDir : null, manifestState, version };
|
|
141
159
|
};
|
|
@@ -165,6 +183,51 @@ export const MEMORY_ORCH_TEMPLATE_REL = 'references/templates/orchestration.json
|
|
|
165
183
|
const MEMORY_BEHIND_NOTE =
|
|
166
184
|
"the memory installed here doesn't include the current orchestration template — refresh it with `npx @sabaiway/agent-workflow-memory@latest init`, then restart the session.";
|
|
167
185
|
|
|
186
|
+
// ── the bridge freshness probe (deterministic-first — INV-A / INV-B) ─────────────
|
|
187
|
+
// The bridges are not npm packages: their ONLY delivery channel is the copy bundled inside this kit
|
|
188
|
+
// (`bridges/<name>/`, placed by `/agent-workflow-kit setup`). A placed bridge therefore has exactly
|
|
189
|
+
// one authoritative freshness comparison — its installed version (readAuthoritativeVersion, already
|
|
190
|
+
// on the row) vs the kit-bundled mirror's capability.json. BOTH are local files, so the "never
|
|
191
|
+
// checks npm" invariant holds. The probe sets the internal row field `freshness`
|
|
192
|
+
// ('behind' | 'current' | 'unknown'); refreshOf derives the public refresh block STRUCTURALLY from
|
|
193
|
+
// that field (INV-2 — never parsed from caveat text). INV-B: an unreadable/unparseable version on
|
|
194
|
+
// EITHER side degrades to 'unknown' + a plain-language note — never a false claim in either direction.
|
|
195
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
196
|
+
// bridges/ ships beside tools/ in both the repo and the installed kit (the setup-backends.mjs
|
|
197
|
+
// resolution), so this resolves in both. Injectable via deps.bundleRoot for tests.
|
|
198
|
+
const BUNDLE_ROOT = resolve(__dirname, '..', 'bridges');
|
|
199
|
+
|
|
200
|
+
// Crash-safe on the bundled side: an absent / unreadable / malformed bundle manifest → null (INV-B
|
|
201
|
+
// 'unknown'), never a throw — the read-only survey must survive a broken kit install.
|
|
202
|
+
const readBundledBridgeVersion = (name, deps = {}) => {
|
|
203
|
+
const read = deps.readFile ?? readFileSync;
|
|
204
|
+
const root = deps.bundleRoot ?? BUNDLE_ROOT;
|
|
205
|
+
try {
|
|
206
|
+
const manifest = JSON.parse(String(read(join(root, name, 'capability.json'), 'utf8')));
|
|
207
|
+
return typeof manifest.version === 'string' ? manifest.version : null;
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const bridgeBehindNote = (display, placed, bundled) =>
|
|
214
|
+
`the ${display} installed here (v${placed}) is older than the copy bundled with this kit (v${bundled}) — refresh it with \`/agent-workflow-kit setup\`.`;
|
|
215
|
+
// The recovery differs per failing SIDE: `setup` re-places FROM the bundle, so it can repair an
|
|
216
|
+
// unreadable INSTALLED copy but can never repair an unreadable BUNDLED copy — that needs a kit
|
|
217
|
+
// refresh first (the npx installer), then `setup`.
|
|
218
|
+
const bridgeUnknownNote = (display, side, recovery) =>
|
|
219
|
+
`couldn't compare the ${display} installed here with the copy bundled with this kit (${side}), so its freshness is unknown — ${recovery}.`;
|
|
220
|
+
const UNKNOWN_SIDES = Object.freeze({
|
|
221
|
+
placed: {
|
|
222
|
+
side: 'the installed copy has no readable version',
|
|
223
|
+
recovery: '`/agent-workflow-kit setup` re-places the bundled copy',
|
|
224
|
+
},
|
|
225
|
+
bundled: {
|
|
226
|
+
side: "the kit's bundled copy has no readable version",
|
|
227
|
+
recovery: 'refresh the kit first (`npx @sabaiway/agent-workflow-kit@latest init`), then `/agent-workflow-kit setup`',
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
168
231
|
export const surveyFamily = (deps = {}) =>
|
|
169
232
|
FAMILY_MEMBERS.map((member) => {
|
|
170
233
|
const row = classifyMember(member, deps);
|
|
@@ -184,8 +247,30 @@ export const surveyFamily = (deps = {}) =>
|
|
|
184
247
|
// Only attach when it is provably ABSENT (a non-ENOENT probe error → 'unknown' → skip, never a
|
|
185
248
|
// false "missing" claim). Mirrors the engine-caveat SHAPE; keyed on the Step-2.4 required asset.
|
|
186
249
|
if (row.kind === 'memory-substrate' && row.manifestState === OK && row.skillDir) {
|
|
187
|
-
|
|
250
|
+
const templateProbe = probeMarker(join(row.skillDir, MEMORY_ORCH_TEMPLATE_REL), deps);
|
|
251
|
+
if (templateProbe === 'absent') {
|
|
188
252
|
row.caveats = [...(row.caveats ?? []), MEMORY_BEHIND_NOTE];
|
|
253
|
+
} else if (templateProbe === 'unknown') {
|
|
254
|
+
// Could not verify → this row must not be counted "checked, current" by the verdict (INV-B).
|
|
255
|
+
row.freshness = FRESH_UNKNOWN;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Bridge freshness probe (INV-A / INV-B): only for a provably-OURS placed bridge (manifestState
|
|
259
|
+
// OK). Compares two LOCAL files; a non-OK / absent bridge is out of scope ('not-checked').
|
|
260
|
+
if (row.kind === 'execution-backend' && row.manifestState === OK && row.skillDir) {
|
|
261
|
+
const bundled = readBundledBridgeVersion(row.name, deps);
|
|
262
|
+
const cmp = compareSemver(row.version, bundled);
|
|
263
|
+
if (cmp === null) {
|
|
264
|
+
const { side, recovery } = parseSemver(row.version) === null ? UNKNOWN_SIDES.placed : UNKNOWN_SIDES.bundled;
|
|
265
|
+
row.freshness = FRESH_UNKNOWN;
|
|
266
|
+
row.caveats = [...(row.caveats ?? []), bridgeUnknownNote(displayOf(row.name), side, recovery)];
|
|
267
|
+
} else if (cmp < 0) {
|
|
268
|
+
row.freshness = FRESH_BEHIND;
|
|
269
|
+
row.caveats = [...(row.caveats ?? []), bridgeBehindNote(displayOf(row.name), row.version, bundled)];
|
|
270
|
+
} else {
|
|
271
|
+
// Equal OR newer-than-bundled → not behind. A newer placed bridge is a Phase-2 concern for
|
|
272
|
+
// the refresh DRIVER (INV-D never-downgrade skip); the read-only status axis never flags it.
|
|
273
|
+
row.freshness = FRESH_CURRENT;
|
|
189
274
|
}
|
|
190
275
|
}
|
|
191
276
|
return row;
|
|
@@ -376,18 +461,31 @@ export const surveyBridges = (deps = {}) => {
|
|
|
376
461
|
});
|
|
377
462
|
};
|
|
378
463
|
|
|
379
|
-
// INV-2 (structural refresh — additive, derived, never parsed from a caveat string)
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
464
|
+
// INV-2 (structural refresh — additive, derived, never parsed from a caveat string), per KIND:
|
|
465
|
+
// • memory / engine (REFRESHABLE_KINDS): `behind` ⟷ an OK row carrying a refresh-recommending
|
|
466
|
+
// caveat (surveyFamily attaches caveats to those kinds only then, and every such caveat is
|
|
467
|
+
// refresh-recommending); `recommend` is composed from FAMILY_MEMBERS[].npm — never the caveat text.
|
|
468
|
+
// • execution-backend: `behind` ⟷ the bridge freshness probe's row field (freshness === 'behind');
|
|
469
|
+
// `recommend` is composed PER-KIND — bridges have npm:null (placed by `setup`, never npx), so the
|
|
470
|
+
// runnable recovery is `/agent-workflow-kit setup`, never an npx composition.
|
|
471
|
+
// • composition-root (the kit): no freshness probe exists on this surface (the two-axes doctrine —
|
|
472
|
+
// kit freshness is the npx installer's axis) → never behind, 'not-checked'.
|
|
473
|
+
// `freshness` is the checked-vs-unknown signal the zero-behind verdict scopes itself with (INV-C):
|
|
474
|
+
// behind:false alone cannot distinguish "checked, current" from "could not be checked".
|
|
386
475
|
const REFRESHABLE_KINDS = new Set(['memory-substrate', 'methodology-engine']);
|
|
387
476
|
const npmOf = (name) => FAMILY_MEMBERS.find((m) => m.name === name)?.npm ?? null;
|
|
477
|
+
export const BRIDGE_REFRESH_RECOMMEND = '/agent-workflow-kit setup';
|
|
388
478
|
const refreshOf = (m) => {
|
|
479
|
+
if (m.kind === 'execution-backend') {
|
|
480
|
+
const freshness = m.freshness ?? FRESH_NOT_CHECKED;
|
|
481
|
+
const behind = freshness === FRESH_BEHIND;
|
|
482
|
+
return { behind, recommend: behind ? BRIDGE_REFRESH_RECOMMEND : null, freshness };
|
|
483
|
+
}
|
|
389
484
|
const behind = REFRESHABLE_KINDS.has(m.kind) && Boolean(m.caveats?.length);
|
|
390
|
-
|
|
485
|
+
const freshness = REFRESHABLE_KINDS.has(m.kind) && m.manifestState === OK
|
|
486
|
+
? m.freshness ?? (behind ? FRESH_BEHIND : FRESH_CURRENT)
|
|
487
|
+
: FRESH_NOT_CHECKED;
|
|
488
|
+
return { behind, recommend: behind ? `npx ${npmOf(m.name)}@latest init` : null, freshness };
|
|
391
489
|
};
|
|
392
490
|
|
|
393
491
|
export const buildEnvelope = (family, project = null, extras = {}) => {
|
|
@@ -399,7 +497,7 @@ export const buildEnvelope = (family, project = null, extras = {}) => {
|
|
|
399
497
|
state: STATE_PUBLIC[m.manifestState] ?? STATE_PUBLIC[UNKNOWN],
|
|
400
498
|
};
|
|
401
499
|
if (m.caveats?.length) entry.notes = m.caveats; // plain-language observations (Steps 2.2 / engine)
|
|
402
|
-
entry.refresh = refreshOf(m); // additive (INV-1): always present; { behind, recommend } (INV-2)
|
|
500
|
+
entry.refresh = refreshOf(m); // additive (INV-1): always present; { behind, recommend, freshness } (INV-2)
|
|
403
501
|
return entry;
|
|
404
502
|
});
|
|
405
503
|
const envelope = { deploymentHead: EXPECTED_WORKFLOW_VERSION, installed };
|
package/tools/labels.mjs
CHANGED
|
@@ -41,6 +41,19 @@ export const STATE_PUBLIC = Object.freeze({
|
|
|
41
41
|
// "hidden fence" / marker terms. ('hidden' here is the PUBLIC visibility word, not the internal fence.)
|
|
42
42
|
export const VISIBILITY_PUBLIC = Object.freeze({ visible: 'visible', hidden: 'hidden', ambiguous: 'unclear' });
|
|
43
43
|
|
|
44
|
+
// ── refresh freshness tokens (PUBLIC — the envelope's installed[].refresh.freshness) ────────────────
|
|
45
|
+
// The checked-vs-unknown signal the zero-behind verdict scopes itself with (INV-C): `behind:false`
|
|
46
|
+
// alone cannot distinguish "checked, current" from "could not be checked".
|
|
47
|
+
// 'current' / 'behind' — a freshness probe RAN and concluded (these two are the "checked" scope);
|
|
48
|
+
// 'unknown' — a probe ran but could not conclude (INV-B: never collapsed to a claim
|
|
49
|
+
// in either direction);
|
|
50
|
+
// 'not-checked' — no freshness probe exists for this member/state (e.g. the kit itself:
|
|
51
|
+
// its freshness is the npx installer's axis, never checked here).
|
|
52
|
+
export const FRESH_CURRENT = 'current';
|
|
53
|
+
export const FRESH_BEHIND = 'behind';
|
|
54
|
+
export const FRESH_UNKNOWN = 'unknown';
|
|
55
|
+
export const FRESH_NOT_CHECKED = 'not-checked';
|
|
56
|
+
|
|
44
57
|
// Short, user-facing labels for the version block (kit · memory · engine · codex-bridge · …), so the
|
|
45
58
|
// render is deterministic and the agent never invents a label.
|
|
46
59
|
export const DISPLAY_NAMES = Object.freeze({
|
package/tools/manifest/schema.md
CHANGED
|
@@ -38,6 +38,21 @@ Each `roles.<role>` is an object:
|
|
|
38
38
|
- `template` (string, optional) — an in-skill prompt/template path (e.g.
|
|
39
39
|
`references/review-prompt.md`); repo-relative, **must exist**.
|
|
40
40
|
- `modes`, `output` (optional) — e.g. `["plan","code"]`, `"advisory"`.
|
|
41
|
+
- `contract` (object, optional; execution-backend bridges) — the machine-readable **driving
|
|
42
|
+
contract** for a dispatchable recipe role (`review` / `execute`; the `probe` role carries none).
|
|
43
|
+
Rides as a validator-tolerated extra field (format-checked by the bridge/kit drift-guard tests,
|
|
44
|
+
not by `validate.mjs`), and is the single source the point-of-use advisor
|
|
45
|
+
(`/agent-workflow-kit procedures`) and each wrapper's `--help` render verbatim:
|
|
46
|
+
- `invocations` (string[], non-empty) — exact copy-pasteable invocation descriptors, one per
|
|
47
|
+
mode/variant, incl. operand placeholders (`<plan-file>`, `[extra focus...]`).
|
|
48
|
+
- `grounding` (string) — the grounding note (e.g. agy's `--facts @f` / `--decided @f` levers, or
|
|
49
|
+
"automatic — …" when the wrapper grounds itself).
|
|
50
|
+
- `flags` (string[], optional) — the closed per-mode flag descriptor set (closed-grammar
|
|
51
|
+
wrappers only).
|
|
52
|
+
- `continue` (string[]) — round-2 / resume invocation descriptors; `[]` when one-shot.
|
|
53
|
+
- `passthrough` (object, optional) — the guarded `--` passthrough tiers:
|
|
54
|
+
`{ policy: "guarded", blocked: string[], probeRelaxed: string[] }`, matching the wrapper's
|
|
55
|
+
real case-arm patterns (pinned by the source-level reverse-guard test).
|
|
41
56
|
|
|
42
57
|
## Path-field rules (Windows-safe, traversal-safe)
|
|
43
58
|
|
package/tools/procedures.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { readFileSync, lstatSync } from 'node:fs';
|
|
20
20
|
import { homedir } from 'node:os';
|
|
21
21
|
import { pathToFileURL } from 'node:url';
|
|
22
|
-
import { detectBackends, wrapperCmdFor } from './detect-backends.mjs';
|
|
22
|
+
import { detectBackends, wrapperCmdFor, wrapperContractFor } from './detect-backends.mjs';
|
|
23
23
|
import { ACTIVITIES, resolveActivityRecipe, planRecipe } from './recipes.mjs';
|
|
24
24
|
import { resolveEngineDir, readEngineFragment, PROCEDURES_FRAGMENT_REL } from './engine-source.mjs';
|
|
25
25
|
// The config schema/read core lives in orchestration-config.mjs (the single config contract). procedures
|
|
@@ -131,10 +131,16 @@ const resolveAllSlots = ({ activity, config, detection, overrides }) =>
|
|
|
131
131
|
// The concrete wrapper set this slot's EFFECTIVE recipe dispatches (empty for solo). Reuse
|
|
132
132
|
// planRecipe's drift-guarded dispatch for WHICH backends, then resolve each (backend, role) to its
|
|
133
133
|
// manifest wrapper cmd via the bridge registry — no wrapper name is hand-composed here.
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
const { dispatch } = planRecipe(resolved.recipe, detection);
|
|
135
|
+
const backends = dispatch.map((d) => wrapperCmdFor(d.backend, d.role)).filter(Boolean);
|
|
136
|
+
// The full DRIVING CONTRACT per dispatched (backend, role) — resolved HERE, on the raw dispatch
|
|
137
|
+
// pairs, BEFORE they are flattened to wrapper names (the name array cannot reconstruct the role).
|
|
138
|
+
// Every slot with a non-empty dispatch gets contracts — including execute=delegated; the contract
|
|
139
|
+
// is NEVER gated by REVIEW_RECIPES (that set gates only the review-loop economics block).
|
|
140
|
+
const contracts = dispatch
|
|
141
|
+
.map((d) => ({ backend: d.backend, role: d.role, cmd: wrapperCmdFor(d.backend, d.role), contract: wrapperContractFor(d.backend, d.role) }))
|
|
142
|
+
.filter((c) => c.cmd && c.contract);
|
|
143
|
+
return { slot, ...resolved, backends, contracts };
|
|
138
144
|
});
|
|
139
145
|
|
|
140
146
|
// An unsatisfiable EXPLICIT override is the only "warning" (loud, flagged for the agent to relay). A
|
|
@@ -182,6 +188,28 @@ const reviewLoopAdvice = (slots) =>
|
|
|
182
188
|
]
|
|
183
189
|
: [];
|
|
184
190
|
|
|
191
|
+
// The verbatim per-backend DRIVING CONTRACT block (M-contract): the exact invocation descriptor(s),
|
|
192
|
+
// the closed flag set, the grounding note, the round-2/continue delta, and the guarded passthrough
|
|
193
|
+
// tiers — every descriptor printed VERBATIM from the registry mirror of the bridge manifest
|
|
194
|
+
// roles[role].contract (drift-guarded), never re-derived or re-worded here. Rendered for EVERY
|
|
195
|
+
// dispatched backend of EVERY slot (review AND execute=delegated); each wrapper's --help prints the
|
|
196
|
+
// same contract, so the agent never needs to open the wrapper source.
|
|
197
|
+
const contractLines = ({ cmd, contract }) => {
|
|
198
|
+
const lines = [` ${cmd} — driving contract (as bundled with this kit; copy-paste — \`${cmd} --help\` prints the same):`];
|
|
199
|
+
for (const inv of contract.invocations) lines.push(` ${inv}`);
|
|
200
|
+
for (const f of contract.flags ?? []) lines.push(` ${f}`);
|
|
201
|
+
if (contract.grounding) lines.push(` grounding: ${contract.grounding}`);
|
|
202
|
+
const cont = contract.continue ?? [];
|
|
203
|
+
if (cont.length) {
|
|
204
|
+
lines.push(' round-2 delta (resume — never re-send the reviewed artifact):');
|
|
205
|
+
for (const c of cont) lines.push(` ${c}`);
|
|
206
|
+
}
|
|
207
|
+
if (contract.passthrough) {
|
|
208
|
+
lines.push(` passthrough after '--' is ${contract.passthrough.policy}: blocked always: ${contract.passthrough.blocked.join(' ')}; relaxed only under CODEX_PROBE=1: ${contract.passthrough.probeRelaxed.join(' ')}`);
|
|
209
|
+
}
|
|
210
|
+
return lines;
|
|
211
|
+
};
|
|
212
|
+
|
|
185
213
|
const formatHuman = ({ activity, section, slots, warnings }) => {
|
|
186
214
|
const lines = [
|
|
187
215
|
section,
|
|
@@ -192,6 +220,7 @@ const formatHuman = ({ activity, section, slots, warnings }) => {
|
|
|
192
220
|
const arrow = s.degradedFrom ? ` (requested ${s.degradedFrom} → degraded)` : '';
|
|
193
221
|
lines.push(` ${s.slot}: ${s.recipe} — ${SOURCE_LABEL[s.source]}${arrow}${backendSetLabel(s.backends)}`);
|
|
194
222
|
if (s.reason) lines.push(` ↳ ${s.reason}`);
|
|
223
|
+
for (const c of s.contracts ?? []) lines.push(...contractLines(c));
|
|
195
224
|
}
|
|
196
225
|
const advice = reviewLoopAdvice(slots);
|
|
197
226
|
if (advice.length) lines.push('', ...advice);
|
|
@@ -206,7 +235,9 @@ const buildJson = ({ activity, section, slots, configSource, warnings }) => ({
|
|
|
206
235
|
activity,
|
|
207
236
|
section,
|
|
208
237
|
slots: Object.fromEntries(
|
|
209
|
-
|
|
238
|
+
// `backends: string[]` is the STABLE pre-existing shape (wrapper names) — never repurposed.
|
|
239
|
+
// `contracts` is the ADDITIVE per-dispatch driving-contract field (empty for solo).
|
|
240
|
+
slots.map((s) => [s.slot, { recipe: s.recipe, source: s.source, degradedFrom: s.degradedFrom, reason: s.reason, backends: s.backends, contracts: s.contracts }]),
|
|
210
241
|
),
|
|
211
242
|
reviewLoop: reviewLoopAdvice(slots),
|
|
212
243
|
configSource,
|