@sabaiway/agent-workflow-kit 10.3.0 → 10.5.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 +85 -0
- package/README.md +5 -5
- package/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +7 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +69 -17
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +73 -2
- package/bridges/antigravity-cli-bridge/capability.json +2 -2
- package/bridges/antigravity-cli-bridge/references/review-prompt.md +3 -0
- package/bridges/codex-cli-bridge/SKILL.md +8 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +1 -1
- package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +1 -1
- package/bridges/codex-cli-bridge/bin/codex-review.sh +89 -18
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +55 -2
- package/bridges/codex-cli-bridge/capability.json +2 -2
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/agents/executor.md +40 -0
- package/references/agents/review-lens.md +5 -3
- package/references/modes/agents.md +9 -4
- package/references/modes/procedures.md +21 -8
- package/references/modes/recipes.md +7 -4
- package/references/modes/recommendations.md +3 -1
- package/references/modes/set-recipe.md +23 -6
- package/references/modes/status.md +2 -2
- package/references/modes/upgrade.md +1 -1
- package/references/modes/velocity.md +1 -0
- package/references/shared/composition-handoff.md +1 -1
- package/references/shared/deploy-tail.md +1 -1
- package/references/templates/orchestration.json +1 -1
- package/tools/autonomy-config.mjs +1 -1
- package/tools/bridge-posture.mjs +48 -0
- package/tools/carriers.mjs +152 -0
- package/tools/cheap-agents-read.mjs +234 -0
- package/tools/cheap-agents.mjs +101 -109
- package/tools/commands.mjs +3 -3
- package/tools/detect-backends.mjs +2 -2
- package/tools/direct-run.mjs +9 -0
- package/tools/family-registry.mjs +38 -18
- package/tools/flow-check.mjs +2 -7
- package/tools/fold-scope.mjs +5 -60
- package/tools/grounding.mjs +2 -2
- package/tools/inject-methodology.mjs +4 -0
- package/tools/orchestration-config.mjs +23 -61
- package/tools/orchestration-readme.mjs +70 -0
- package/tools/plan-shape-cli.mjs +112 -0
- package/tools/plan-shape-facts.mjs +204 -0
- package/tools/plan-shape.mjs +348 -0
- package/tools/procedures.mjs +197 -83
- package/tools/recipes.mjs +183 -230
- package/tools/recommendations.mjs +77 -11
- package/tools/renderers.mjs +27 -7
- package/tools/repo-lex.mjs +40 -0
- package/tools/review-roster-resolve.mjs +104 -0
- package/tools/review-roster.mjs +128 -0
- package/tools/review-rounds-cli.mjs +92 -0
- package/tools/review-rounds.mjs +115 -0
- package/tools/review-state.mjs +10 -11
- package/tools/set-recipe-roster.mjs +167 -0
- package/tools/set-recipe.mjs +138 -42
- package/tools/velocity-profile.mjs +8 -22
- package/tools/view-model.mjs +17 -3
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { isRenderableLine } from './repo-lex.mjs';
|
|
2
|
+
import { isShipVerdict, isRecognizedVerdict } from './core-evidence.mjs';
|
|
3
|
+
|
|
4
|
+
export const SIGNALS = Object.freeze({
|
|
5
|
+
NO_RECEIPTS: 'no receipts for <path>',
|
|
6
|
+
INCOMPLETE: 'incomplete round — <backend> missing: dispatch it',
|
|
7
|
+
CONVERGED: 'converged',
|
|
8
|
+
CROSSOVER: 'crossover — stop: diff-review',
|
|
9
|
+
CAP_REACHED: 'cap reached — classify each surviving finding: fixable-bug / inherent-layer-residual / escalate',
|
|
10
|
+
FOLD_AND_RE_REVIEW: 'round 1 — fold and re-review',
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const isNonNegativeInteger = (value) => Number.isInteger(value) && value >= 0;
|
|
14
|
+
const isPlainLine = isRenderableLine;
|
|
15
|
+
|
|
16
|
+
const invalidFieldOf = (receipt) => {
|
|
17
|
+
if (typeof receipt?.artifactPath !== 'string') return 'artifactPath';
|
|
18
|
+
if (typeof receipt.fingerprint !== 'string' || !/^[0-9a-f]{64}$/u.test(receipt.fingerprint)) return 'fingerprint';
|
|
19
|
+
if (!isPlainLine(receipt.backend) || receipt.backend.length === 0) return 'backend';
|
|
20
|
+
if (receipt.probe !== false) return 'probe';
|
|
21
|
+
if (!isPlainLine(receipt.verdict)) return 'verdict';
|
|
22
|
+
if (!isNonNegativeInteger(receipt.durationS)) return 'durationS';
|
|
23
|
+
if (!isNonNegativeInteger(receipt.blocking)) return 'blocking';
|
|
24
|
+
return null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const groupRounds = (receipts, obligation) => {
|
|
28
|
+
const expected = new Set(obligation.backends);
|
|
29
|
+
const rounds = [];
|
|
30
|
+
const invalid = [];
|
|
31
|
+
const unexpected = [];
|
|
32
|
+
for (const receipt of receipts) {
|
|
33
|
+
const invalidField = invalidFieldOf(receipt);
|
|
34
|
+
if (invalidField !== null) {
|
|
35
|
+
invalid.push({ field: invalidField, receipt });
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (!expected.has(receipt.backend)) {
|
|
39
|
+
unexpected.push(receipt);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const previous = rounds.at(-1);
|
|
43
|
+
const round = previous?.fingerprint === receipt.fingerprint
|
|
44
|
+
? previous
|
|
45
|
+
: { fingerprint: receipt.fingerprint, byBackend: {} };
|
|
46
|
+
if (round !== previous) rounds.push(round);
|
|
47
|
+
round.byBackend[receipt.backend] = receipt;
|
|
48
|
+
}
|
|
49
|
+
return { rounds, invalid, unexpected };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const isComplete = (round, obligation) => {
|
|
53
|
+
const present = obligation.backends.filter((backend) => round.byBackend[backend] !== undefined);
|
|
54
|
+
return obligation.perBackend
|
|
55
|
+
? present.length === obligation.backends.length
|
|
56
|
+
: present.length >= obligation.minShip;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const isConverged = (round, obligation) => obligation.backends
|
|
60
|
+
.map((backend) => round.byBackend[backend])
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.every((receipt) => receipt.blocking === 0 && isShipVerdict(receipt.verdict));
|
|
63
|
+
|
|
64
|
+
const hasCrossover = (rounds, obligation) => {
|
|
65
|
+
const [earlier, latest] = rounds.slice(-2);
|
|
66
|
+
return obligation.backends.some((shipBackend) => {
|
|
67
|
+
const shipEarlier = earlier.byBackend[shipBackend];
|
|
68
|
+
const shipLatest = latest.byBackend[shipBackend];
|
|
69
|
+
if (!isShipVerdict(shipEarlier?.verdict) || !isShipVerdict(shipLatest?.verdict)) return false;
|
|
70
|
+
return obligation.backends.some((negativeBackend) => {
|
|
71
|
+
if (negativeBackend === shipBackend) return false;
|
|
72
|
+
const negativeEarlier = earlier.byBackend[negativeBackend]?.verdict;
|
|
73
|
+
const negativeLatest = latest.byBackend[negativeBackend]?.verdict;
|
|
74
|
+
return isRecognizedVerdict(negativeEarlier) && !isShipVerdict(negativeEarlier)
|
|
75
|
+
&& isRecognizedVerdict(negativeLatest) && !isShipVerdict(negativeLatest);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const signalFor = (rounds, obligation, artifactPath) => {
|
|
81
|
+
if (rounds.length === 0) return SIGNALS.NO_RECEIPTS.replace('<path>', () => artifactPath);
|
|
82
|
+
const latest = rounds.at(-1);
|
|
83
|
+
if (!isComplete(latest, obligation)) {
|
|
84
|
+
const missing = obligation.backends.find((backend) => latest.byBackend[backend] === undefined);
|
|
85
|
+
return SIGNALS.INCOMPLETE.replace('<backend>', () => missing);
|
|
86
|
+
}
|
|
87
|
+
if (isConverged(latest, obligation)) return SIGNALS.CONVERGED;
|
|
88
|
+
const complete = rounds.filter((round) => isComplete(round, obligation));
|
|
89
|
+
if (complete.length >= 2 && hasCrossover(complete, obligation)) return SIGNALS.CROSSOVER;
|
|
90
|
+
if (complete.length >= 2) return SIGNALS.CAP_REACHED;
|
|
91
|
+
return SIGNALS.FOLD_AND_RE_REVIEW;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const renderRounds = ({ rounds, invalid = [], unexpected = [], obligation, artifactPath, pathless = 0, malformed = 0 }) => {
|
|
95
|
+
const lines = [];
|
|
96
|
+
rounds.reduce((total, round, index) => {
|
|
97
|
+
const receipts = obligation.backends.map((backend) => round.byBackend[backend]).filter(Boolean);
|
|
98
|
+
const duration = receipts.reduce((sum, receipt) => sum + receipt.durationS, 0);
|
|
99
|
+
const cells = obligation.backends.map((backend) => {
|
|
100
|
+
const receipt = round.byBackend[backend];
|
|
101
|
+
return receipt === undefined
|
|
102
|
+
? `${backend}: missing`
|
|
103
|
+
: `${backend}: ${receipt.verdict} (${receipt.blocking} blocking, ${receipt.durationS}s)`;
|
|
104
|
+
});
|
|
105
|
+
const next = total + duration;
|
|
106
|
+
lines.push(`round ${index + 1} · ${cells.join(' · ')} · receipted duration: ${duration}s · cumulative: ${next}s`);
|
|
107
|
+
return next;
|
|
108
|
+
}, 0);
|
|
109
|
+
for (const entry of invalid) lines.push(`invalid: ${entry.field}`);
|
|
110
|
+
for (const receipt of unexpected) lines.push(`unexpected: ${receipt.backend}`);
|
|
111
|
+
lines.push(`pathless plan/diff receipts: ${pathless}`);
|
|
112
|
+
lines.push(`malformed receipt lines: ${malformed}`);
|
|
113
|
+
lines.push(`signal: ${signalFor(rounds, obligation, artifactPath)}`);
|
|
114
|
+
return lines.join('\n');
|
|
115
|
+
};
|
package/tools/review-state.mjs
CHANGED
|
@@ -93,9 +93,9 @@ import { join, dirname } from 'node:path';
|
|
|
93
93
|
import { fileURLToPath } from 'node:url';
|
|
94
94
|
import { spawnSync } from 'node:child_process';
|
|
95
95
|
import { createHash } from 'node:crypto';
|
|
96
|
-
import { detectBackends
|
|
96
|
+
import { detectBackends } from './detect-backends.mjs';
|
|
97
97
|
import { isDirectRun } from './direct-run.mjs';
|
|
98
|
-
import { resolveActivityRecipe, DISPLAY_ALIASES, requiredBackendsForConfiguredRecipe } from './recipes.mjs';
|
|
98
|
+
import { resolveActivityRecipe, DISPLAY_ALIASES, requiredBackendsForConfiguredRecipe, composeReadiness } from './recipes.mjs';
|
|
99
99
|
import { CONFIG_REL, fail, loadConfig } from './orchestration-config.mjs';
|
|
100
100
|
import { resolveFlowStorePath, readFlowStore, deriveFlowOwner, readPlanFrontmatterId } from './flow-store.mjs';
|
|
101
101
|
import { CHAIN_KIND, authoritativeFlowRecords } from './flow-record.mjs';
|
|
@@ -309,16 +309,16 @@ export const degradeRecordSet = ({ cwd, env = process.env, fingerprint }) => {
|
|
|
309
309
|
// work-tree ROOT when one exists — the fingerprint is root-anchored, so a subdirectory invocation
|
|
310
310
|
// must read the same config/plans or a dirty unreceipted tree could false-PASS as "no plan in
|
|
311
311
|
// flight". Outside a git tree the cwd is the only anchor (and --check exits 0).
|
|
312
|
-
export const buildState = ({ cwd, env = process.env, detect = detectBackends, lstat = lstatSync, readFile = readFileSync } = {}) => {
|
|
312
|
+
export const buildState = ({ cwd, env = process.env, detect = detectBackends, surveyVehicle, lstat = lstatSync, readFile = readFileSync } = {}) => {
|
|
313
313
|
const root = gitLine(['rev-parse', '--show-toplevel'], cwd) ?? cwd;
|
|
314
314
|
const { config, source: configSource } = loadConfig(root);
|
|
315
|
-
|
|
315
|
+
// A bridge-detector throw reaches the hook, never a catch that would also lose the surveyed
|
|
316
|
+
// executor vehicle: bridge readiness goes unknown (fail closed below), the carrier stays known.
|
|
316
317
|
let detectionWarning = null;
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
}
|
|
318
|
+
const onDetectError = (err) => {
|
|
319
|
+
detectionWarning = `backend detection failed (${(err && err.message) || err}) — bridge readiness unknown.`;
|
|
320
|
+
};
|
|
321
|
+
const detection = composeReadiness(root, { detect, surveyVehicle, onDetectError });
|
|
322
322
|
// The resolver stays for DISPLAY/diagnostics only; the OBLIGATIONS come from the configured
|
|
323
323
|
// recipe (never the readiness-degraded effective one — no silent solo).
|
|
324
324
|
const resolved = resolveActivityRecipe({ config: config ?? {}, readiness: detection, activity: ACTIVITY, slot: SLOT });
|
|
@@ -439,7 +439,6 @@ export const buildState = ({ cwd, env = process.env, detect = detectBackends, ls
|
|
|
439
439
|
degradedExempt,
|
|
440
440
|
maskedUntracked: countNeverCommittableUntracked(cwd, { lstat }),
|
|
441
441
|
detectionWarning,
|
|
442
|
-
anyReviewerReady: detection.some((b) => b.readiness === READY),
|
|
443
442
|
flowPresent,
|
|
444
443
|
flowArmed,
|
|
445
444
|
flowBrokenReason,
|
|
@@ -733,7 +732,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
733
732
|
if (argv.includes('--help') || argv.includes('-h')) return { code: 0, stdout: HELP, stderr: '' };
|
|
734
733
|
const unknown = argv.find((a) => !KNOWN_ARGS.has(a));
|
|
735
734
|
if (unknown !== undefined) throw fail(2, `unknown argument: ${unknown}`);
|
|
736
|
-
const state = buildState({ cwd, env, detect, lstat: ctx.lstat, readFile: ctx.readFile });
|
|
735
|
+
const state = buildState({ cwd, env, detect, surveyVehicle: ctx.surveyVehicle, lstat: ctx.lstat, readFile: ctx.readFile });
|
|
737
736
|
const check = decideCheck(state);
|
|
738
737
|
// The mask advisory is NON-FAILING by contract: one notice line, never an exit-code arm.
|
|
739
738
|
const advisory = maskAdvisoryLine(state);
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addReviewer,
|
|
3
|
+
expandShorthand,
|
|
4
|
+
lensMembersOf,
|
|
5
|
+
obligationsOf,
|
|
6
|
+
parseSlotToken,
|
|
7
|
+
removeReviewer,
|
|
8
|
+
} from './review-roster.mjs';
|
|
9
|
+
import { isReadyMember, resolveRoster, skippedLine } from './review-roster-resolve.mjs';
|
|
10
|
+
import { applySetOps, assertSlot, fail } from './orchestration-config.mjs';
|
|
11
|
+
import { refuseDirectRun } from './direct-run.mjs';
|
|
12
|
+
|
|
13
|
+
const REVIEWER_KINDS = new Set(['add-reviewer', 'remove-reviewer']);
|
|
14
|
+
|
|
15
|
+
const parseQualified = (token, flag) => {
|
|
16
|
+
const equals = token.indexOf('=');
|
|
17
|
+
const qualified = equals < 0 ? token : token.slice(0, equals);
|
|
18
|
+
const dot = qualified.indexOf('.');
|
|
19
|
+
if (equals <= 0 || equals === token.length - 1 || dot <= 0 || dot === qualified.length - 1) {
|
|
20
|
+
throw fail(2, `--${flag} must be <activity>.review=<member> (got "${token}")`);
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
activity: qualified.slice(0, dot),
|
|
24
|
+
slot: qualified.slice(dot + 1),
|
|
25
|
+
member: token.slice(equals + 1),
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const parseReviewerOp = (kind, token) => {
|
|
30
|
+
if (!REVIEWER_KINDS.has(kind)) throw fail(2, `unknown reviewer op: ${kind}`);
|
|
31
|
+
const parsed = parseQualified(token, kind);
|
|
32
|
+
if (assertSlot(parsed.activity, parsed.slot) !== 'review') {
|
|
33
|
+
throw fail(2, `--${kind} requires a review slot (got "${parsed.activity}.${parsed.slot}")`);
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
parseSlotToken(parsed.member);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw fail(2, error.message);
|
|
39
|
+
}
|
|
40
|
+
return { kind, ...parsed };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const membersOf = (value) => {
|
|
44
|
+
if (Array.isArray(value)) return value;
|
|
45
|
+
const expanded = expandShorthand(value);
|
|
46
|
+
return expanded.lossless ? expanded.members : null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const sameMembers = (left, right) => {
|
|
50
|
+
const a = membersOf(left);
|
|
51
|
+
const b = membersOf(right);
|
|
52
|
+
return a !== null && b !== null && a.length === b.length && a.every((member, index) => member === b[index]);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const reviewedRefusal = (activity) => fail(
|
|
56
|
+
2,
|
|
57
|
+
`reviewed has no lossless roster expansion — run --set ${activity}.review=council first, or use --add-reviewer ${activity}.review=codex-review / --add-reviewer ${activity}.review=agy-review on a solo slot`,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const applyOne = (value, op) => {
|
|
61
|
+
if (membersOf(value) === null) throw reviewedRefusal(op.activity);
|
|
62
|
+
try {
|
|
63
|
+
return op.kind === 'add-reviewer'
|
|
64
|
+
? addReviewer(value, op.member)
|
|
65
|
+
: removeReviewer(value, op.member);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw fail(2, error.code === 'last-member' ? `${error.message} — run --set ${op.activity}.review=solo` : error.message);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const applyReviewerOps = (current, ops, { defaults = {}, seedReadme = null } = {}) => {
|
|
72
|
+
const states = new Map();
|
|
73
|
+
for (const op of ops) {
|
|
74
|
+
const key = `${op.activity}.${op.slot}`;
|
|
75
|
+
const raw = current?.[op.activity]?.[op.slot] ?? null;
|
|
76
|
+
const state = states.get(key) ?? {
|
|
77
|
+
activity: op.activity,
|
|
78
|
+
slot: op.slot,
|
|
79
|
+
from: raw,
|
|
80
|
+
beforeValue: raw ?? defaults[key],
|
|
81
|
+
value: raw ?? defaults[key],
|
|
82
|
+
named: new Set(),
|
|
83
|
+
};
|
|
84
|
+
if (state.value === undefined) throw fail(2, `no computed default supplied for ${key}`);
|
|
85
|
+
state.value = applyOne(state.value, op);
|
|
86
|
+
if (op.kind === 'add-reviewer') state.named.add(parseSlotToken(op.member).stem);
|
|
87
|
+
states.set(key, state);
|
|
88
|
+
}
|
|
89
|
+
const rows = [...states.values()].map((state) => {
|
|
90
|
+
const changed = !sameMembers(state.beforeValue, state.value);
|
|
91
|
+
return {
|
|
92
|
+
...state,
|
|
93
|
+
changed,
|
|
94
|
+
to: changed ? state.value : state.from,
|
|
95
|
+
afterValue: changed ? state.value : state.beforeValue,
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
const changes = rows.filter((row) => row.changed).map((row) => ({
|
|
99
|
+
kind: 'set', activity: row.activity, slot: row.slot, recipe: row.to,
|
|
100
|
+
}));
|
|
101
|
+
return {
|
|
102
|
+
config: changes.length ? applySetOps(current, changes, { seedReadme }) : current,
|
|
103
|
+
rows,
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const gateLabel = (value) => {
|
|
108
|
+
const members = membersOf(value) ?? [];
|
|
109
|
+
if (members.length === 0) return 'solo []';
|
|
110
|
+
const obligation = obligationsOf(members);
|
|
111
|
+
return `${obligation.recipe} [${obligation.backends.join(', ')}]`;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const valueLabel = (value) => value == null
|
|
115
|
+
? '(computed default)'
|
|
116
|
+
: Array.isArray(value) ? JSON.stringify(value) : value;
|
|
117
|
+
|
|
118
|
+
const lensRemedy = (parsed, member, agentsApply) => {
|
|
119
|
+
if (parsed.kind !== 'lens' || member.state !== 'missing') return null;
|
|
120
|
+
if (parsed.template === null) return `HAND-APPLY: create .claude/agents/${parsed.stem}.md as a read-only vehicle`;
|
|
121
|
+
return agentsApply ? `to place it, run exactly: ${agentsApply}` : null;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const persistedLensStems = (config) => new Set(lensMembersOf(config ?? {})
|
|
125
|
+
.map((member) => parseSlotToken(member))
|
|
126
|
+
.filter((parsed) => parsed.derived)
|
|
127
|
+
.map((parsed) => parsed.stem));
|
|
128
|
+
|
|
129
|
+
export const renderRosterPreview = (row, { agentsApply, wrote = false, persistedLenses = new Set() } = {}) => {
|
|
130
|
+
const lines = [row.changed
|
|
131
|
+
? ` ${row.activity}.${row.slot}: ${valueLabel(row.from)} → ${valueLabel(row.to)}`
|
|
132
|
+
: ` ${row.activity}.${row.slot}: already ${valueLabel(row.from)} (no change)`];
|
|
133
|
+
const persisted = new Set((membersOf(row.from) ?? []).map((member) => parseSlotToken(member).stem));
|
|
134
|
+
for (const member of row.roster) {
|
|
135
|
+
const parsed = parseSlotToken(member.member);
|
|
136
|
+
const apply = lensRemedy(parsed, member, agentsApply);
|
|
137
|
+
const remedy = [member.reason, apply].filter(Boolean).join('; ') || null;
|
|
138
|
+
const posture = member.posture == null ? '' : ` (${member.posture})`;
|
|
139
|
+
lines.push(` ↳ ${member.member}: ${isReadyMember(member) ? member.state : skippedLine(member, remedy)}${posture}`);
|
|
140
|
+
if (apply && parsed.derived && !wrote && !persisted.has(parsed.stem) && !persistedLenses.has(parsed.stem)) {
|
|
141
|
+
lines.push(' after --write — the agents writer derives this lens from what docs/ai/orchestration.json names');
|
|
142
|
+
}
|
|
143
|
+
if (parsed.kind === 'lens' && parsed.template === null && row.named.has(parsed.stem)) {
|
|
144
|
+
lines.push(` resolved as a lens with no bundled template — a hand-written vehicle; if you meant a bridge, the review cmds are ${expandShorthand('council').members.join(', ')}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
lines.push(` gate: ${gateLabel(row.beforeValue)} → ${gateLabel(row.afterValue)}`);
|
|
148
|
+
return lines.join('\n');
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const resolveReviewerRows = (rows, deps = {}) => rows.map((row) => {
|
|
152
|
+
const members = membersOf(row.afterValue);
|
|
153
|
+
return {
|
|
154
|
+
...row,
|
|
155
|
+
roster: members.length === 0 ? [] : resolveRoster({ value: members, ...deps }),
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
export const rosterJsonRows = (rows, changed) => rows.map((row) => changed ? ({
|
|
160
|
+
activity: row.activity, slot: row.slot, from: row.from, to: row.to,
|
|
161
|
+
effective: row.effective, degradedFrom: row.degradedFrom ?? null, reason: row.reason ?? null,
|
|
162
|
+
roster: row.roster ?? null,
|
|
163
|
+
}) : ({
|
|
164
|
+
activity: row.activity, slot: row.slot, recipe: row.from, roster: row.roster ?? null,
|
|
165
|
+
}));
|
|
166
|
+
|
|
167
|
+
refuseDirectRun(import.meta.url);
|
package/tools/set-recipe.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// set-recipe.mjs — the WRITER for docs/ai/orchestration.json. The division of labor (AD-025): the AGENT
|
|
3
|
-
// turns plain language into explicit `--set <activity>.<slot>=<
|
|
3
|
+
// turns plain language into explicit `--set <activity>.<slot>=<value>` / `--unset <activity>.<slot>`
|
|
4
4
|
// ops; the KIT does the deterministic validate → merge → preview → write. The kit ships NO NL parser
|
|
5
5
|
// (stays dependency-free + deterministic) and performs no `all`-magic — the agent expands "both review"
|
|
6
6
|
// into explicit per-activity ops (asking if scope is unclear).
|
|
@@ -17,15 +17,28 @@
|
|
|
17
17
|
// language when narrating. Exit codes: 0 success (an explicit recipe that gracefully degrades is still
|
|
18
18
|
// 0); 2 usage (bad/duplicate op, --write with zero ops); 1 config error (malformed/unreadable config)
|
|
19
19
|
// or a write STOP (no deployment / symlinked leaf). main(argv, ctx) → { code, stdout, stderr }; cwd /
|
|
20
|
-
// env / home / detect / fs are injectable for host-independent tests.
|
|
20
|
+
// env / home / detect / surveyVehicle / fs are injectable for host-independent tests.
|
|
21
|
+
//
|
|
22
|
+
// It writes every slot of every activity in the registry (carriers.mjs, via recipes.mjs) — the
|
|
23
|
+
// carrier slots and the `routine.parallel` switch included — and resolves each preview against the
|
|
24
|
+
// SAME readiness the recipes advisor composes: detected backends plus the executor-vehicle survey.
|
|
21
25
|
//
|
|
22
26
|
// Dependency-free, Node >= 22. No side effects on import (the isDirectRun idiom).
|
|
23
27
|
|
|
24
28
|
import { readFileSync, lstatSync } from 'node:fs';
|
|
25
|
-
import { homedir } from 'node:os';
|
|
26
|
-
import { detectBackends } from './detect-backends.mjs';
|
|
27
29
|
import { isDirectRun } from './direct-run.mjs';
|
|
28
|
-
import {
|
|
30
|
+
import { settingsSnapshot } from './bridge-settings-read.mjs';
|
|
31
|
+
import { posturesByBackend } from './bridge-posture.mjs';
|
|
32
|
+
import { surveyVehicle } from './cheap-agents-read.mjs';
|
|
33
|
+
import { applyCheapAgentsCommand } from './cheap-agents.mjs';
|
|
34
|
+
import {
|
|
35
|
+
ACTIVITIES,
|
|
36
|
+
SLOT_RECIPES,
|
|
37
|
+
EXECUTOR_APPLY,
|
|
38
|
+
composeReadiness,
|
|
39
|
+
resolveActivityRecipe,
|
|
40
|
+
composeActiveRecipeLine,
|
|
41
|
+
} from './recipes.mjs';
|
|
29
42
|
import { loadAutonomy, resolveAutonomy } from './autonomy-config.mjs';
|
|
30
43
|
import {
|
|
31
44
|
CONFIG_REL,
|
|
@@ -35,26 +48,40 @@ import {
|
|
|
35
48
|
parseOp,
|
|
36
49
|
applySetOps,
|
|
37
50
|
serializeConfig,
|
|
51
|
+
refreshReadme,
|
|
38
52
|
CANON_README,
|
|
39
53
|
} from './orchestration-config.mjs';
|
|
40
54
|
import { writeConfig as writeConfigFs } from './orchestration-write.mjs';
|
|
55
|
+
import {
|
|
56
|
+
applyReviewerOps,
|
|
57
|
+
parseReviewerOp,
|
|
58
|
+
persistedLensStems,
|
|
59
|
+
renderRosterPreview,
|
|
60
|
+
resolveReviewerRows,
|
|
61
|
+
rosterJsonRows,
|
|
62
|
+
} from './set-recipe-roster.mjs';
|
|
41
63
|
|
|
42
64
|
// ── argument parsing (usage errors → exit 2) ────────────────────────────────────────
|
|
43
65
|
|
|
44
|
-
// Parse argv → { ops, write, json }.
|
|
45
|
-
//
|
|
46
|
-
// token → exit 2. `--set=<tok>` / `--unset=<tok>` inline forms are accepted too.
|
|
66
|
+
// Parse argv → { ops, write, json }. Fixed ops stay unique per slot; reviewer list ops accumulate.
|
|
67
|
+
// A `--write` with zero ops, an unknown flag, or a bad token → exit 2. Inline forms are accepted too.
|
|
47
68
|
const parseArgs = (argv) => {
|
|
48
69
|
const ops = [];
|
|
49
|
-
const
|
|
70
|
+
const fixed = new Set();
|
|
71
|
+
const reviewer = new Set();
|
|
50
72
|
let write = false;
|
|
51
73
|
let json = false;
|
|
52
74
|
const takeOp = (kind, tok) => {
|
|
53
|
-
|
|
54
|
-
const
|
|
75
|
+
const reviewerKind = kind === 'add-reviewer' || kind === 'remove-reviewer';
|
|
76
|
+
const form = reviewerKind ? '<activity>.review=<member>' : `<activity>.<slot>${kind === 'set' ? '=<value>' : ''}`;
|
|
77
|
+
if (tok === undefined || tok.startsWith('--')) throw fail(2, `--${kind} requires ${form}`);
|
|
78
|
+
const op = reviewerKind ? parseReviewerOp(kind, tok) : parseOp(kind, tok);
|
|
55
79
|
const key = `${op.activity}.${op.slot}`;
|
|
56
|
-
if (
|
|
57
|
-
|
|
80
|
+
if (fixed.has(key) || (!reviewerKind && reviewer.has(key))) {
|
|
81
|
+
throw fail(2, `duplicate op for "${key}" — --set/--unset name each activity.slot at most once and cannot mix with reviewer list ops`);
|
|
82
|
+
}
|
|
83
|
+
if (reviewerKind) reviewer.add(key);
|
|
84
|
+
else fixed.add(key);
|
|
58
85
|
ops.push(op);
|
|
59
86
|
};
|
|
60
87
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -63,23 +90,39 @@ const parseArgs = (argv) => {
|
|
|
63
90
|
else if (a === '--write') write = true;
|
|
64
91
|
else if (a === '--set') { takeOp('set', argv[i + 1]); i += 1; }
|
|
65
92
|
else if (a === '--unset') { takeOp('unset', argv[i + 1]); i += 1; }
|
|
93
|
+
else if (a === '--add-reviewer') { takeOp('add-reviewer', argv[i + 1]); i += 1; }
|
|
94
|
+
else if (a === '--remove-reviewer') { takeOp('remove-reviewer', argv[i + 1]); i += 1; }
|
|
66
95
|
else if (a.startsWith('--set=')) takeOp('set', a.slice('--set='.length));
|
|
67
96
|
else if (a.startsWith('--unset=')) takeOp('unset', a.slice('--unset='.length));
|
|
97
|
+
else if (a.startsWith('--add-reviewer=')) takeOp('add-reviewer', a.slice('--add-reviewer='.length));
|
|
98
|
+
else if (a.startsWith('--remove-reviewer=')) takeOp('remove-reviewer', a.slice('--remove-reviewer='.length));
|
|
68
99
|
else if (a.startsWith('-')) throw fail(2, `unknown flag: ${a}`);
|
|
69
100
|
else throw fail(2, `unexpected argument: ${a}`);
|
|
70
101
|
}
|
|
71
|
-
if (write && ops.length === 0) throw fail(2, 'nothing to write — pass at least one --set/--unset (a bare --write is a no-op)');
|
|
102
|
+
if (write && ops.length === 0) throw fail(2, 'nothing to write — pass at least one --set/--unset/--add-reviewer/--remove-reviewer (a bare --write is a no-op)');
|
|
72
103
|
return { ops, write, json };
|
|
73
104
|
};
|
|
74
105
|
|
|
75
106
|
// ── effective-recipe resolution per op (degradation honesty) ────────────────────────
|
|
76
107
|
|
|
108
|
+
// The readiness EVERY resolution here runs against: the detected bridges plus the executor-vehicle
|
|
109
|
+
// survey, composed by the one helper the recipes CLI uses. Detection is a SECONDARY input — a bridge
|
|
110
|
+
// detector throw must NOT block the write (the config write is readiness-independent) and must not
|
|
111
|
+
// cost the CARRIER either: the hook warns, the bridge half floors at not-ready, the vehicle survives.
|
|
112
|
+
const composeReadinessOrWarn = (cwd, deps, warnings) =>
|
|
113
|
+
composeReadiness(cwd, {
|
|
114
|
+
...deps,
|
|
115
|
+
onDetectError: (err) => warnings.push(
|
|
116
|
+
`backend detection failed (${(err && err.message) || err}) — treating every bridge as not ready; recipes needing a bridge degrade to solo (the executor vehicle is unaffected).`,
|
|
117
|
+
),
|
|
118
|
+
});
|
|
119
|
+
|
|
77
120
|
// A single op's before/after value + the effective recipe it resolves to here (vs live readiness).
|
|
78
121
|
// `to` is null for an unset (falls to the computed default). degradedFrom/reason carry the honesty.
|
|
79
|
-
const resolveOp = (op, current, after,
|
|
122
|
+
const resolveOp = (op, current, after, readiness) => {
|
|
80
123
|
const from = current?.[op.activity]?.[op.slot] ?? null;
|
|
81
124
|
const to = after?.[op.activity]?.[op.slot] ?? null;
|
|
82
|
-
const r = resolveActivityRecipe({ config: after ?? {}, readiness
|
|
125
|
+
const r = resolveActivityRecipe({ config: after ?? {}, readiness, activity: op.activity, slot: op.slot });
|
|
83
126
|
return { activity: op.activity, slot: op.slot, from, to, effective: r.recipe, degradedFrom: r.degradedFrom, reason: r.reason };
|
|
84
127
|
};
|
|
85
128
|
|
|
@@ -92,15 +135,21 @@ const effectiveLine = (e) =>
|
|
|
92
135
|
? `effective here: ${e.effective} (requested ${e.degradedFrom} → degraded: ${e.reason})`
|
|
93
136
|
: `effective here: ${e.effective}`;
|
|
94
137
|
|
|
95
|
-
const formatHuman = ({ changed, unchanged, warnings, willWrite, wrote, fileBody, activeLine }) => {
|
|
138
|
+
const formatHuman = ({ changed, unchanged, warnings, willWrite, wrote, fileBody, activeLine, agentsApply, persistedLenses }) => {
|
|
96
139
|
const lines = [];
|
|
97
140
|
if (wrote) lines.push(`wrote ${CONFIG_REL}`);
|
|
98
141
|
else if (changed.length) lines.push(`set-recipe — preview (nothing written; re-run with --write to apply)`);
|
|
99
142
|
for (const e of changed) {
|
|
143
|
+
if (Array.isArray(e.roster)) {
|
|
144
|
+
lines.push(renderRosterPreview(e, { agentsApply, wrote, persistedLenses }));
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
100
147
|
lines.push(` ${e.activity}.${e.slot}: ${valueLabel(e.from)} → ${valueLabel(e.to)}`);
|
|
101
148
|
lines.push(` ↳ ${effectiveLine(e)}`);
|
|
102
149
|
}
|
|
103
|
-
for (const e of unchanged) lines.push(
|
|
150
|
+
for (const e of unchanged) lines.push(Array.isArray(e.roster)
|
|
151
|
+
? renderRosterPreview(e, { agentsApply, wrote, persistedLenses })
|
|
152
|
+
: ` ${e.activity}.${e.slot}: already ${valueLabel(e.from)} (no change)`);
|
|
104
153
|
for (const w of warnings) lines.push(` ⚠ ${w}`);
|
|
105
154
|
if (wrote && fileBody) lines.push('', `${CONFIG_REL} now reads:`, fileBody.replace(/\n$/, ''));
|
|
106
155
|
// The post-write discovery echo (AD-038): after every successful write, paste the freshly composed
|
|
@@ -117,8 +166,8 @@ const formatHuman = ({ changed, unchanged, warnings, willWrite, wrote, fileBody,
|
|
|
117
166
|
};
|
|
118
167
|
|
|
119
168
|
const buildJson = ({ changed, unchanged, warnings, writtenPath, noop, activeLine }) => ({
|
|
120
|
-
changed: changed
|
|
121
|
-
unchanged: unchanged
|
|
169
|
+
changed: rosterJsonRows(changed, true),
|
|
170
|
+
unchanged: rosterJsonRows(unchanged, false),
|
|
122
171
|
writtenPath: writtenPath ?? null,
|
|
123
172
|
noop,
|
|
124
173
|
warnings,
|
|
@@ -127,19 +176,44 @@ const buildJson = ({ changed, unchanged, warnings, writtenPath, noop, activeLine
|
|
|
127
176
|
activeLine: activeLine ?? null,
|
|
128
177
|
});
|
|
129
178
|
|
|
179
|
+
// The writable surface, rendered FROM the registry — never re-typed as literals, so an activity, a
|
|
180
|
+
// slot or an accepted value added to the table shows up in the help (and in the doc that quotes it).
|
|
181
|
+
const ACTIVITY_LINES = Object.entries(ACTIVITIES)
|
|
182
|
+
.map(([activity, def]) => ` ${activity} → ${Object.keys(def.slots).join(', ')}`)
|
|
183
|
+
.join('\n');
|
|
184
|
+
|
|
185
|
+
const VALUE_LINES = Object.entries(SLOT_RECIPES)
|
|
186
|
+
.map(([slotType, values]) => ` ${slotType} slots accept ${values.join(' | ')}`)
|
|
187
|
+
.join('\n');
|
|
188
|
+
|
|
189
|
+
const QUALIFIED_SLOTS = Object.entries(ACTIVITIES)
|
|
190
|
+
.flatMap(([activity, def]) => Object.keys(def.slots).map((slot) => `${activity}.${slot}`))
|
|
191
|
+
.join(', ');
|
|
192
|
+
|
|
130
193
|
const HELP = `set-recipe — write the per-project orchestration config (docs/ai/orchestration.json).
|
|
131
194
|
|
|
132
195
|
Usage:
|
|
133
|
-
node set-recipe.mjs [--set <activity>.<slot>=<
|
|
196
|
+
node set-recipe.mjs [--set <activity>.<slot>=<value>]... [--unset <activity>.<slot>]...
|
|
197
|
+
[--add-reviewer <activity>.review=<member>]...
|
|
198
|
+
[--remove-reviewer <activity>.review=<member>]... [--write] [--json]
|
|
134
199
|
|
|
135
|
-
--set <activity>.<slot>=<
|
|
200
|
+
--set <activity>.<slot>=<value> pin a value (fully-qualified; e.g. plan-authoring.review=council)
|
|
136
201
|
--unset <activity>.<slot> return a slot to its computed default
|
|
202
|
+
--add-reviewer <activity>.review=<member> append a reviewer (same-slot ops accumulate in argv order)
|
|
203
|
+
--remove-reviewer <activity>.review=<member> remove a reviewer (same-slot ops accumulate in argv order)
|
|
137
204
|
--write apply the change (default: preview only — writes nothing)
|
|
138
205
|
--json machine-readable output
|
|
139
206
|
--help, -h this help
|
|
140
207
|
|
|
141
|
-
Activities
|
|
142
|
-
|
|
208
|
+
Activities and their slots:
|
|
209
|
+
${ACTIVITY_LINES}
|
|
210
|
+
|
|
211
|
+
Accepted values per slot type:
|
|
212
|
+
${VALUE_LINES}
|
|
213
|
+
|
|
214
|
+
A carrier slot set to subagent needs the executor vehicle placed in this project — ${EXECUTOR_APPLY};
|
|
215
|
+
without it the slot resolves to solo with the reason stated. routine.parallel is a flag, not a
|
|
216
|
+
recipe: it never degrades.
|
|
143
217
|
|
|
144
218
|
Previews by default; --write applies via an atomic, symlink/TOCTOU-safe write behind a deployment gate.
|
|
145
219
|
Config writer only: it NEVER runs a backend and NEVER commits. Hand-editing the file stays fully supported.
|
|
@@ -152,7 +226,7 @@ Exit codes: 0 success (an explicit recipe that gracefully degrades is still 0);
|
|
|
152
226
|
|
|
153
227
|
export const main = (argv, ctx = {}) => {
|
|
154
228
|
const cwd = ctx.cwd ?? process.cwd();
|
|
155
|
-
const
|
|
229
|
+
const readinessDeps = { detect: ctx.detect, surveyVehicle: ctx.surveyVehicle };
|
|
156
230
|
const readFile = ctx.readFileSync ?? readFileSync;
|
|
157
231
|
const lstat = ctx.lstatSync ?? lstatSync;
|
|
158
232
|
const writeConfig = ctx.writeConfig ?? writeConfigFs;
|
|
@@ -170,23 +244,43 @@ export const main = (argv, ctx = {}) => {
|
|
|
170
244
|
return { code: 0, stdout: JSON.stringify(buildJson({ changed: [], unchanged: [], warnings: [], writtenPath: null, noop: true }), null, 2), stderr: '' };
|
|
171
245
|
}
|
|
172
246
|
const shown = current == null ? `(no ${CONFIG_REL} yet — computed defaults apply)` : serializeConfig(current).replace(/\n$/, '');
|
|
173
|
-
const hint = `\nPass --set <activity>.<slot>=<
|
|
247
|
+
const hint = `\nPass --set <activity>.<slot>=<value> (preview) then --write to apply. Activities/slots: ${QUALIFIED_SLOTS}.`;
|
|
174
248
|
return { code: 0, stdout: `${source === 'none' ? '' : `${CONFIG_REL}:\n`}${shown}${hint}`, stderr: '' };
|
|
175
249
|
}
|
|
176
250
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
// the write (the config write is readiness-independent): treat all backends as not-ready, warn, exit 0.
|
|
251
|
+
// The merged config, then the _README refresh: a note that normalize-matches a KNOWN PRIOR canonical
|
|
252
|
+
// is replaced by the current one on a touched write, while a customized note stays untouched.
|
|
253
|
+
const render = { agentsApply: applyCheapAgentsCommand(cwd), persistedLenses: persistedLensStems(current) };
|
|
181
254
|
const warnings = [];
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
255
|
+
const readiness = composeReadinessOrWarn(cwd, readinessDeps, warnings);
|
|
256
|
+
const reviewerOps = ops.filter((op) => op.kind === 'add-reviewer' || op.kind === 'remove-reviewer');
|
|
257
|
+
const fixedOps = ops.filter((op) => op.kind === 'set' || op.kind === 'unset');
|
|
258
|
+
const fixedAfter = fixedOps.length
|
|
259
|
+
? applySetOps(current, fixedOps, { seedReadme: CANON_README })
|
|
260
|
+
: current;
|
|
261
|
+
const defaults = Object.fromEntries(reviewerOps.map((op) => [
|
|
262
|
+
`${op.activity}.${op.slot}`,
|
|
263
|
+
resolveActivityRecipe({ config: {}, readiness, activity: op.activity, slot: op.slot }).recipe,
|
|
264
|
+
]));
|
|
265
|
+
const reviewerResult = applyReviewerOps(fixedAfter, reviewerOps, { defaults, seedReadme: CANON_README });
|
|
266
|
+
const after = refreshReadme(reviewerResult.config ?? {}).config;
|
|
267
|
+
const surveyLens = ctx.surveyLens ?? ((spec) => surveyVehicle(cwd, spec, ctx));
|
|
268
|
+
const hasRoster = reviewerOps.length > 0 || Object.values(after).some((activity) => Array.isArray(activity?.review));
|
|
269
|
+
const settings = hasRoster ? settingsSnapshot({
|
|
270
|
+
getenv: ctx.env, home: ctx.home, readFile: ctx.readFileSync, lstat: ctx.lstatSync,
|
|
271
|
+
}) : null;
|
|
272
|
+
const postures = ctx.postures ?? (hasRoster ? posturesByBackend({ settings }) : {});
|
|
273
|
+
const reviewerRows = resolveReviewerRows(reviewerResult.rows, { readiness, surveyLens, postures })
|
|
274
|
+
.map((row) => {
|
|
275
|
+
const resolved = resolveActivityRecipe({
|
|
276
|
+
config: after, readiness, activity: row.activity, slot: row.slot, surveyLens, postures,
|
|
277
|
+
});
|
|
278
|
+
return { ...row, effective: resolved.recipe, degradedFrom: null, reason: null };
|
|
279
|
+
});
|
|
280
|
+
const resolved = [
|
|
281
|
+
...fixedOps.map((op) => resolveOp(op, current, after, readiness)),
|
|
282
|
+
...reviewerRows,
|
|
283
|
+
];
|
|
190
284
|
const changed = resolved.filter((e) => e.from !== e.to);
|
|
191
285
|
const unchanged = resolved.filter((e) => e.from === e.to);
|
|
192
286
|
const noop = changed.length === 0;
|
|
@@ -194,7 +288,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
194
288
|
if (!write) {
|
|
195
289
|
const stdout = json
|
|
196
290
|
? JSON.stringify(buildJson({ changed, unchanged, warnings, writtenPath: null, noop }), null, 2)
|
|
197
|
-
: formatHuman({ changed, unchanged, warnings, willWrite: !noop, wrote: false });
|
|
291
|
+
: formatHuman({ changed, unchanged, warnings, willWrite: !noop, wrote: false, ...render });
|
|
198
292
|
return { code: 0, stdout, stderr: '' };
|
|
199
293
|
}
|
|
200
294
|
|
|
@@ -202,7 +296,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
202
296
|
if (noop) {
|
|
203
297
|
const stdout = json
|
|
204
298
|
? JSON.stringify(buildJson({ changed, unchanged, warnings, writtenPath: null, noop: true }), null, 2)
|
|
205
|
-
: formatHuman({ changed, unchanged, warnings, willWrite: false, wrote: false });
|
|
299
|
+
: formatHuman({ changed, unchanged, warnings, willWrite: false, wrote: false, ...render });
|
|
206
300
|
return { code: 0, stdout, stderr: '' };
|
|
207
301
|
}
|
|
208
302
|
|
|
@@ -220,10 +314,12 @@ export const main = (argv, ctx = {}) => {
|
|
|
220
314
|
return { error: (err && err.message) || String(err) };
|
|
221
315
|
}
|
|
222
316
|
})();
|
|
223
|
-
const activeLine = composeActiveRecipeLine(
|
|
317
|
+
const activeLine = composeActiveRecipeLine(
|
|
318
|
+
{ config: after, source: CONFIG_REL }, readiness, autonomyFacts, { surveyLens, postures },
|
|
319
|
+
);
|
|
224
320
|
const stdout = json
|
|
225
321
|
? JSON.stringify(buildJson({ changed, unchanged, warnings, writtenPath, noop: false, activeLine }), null, 2)
|
|
226
|
-
: formatHuman({ changed, unchanged, warnings, wrote: true, fileBody, activeLine });
|
|
322
|
+
: formatHuman({ changed, unchanged, warnings, wrote: true, fileBody, activeLine, ...render });
|
|
227
323
|
return { code: 0, stdout, stderr: '' };
|
|
228
324
|
} catch (err) {
|
|
229
325
|
return { code: err.exitCode ?? 1, stdout: '', stderr: `set-recipe: ${err.message}` };
|