@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
package/tools/procedures.mjs
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
//
|
|
4
4
|
// It composes the AD-018 orchestration recipes into NAMED activities: it reads the canonical procedure
|
|
5
5
|
// steps LIVE from the installed agent-workflow-engine (references/procedures.md — AD-016 live read, no
|
|
6
|
-
// bundled mirror), reads the per-project, hand-edited config (docs/ai/orchestration.json),
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
6
|
+
// bundled mirror), reads the per-project, hand-edited config (docs/ai/orchestration.json), composes the
|
|
7
|
+
// readiness every caller composes (detected backends + the executor vehicle), and prints the activity's
|
|
8
|
+
// steps VERBATIM + the resolved effective recipe per slot, plus the project's DECLARED source-size
|
|
9
|
+
// practice when it declares one (D-17 U1).
|
|
10
10
|
//
|
|
11
11
|
// Invariants (mirror recipes.mjs): pure-where-possible, READ-ONLY (never writes, never commits, never
|
|
12
12
|
// runs a subscription CLI). The deterministic resolution lives in the kit (resolveActivityRecipe), not
|
|
@@ -26,29 +26,28 @@ import { detectBackends, wrapperCmdFor, wrapperContractFor } from './detect-back
|
|
|
26
26
|
// core only — never the writer — so this read-only advisor never imports the atomic-write core.
|
|
27
27
|
import { loadRegistry, allowedLabel } from './bridge-settings-read.mjs';
|
|
28
28
|
import { isDirectRun } from './direct-run.mjs';
|
|
29
|
-
import { ACTIVITIES, resolveActivityRecipe, planRecipe } from './recipes.mjs';
|
|
29
|
+
import { ACTIVITIES, SLOT_RECIPES, isSwitchSlot, resolveActivityRecipe, planRecipe, composeReadiness } from './recipes.mjs';
|
|
30
|
+
// The dispatch-form wording, from the pure (fs-free) leaf that owns it: composed here, never re-worded.
|
|
31
|
+
import { dispatchForm, parallelLine } from './carriers.mjs';
|
|
30
32
|
import { resolveEngineDir, readEngineFragment, PROCEDURES_FRAGMENT_REL } from './engine-source.mjs';
|
|
31
33
|
// The plan-in-flight detector (AD-038) — imported from the plan-files.mjs LEAF (read-only fs by
|
|
32
34
|
// construction); the WRITER-capable grounding.mjs is only NAMED in rendered text, never imported.
|
|
33
35
|
import { plansInFlight, PLANS_REL } from './plan-files.mjs';
|
|
34
36
|
// The family's ONE shell quoter for a RENDERED command operand (bare when the value is already safe,
|
|
35
37
|
// single-quoted otherwise) — the same leaf eight other command renderers here read through.
|
|
36
|
-
import { shellQuoteArg } from './repo-lex.mjs';
|
|
37
|
-
// The config schema/read core
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// CONFIG_REL is RE-EXPORTED so existing importers (procedures.test.mjs, historically) keep their
|
|
41
|
-
// import site working.
|
|
38
|
+
import { shellQuoteArg, isSeedablePathToken, isRenderableLine, escapeForDisplay, isArtifactPathCarriable } from './repo-lex.mjs';
|
|
39
|
+
// The config schema/read core (orchestration-config.mjs, the single config contract): the reader +
|
|
40
|
+
// the SHARED slot/recipe validity, never the fs-writer (orchestration-write.mjs) DIRECTLY — the
|
|
41
|
+
// import-split test pins the direct-import rule.
|
|
42
42
|
import { CONFIG_REL, fail, loadConfig, assertSlotRecipe } from './orchestration-config.mjs';
|
|
43
43
|
import { AUTONOMY_REL, loadAutonomy, resolveAutonomy, isSparseSeedConfig } from './autonomy-config.mjs';
|
|
44
|
-
// The flow armed-halves probe (P8): read-only store presence/adoption facts
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
// narrated: this advisor's import closure reaches NO write-API module and the tools graph is
|
|
49
|
-
// acyclic — pinned by test/read-graph-purity.test.mjs (FLOW-READ-GRAPH-PURITY).
|
|
44
|
+
// The flow armed-halves probe (P8): read-only store presence/adoption facts, from the read module
|
|
45
|
+
// that OWNS no write API — never the mixed flow-store module (append API) DIRECTLY, like never
|
|
46
|
+
// orchestration-write (the import-split test pins both direct rules; the TRANSITIVE claim is
|
|
47
|
+
// structural — test/read-graph-purity.test.mjs pins it).
|
|
50
48
|
import { resolveFlowStorePath, readFlowStore } from './flow-store-read.mjs';
|
|
51
49
|
import { CHAIN_KIND } from './flow-record.mjs';
|
|
50
|
+
import { readRegistration } from './mcp-registration.mjs';
|
|
52
51
|
// The declared source-size practice (D-17 U1), read through the practice's PURE READ core — never
|
|
53
52
|
// source-size-check.mjs, which owns the writer half: this advisor is a read root of
|
|
54
53
|
// test/read-graph-purity.test.mjs, and the core exists so a surface can ask without reaching a writer.
|
|
@@ -57,18 +56,17 @@ export { CONFIG_REL };
|
|
|
57
56
|
|
|
58
57
|
// ── argument + override parsing (usage errors → exit 2) ─────────────────────────────
|
|
59
58
|
|
|
60
|
-
// Parse the activity's --override <slot>=<
|
|
59
|
+
// Parse the activity's --override <slot>=<value> tokens into a { slot: recipe } map, validating each
|
|
61
60
|
// against the SHARED slot/recipe validity table (assertSlotRecipe — the SAME accept/reject the set-recipe
|
|
62
|
-
// op parser uses, drift-guarded). Every malformed token is a USAGE error (exit 2): a bare `<recipe
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// CLI arg), unlike the fully-qualified `--set <activity>.<slot>=<recipe>` the writer takes.
|
|
61
|
+
// op parser uses, drift-guarded). Every malformed token is a USAGE error (exit 2): a bare `<recipe>`, an
|
|
62
|
+
// unknown slot for the activity, an invalid recipe-for-slot, or a duplicate slot. (An override naming a
|
|
63
|
+
// recipe whose backend merely is not `ready` is NOT a usage error — it degrades loudly at resolution
|
|
64
|
+
// time, exit 0.) The grammar stays activity-SCOPED, unlike the writer's `--set <activity>.<slot>=<x>`.
|
|
67
65
|
const parseOverrides = (tokens, activity) => {
|
|
68
66
|
const overrides = {};
|
|
69
67
|
for (const tok of tokens) {
|
|
70
68
|
const eq = tok.indexOf('=');
|
|
71
|
-
if (eq <= 0) throw fail(2, `--override must be <slot>=<
|
|
69
|
+
if (eq <= 0) throw fail(2, `--override must be <slot>=<value> (got "${tok}")`);
|
|
72
70
|
const slot = tok.slice(0, eq);
|
|
73
71
|
const recipe = tok.slice(eq + 1);
|
|
74
72
|
assertSlotRecipe(activity, slot, recipe); // shared validity (unknown slot / invalid recipe → exit 2)
|
|
@@ -91,7 +89,7 @@ const parseArgs = (argv) => {
|
|
|
91
89
|
json = true;
|
|
92
90
|
} else if (a === '--override') {
|
|
93
91
|
const tok = argv[i + 1];
|
|
94
|
-
if (tok === undefined || tok.startsWith('--')) throw fail(2, '--override requires <slot>=<
|
|
92
|
+
if (tok === undefined || tok.startsWith('--')) throw fail(2, '--override requires <slot>=<value>');
|
|
95
93
|
overrideTokens.push(tok);
|
|
96
94
|
i += 1;
|
|
97
95
|
} else if (a.startsWith('--override=')) {
|
|
@@ -162,30 +160,39 @@ const resolveAllSlots = ({ activity, config, detection, overrides }) => {
|
|
|
162
160
|
}
|
|
163
161
|
})();
|
|
164
162
|
const knobsFor = (cmd) => [...registry.values()].filter((k) => (k.appliesTo ?? []).includes(cmd));
|
|
165
|
-
return Object.
|
|
163
|
+
return Object.entries(ACTIVITIES[activity].slots).map(([slot, slotType]) => {
|
|
166
164
|
const resolved = resolveActivityRecipe({ config: config ?? {}, readiness: detection, activity, slot, override: overrides[slot] });
|
|
165
|
+
if (isSwitchSlot(slotType)) return { slot, slotType, ...resolved, backends: [], contracts: [], vehicles: [] };
|
|
167
166
|
// The concrete wrapper set this slot's EFFECTIVE recipe dispatches (empty for solo). Reuse
|
|
168
167
|
// planRecipe's drift-guarded dispatch for WHICH backends, then resolve each (backend, role) to its
|
|
169
|
-
// manifest wrapper cmd via the bridge registry — no wrapper name is hand-composed here.
|
|
170
|
-
|
|
171
|
-
const
|
|
168
|
+
// manifest wrapper cmd via the bridge registry — no wrapper name is hand-composed here. A vehicle
|
|
169
|
+
// step is NOT a bridge: it carries its own state and is never looked up in a manifest.
|
|
170
|
+
const { dispatch } = resolved.roster ? { dispatch: [] } : planRecipe(resolved.recipe, detection);
|
|
171
|
+
const vehicles = dispatch.filter((d) => d.vehicle != null).map((d) => ({ backend: d.backend, state: d.vehicle }));
|
|
172
|
+
const bridged = dispatch.filter((d) => d.vehicle == null);
|
|
173
|
+
const backends = bridged.map((d) => wrapperCmdFor(d.backend, d.role)).filter(Boolean);
|
|
172
174
|
// The full DRIVING CONTRACT per dispatched (backend, role) — resolved HERE, on the raw dispatch
|
|
173
175
|
// pairs, BEFORE they are flattened to wrapper names (the name array cannot reconstruct the role).
|
|
174
176
|
// Every slot with a non-empty dispatch gets contracts — including execute=delegated; the contract
|
|
175
177
|
// is NEVER gated by REVIEW_RECIPES (that set gates only the review-loop economics block).
|
|
176
|
-
const contracts =
|
|
178
|
+
const contracts = bridged
|
|
177
179
|
.map((d) => ({ backend: d.backend, role: d.role, cmd: wrapperCmdFor(d.backend, d.role), contract: wrapperContractFor(d.backend, d.role) }))
|
|
178
180
|
.filter((c) => c.cmd && c.contract)
|
|
179
181
|
// `retired` rides along: without it this surface advertised a RETIRED key as an ordinary
|
|
180
182
|
// settable knob, while the writer refuses to set it — a driving contract that contradicts the
|
|
181
183
|
// tool it points at.
|
|
182
184
|
.map((c) => ({ ...c, settings: knobsFor(c.cmd).map((k) => ({ key: k.key, allowed: allowedLabel(k), retired: k.retired ?? null })) }));
|
|
183
|
-
return { slot, ...resolved, backends, contracts };
|
|
185
|
+
return { slot, slotType, ...resolved, backends, contracts, vehicles };
|
|
184
186
|
});
|
|
185
187
|
};
|
|
186
188
|
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
+
// The routine switch reads against the EFFECTIVE carrier: the resolved recipe of the activity's
|
|
190
|
+
// carrier-typed slot (registry-driven — no slot name is spelled here), solo when it has none.
|
|
191
|
+
const effectiveCarrier = (slots) => slots.find((s) => s.slotType === 'carrier')?.recipe ?? 'solo';
|
|
192
|
+
|
|
193
|
+
// An unsatisfiable EXPLICIT override is a warning (loud, flagged for the agent to relay); a graceful
|
|
194
|
+
// config/default degradation remains a per-slot reason. Canon/registry slot-skew warnings are
|
|
195
|
+
// collected separately, after the live section is read.
|
|
189
196
|
const collectWarnings = (slots) =>
|
|
190
197
|
slots
|
|
191
198
|
.filter((s) => s.overrideUnsatisfied)
|
|
@@ -211,24 +218,34 @@ const backendSetLabel = (backends) =>
|
|
|
211
218
|
: ` → ${backends[0]}`;
|
|
212
219
|
|
|
213
220
|
// The review-loop economics block (M1 + M6's firing half) — printed when the activity engages a review
|
|
214
|
-
//
|
|
221
|
+
// member: a slot RESOLVING reviewed | council, a roster, or one whose config or override REQUESTED a
|
|
222
|
+
// review recipe and degraded (readiness removes no configured obligation, and the table still judges
|
|
223
|
+
// it); omitted only for a solo nobody asked to be otherwise. It paraphrases the procedures.md
|
|
215
224
|
// Fold + loop step + orchestration §4 canon (no rival rule): the ≤2-round architecture cap, the bar met by RAISING a
|
|
216
225
|
// surviving major to an acceptance invariant (not exhausting prose), backend divergence = the crossover
|
|
217
226
|
// stop, the thin-plan/diff-review carve-out, a self-consistency read before every re-review, and the
|
|
218
227
|
// REQUIRED per-round structured emission {round N · finding-origin tally · per-backend verdict}. Only a
|
|
219
|
-
// review slot can resolve reviewed|council (execute floors at solo|delegated), so
|
|
228
|
+
// review slot can resolve or request reviewed|council (execute floors at solo|delegated), so the gate
|
|
229
|
+
// reads the recipe, the requested recipe and the roster.
|
|
220
230
|
const REVIEW_RECIPES = new Set(['reviewed', 'council']);
|
|
231
|
+
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.';
|
|
232
|
+
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.';
|
|
221
233
|
// activity-aware (AD-046): the triage classification vocabulary rides EVERY review-backed activity;
|
|
222
234
|
// the LEDGER pointer renders ONLY for plan-execution — the ledger is plan-execution-scoped (AD-045),
|
|
223
235
|
// and pointing plan-authoring at it would send rounds of the wrong activity into the code loop's gate.
|
|
224
|
-
|
|
225
|
-
|
|
236
|
+
// A lens-only roster resolves to `solo` and a fully degraded request resolves to `solo`: both still run
|
|
237
|
+
// a review round, which is why the gate never reads the effective recipe alone.
|
|
238
|
+
const reviewLoopAdvice = (slots, activity, flowArmed = false, plans = []) =>
|
|
239
|
+
slots.some((s) => s.roster != null || REVIEW_RECIPES.has(s.recipe) || REVIEW_RECIPES.has(s.degradedFrom))
|
|
226
240
|
? [
|
|
227
241
|
'Review-loop economics (procedures.md Fold + loop · orchestration.md §4) — the review this recipe runs:',
|
|
242
|
+
CONSULT_LINE,
|
|
243
|
+
...(activity === 'plan-execution' && flowArmed ? [ARMED_CONSULT_LINE] : []),
|
|
228
244
|
' • 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.',
|
|
229
245
|
' • Backend divergence (one backend grounded-ships while another keeps revising mechanics) IS the crossover stop.',
|
|
230
246
|
' • Route an all-mechanics/CI or prose-only artifact to a thin plan + diff-review; run a self-consistency read before every re-review.',
|
|
231
247
|
' • Each round MUST emit {round N · finding-origin tally (first-draft / fold-induced / mechanics) · per-backend verdict} so the crossover is a computed signal.',
|
|
248
|
+
...(activity === 'plan-authoring' ? roundRenderAdvice(slots, plans) : []),
|
|
232
249
|
' • 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.',
|
|
233
250
|
...(activity === 'plan-execution'
|
|
234
251
|
? [
|
|
@@ -241,38 +258,119 @@ const reviewLoopAdvice = (slots, activity) =>
|
|
|
241
258
|
// The grounding pre-step (AD-038, extending the AD-033 verbatim-contract rendering): whenever the
|
|
242
259
|
// resolved dispatch includes agy-review, print the CONCRETE facts-assembly invocation + the
|
|
243
260
|
// --facts form as a copy-paste pre-step — population, not placeholders. Plan-path population rule
|
|
244
|
-
// (the review-state plan-in-flight detector): exactly ONE plan in flight → render it
|
|
245
|
-
// zero or
|
|
261
|
+
// (the review-state plan-in-flight detector): exactly ONE renderable plan in flight → render it
|
|
262
|
+
// populated; zero, several, or a name a one-line render cannot carry → the explicit placeholder + a
|
|
263
|
+
// one-line discovery caveat; a name the receipt encoder refuses falls back on the receipt-minting
|
|
264
|
+
// or receipt-matching command only (`agy-review plan`, the round table). The
|
|
246
265
|
// suggested --out lives OUTSIDE the repo (/tmp) — grounding.mjs refuses a non-scratch destination.
|
|
247
266
|
// Exported for the bridge-tier byte-parity pin (AD-044 Plan 4): the velocity tier seeds the
|
|
248
267
|
// grounding rule in EXACTLY this rendered spelling — `node "${GROUNDING_TOOL}"` — so seeded and
|
|
249
268
|
// rendered forms can never drift apart.
|
|
250
269
|
export const GROUNDING_TOOL = join(dirname(fileURLToPath(import.meta.url)), 'grounding.mjs');
|
|
270
|
+
export const REVIEW_ROUNDS_TOOL = join(dirname(fileURLToPath(import.meta.url)), 'review-rounds-cli.mjs');
|
|
271
|
+
export const REPO_SEARCH_TOOL = join(dirname(fileURLToPath(import.meta.url)), 'repo-search.mjs');
|
|
251
272
|
const GROUNDING_FACTS_OUT = '/tmp/review-facts.md';
|
|
273
|
+
const MINTS_RECEIPT = Object.freeze({ mintsReceipt: true });
|
|
274
|
+
|
|
275
|
+
// The round table judges what review-rounds-cli resolves from the config (S27), never this render's
|
|
276
|
+
// override or degradation, so the advisor states that fact beneath the command instead of predicting
|
|
277
|
+
// when the two agree; a bridge-less roster gets the fact that no receipt can exist in the command's place.
|
|
278
|
+
// A requested review recipe renders the command even when every bridge is unavailable here: readiness
|
|
279
|
+
// degradation removes no configured obligation, so the table still has something to say.
|
|
280
|
+
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.";
|
|
281
|
+
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.";
|
|
282
|
+
const roundRenderAdvice = (slots, plans) => {
|
|
283
|
+
const review = slots.find((s) => s.slot === 'review');
|
|
284
|
+
if (review === undefined) return [];
|
|
285
|
+
const roster = review.roster ?? null;
|
|
286
|
+
const shouldRenderCommand = roster === null
|
|
287
|
+
? (review.backends ?? []).length > 0 || REVIEW_RECIPES.has(review.degradedFrom)
|
|
288
|
+
: roster.some((row) => row.kind === 'bridge');
|
|
289
|
+
if (!shouldRenderCommand) return [ROUND_RENDER_NO_BRIDGE_LINE];
|
|
290
|
+
const lensMembers = (roster ?? []).filter((row) => row.kind === 'lens').map((row) => row.member);
|
|
291
|
+
const operand = populatedPlan(plans, MINTS_RECEIPT);
|
|
292
|
+
return [
|
|
293
|
+
` • 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>'}`,
|
|
294
|
+
...planDiscoveryCaveat(plans, '--artifact', 'populate --artifact with the plan file under review.', operand === null ? ['--artifact'] : []),
|
|
295
|
+
ROUND_RENDER_FACT_LINE,
|
|
296
|
+
...(lensMembers.length > 0 ? [` ↳ the table carries the bridge verdicts only — add each lens member's verdict (or silent) by hand: ${lensMembers.join(', ')}`] : []),
|
|
297
|
+
];
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// The ONE plan in flight as a pasteable operand: shell-significant bytes ride shellQuoteArg; a name a
|
|
301
|
+
// one-line render cannot carry is never populated, nor one the receipt encoder refuses (S21) for a
|
|
302
|
+
// command that MINTS or MATCHES a plan receipt: agy-review plan would be refused pre-spend by name, the
|
|
303
|
+
// round table would match no receipt.
|
|
304
|
+
const populatedPlan = (plans, { mintsReceipt = false } = {}) => {
|
|
305
|
+
if (plans.length !== 1 || !isRenderableLine(plans[0])) return null;
|
|
306
|
+
if (mintsReceipt && !isArtifactPathCarriable(plans[0])) return null;
|
|
307
|
+
return shellQuoteArg(`${PLANS_REL}/${plans[0]}`);
|
|
308
|
+
};
|
|
309
|
+
const planDiscoveryCaveat = (plans, flag, noPlanAction, fellBack = []) => {
|
|
310
|
+
if (plans.length === 0) return [` ↳ plan discovery: no plan in flight under ${PLANS_REL} — ${noPlanAction}`];
|
|
311
|
+
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.`];
|
|
312
|
+
return fellBack.length === 0
|
|
313
|
+
? []
|
|
314
|
+
: [` ↳ 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.`];
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// The kit-tools tier seeds `Bash(node <abs>/tools/repo-search.mjs:*)` BARE (velocity-profile.mjs
|
|
318
|
+
// deriveKitToolsAllowlist, the same predicate), so a seedable path renders bare — a quoted path is
|
|
319
|
+
// a different prefix and the allow rule is dead. A path the tier could never seed has no rule to
|
|
320
|
+
// match and is single-quoted for a safe paste (double quotes would still expand `$` and backticks).
|
|
321
|
+
const renderToolPath = (abs) => (isSeedablePathToken(abs) ? abs : shellQuoteArg(abs));
|
|
322
|
+
// `registered` is the registration on disk, never the tool's availability in this session — the
|
|
323
|
+
// command line stays as the fallback under the typed form. `toolPath` is a test seam (an unseedable
|
|
324
|
+
// kit path is not constructible from a test against the real checkout).
|
|
325
|
+
const readersSweepAdvice = (activity, registered, toolPath = REPO_SEARCH_TOOL) => {
|
|
326
|
+
if (activity !== 'plan-authoring') return [];
|
|
327
|
+
return [
|
|
328
|
+
'Readers sweep (before the first review) — for every changed config key, registry entry, exported constant, receipt field or canon sentence:',
|
|
329
|
+
...(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)'] : []),
|
|
330
|
+
` run: node ${renderToolPath(toolPath)} --pattern <the literal> (a pattern carrying a shell-significant byte goes through --pattern-file <f> instead)`,
|
|
331
|
+
' classify every reader as a ledger row, a stated non-goal, or unchanged with its proof',
|
|
332
|
+
];
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// The canon's own claim — the kit "parses only each section's `Slots:` line" — made true: an installed
|
|
336
|
+
// engine older (or newer) than this kit's registry prints its section verbatim, so the skew is SAID.
|
|
337
|
+
// A section without a Slots line is a customized canon, not a skew: silent. Never the exit code.
|
|
338
|
+
const SLOTS_LINE = /^Slots:[ \t]*(.+?)[ \t]*$/mu;
|
|
339
|
+
const slotSkewWarning = (section, activity) => {
|
|
340
|
+
const listed = section.match(SLOTS_LINE)?.[1].split(',').map((s) => s.trim()).filter(Boolean);
|
|
341
|
+
const registry = Object.keys(ACTIVITIES[activity].slots);
|
|
342
|
+
if (listed == null || listed.join(',') === registry.join(',')) return [];
|
|
343
|
+
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.`];
|
|
344
|
+
};
|
|
252
345
|
const groundingPreStepAdvice = (activity, slots, plans) => {
|
|
253
346
|
if (!slots.some((s) => (s.backends ?? []).includes('agy-review'))) return [];
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
347
|
+
const operand = populatedPlan(plans);
|
|
348
|
+
const planArg = operand === null ? '--plan <path>' : `--plan ${operand}`;
|
|
349
|
+
const reviewOperand = populatedPlan(plans, MINTS_RECEIPT);
|
|
350
|
+
// plan-authoring reviews the plan FILE — a plain name in flight renders the review command populated;
|
|
351
|
+
// the renderability and receipt-carriability fallbacks are the only placeholders a known path produces.
|
|
257
352
|
const reviewForm =
|
|
258
353
|
activity === 'plan-authoring'
|
|
259
|
-
?
|
|
260
|
-
?
|
|
261
|
-
:
|
|
354
|
+
? reviewOperand === null
|
|
355
|
+
? 'agy-review plan <plan-file>'
|
|
356
|
+
: `agy-review plan ${reviewOperand}`
|
|
262
357
|
: 'agy-review code';
|
|
263
358
|
// `run:`/`then:` prefixes keep these POPULATED command lines machine-distinguishable from the
|
|
264
359
|
// verbatim contract DESCRIPTORS above (the descriptor drift guard set-equals bare wrapper lines).
|
|
265
|
-
//
|
|
360
|
+
// The TOOL path stays double-quoted (the bridge tier seeds that exact byte-form); the plan operand
|
|
361
|
+
// rides shellQuoteArg — bare when safe, single-quoted otherwise.
|
|
266
362
|
const lines = [
|
|
267
363
|
'Grounding pre-step (agy is dispatched — assemble the verified facts BEFORE the review; grounding.mjs slices verbatim, judgment additions stay yours):',
|
|
268
364
|
` run: node "${GROUNDING_TOOL}" --constraints --autonomy ${planArg} --out ${GROUNDING_FACTS_OUT}`,
|
|
269
365
|
` then: ${reviewForm} --facts @${GROUNDING_FACTS_OUT}`,
|
|
270
366
|
];
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
367
|
+
const fellBack = [operand === null ? '--plan' : null, activity === 'plan-authoring' && reviewOperand === null ? 'agy-review plan' : null].filter(Boolean);
|
|
368
|
+
lines.push(...planDiscoveryCaveat(
|
|
369
|
+
plans,
|
|
370
|
+
'--plan',
|
|
371
|
+
'substitute the plan file you are reviewing against, or drop --plan for constraints+autonomy facts.',
|
|
372
|
+
fellBack,
|
|
373
|
+
));
|
|
276
374
|
return lines;
|
|
277
375
|
};
|
|
278
376
|
|
|
@@ -410,12 +508,11 @@ const flowHalvesAdvice = (flow, probe) => {
|
|
|
410
508
|
// plan is being written. Composed from the project's live declaration, never from constants here.
|
|
411
509
|
// Each config state speaks as itself: ABSENT renders NOTHING (a project that declares no practice must
|
|
412
510
|
// not be handed invented limits); AUTHORED and INCOMPLETE render the declared caps plus the honest
|
|
413
|
-
// "nothing is recorded yet" line
|
|
414
|
-
//
|
|
511
|
+
// "nothing is recorded yet" line (treating INCOMPLETE as MINTED would report a half record as the
|
|
512
|
+
// whole tree's debt); MINTED renders the recorded counts too.
|
|
415
513
|
// A config that cannot be read renders ONE loud line carrying the reader's own message and the render
|
|
416
|
-
// still completes: the exit code for a broken source-size config belongs to the practice's own
|
|
417
|
-
//
|
|
418
|
-
// contract is about its own config and the engine.
|
|
514
|
+
// still completes: the exit code for a broken source-size config belongs to the practice's own checker,
|
|
515
|
+
// while THIS tool's exit contract is about its own config and the engine.
|
|
419
516
|
|
|
420
517
|
export const DECLARED_PRACTICE_HEADER = `Declared source-size practice (${SOURCE_SIZE_CONFIG_REL}) — known BEFORE the code is written:`;
|
|
421
518
|
|
|
@@ -482,24 +579,34 @@ const contractLines = ({ cmd, contract, settings }) => {
|
|
|
482
579
|
return lines;
|
|
483
580
|
};
|
|
484
581
|
|
|
485
|
-
const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flowHalves, declaredPractice, foldScope, specCheck }) => {
|
|
582
|
+
const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flowHalves, flowArmed, readersSweep, declaredPractice, foldScope, specCheck }) => {
|
|
486
583
|
const lines = [
|
|
487
584
|
section,
|
|
488
585
|
'',
|
|
489
|
-
`resolved recipes for "${activity}" (read-only — the orchestrator runs the recipe via the bridge skills and owns any commit;
|
|
586
|
+
`resolved recipes for "${activity}" (read-only — the orchestrator runs the recipe via the bridge skills or the executor vehicle and owns any commit; every other carrier never commits):`,
|
|
490
587
|
];
|
|
588
|
+
const carrier = effectiveCarrier(slots);
|
|
491
589
|
for (const s of slots) {
|
|
492
590
|
const arrow = s.degradedFrom ? ` (requested ${s.degradedFrom} → degraded)` : '';
|
|
493
|
-
|
|
591
|
+
// A switch slot states what the flag DOES under the effective carrier; both keep the source suffix.
|
|
592
|
+
lines.push(isSwitchSlot(s.slotType)
|
|
593
|
+
? ` ${parallelLine({ value: s.recipe, carrier })} — ${SOURCE_LABEL[s.source]}`
|
|
594
|
+
: ` ${s.slot}: ${s.recipe} — ${SOURCE_LABEL[s.source]}${arrow}${backendSetLabel(s.backends)}`);
|
|
494
595
|
if (s.reason) lines.push(` ↳ ${s.reason}`);
|
|
596
|
+
// The form replaces the one-line vehicle mention: a carrier never told how to carry is a name,
|
|
597
|
+
// not an instruction. Indented like the driving contracts beside it.
|
|
598
|
+
for (const v of s.vehicles ?? []) {
|
|
599
|
+
for (const line of dispatchForm({ activity, slot: s.slot, state: v.state })) lines.push(` ${line}`);
|
|
600
|
+
}
|
|
495
601
|
for (const c of s.contracts ?? []) lines.push(...contractLines(c));
|
|
496
602
|
}
|
|
497
603
|
if ((flowHalves ?? []).length) lines.push('', ...flowHalves);
|
|
498
604
|
const autonomyBlock = autonomyAdvice(activity, autonomy);
|
|
499
605
|
if (autonomyBlock.length) lines.push('', ...autonomyBlock);
|
|
606
|
+
if (readersSweep.length) lines.push('', ...readersSweep);
|
|
500
607
|
const grounding = groundingPreStepAdvice(activity, slots, plans);
|
|
501
608
|
if (grounding.length) lines.push('', ...grounding);
|
|
502
|
-
const advice = reviewLoopAdvice(slots, activity);
|
|
609
|
+
const advice = reviewLoopAdvice(slots, activity, flowArmed, plans);
|
|
503
610
|
if (advice.length) lines.push('', ...advice);
|
|
504
611
|
if (foldScope.length) lines.push('', ...foldScope);
|
|
505
612
|
if (specCheck.length) lines.push('', ...specCheck);
|
|
@@ -512,15 +619,19 @@ const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flow
|
|
|
512
619
|
return lines.join('\n');
|
|
513
620
|
};
|
|
514
621
|
|
|
515
|
-
const buildJson = ({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves, declaredPractice, foldScope, specCheck }) => ({
|
|
622
|
+
const buildJson = ({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves, flowArmed, readersSweep, declaredPractice, foldScope, specCheck }) => ({
|
|
516
623
|
activity,
|
|
517
624
|
section,
|
|
518
625
|
slots: Object.fromEntries(
|
|
519
626
|
// `backends: string[]` is the STABLE pre-existing shape (wrapper names) — never repurposed.
|
|
520
627
|
// `contracts` is the ADDITIVE per-dispatch driving-contract field (empty for solo).
|
|
521
|
-
slots.map((s) => [s.slot, {
|
|
628
|
+
slots.map((s) => [s.slot, {
|
|
629
|
+
recipe: s.recipe, source: s.source, degradedFrom: s.degradedFrom, reason: s.reason, backends: s.backends, contracts: s.contracts,
|
|
630
|
+
...(s.roster ? { roster: s.roster } : {}),
|
|
631
|
+
}]),
|
|
522
632
|
),
|
|
523
|
-
reviewLoop: reviewLoopAdvice(slots, activity),
|
|
633
|
+
reviewLoop: reviewLoopAdvice(slots, activity, flowArmed, plans),
|
|
634
|
+
readersSweep,
|
|
524
635
|
// ADDITIVE (AD-038): the populated grounding pre-step, structured (empty when agy is not dispatched).
|
|
525
636
|
groundingPreStep: groundingPreStepAdvice(activity, slots, plans),
|
|
526
637
|
// ADDITIVE (cost-tiered execution): the unconditional cost-lane advisory, structured.
|
|
@@ -544,16 +655,17 @@ const buildJson = ({ activity, section, slots, configSource, warnings, plans, au
|
|
|
544
655
|
const HELP = `procedures — read-only activity-procedures advisor for the agent-workflow family.
|
|
545
656
|
|
|
546
657
|
Usage:
|
|
547
|
-
node procedures.mjs <activity> [--override <slot>=<
|
|
658
|
+
node procedures.mjs <activity> [--override <slot>=<value>]... [--json]
|
|
548
659
|
|
|
549
660
|
Activities: ${Object.keys(ACTIVITIES).join(', ')}
|
|
550
|
-
Slots:
|
|
551
|
-
|
|
661
|
+
Slots: ${Object.entries(ACTIVITIES).map(([a, d]) => `${a} → ${Object.keys(d.slots).join(', ')}`).join('; ')}
|
|
662
|
+
Accepted values: ${Object.entries(SLOT_RECIPES).map(([type, values]) => `${type} accepts ${values.join('|')}`).join('; ')}
|
|
552
663
|
|
|
553
664
|
Reads the activity's procedure steps LIVE from the installed agent-workflow-engine
|
|
554
665
|
(references/procedures.md), resolves the effective recipe per slot from
|
|
555
|
-
${CONFIG_REL} + the read-only backend detector, and prints
|
|
556
|
-
|
|
666
|
+
${CONFIG_REL} + the read-only backend detector plus the executor-vehicle survey, and prints
|
|
667
|
+
both. A per-run
|
|
668
|
+
--override <slot>=<value> (repeatable) overrides the configured/default recipe for that slot.
|
|
557
669
|
Read-only: never writes, never commits, never runs a subscription CLI.
|
|
558
670
|
|
|
559
671
|
Also prints the project's DECLARED source-size practice (${SOURCE_SIZE_CONFIG_REL}) when it declares
|
|
@@ -582,20 +694,19 @@ export const main = (argv, ctx = {}) => {
|
|
|
582
694
|
const { activity, overrides, json } = parseArgs(argv);
|
|
583
695
|
const { config, source: configSource } = loadConfig(cwd, readFile, lstat);
|
|
584
696
|
const section = extractSection(readProceduresCanon(env, home), activity);
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
//
|
|
697
|
+
// Readiness is a SECONDARY input — it only refines the recipe. A corrupt bridge must NOT fail
|
|
698
|
+
// activity resolution as a config/engine error: the detector-failure hook floors the bridge half
|
|
699
|
+
// at not-ready and warns (exit 0), while the surveyed executor vehicle survives untouched.
|
|
588
700
|
const detectWarnings = [];
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
701
|
+
const detection = composeReadiness(cwd, {
|
|
702
|
+
detect,
|
|
703
|
+
surveyVehicle: ctx.surveyVehicle,
|
|
704
|
+
onDetectError: (err) => detectWarnings.push(
|
|
705
|
+
`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).`,
|
|
706
|
+
),
|
|
707
|
+
});
|
|
597
708
|
const slots = resolveAllSlots({ activity, config, detection, overrides });
|
|
598
|
-
const warnings = [...detectWarnings, ...collectWarnings(slots)];
|
|
709
|
+
const warnings = [...detectWarnings, ...collectWarnings(slots), ...slotSkewWarning(section, activity)];
|
|
599
710
|
const plans = plansInFlight(cwd);
|
|
600
711
|
// The autonomy facts (AD-044 Plan 4): resolved levels + red-lines from the policy file. A
|
|
601
712
|
// malformed policy renders LOUDLY in the block AND flips the exit to 1 (config error) — the
|
|
@@ -615,13 +726,16 @@ export const main = (argv, ctx = {}) => {
|
|
|
615
726
|
// The flow armed-halves block (P8): probed ONLY when the config carries a flow block — an
|
|
616
727
|
// unarmed project keeps byte-identical output (human AND JSON) and never pays the store probe.
|
|
617
728
|
const flowProbe = ctx.flowProbe ?? defaultFlowProbe;
|
|
618
|
-
const
|
|
729
|
+
const flowState = config?.flow == null ? null : flowProbe(cwd);
|
|
730
|
+
const flowHalves = flowState == null ? null : flowHalvesAdvice(config.flow, flowState);
|
|
731
|
+
const registrationReader = ctx.readRegistration ?? readRegistration;
|
|
732
|
+
const readersSweep = readersSweepAdvice(activity, activity === 'plan-authoring' && registrationReader(cwd).registered, ctx.repoSearchTool);
|
|
619
733
|
const declaredPractice = declaredPracticeAdvice(cwd, readFile, lstat);
|
|
620
734
|
const foldScope = foldScopeAdvice(activity, config, plans);
|
|
621
735
|
const specCheck = specCheckAdvice(activity);
|
|
622
736
|
const stdout = json
|
|
623
|
-
? JSON.stringify(buildJson({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves, declaredPractice, foldScope, specCheck }), null, 2)
|
|
624
|
-
: formatHuman({ activity, section, slots, warnings, plans, autonomy, flowHalves, declaredPractice, foldScope, specCheck });
|
|
737
|
+
? JSON.stringify(buildJson({ activity, section, slots, configSource, warnings, plans, autonomy, flowHalves, flowArmed: flowState?.armed === true, readersSweep, declaredPractice, foldScope, specCheck }), null, 2)
|
|
738
|
+
: formatHuman({ activity, section, slots, warnings, plans, autonomy, flowHalves, flowArmed: flowState?.armed === true, readersSweep, declaredPractice, foldScope, specCheck });
|
|
625
739
|
if (autonomy?.error) {
|
|
626
740
|
return { code: 1, stdout, stderr: `procedures: malformed ${AUTONOMY_REL} — ${autonomy.error}` };
|
|
627
741
|
}
|