@sabaiway/agent-workflow-kit 10.4.0 → 10.6.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 +109 -0
- package/README.md +1 -1
- 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/review-lens.md +5 -3
- package/references/modes/agents.md +1 -1
- package/references/modes/core-evidence.md +1 -1
- package/references/modes/procedures.md +11 -5
- package/references/modes/recipes.md +2 -2
- package/references/modes/review-state.md +3 -3
- package/references/modes/set-recipe.md +4 -4
- package/references/modes/status.md +1 -1
- package/references/modes/velocity.md +1 -0
- package/references/templates/orchestration.json +1 -1
- package/tools/bridge-posture.mjs +48 -0
- package/tools/carriers.mjs +21 -9
- package/tools/cheap-agents-read.mjs +86 -24
- package/tools/cheap-agents.mjs +47 -7
- package/tools/commit-guard.mjs +4 -3
- package/tools/detect-backends.mjs +2 -2
- package/tools/direct-run.mjs +3 -0
- package/tools/dispatch-store-read.mjs +269 -0
- package/tools/dispatch-store.mjs +21 -357
- package/tools/fold-scope.mjs +5 -60
- package/tools/grounding.mjs +2 -2
- package/tools/held-session.mjs +165 -0
- package/tools/orchestration-config.mjs +19 -78
- 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 +161 -31
- package/tools/recipes.mjs +60 -79
- 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 +85 -6
- package/tools/set-recipe-roster.mjs +167 -0
- package/tools/set-recipe.mjs +80 -23
- package/tools/velocity-profile.mjs +8 -22
package/tools/procedures.mjs
CHANGED
|
@@ -35,7 +35,7 @@ import { resolveEngineDir, readEngineFragment, PROCEDURES_FRAGMENT_REL } from '.
|
|
|
35
35
|
import { plansInFlight, PLANS_REL } from './plan-files.mjs';
|
|
36
36
|
// The family's ONE shell quoter for a RENDERED command operand (bare when the value is already safe,
|
|
37
37
|
// single-quoted otherwise) — the same leaf eight other command renderers here read through.
|
|
38
|
-
import { shellQuoteArg } from './repo-lex.mjs';
|
|
38
|
+
import { shellQuoteArg, isSeedablePathToken, isRenderableLine, escapeForDisplay, isArtifactPathCarriable } from './repo-lex.mjs';
|
|
39
39
|
// The config schema/read core (orchestration-config.mjs, the single config contract): the reader +
|
|
40
40
|
// the SHARED slot/recipe validity, never the fs-writer (orchestration-write.mjs) DIRECTLY — the
|
|
41
41
|
// import-split test pins the direct-import rule.
|
|
@@ -46,7 +46,10 @@ import { AUTONOMY_REL, loadAutonomy, resolveAutonomy, isSparseSeedConfig } from
|
|
|
46
46
|
// orchestration-write (the import-split test pins both direct rules; the TRANSITIVE claim is
|
|
47
47
|
// structural — test/read-graph-purity.test.mjs pins it).
|
|
48
48
|
import { resolveFlowStorePath, readFlowStore } from './flow-store-read.mjs';
|
|
49
|
+
import { readDelegationLedger } from './dispatch-store-read.mjs';
|
|
50
|
+
import { foldLaneLines, HELD_RECEIPT_BACKEND, judgeLedger } from './held-session.mjs';
|
|
49
51
|
import { CHAIN_KIND } from './flow-record.mjs';
|
|
52
|
+
import { readRegistration } from './mcp-registration.mjs';
|
|
50
53
|
// The declared source-size practice (D-17 U1), read through the practice's PURE READ core — never
|
|
51
54
|
// source-size-check.mjs, which owns the writer half: this advisor is a read root of
|
|
52
55
|
// test/read-graph-purity.test.mjs, and the core exists so a surface can ask without reaching a writer.
|
|
@@ -166,7 +169,7 @@ const resolveAllSlots = ({ activity, config, detection, overrides }) => {
|
|
|
166
169
|
// planRecipe's drift-guarded dispatch for WHICH backends, then resolve each (backend, role) to its
|
|
167
170
|
// manifest wrapper cmd via the bridge registry — no wrapper name is hand-composed here. A vehicle
|
|
168
171
|
// step is NOT a bridge: it carries its own state and is never looked up in a manifest.
|
|
169
|
-
const { dispatch } = planRecipe(resolved.recipe, detection);
|
|
172
|
+
const { dispatch } = resolved.roster ? { dispatch: [] } : planRecipe(resolved.recipe, detection);
|
|
170
173
|
const vehicles = dispatch.filter((d) => d.vehicle != null).map((d) => ({ backend: d.backend, state: d.vehicle }));
|
|
171
174
|
const bridged = dispatch.filter((d) => d.vehicle == null);
|
|
172
175
|
const backends = bridged.map((d) => wrapperCmdFor(d.backend, d.role)).filter(Boolean);
|
|
@@ -189,8 +192,9 @@ const resolveAllSlots = ({ activity, config, detection, overrides }) => {
|
|
|
189
192
|
// carrier-typed slot (registry-driven — no slot name is spelled here), solo when it has none.
|
|
190
193
|
const effectiveCarrier = (slots) => slots.find((s) => s.slotType === 'carrier')?.recipe ?? 'solo';
|
|
191
194
|
|
|
192
|
-
// An unsatisfiable EXPLICIT override is
|
|
193
|
-
//
|
|
195
|
+
// An unsatisfiable EXPLICIT override is a warning (loud, flagged for the agent to relay); a graceful
|
|
196
|
+
// config/default degradation remains a per-slot reason. Canon/registry slot-skew warnings are
|
|
197
|
+
// collected separately, after the live section is read.
|
|
194
198
|
const collectWarnings = (slots) =>
|
|
195
199
|
slots
|
|
196
200
|
.filter((s) => s.overrideUnsatisfied)
|
|
@@ -216,24 +220,34 @@ const backendSetLabel = (backends) =>
|
|
|
216
220
|
: ` → ${backends[0]}`;
|
|
217
221
|
|
|
218
222
|
// The review-loop economics block (M1 + M6's firing half) — printed when the activity engages a review
|
|
219
|
-
//
|
|
223
|
+
// member: a slot RESOLVING reviewed | council, a roster, or one whose config or override REQUESTED a
|
|
224
|
+
// review recipe and degraded (readiness removes no configured obligation, and the table still judges
|
|
225
|
+
// it); omitted only for a solo nobody asked to be otherwise. It paraphrases the procedures.md
|
|
220
226
|
// Fold + loop step + orchestration §4 canon (no rival rule): the ≤2-round architecture cap, the bar met by RAISING a
|
|
221
227
|
// surviving major to an acceptance invariant (not exhausting prose), backend divergence = the crossover
|
|
222
228
|
// stop, the thin-plan/diff-review carve-out, a self-consistency read before every re-review, and the
|
|
223
229
|
// REQUIRED per-round structured emission {round N · finding-origin tally · per-backend verdict}. Only a
|
|
224
|
-
// review slot can resolve reviewed|council (execute floors at solo|delegated), so
|
|
230
|
+
// review slot can resolve or request reviewed|council (execute floors at solo|delegated), so the gate
|
|
231
|
+
// reads the recipe, the requested recipe and the roster.
|
|
225
232
|
const REVIEW_RECIPES = new Set(['reviewed', 'council']);
|
|
233
|
+
const CONSULT_LINE = ' • Before every fold of a finding raised by a review member (a bridge backend or a placed lens): ASK that member whether the proposed fold solves it and adds no new problem; WAIT for its answer, READ it, then edit only as accepted or corrected — agy: agy-review --continue --decided @f --focus "Finding: <finding>. Proposed fold: <exact fold>. Does this proposed fold solve the finding and add no new problem? Reply accept, or correct with exact replacement text."; codex: fresh codex-review plan <consult-brief> written before the working tree changes; a placed lens: re-dispatch the same lens vehicle with the finding and the proposed fold. A self-review finding, or any finding when no review member ran, is folded directly — the exemption is the finding\'s ORIGIN, never the recipe word on the slot line.';
|
|
234
|
+
const ARMED_CONSULT_LINE = ' • ARMED pre-fold sequence for a bridge-raised finding: the round is open → dispatch that bridge\'s consult with a nonce → WAIT and READ → fold accepted or corrected → flow-writer consult-attestation <planId> --backend <id> --nonce <n> --proposed-fix-digest <the-sha256-of-the-fold-text> → then edit. A lens-raised finding re-dispatches the lens without a nonce, WAIT and READ, then edit as accepted or corrected — it mints no manifest and no attestation — only its per-round participation rides its internal-attestation.';
|
|
226
235
|
// activity-aware (AD-046): the triage classification vocabulary rides EVERY review-backed activity;
|
|
227
236
|
// the LEDGER pointer renders ONLY for plan-execution — the ledger is plan-execution-scoped (AD-045),
|
|
228
237
|
// and pointing plan-authoring at it would send rounds of the wrong activity into the code loop's gate.
|
|
229
|
-
|
|
230
|
-
|
|
238
|
+
// A lens-only roster resolves to `solo` and a fully degraded request resolves to `solo`: both still run
|
|
239
|
+
// a review round, which is why the gate never reads the effective recipe alone.
|
|
240
|
+
const reviewLoopAdvice = (slots, activity, flowArmed = false, plans = []) =>
|
|
241
|
+
slots.some((s) => s.roster != null || REVIEW_RECIPES.has(s.recipe) || REVIEW_RECIPES.has(s.degradedFrom))
|
|
231
242
|
? [
|
|
232
243
|
'Review-loop economics (procedures.md Fold + loop · orchestration.md §4) — the review this recipe runs:',
|
|
244
|
+
CONSULT_LINE,
|
|
245
|
+
...(activity === 'plan-execution' && flowArmed ? [ARMED_CONSULT_LINE] : []),
|
|
233
246
|
' • Cap architecture plan-review at ≤2 rounds; the bar is met by RAISING a surviving major to an acceptance invariant (or handing it to Execute/diff-review), never by exhausting the strictest backend.',
|
|
234
247
|
' • Backend divergence (one backend grounded-ships while another keeps revising mechanics) IS the crossover stop.',
|
|
235
248
|
' • Route an all-mechanics/CI or prose-only artifact to a thin plan + diff-review; run a self-consistency read before every re-review.',
|
|
236
249
|
' • Each round MUST emit {round N · finding-origin tally (first-draft / fold-induced / mechanics) · per-backend verdict} so the crossover is a computed signal.',
|
|
250
|
+
...(activity === 'plan-authoring' ? roundRenderAdvice(slots, plans) : []),
|
|
237
251
|
' • At the cap, classify every surviving blocking finding: fixable-bug (fold ONCE as a red→green test, re-review) / inherent-layer-residual (document + raise to an acceptance criterion) / escalate (the maintainer decides); a minor never forces triage.',
|
|
238
252
|
...(activity === 'plan-execution'
|
|
239
253
|
? [
|
|
@@ -246,38 +260,119 @@ const reviewLoopAdvice = (slots, activity) =>
|
|
|
246
260
|
// The grounding pre-step (AD-038, extending the AD-033 verbatim-contract rendering): whenever the
|
|
247
261
|
// resolved dispatch includes agy-review, print the CONCRETE facts-assembly invocation + the
|
|
248
262
|
// --facts form as a copy-paste pre-step — population, not placeholders. Plan-path population rule
|
|
249
|
-
// (the review-state plan-in-flight detector): exactly ONE plan in flight → render it
|
|
250
|
-
// zero or
|
|
263
|
+
// (the review-state plan-in-flight detector): exactly ONE renderable plan in flight → render it
|
|
264
|
+
// populated; zero, several, or a name a one-line render cannot carry → the explicit placeholder + a
|
|
265
|
+
// one-line discovery caveat; a name the receipt encoder refuses falls back on the receipt-minting
|
|
266
|
+
// or receipt-matching command only (`agy-review plan`, the round table). The
|
|
251
267
|
// suggested --out lives OUTSIDE the repo (/tmp) — grounding.mjs refuses a non-scratch destination.
|
|
252
268
|
// Exported for the bridge-tier byte-parity pin (AD-044 Plan 4): the velocity tier seeds the
|
|
253
269
|
// grounding rule in EXACTLY this rendered spelling — `node "${GROUNDING_TOOL}"` — so seeded and
|
|
254
270
|
// rendered forms can never drift apart.
|
|
255
271
|
export const GROUNDING_TOOL = join(dirname(fileURLToPath(import.meta.url)), 'grounding.mjs');
|
|
272
|
+
export const REVIEW_ROUNDS_TOOL = join(dirname(fileURLToPath(import.meta.url)), 'review-rounds-cli.mjs');
|
|
273
|
+
export const REPO_SEARCH_TOOL = join(dirname(fileURLToPath(import.meta.url)), 'repo-search.mjs');
|
|
256
274
|
const GROUNDING_FACTS_OUT = '/tmp/review-facts.md';
|
|
275
|
+
const MINTS_RECEIPT = Object.freeze({ mintsReceipt: true });
|
|
276
|
+
|
|
277
|
+
// The round table judges what review-rounds-cli resolves from the config (S27), never this render's
|
|
278
|
+
// override or degradation, so the advisor states that fact beneath the command instead of predicting
|
|
279
|
+
// when the two agree; a bridge-less roster gets the fact that no receipt can exist in the command's place.
|
|
280
|
+
// A requested review recipe renders the command even when every bridge is unavailable here: readiness
|
|
281
|
+
// degradation removes no configured obligation, so the table still has something to say.
|
|
282
|
+
const ROUND_RENDER_FACT_LINE = " ↳ the table judges the obligation review-rounds-cli resolves from docs/ai/orchestration.json (S27) — the configured recipe, or the computed default for a silent slot — never this run's --override or a degraded recipe; it reads receipts only: a backend that did not run shows as missing (no receipts when none ran), and its degrade record is judged by review-state and core-evidence summary, not here.";
|
|
283
|
+
const ROUND_RENDER_NO_BRIDGE_LINE = " • Round render: a roster with no bridge mints no receipt, so review-rounds-cli cannot supply this round's verdicts — emit the finding-origin tally plus each lens member's verdict (or silent) directly.";
|
|
284
|
+
const roundRenderAdvice = (slots, plans) => {
|
|
285
|
+
const review = slots.find((s) => s.slot === 'review');
|
|
286
|
+
if (review === undefined) return [];
|
|
287
|
+
const roster = review.roster ?? null;
|
|
288
|
+
const shouldRenderCommand = roster === null
|
|
289
|
+
? (review.backends ?? []).length > 0 || REVIEW_RECIPES.has(review.degradedFrom)
|
|
290
|
+
: roster.some((row) => row.kind === 'bridge');
|
|
291
|
+
if (!shouldRenderCommand) return [ROUND_RENDER_NO_BRIDGE_LINE];
|
|
292
|
+
const lensMembers = (roster ?? []).filter((row) => row.kind === 'lens').map((row) => row.member);
|
|
293
|
+
const operand = populatedPlan(plans, MINTS_RECEIPT);
|
|
294
|
+
return [
|
|
295
|
+
` • Round render (verdict half of the per-round emission; the finding-origin tally stays the orchestrator's): node ${renderToolPath(REVIEW_ROUNDS_TOOL)} --artifact ${operand ?? '<plan-file>'}`,
|
|
296
|
+
...planDiscoveryCaveat(plans, '--artifact', 'populate --artifact with the plan file under review.', operand === null ? ['--artifact'] : []),
|
|
297
|
+
ROUND_RENDER_FACT_LINE,
|
|
298
|
+
...(lensMembers.length > 0 ? [` ↳ the table carries the bridge verdicts only — add each lens member's verdict (or silent) by hand: ${lensMembers.join(', ')}`] : []),
|
|
299
|
+
];
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// The ONE plan in flight as a pasteable operand: shell-significant bytes ride shellQuoteArg; a name a
|
|
303
|
+
// one-line render cannot carry is never populated, nor one the receipt encoder refuses (S21) for a
|
|
304
|
+
// command that MINTS or MATCHES a plan receipt: agy-review plan would be refused pre-spend by name, the
|
|
305
|
+
// round table would match no receipt.
|
|
306
|
+
const populatedPlan = (plans, { mintsReceipt = false } = {}) => {
|
|
307
|
+
if (plans.length !== 1 || !isRenderableLine(plans[0])) return null;
|
|
308
|
+
if (mintsReceipt && !isArtifactPathCarriable(plans[0])) return null;
|
|
309
|
+
return shellQuoteArg(`${PLANS_REL}/${plans[0]}`);
|
|
310
|
+
};
|
|
311
|
+
const planDiscoveryCaveat = (plans, flag, noPlanAction, fellBack = []) => {
|
|
312
|
+
if (plans.length === 0) return [` ↳ plan discovery: no plan in flight under ${PLANS_REL} — ${noPlanAction}`];
|
|
313
|
+
if (plans.length > 1) return [` ↳ plan discovery: ${plans.length} plans in flight under ${PLANS_REL} (${plans.map(escapeForDisplay).join(', ')}) — populate ${flag} with the one under review.`];
|
|
314
|
+
return fellBack.length === 0
|
|
315
|
+
? []
|
|
316
|
+
: [` ↳ plan discovery: the plan in flight ${escapeForDisplay(plans[0])} carries a character that either a review receipt or a rendered command cannot carry — ${fellBack.join(' and ')} ${fellBack.length === 1 ? 'stays a placeholder' : 'stay placeholders'}; rename the plan.`];
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// The kit-tools tier seeds `Bash(node <abs>/tools/repo-search.mjs:*)` BARE (velocity-profile.mjs
|
|
320
|
+
// deriveKitToolsAllowlist, the same predicate), so a seedable path renders bare — a quoted path is
|
|
321
|
+
// a different prefix and the allow rule is dead. A path the tier could never seed has no rule to
|
|
322
|
+
// match and is single-quoted for a safe paste (double quotes would still expand `$` and backticks).
|
|
323
|
+
const renderToolPath = (abs) => (isSeedablePathToken(abs) ? abs : shellQuoteArg(abs));
|
|
324
|
+
// `registered` is the registration on disk, never the tool's availability in this session — the
|
|
325
|
+
// command line stays as the fallback under the typed form. `toolPath` is a test seam (an unseedable
|
|
326
|
+
// kit path is not constructible from a test against the real checkout).
|
|
327
|
+
const readersSweepAdvice = (activity, registered, toolPath = REPO_SEARCH_TOOL) => {
|
|
328
|
+
if (activity !== 'plan-authoring') return [];
|
|
329
|
+
return [
|
|
330
|
+
'Readers sweep (before the first review) — for every changed config key, registry entry, exported constant, receipt field or canon sentence:',
|
|
331
|
+
...(registered ? [' use: repo_search {"pattern": "<the literal>"} — the kit\'s read-only MCP tool (registration complete; if it is not loaded in this session, the command below)'] : []),
|
|
332
|
+
` run: node ${renderToolPath(toolPath)} --pattern <the literal> (a pattern carrying a shell-significant byte goes through --pattern-file <f> instead)`,
|
|
333
|
+
' classify every reader as a ledger row, a stated non-goal, or unchanged with its proof',
|
|
334
|
+
];
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// The canon's own claim — the kit "parses only each section's `Slots:` line" — made true: an installed
|
|
338
|
+
// engine older (or newer) than this kit's registry prints its section verbatim, so the skew is SAID.
|
|
339
|
+
// A section without a Slots line is a customized canon, not a skew: silent. Never the exit code.
|
|
340
|
+
const SLOTS_LINE = /^Slots:[ \t]*(.+?)[ \t]*$/mu;
|
|
341
|
+
const slotSkewWarning = (section, activity) => {
|
|
342
|
+
const listed = section.match(SLOTS_LINE)?.[1].split(',').map((s) => s.trim()).filter(Boolean);
|
|
343
|
+
const registry = Object.keys(ACTIVITIES[activity].slots);
|
|
344
|
+
if (listed == null || listed.join(',') === registry.join(',')) return [];
|
|
345
|
+
return [`the installed engine's canon lists slots (${listed.join(', ')}) for ${activity} while this kit's registry names (${registry.join(', ')}) — the resolved lines follow the registry; the engine and this kit are out of step — /agent-workflow-kit status names which member is behind: upgrade that one. Tell the user.`];
|
|
346
|
+
};
|
|
257
347
|
const groundingPreStepAdvice = (activity, slots, plans) => {
|
|
258
348
|
if (!slots.some((s) => (s.backends ?? []).includes('agy-review'))) return [];
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
349
|
+
const operand = populatedPlan(plans);
|
|
350
|
+
const planArg = operand === null ? '--plan <path>' : `--plan ${operand}`;
|
|
351
|
+
const reviewOperand = populatedPlan(plans, MINTS_RECEIPT);
|
|
352
|
+
// plan-authoring reviews the plan FILE — a plain name in flight renders the review command populated;
|
|
353
|
+
// the renderability and receipt-carriability fallbacks are the only placeholders a known path produces.
|
|
262
354
|
const reviewForm =
|
|
263
355
|
activity === 'plan-authoring'
|
|
264
|
-
?
|
|
265
|
-
?
|
|
266
|
-
:
|
|
356
|
+
? reviewOperand === null
|
|
357
|
+
? 'agy-review plan <plan-file>'
|
|
358
|
+
: `agy-review plan ${reviewOperand}`
|
|
267
359
|
: 'agy-review code';
|
|
268
360
|
// `run:`/`then:` prefixes keep these POPULATED command lines machine-distinguishable from the
|
|
269
361
|
// verbatim contract DESCRIPTORS above (the descriptor drift guard set-equals bare wrapper lines).
|
|
270
|
-
//
|
|
362
|
+
// The TOOL path stays double-quoted (the bridge tier seeds that exact byte-form); the plan operand
|
|
363
|
+
// rides shellQuoteArg — bare when safe, single-quoted otherwise.
|
|
271
364
|
const lines = [
|
|
272
365
|
'Grounding pre-step (agy is dispatched — assemble the verified facts BEFORE the review; grounding.mjs slices verbatim, judgment additions stay yours):',
|
|
273
366
|
` run: node "${GROUNDING_TOOL}" --constraints --autonomy ${planArg} --out ${GROUNDING_FACTS_OUT}`,
|
|
274
367
|
` then: ${reviewForm} --facts @${GROUNDING_FACTS_OUT}`,
|
|
275
368
|
];
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
369
|
+
const fellBack = [operand === null ? '--plan' : null, activity === 'plan-authoring' && reviewOperand === null ? 'agy-review plan' : null].filter(Boolean);
|
|
370
|
+
lines.push(...planDiscoveryCaveat(
|
|
371
|
+
plans,
|
|
372
|
+
'--plan',
|
|
373
|
+
'substitute the plan file you are reviewing against, or drop --plan for constraints+autonomy facts.',
|
|
374
|
+
fellBack,
|
|
375
|
+
));
|
|
281
376
|
return lines;
|
|
282
377
|
};
|
|
283
378
|
|
|
@@ -486,7 +581,22 @@ const contractLines = ({ cmd, contract, settings }) => {
|
|
|
486
581
|
return lines;
|
|
487
582
|
};
|
|
488
583
|
|
|
489
|
-
const
|
|
584
|
+
const foldLaneAdvice = ({
|
|
585
|
+
activity,
|
|
586
|
+
slots,
|
|
587
|
+
cwd,
|
|
588
|
+
env,
|
|
589
|
+
resolveStore,
|
|
590
|
+
readStore,
|
|
591
|
+
audit,
|
|
592
|
+
readHead,
|
|
593
|
+
}) => {
|
|
594
|
+
if (activity !== 'plan-execution' || slots.find((slot) => slot.slot === 'execute')?.recipe !== 'delegated') return [];
|
|
595
|
+
const ledger = readDelegationLedger(cwd, env, { resolveStore, readStore, audit, readHead });
|
|
596
|
+
return foldLaneLines(judgeLedger(ledger, { backend: HELD_RECEIPT_BACKEND, degrades: [] }));
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flowHalves, flowArmed, readersSweep, declaredPractice, foldScope, specCheck, foldLane }) => {
|
|
490
600
|
const lines = [
|
|
491
601
|
section,
|
|
492
602
|
'',
|
|
@@ -506,13 +616,15 @@ const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flow
|
|
|
506
616
|
for (const line of dispatchForm({ activity, slot: s.slot, state: v.state })) lines.push(` ${line}`);
|
|
507
617
|
}
|
|
508
618
|
for (const c of s.contracts ?? []) lines.push(...contractLines(c));
|
|
619
|
+
if (s.slot === 'execute' && foldLane.length) lines.push('', ...foldLane);
|
|
509
620
|
}
|
|
510
621
|
if ((flowHalves ?? []).length) lines.push('', ...flowHalves);
|
|
511
622
|
const autonomyBlock = autonomyAdvice(activity, autonomy);
|
|
512
623
|
if (autonomyBlock.length) lines.push('', ...autonomyBlock);
|
|
624
|
+
if (readersSweep.length) lines.push('', ...readersSweep);
|
|
513
625
|
const grounding = groundingPreStepAdvice(activity, slots, plans);
|
|
514
626
|
if (grounding.length) lines.push('', ...grounding);
|
|
515
|
-
const advice = reviewLoopAdvice(slots, activity);
|
|
627
|
+
const advice = reviewLoopAdvice(slots, activity, flowArmed, plans);
|
|
516
628
|
if (advice.length) lines.push('', ...advice);
|
|
517
629
|
if (foldScope.length) lines.push('', ...foldScope);
|
|
518
630
|
if (specCheck.length) lines.push('', ...specCheck);
|
|
@@ -525,15 +637,19 @@ const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flow
|
|
|
525
637
|
return lines.join('\n');
|
|
526
638
|
};
|
|
527
639
|
|
|
528
|
-
const buildJson = ({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves, declaredPractice, foldScope, specCheck }) => ({
|
|
640
|
+
const buildJson = ({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves, flowArmed, readersSweep, declaredPractice, foldScope, specCheck, foldLane }) => ({
|
|
529
641
|
activity,
|
|
530
642
|
section,
|
|
531
643
|
slots: Object.fromEntries(
|
|
532
644
|
// `backends: string[]` is the STABLE pre-existing shape (wrapper names) — never repurposed.
|
|
533
645
|
// `contracts` is the ADDITIVE per-dispatch driving-contract field (empty for solo).
|
|
534
|
-
slots.map((s) => [s.slot, {
|
|
646
|
+
slots.map((s) => [s.slot, {
|
|
647
|
+
recipe: s.recipe, source: s.source, degradedFrom: s.degradedFrom, reason: s.reason, backends: s.backends, contracts: s.contracts,
|
|
648
|
+
...(s.roster ? { roster: s.roster } : {}),
|
|
649
|
+
}]),
|
|
535
650
|
),
|
|
536
|
-
reviewLoop: reviewLoopAdvice(slots, activity),
|
|
651
|
+
reviewLoop: reviewLoopAdvice(slots, activity, flowArmed, plans),
|
|
652
|
+
readersSweep,
|
|
537
653
|
// ADDITIVE (AD-038): the populated grounding pre-step, structured (empty when agy is not dispatched).
|
|
538
654
|
groundingPreStep: groundingPreStepAdvice(activity, slots, plans),
|
|
539
655
|
// ADDITIVE (cost-tiered execution): the unconditional cost-lane advisory, structured.
|
|
@@ -542,6 +658,7 @@ const buildJson = ({ activity, section, slots, configSource, warnings, plans, au
|
|
|
542
658
|
foldScope,
|
|
543
659
|
// ADDITIVE (spec layer 2b): the spec-store block, structured (empty outside plan-execution).
|
|
544
660
|
specCheck,
|
|
661
|
+
foldLane,
|
|
545
662
|
// ADDITIVE (AD-044 Plan 4): the per-activity autonomy block, structured (empty when unresolvable).
|
|
546
663
|
autonomy: autonomyAdvice(activity, autonomy),
|
|
547
664
|
// ADDITIVE (D-17 U1): the SAME composed lines the human render prints — one array, two renders, so
|
|
@@ -608,7 +725,7 @@ export const main = (argv, ctx = {}) => {
|
|
|
608
725
|
),
|
|
609
726
|
});
|
|
610
727
|
const slots = resolveAllSlots({ activity, config, detection, overrides });
|
|
611
|
-
const warnings = [...detectWarnings, ...collectWarnings(slots)];
|
|
728
|
+
const warnings = [...detectWarnings, ...collectWarnings(slots), ...slotSkewWarning(section, activity)];
|
|
612
729
|
const plans = plansInFlight(cwd);
|
|
613
730
|
// The autonomy facts (AD-044 Plan 4): resolved levels + red-lines from the policy file. A
|
|
614
731
|
// malformed policy renders LOUDLY in the block AND flips the exit to 1 (config error) — the
|
|
@@ -628,13 +745,26 @@ export const main = (argv, ctx = {}) => {
|
|
|
628
745
|
// The flow armed-halves block (P8): probed ONLY when the config carries a flow block — an
|
|
629
746
|
// unarmed project keeps byte-identical output (human AND JSON) and never pays the store probe.
|
|
630
747
|
const flowProbe = ctx.flowProbe ?? defaultFlowProbe;
|
|
631
|
-
const
|
|
748
|
+
const flowState = config?.flow == null ? null : flowProbe(cwd);
|
|
749
|
+
const flowHalves = flowState == null ? null : flowHalvesAdvice(config.flow, flowState);
|
|
750
|
+
const registrationReader = ctx.readRegistration ?? readRegistration;
|
|
751
|
+
const readersSweep = readersSweepAdvice(activity, activity === 'plan-authoring' && registrationReader(cwd).registered, ctx.repoSearchTool);
|
|
632
752
|
const declaredPractice = declaredPracticeAdvice(cwd, readFile, lstat);
|
|
633
753
|
const foldScope = foldScopeAdvice(activity, config, plans);
|
|
634
754
|
const specCheck = specCheckAdvice(activity);
|
|
755
|
+
const foldLane = foldLaneAdvice({
|
|
756
|
+
activity,
|
|
757
|
+
slots,
|
|
758
|
+
cwd,
|
|
759
|
+
env,
|
|
760
|
+
resolveStore: ctx.resolveDelegationStorePath,
|
|
761
|
+
readStore: ctx.readDelegationStore,
|
|
762
|
+
audit: ctx.auditDelegationStoreSemantics,
|
|
763
|
+
readHead: ctx.readHeadInstant,
|
|
764
|
+
});
|
|
635
765
|
const stdout = json
|
|
636
|
-
? JSON.stringify(buildJson({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves, declaredPractice, foldScope, specCheck }), null, 2)
|
|
637
|
-
: formatHuman({ activity, section, slots, warnings, plans, autonomy, flowHalves, declaredPractice, foldScope, specCheck });
|
|
766
|
+
? JSON.stringify(buildJson({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves, flowArmed: flowState?.armed === true, readersSweep, declaredPractice, foldScope, specCheck, foldLane }), null, 2)
|
|
767
|
+
: formatHuman({ activity, section, slots, warnings, plans, autonomy, flowHalves, flowArmed: flowState?.armed === true, readersSweep, declaredPractice, foldScope, specCheck, foldLane });
|
|
638
768
|
if (autonomy?.error) {
|
|
639
769
|
return { code: 1, stdout, stderr: `procedures: malformed ${AUTONOMY_REL} — ${autonomy.error}` };
|
|
640
770
|
}
|
package/tools/recipes.mjs
CHANGED
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// A "recipe" is a NAMED orchestration pattern over the family's optional carriers: the
|
|
5
|
-
// subscription-CLI bridges (codex-cli-bridge → `codex`, antigravity-cli-bridge → `agy`) and the
|
|
6
|
-
// executor subagent vehicle. The ENGINE owns the canonical narrative
|
|
7
|
-
// (agent-workflow-engine/references/orchestration.md, kept in lockstep by the recipe-name parity
|
|
8
|
-
// guard in recipes.test.mjs); this module owns the EXECUTABLE dispatch. The activity/slot registry
|
|
9
|
-
// lives in carriers.mjs, re-exported here so every importer keeps its './recipes.mjs' path.
|
|
10
|
-
// Invariants: pure (no fs/network/CLI in planRecipe/recommendRecipe), read-only, NEVER runs a
|
|
11
|
-
// subscription CLI. The orchestrator executes a recipe and always makes the single commit; a
|
|
12
|
-
// backend or a subagent is advisory or delegated, never autonomous. Dependency-free, Node >= 22.
|
|
2
|
+
// Read-only recipe planner. The engine owns the narrative; carriers.mjs owns the activity/slot
|
|
3
|
+
// registry re-exported here. Pure planning functions never run a subscription CLI.
|
|
13
4
|
|
|
14
5
|
import { existsSync, readFileSync } from 'node:fs';
|
|
15
6
|
import { dirname, join, resolve } from 'node:path';
|
|
16
|
-
// The READ-ONLY settings core, never the writer: this advisor never pulls in the atomic-write core.
|
|
17
7
|
import { settingsSnapshot, DEFAULT_BUNDLE_ROOT } from './bridge-settings-read.mjs';
|
|
18
8
|
import { isDirectRun } from './direct-run.mjs';
|
|
19
9
|
import {
|
|
@@ -29,10 +19,13 @@ import {
|
|
|
29
19
|
vehicleDegradeReason,
|
|
30
20
|
EXECUTOR_APPLY,
|
|
31
21
|
safeLine,
|
|
22
|
+
DISPLAY_ALIASES,
|
|
23
|
+
BACKEND_PRIORITY,
|
|
32
24
|
} from './carriers.mjs';
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
import {
|
|
25
|
+
import { surveyExecutorVehicle, surveyVehicle } from './cheap-agents-read.mjs';
|
|
26
|
+
import { obligationsOf } from './review-roster.mjs';
|
|
27
|
+
import { resolveRoster, activeLineCell, remedyFor } from './review-roster-resolve.mjs';
|
|
28
|
+
import { posturesByBackend } from './bridge-posture.mjs';
|
|
36
29
|
import {
|
|
37
30
|
detectBackends,
|
|
38
31
|
wrapperCmdFor,
|
|
@@ -43,32 +36,32 @@ import {
|
|
|
43
36
|
DEGRADED,
|
|
44
37
|
} from './detect-backends.mjs';
|
|
45
38
|
|
|
46
|
-
export { ACTIVITIES, POLICY_ACTIVITIES, SLOT_RECIPES, isSwitchSlot, EXECUTOR_APPLY, safeLine };
|
|
39
|
+
export { ACTIVITIES, POLICY_ACTIVITIES, SLOT_RECIPES, isSwitchSlot, EXECUTOR_APPLY, safeLine, DISPLAY_ALIASES };
|
|
47
40
|
|
|
48
|
-
const CODEX =
|
|
49
|
-
const AGY = 'antigravity-cli-bridge';
|
|
41
|
+
const [CODEX, AGY] = BACKEND_PRIORITY;
|
|
50
42
|
|
|
51
|
-
//
|
|
52
|
-
export const DISPLAY_ALIASES = { [CODEX]: 'codex', [AGY]: 'agy' };
|
|
53
|
-
|
|
54
|
-
// The provider → role table, keyed by the name the readiness array carries, NOT the display alias.
|
|
55
|
-
// The bridge rows are drift-guarded against each capability.json `provides[]`; the executor row is
|
|
56
|
-
// the vehicle withVehicle appends, and the ONLY provider of `carry`.
|
|
43
|
+
// Keyed by readiness-array provider name; executor is the only `carry` provider.
|
|
57
44
|
export const BACKEND_ROLES = {
|
|
58
45
|
[CODEX]: ['execute', 'review'],
|
|
59
46
|
[AGY]: ['review', 'probe'],
|
|
60
47
|
[EXECUTOR_PROVIDER]: [CARRY_ROLE],
|
|
61
48
|
};
|
|
62
49
|
|
|
63
|
-
// Review obligations from the CONFIGURED
|
|
64
|
-
// the readiness-degraded one.
|
|
65
|
-
export const requiredBackendsForConfiguredRecipe = ({ config, readiness = [], detectionFailed = false } = {}) => {
|
|
66
|
-
|
|
50
|
+
// Review obligations from the CONFIGURED activity's review recipe — the RAW config value, never
|
|
51
|
+
// the readiness-degraded one. The default preserves review-state and flow-check's backend set (#42).
|
|
52
|
+
export const requiredBackendsForConfiguredRecipe = ({ config, readiness = [], detectionFailed = false, activity = 'plan-execution' } = {}) => {
|
|
53
|
+
if (!Object.hasOwn(ACTIVITIES, activity) || !Object.hasOwn(ACTIVITIES[activity].slots, 'review')) {
|
|
54
|
+
throw new Error(`activity "${activity}" has no review slot`);
|
|
55
|
+
}
|
|
56
|
+
const configured = config?.[activity]?.review;
|
|
67
57
|
const providers = Object.values(DISPLAY_ALIASES); // every review-capable backend, codex first
|
|
68
58
|
if (configured == null && detectionFailed) {
|
|
69
59
|
// No config + no readiness signal: the computed default is UNKNOWABLE — fail closed upstream.
|
|
70
60
|
return { recipe: null, source: 'default', backends: [], minShip: 0, perBackend: false, unknowable: true };
|
|
71
61
|
}
|
|
62
|
+
if (Array.isArray(configured)) {
|
|
63
|
+
return { ...obligationsOf(configured), source: 'config', unknowable: false };
|
|
64
|
+
}
|
|
72
65
|
// Role-filtered: a ready EXECUTOR vehicle is a carry provider, never a reviewer, so it must not
|
|
73
66
|
// turn a silent review config into `reviewed`.
|
|
74
67
|
const anyReady = readyProvidersOf('review', readiness).length >= 1;
|
|
@@ -90,8 +83,6 @@ export const BACKEND_META = {
|
|
|
90
83
|
},
|
|
91
84
|
};
|
|
92
85
|
|
|
93
|
-
// Deterministic tie-break: codex before agy — the more reliable default for substantive reviews.
|
|
94
|
-
const BACKEND_PRIORITY = [CODEX, AGY];
|
|
95
86
|
const priorityIndex = (name) => {
|
|
96
87
|
const i = BACKEND_PRIORITY.indexOf(name);
|
|
97
88
|
return i === -1 ? BACKEND_PRIORITY.length : i;
|
|
@@ -137,14 +128,6 @@ export const RECIPES = [
|
|
|
137
128
|
|
|
138
129
|
const recipeById = (id) => RECIPES.find((r) => r.id === id);
|
|
139
130
|
|
|
140
|
-
// Read-only file-presence remedies — never a claim about whether a backend's service is responsive.
|
|
141
|
-
const READINESS_REASON = {
|
|
142
|
-
[NEEDS_SKILL]: 'bridge skill not installed — run /agent-workflow-kit setup',
|
|
143
|
-
[NEEDS_CLI]: 'the CLI is not installed',
|
|
144
|
-
[NEEDS_CREDENTIALS]: 'not signed in (credentials missing)',
|
|
145
|
-
[DEGRADED]: 'wrapper not on PATH — run /agent-workflow-kit setup',
|
|
146
|
-
};
|
|
147
|
-
|
|
148
131
|
// ── pure planner ───────────────────────────────────────────────────────────────
|
|
149
132
|
|
|
150
133
|
const providersOf = (role, detection) => detection.filter((b) => (BACKEND_ROLES[b.name] ?? []).includes(role));
|
|
@@ -171,7 +154,7 @@ const degradeReason = (recipe, detection) => {
|
|
|
171
154
|
.filter((b) => b.readiness !== READY)
|
|
172
155
|
.map((b) => (b.name === EXECUTOR_PROVIDER
|
|
173
156
|
? vehicleDegradeReason(b.vehicle, EXECUTOR_APPLY)
|
|
174
|
-
: `${DISPLAY_ALIASES[b.name] ?? b.name}: ${
|
|
157
|
+
: `${DISPLAY_ALIASES[b.name] ?? b.name}: ${remedyFor({ readiness: b.readiness })}`))
|
|
175
158
|
.join('; ');
|
|
176
159
|
return `${recipe.title} needs ${recipe.minBackends} provider(s) providing ${recipe.role}, but only ${ready.length} ready${detail ? ` — ${detail}` : ''}`;
|
|
177
160
|
};
|
|
@@ -268,7 +251,7 @@ const computedDefaultForSlot = (slotType, detection) => {
|
|
|
268
251
|
// The effective recipe for ONE slot. Precedence: an explicit `override` (degrades LOUDLY, so the
|
|
269
252
|
// agent tells the user) > the `config` entry (graceful) > the computed default. Satisfiability and
|
|
270
253
|
// the lattice REUSE planRecipe — one source. Pure; never mutates.
|
|
271
|
-
export const resolveActivityRecipe = ({ config = {}, readiness = [], activity, slot, override } = {}) => {
|
|
254
|
+
export const resolveActivityRecipe = ({ config = {}, readiness = [], activity, slot, override, surveyLens, postures } = {}) => {
|
|
272
255
|
const activityDef = ACTIVITIES[activity];
|
|
273
256
|
if (!activityDef) throw new Error(`unknown activity: ${activity}`);
|
|
274
257
|
const slotType = activityDef.slots[slot];
|
|
@@ -278,6 +261,21 @@ export const resolveActivityRecipe = ({ config = {}, readiness = [], activity, s
|
|
|
278
261
|
const requested = override ?? configured ?? computedDefaultForSlot(slotType, readiness);
|
|
279
262
|
const source = override != null ? 'override' : configured != null ? 'config' : 'default';
|
|
280
263
|
|
|
264
|
+
if (Array.isArray(requested)) {
|
|
265
|
+
if (slotType !== 'review' || source !== 'config') {
|
|
266
|
+
throw new Error(`invalid roster for ${slotType} slot of "${activity}"`);
|
|
267
|
+
}
|
|
268
|
+
const obligations = obligationsOf(requested);
|
|
269
|
+
return {
|
|
270
|
+
recipe: obligations.recipe,
|
|
271
|
+
source,
|
|
272
|
+
degradedFrom: null,
|
|
273
|
+
reason: null,
|
|
274
|
+
overrideUnsatisfied: false,
|
|
275
|
+
roster: resolveRoster({ value: requested, readiness, surveyLens, postures }),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
281
279
|
// Defensive: the IO shell and the CLI validate first, so a stray value here is a programmer error
|
|
282
280
|
// — surfaced loudly rather than silently coerced into a neighbour recipe.
|
|
283
281
|
if (!(SLOT_RECIPES[slotType] ?? []).includes(requested)) {
|
|
@@ -300,42 +298,14 @@ export const resolveActivityRecipe = ({ config = {}, readiness = [], activity, s
|
|
|
300
298
|
};
|
|
301
299
|
};
|
|
302
300
|
|
|
303
|
-
//
|
|
304
|
-
// Machine-composed so the agent composes NOTHING factual (a session once echoed SKILL.md's canonical
|
|
305
|
-
// example while the detector said otherwise). Always exactly one line.
|
|
306
|
-
|
|
307
|
-
// The CONFIGURED dispatch posture: the bundled manifests' pins overlaid with the active
|
|
308
|
-
// bridge-settings. No pins or a corrupt bundle → null; the posture segment is a fact, never a gate.
|
|
301
|
+
// Configured posture from bundled manifest pins plus bridge settings; corruption returns null.
|
|
309
302
|
export const composeConfiguredPosture = (ctx = {}) => {
|
|
310
303
|
try {
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
const parts =
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
try {
|
|
317
|
-
manifest = JSON.parse(String(read(join(bundleRoot, name, 'capability.json'), 'utf8')));
|
|
318
|
-
} catch {
|
|
319
|
-
return null; // an unreadable/corrupt manifest is CORRUPTION — a partial posture would lie
|
|
320
|
-
}
|
|
321
|
-
if (!Object.hasOwn(manifest, 'posture')) continue; // a pre-D5 bridge — legitimate absence
|
|
322
|
-
const posture = manifest.posture;
|
|
323
|
-
// A present-but-invalid block nulls the WHOLE render — never a partial/mangled line.
|
|
324
|
-
const invalid =
|
|
325
|
-
posture === null || typeof posture !== 'object' || Array.isArray(posture) ||
|
|
326
|
-
typeof posture.model !== 'string' || posture.model.length === 0 ||
|
|
327
|
-
(Object.hasOwn(posture, 'effort') && (typeof posture.effort !== 'string' || posture.effort.length === 0)) ||
|
|
328
|
-
(Object.hasOwn(posture, 'tier') && posture.tier !== null && (typeof posture.tier !== 'string' || posture.tier.length === 0)) ||
|
|
329
|
-
Object.keys(posture).some((k) => !['model', 'effort', 'tier'].includes(k));
|
|
330
|
-
if (invalid) return null;
|
|
331
|
-
const seg = [`model=${posture.model}`];
|
|
332
|
-
if (Object.hasOwn(posture, 'effort')) seg.push(`effort=${posture.effort}`);
|
|
333
|
-
if (Object.hasOwn(posture, 'tier')) {
|
|
334
|
-
const knob = (ctx.settings?.active ?? []).find((s) => s.key === 'CODEX_SERVICE_TIER' && s.bridge === name);
|
|
335
|
-
seg.push(knob ? `tier=${knob.value} (bridge-settings)` : `tier=${posture.tier ?? 'standard'}`);
|
|
336
|
-
}
|
|
337
|
-
parts.push(`${DISPLAY_ALIASES[name] ?? name} ${seg.join(' ')}`);
|
|
338
|
-
}
|
|
304
|
+
const rows = posturesByBackend({ bundleRoot: ctx.bundleRoot ?? DEFAULT_BUNDLE_ROOT, settings: ctx.settings ?? { active: [] }, readFile: ctx.readFile ?? readFileSync });
|
|
305
|
+
if (Object.values(rows).some((row) => row.state === 'unreadable')) return null;
|
|
306
|
+
const parts = Object.entries(rows)
|
|
307
|
+
.filter(([, row]) => row.state === 'valid')
|
|
308
|
+
.map(([receiptId, row]) => `${receiptId} ${row.posture}`);
|
|
339
309
|
return parts.length ? parts.join(' · ') : null;
|
|
340
310
|
} catch {
|
|
341
311
|
return null;
|
|
@@ -422,19 +392,25 @@ export const composeAutonomyFacts = async (cwd, deps = {}) => {
|
|
|
422
392
|
// ONE line rendering the CONFIGURED recipe of every activity/slot with its source, degradation and
|
|
423
393
|
// dispatched wrappers — contrasted with the readiness-RECOMMENDED recipe, which is NOT what runs.
|
|
424
394
|
// The session-start checklist and the handover "Active recipes:" slot paste it verbatim.
|
|
425
|
-
export const composeActiveRecipeLine = ({ config, source } = {}, detection, autonomy = null) => {
|
|
395
|
+
export const composeActiveRecipeLine = ({ config, source } = {}, detection, autonomy = null, rosterDeps = null) => {
|
|
426
396
|
const cells = [];
|
|
427
397
|
for (const [activity, def] of Object.entries(ACTIVITIES)) {
|
|
428
398
|
const level = autonomy?.activities?.[activity]?.autonomy;
|
|
429
399
|
const auto = level ? `; autonomy ${level}` : '';
|
|
430
400
|
for (const [slot, slotType] of Object.entries(def.slots)) {
|
|
431
|
-
const r = resolveActivityRecipe({
|
|
432
|
-
|
|
401
|
+
const r = resolveActivityRecipe({
|
|
402
|
+
config: config ?? {}, readiness: detection, activity, slot,
|
|
403
|
+
surveyLens: rosterDeps?.surveyLens, postures: rosterDeps?.postures,
|
|
404
|
+
});
|
|
405
|
+
const dispatch = isSwitchSlot(slotType) || r.roster ? [] : planRecipe(r.recipe, detection).dispatch;
|
|
433
406
|
const wrappers = dispatch.map((d) => wrapperCmdFor(d.backend, d.role)).filter(Boolean);
|
|
434
407
|
const srcLabel = r.source === 'config' ? 'configured' : 'computed default';
|
|
408
|
+
const renderedValue = r.roster
|
|
409
|
+
? activeLineCell(r.roster, { states: rosterDeps !== null })
|
|
410
|
+
: r.recipe;
|
|
435
411
|
const head = r.degradedFrom
|
|
436
412
|
? `${activity}.${slot} = ${r.degradedFrom} (${srcLabel}; degrades here to ${r.recipe} — ${r.reason}${auto})`
|
|
437
|
-
: `${activity}.${slot} = ${
|
|
413
|
+
: `${activity}.${slot} = ${renderedValue} (${srcLabel}${isSwitchSlot(slotType) ? '; switch' : ''}${auto})`;
|
|
438
414
|
const suffix =
|
|
439
415
|
wrappers.length >= 2
|
|
440
416
|
? ` → every backend every round: ${wrappers.join(' + ')}`
|
|
@@ -553,7 +529,12 @@ exclusive. Detection only — never writes, never commits, never runs a subscrip
|
|
|
553
529
|
// Lazy: orchestration-config.mjs statically imports this module — no static cycle.
|
|
554
530
|
const { loadConfig } = await import('./orchestration-config.mjs');
|
|
555
531
|
try {
|
|
556
|
-
|
|
532
|
+
const snapshot = settingsSnapshot();
|
|
533
|
+
const surveyLens = deps.surveyLens ?? ((spec) => surveyVehicle(cwd, spec, deps));
|
|
534
|
+
console.log(composeActiveRecipeLine(
|
|
535
|
+
loadConfig(cwd), readiness, await composeAutonomyFacts(cwd),
|
|
536
|
+
{ surveyLens, postures: posturesByBackend({ settings: snapshot }) },
|
|
537
|
+
));
|
|
557
538
|
} catch (err) {
|
|
558
539
|
console.error(`[agent-workflow-kit] ${err.message}`);
|
|
559
540
|
return err.exitCode ?? 1;
|
package/tools/repo-lex.mjs
CHANGED
|
@@ -20,3 +20,43 @@ export const lexicalRepoRelative = (rel) => {
|
|
|
20
20
|
|
|
21
21
|
// POSIX single-quote for pasteable command rendering (display only — never an execution boundary).
|
|
22
22
|
export const shellQuoteArg = (s) => (/^[A-Za-z0-9_/.\-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`);
|
|
23
|
+
|
|
24
|
+
// The bytes a settings-level allow rule cannot see past: command separators, redirections,
|
|
25
|
+
// expansions and globs. ONE home for the seeder (velocity-profile.mjs, the allow-rule writer) and
|
|
26
|
+
// the renders that must spell a path in the seeded byte-form (procedures.mjs) — two predicates for
|
|
27
|
+
// one rule drift apart, and a drifted render is a dead allow rule that simply prompts.
|
|
28
|
+
export const SHELL_METACHARACTERS = Object.freeze([
|
|
29
|
+
'&', '|', ';', '<', '>', '$', '`', '(', ')',
|
|
30
|
+
'\n', '\r', '\t', '\\', '{', '}', '*', '?', '#', '~', '!',
|
|
31
|
+
]);
|
|
32
|
+
export const hasShellMetacharacter = (cmd) => SHELL_METACHARACTERS.some((ch) => cmd.includes(ch));
|
|
33
|
+
|
|
34
|
+
// A string a one-line render can carry: no control character (C0 or C1) and no Unicode line or
|
|
35
|
+
// paragraph separator. The receipt-derived fields are REFUSED on it; a plan name is escaped for display.
|
|
36
|
+
const LINE_BREAKING_SOURCE = '[\\p{Cc}\\p{Zl}\\p{Zp}]';
|
|
37
|
+
const LINE_BREAKING = new RegExp(LINE_BREAKING_SOURCE, 'u');
|
|
38
|
+
const LINE_BREAKING_ALL = new RegExp(LINE_BREAKING_SOURCE, 'gu');
|
|
39
|
+
export const isRenderableLine = (value) => typeof value === 'string' && !LINE_BREAKING.test(value);
|
|
40
|
+
export const escapeForDisplay = (value) => String(value).replace(LINE_BREAKING_ALL, (ch) => `\\u${ch.codePointAt(0).toString(16).padStart(4, '0')}`);
|
|
41
|
+
|
|
42
|
+
// The receipt encoder's carriability rule for an artifact path (S21), the JS twin of the wrappers'
|
|
43
|
+
// refuse_uncarriable_artifact_byte: a quote, a backslash, a C0 control or DEL — deliberately NOT
|
|
44
|
+
// \p{Cc} (C1 is the declared residual, and the two normalizations are parity-pinned on this set).
|
|
45
|
+
// ONE home: the round table's refusal names the byte, the advisor's fallback reads the boolean.
|
|
46
|
+
export const uncarriableArtifactByte = (value) =>
|
|
47
|
+
value.includes('"') ? 'a double quote' : value.includes('\\') ? 'a backslash' : /[\u0000-\u001f\u007f]/u.test(value) ? 'a control' : null;
|
|
48
|
+
export const isArtifactPathCarriable = (value) => typeof value === 'string' && uncarriableArtifactByte(value) === null;
|
|
49
|
+
|
|
50
|
+
// Characters that survive whitespace tokenization but break an UNQUOTED byte-exact path rule:
|
|
51
|
+
// shell quoting syntax and glob brackets (SHELL_METACHARACTERS owns the command-level separators/
|
|
52
|
+
// redirections/expansions — `*`/`?` globs included — but not these four).
|
|
53
|
+
const PATH_BREAKING_CHARACTERS = Object.freeze(["'", '"', '[', ']']);
|
|
54
|
+
|
|
55
|
+
// A path token that can be seeded UNQUOTED into a byte-exact allow rule: POSIX-absolute, no
|
|
56
|
+
// whitespace, no shell metacharacter, no quoting/glob syntax.
|
|
57
|
+
export const isSeedablePathToken = (token) =>
|
|
58
|
+
typeof token === 'string' &&
|
|
59
|
+
token.startsWith('/') &&
|
|
60
|
+
!/\s/u.test(token) &&
|
|
61
|
+
!hasShellMetacharacter(token) &&
|
|
62
|
+
!PATH_BREAKING_CHARACTERS.some((ch) => token.includes(ch));
|