@sabaiway/agent-workflow-kit 10.3.0 → 10.4.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 +30 -0
- package/README.md +5 -5
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/agents/executor.md +40 -0
- package/references/modes/agents.md +9 -4
- package/references/modes/procedures.md +17 -8
- package/references/modes/recipes.md +7 -4
- package/references/modes/recommendations.md +3 -1
- package/references/modes/set-recipe.md +22 -5
- package/references/modes/status.md +2 -2
- package/references/modes/upgrade.md +1 -1
- 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/carriers.mjs +140 -0
- package/tools/cheap-agents-read.mjs +172 -0
- package/tools/cheap-agents.mjs +57 -105
- package/tools/commands.mjs +3 -3
- package/tools/direct-run.mjs +6 -0
- package/tools/family-registry.mjs +38 -18
- package/tools/flow-check.mjs +2 -7
- package/tools/inject-methodology.mjs +4 -0
- package/tools/orchestration-config.mjs +34 -13
- package/tools/procedures.mjs +65 -52
- package/tools/recipes.mjs +156 -184
- package/tools/recommendations.mjs +77 -11
- package/tools/renderers.mjs +27 -7
- package/tools/review-state.mjs +10 -11
- package/tools/set-recipe.mjs +63 -24
- package/tools/view-model.mjs +17 -3
|
@@ -28,7 +28,7 @@ import { parseSemver, compareSemver } from './semver-lite.mjs';
|
|
|
28
28
|
import { validateManifest, readAuthoritativeVersion, UNSUPPORTED, INVALID } from './manifest/validate.mjs';
|
|
29
29
|
import { START_MARKER, excludePath, inferVisibility } from './hide-footprint.mjs';
|
|
30
30
|
import { readEngineFragment, ORCHESTRATION_FRAGMENT_REL, PROCEDURES_FRAGMENT_REL, AUTONOMY_FRAGMENT_REL, LENS_FRAGMENT_REL, LENS_PRIORS_REL } from './engine-source.mjs';
|
|
31
|
-
import { ACTIVITIES, resolveActivityRecipe } from './recipes.mjs';
|
|
31
|
+
import { ACTIVITIES, resolveActivityRecipe, composeReadiness, safeLine } from './recipes.mjs';
|
|
32
32
|
// The config reader lives in orchestration-config.mjs (the single config contract). The read-only status
|
|
33
33
|
// settings-survey reuses THIS reader (one strict-JSON + loud-on-malformed contract), not a second copy.
|
|
34
34
|
import { loadConfig } from './orchestration-config.mjs';
|
|
@@ -52,9 +52,10 @@ import { HOOK_FILE_REL as GATE_HOOK_FILE_REL, isHookWired } from './gate-hook.mj
|
|
|
52
52
|
// writer, which pulls in the atomic-write core) so the status survey stays a pure reader.
|
|
53
53
|
import { settingsSnapshot } from './bridge-settings-read.mjs';
|
|
54
54
|
import { GATES_REL, loadDeclaration } from './run-gates.mjs';
|
|
55
|
-
// The cheap-agents
|
|
56
|
-
// (one implementation, never a drifting copy;
|
|
57
|
-
|
|
55
|
+
// The cheap-agents READ core's bundle reader, placement planner and executor-vehicle survey —
|
|
56
|
+
// reused by the settings survey (one implementation, never a drifting copy; the read core imports
|
|
57
|
+
// only node builtins, no cycle).
|
|
58
|
+
import { readBundledAgents, planPlacement, surveyExecutorVehicle, EXECUTOR_VEHICLE, assertDirSafe, readFsDeps, CLAUDE_DIR, AGENTS_DIR } from './cheap-agents-read.mjs';
|
|
58
59
|
// The status vocabulary (manifestState constants, internal→public maps, display names, the no-leak
|
|
59
60
|
// forbidden set) lives in the frozen labels.mjs LEAF (Plan §4.2 B1) so the import graph is acyclic —
|
|
60
61
|
// nothing imports family-registry for vocabulary. Imported here for internal use; the public subset is
|
|
@@ -486,9 +487,6 @@ export const surveyProject = (projectDir, deps = {}) => {
|
|
|
486
487
|
// consumes THIS, never the human table verbatim. An envelope-shape test pins its shape so later phases
|
|
487
488
|
// (the settings/visibility block) can't silently break the Phase-2 version consumer.
|
|
488
489
|
|
|
489
|
-
// STATE_PUBLIC (internal→public token map) + DISPLAY_NAMES + displayOf now live in labels.mjs (B1) —
|
|
490
|
-
// imported at the top of this file. They are used below exactly as before.
|
|
491
|
-
|
|
492
490
|
// ── the settings survey (Phase 3) — read-only, honest, localized-on-error ──────────
|
|
493
491
|
// Each sub-survey returns a small user-safe object OR a single `{ error }` field (a localized message,
|
|
494
492
|
// never a crash): a malformed/unreadable file in ONE area must not break the rest of `status`. The
|
|
@@ -527,19 +525,21 @@ export const surveyVisibility = (dir, deps = {}) => {
|
|
|
527
525
|
};
|
|
528
526
|
|
|
529
527
|
// orchestration recipes: the EFFECTIVE recipe per slot (config · default · effective), engine-free —
|
|
530
|
-
// shared loadConfig + resolveActivityRecipe + the
|
|
531
|
-
//
|
|
528
|
+
// shared loadConfig + resolveActivityRecipe + the ONE readiness composition (detected backends +
|
|
529
|
+
// the executor vehicle). A malformed config → a localized error field; a detection failure floors
|
|
530
|
+
// at solo (a corrupt bridge must not break the view) but is surfaced as `detectError`, so the
|
|
531
|
+
// render says "couldn't check backends" instead of letting a real solo-default look identical.
|
|
532
532
|
export const surveyRecipes = (dir, deps = {}) => {
|
|
533
|
-
// A detector failure floors recipes at solo (mirrors procedures) but is surfaced as `detectError`, so
|
|
534
|
-
// the render says "couldn't check backends" instead of letting a real solo-default look identical.
|
|
535
533
|
const { detection, error: detectError } = detectSafe(deps);
|
|
534
|
+
const projectDir = resolve(dir);
|
|
535
|
+
const readiness = composeReadiness(projectDir, { ...deps, detect: () => detection });
|
|
536
536
|
try {
|
|
537
|
-
const { config, source } = loadConfig(
|
|
537
|
+
const { config, source } = loadConfig(projectDir, deps.readFile ?? readFileSync, deps.lstat ?? lstatSync);
|
|
538
538
|
const activities = {};
|
|
539
539
|
for (const [activity, def] of Object.entries(ACTIVITIES)) {
|
|
540
540
|
activities[activity] = {};
|
|
541
541
|
for (const slot of Object.keys(def.slots)) {
|
|
542
|
-
const r = resolveActivityRecipe({ config: config ?? {}, readiness
|
|
542
|
+
const r = resolveActivityRecipe({ config: config ?? {}, readiness, activity, slot });
|
|
543
543
|
activities[activity][slot] = { recipe: r.recipe, source: r.source, degradedFrom: r.degradedFrom ?? null };
|
|
544
544
|
}
|
|
545
545
|
}
|
|
@@ -616,14 +616,34 @@ export const surveyGateHook = (dir, deps = {}) => {
|
|
|
616
616
|
};
|
|
617
617
|
|
|
618
618
|
// cheap agents: the kit-placed .claude/agents/ vehicles (Mode: agents) — how many of the bundled
|
|
619
|
-
//
|
|
620
|
-
// the writer preserves it) — the welcome-mat agents rung keys on
|
|
621
|
-
//
|
|
619
|
+
// definitions are present in the project, plus the ONE full-tool vehicle's own state. A customized
|
|
620
|
+
// copy counts as PLACED (it exists; the writer preserves it) — the welcome-mat agents rung keys on
|
|
621
|
+
// zero placed, so the counts span every vehicle, the executor included. The executor state comes
|
|
622
|
+
// from the read core's survey (the subagent carrier's one instrument — one implementation), which
|
|
623
|
+
// answers a state instead of throwing; its reason rides along when it carries one.
|
|
622
624
|
export const surveyCheapAgents = (dir, deps = {}) => {
|
|
623
625
|
try {
|
|
626
|
+
const projectDir = resolve(dir);
|
|
624
627
|
const templates = readBundledAgents(deps);
|
|
625
|
-
const
|
|
626
|
-
|
|
628
|
+
const vehicle = (deps.surveyVehicle ?? surveyExecutorVehicle)(projectDir, deps);
|
|
629
|
+
// The executor is judged by its own survey (which answers a state where the placement plan
|
|
630
|
+
// would throw); the plan covers the read-only vehicles, so a broken executor never hides them,
|
|
631
|
+
// and it never follows a symlinked ancestor — the writer's own guard refuses first.
|
|
632
|
+
const executor = { executor: vehicle.state, ...(vehicle.reason ? { executorReason: safeLine(vehicle.reason) } : {}) };
|
|
633
|
+
try {
|
|
634
|
+
const fs = readFsDeps(deps);
|
|
635
|
+
assertDirSafe(join(projectDir, CLAUDE_DIR), CLAUDE_DIR, fs);
|
|
636
|
+
assertDirSafe(join(projectDir, AGENTS_DIR), AGENTS_DIR, fs);
|
|
637
|
+
const plan = planPlacement(templates.filter((t) => t.name !== EXECUTOR_VEHICLE), projectDir, deps);
|
|
638
|
+
return {
|
|
639
|
+
bundled: templates.length,
|
|
640
|
+
readOnly: templates.filter((t) => t.name !== EXECUTOR_VEHICLE).length,
|
|
641
|
+
placed: plan.filter((p) => p.action !== 'place').length + (['placed', 'customized'].includes(vehicle.state) ? 1 : 0),
|
|
642
|
+
...executor,
|
|
643
|
+
};
|
|
644
|
+
} catch (err) {
|
|
645
|
+
return { error: localizeError(err), ...executor };
|
|
646
|
+
}
|
|
627
647
|
} catch (err) {
|
|
628
648
|
return { error: localizeError(err) };
|
|
629
649
|
}
|
package/tools/flow-check.mjs
CHANGED
|
@@ -29,8 +29,7 @@ import {
|
|
|
29
29
|
resolveReceiptsPath, readReceipts, computeTreeFingerprint,
|
|
30
30
|
} from './core-evidence.mjs';
|
|
31
31
|
import { loadConfig } from './orchestration-config.mjs';
|
|
32
|
-
import { requiredBackendsForConfiguredRecipe, DISPLAY_ALIASES } from './recipes.mjs';
|
|
33
|
-
import { detectBackends } from './detect-backends.mjs';
|
|
32
|
+
import { requiredBackendsForConfiguredRecipe, DISPLAY_ALIASES, composeReadiness } from './recipes.mjs';
|
|
34
33
|
import { decideFlowCheck } from './flow-check-cores.mjs';
|
|
35
34
|
import { short } from './flow-check-rungs.mjs';
|
|
36
35
|
import {
|
|
@@ -116,11 +115,7 @@ export const computeFlowDecision = ({ cwd = process.cwd(), consumer = 'gate', pr
|
|
|
116
115
|
let readiness = [];
|
|
117
116
|
let detectionFailed = false;
|
|
118
117
|
if (configFailure == null && config?.['plan-execution']?.review == null) {
|
|
119
|
-
|
|
120
|
-
readiness = detectBackends();
|
|
121
|
-
} catch {
|
|
122
|
-
detectionFailed = true;
|
|
123
|
-
}
|
|
118
|
+
readiness = composeReadiness(top, { ...probes, onDetectError: () => { detectionFailed = true; } });
|
|
124
119
|
}
|
|
125
120
|
const obligations = configFailure == null
|
|
126
121
|
? requiredBackendsForConfiguredRecipe({ config, readiness, detectionFailed })
|
|
@@ -86,10 +86,14 @@ export const KNOWN_PRIOR_METHODOLOGY_SLOT = [
|
|
|
86
86
|
'> **Workflow methodology** — plan → execute → review. Plans are ephemeral `docs/plans/*.md` (gitignored, **never committed**); every Plan ends with a mandatory **Phase: Cleanup**; series order lives in `docs/plans/queue.md`. Full vocabulary, lifecycle, and the plan-then-execute split live in the project\'s **planning skill** (it overrides the generic `writing-plans`); summary in `docs/ai/agent_rules.md` §5. Named activities (plan-authoring, plan-execution) have procedures — see `/agent-workflow-kit procedures <activity>` for the steps + resolved recipe.',
|
|
87
87
|
// engine 2.1.0 — the pre-canon-rewrite pointer (vocabulary + plan-then-execute wording, with the communication contract).
|
|
88
88
|
'> **Workflow methodology** — plan → execute → review. Plans are ephemeral `docs/plans/*.md` (gitignored, **never committed**); every Plan ends with a mandatory **Phase: Cleanup**; series order lives in `docs/plans/queue.md`. Full vocabulary, lifecycle, and the plan-then-execute split live in the project\'s **planning skill** (it overrides the generic `writing-plans`); summary in `docs/ai/agent_rules.md` §5. Named activities (plan-authoring, plan-execution) have procedures — see `/agent-workflow-kit procedures <activity>` for the steps + resolved recipe. **Communication:** user-facing messages deliver the artifact inline (paste the prompt / diff / command — never "see §X" as a substitute), lead with the result, show exactly what was asked, and never read as mockery (a large artifact: a real summary inline + a link).',
|
|
89
|
+
// engine 4.2.0 — the two-activity pointer, before `routine` joined the named activities.
|
|
90
|
+
'> **Workflow methodology** — plan → execute → review. Plans are ephemeral `docs/plans/*.md` (gitignored, **never committed**); every Plan ends with a mandatory **Phase: Cleanup**; series order lives in `docs/plans/queue.md`. The plan shape, its caps and lifecycle live in the project\'s **planning skill** (it overrides the generic `writing-plans`); summary in `docs/ai/agent_rules.md` §5. Named activities (plan-authoring, plan-execution) have procedures — see `/agent-workflow-kit procedures <activity>` for the steps + resolved recipe. **Communication:** user-facing messages deliver the artifact inline (paste the prompt / diff / command — never "see §X" as a substitute), lead with the result, show exactly what was asked, and never read as mockery (a large artifact: a real summary inline + a link).',
|
|
89
91
|
];
|
|
90
92
|
export const KNOWN_PRIOR_ORCH_SLOT = [
|
|
91
93
|
// v1.3.0 — pre-read-at-start orchestration pointer (recipes vocabulary, no orchestration.json clause).
|
|
92
94
|
'> **Orchestration recipes** — compose plan → execute → review with a named recipe: **Solo** (no backend), **Reviewed** (one backend reviews), **Council** (both review, you synthesize), **Delegated** (a backend executes a bounded sub-task); the orchestrator always commits, a backend is never autonomous. Pick + plan one for this environment with `/agent-workflow-kit recipes` (read-only); the deployed how/why lives in your `docs/ai/` workflow docs.',
|
|
95
|
+
// engine 4.2.0 — the four-recipe pointer with the read-at-start clause, before the Subagent carrier joined the list.
|
|
96
|
+
'> **Orchestration recipes** — compose plan → execute → review with a named recipe: **Solo** (no backend), **Reviewed** (one backend reviews), **Council** (both review, you synthesize), **Delegated** (a backend executes a bounded sub-task); the orchestrator always commits, a backend is never autonomous. Pick + plan one for this environment with `/agent-workflow-kit recipes` (read-only); the deployed how/why lives in your `docs/ai/` workflow docs. At the start of a planning/execution session, read your standing recipe preference in `docs/ai/orchestration.json` — set it in plain language with `/agent-workflow-kit set-recipe` (previews first; hand-edit stays supported).',
|
|
93
97
|
];
|
|
94
98
|
|
|
95
99
|
// A slot descriptor bundles everything the generic engine needs to operate on ONE marker pair.
|
|
@@ -68,7 +68,7 @@ export const assertSlotRecipe = (activity, slot, recipe, exitCode = 2) => {
|
|
|
68
68
|
if (!(SLOT_RECIPES[slotType] ?? []).includes(recipe)) {
|
|
69
69
|
throw fail(
|
|
70
70
|
exitCode,
|
|
71
|
-
`invalid
|
|
71
|
+
`invalid value "${recipe}" for ${slotType} slot of "${activity}" (${slotType} accepts: ${SLOT_RECIPES[slotType].join(', ')})`,
|
|
72
72
|
);
|
|
73
73
|
}
|
|
74
74
|
return slotType;
|
|
@@ -98,14 +98,14 @@ const parseQualified = (lhs, flag) => {
|
|
|
98
98
|
export const parseOp = (kind, token) => {
|
|
99
99
|
if (kind === 'set') {
|
|
100
100
|
const eq = token.indexOf('=');
|
|
101
|
-
if (eq <= 0) throw fail(2, `--set must be <activity>.<slot>=<
|
|
101
|
+
if (eq <= 0) throw fail(2, `--set must be <activity>.<slot>=<value> (got "${token}")`);
|
|
102
102
|
const recipe = token.slice(eq + 1);
|
|
103
|
-
if (!recipe) throw fail(2, `--set must be <activity>.<slot>=<
|
|
103
|
+
if (!recipe) throw fail(2, `--set must be <activity>.<slot>=<value> (got "${token}")`);
|
|
104
104
|
const { activity, slot } = parseQualified(token.slice(0, eq), '--set');
|
|
105
105
|
assertSlotRecipe(activity, slot, recipe);
|
|
106
106
|
return { kind: 'set', activity, slot, recipe };
|
|
107
107
|
}
|
|
108
|
-
if (token.includes('=')) throw fail(2, `--unset takes <activity>.<slot> without a
|
|
108
|
+
if (token.includes('=')) throw fail(2, `--unset takes <activity>.<slot> without a value (got "${token}")`);
|
|
109
109
|
const { activity, slot } = parseQualified(token, '--unset');
|
|
110
110
|
assertSlot(activity, slot);
|
|
111
111
|
return { kind: 'unset', activity, slot };
|
|
@@ -245,7 +245,7 @@ export const validateConfig = (config) => {
|
|
|
245
245
|
if (typeof recipe !== 'string' || !(SLOT_RECIPES[slotType] ?? []).includes(recipe)) {
|
|
246
246
|
throw fail(
|
|
247
247
|
1,
|
|
248
|
-
`${CONFIG_REL}: invalid
|
|
248
|
+
`${CONFIG_REL}: invalid value "${recipe}" for ${slotType} slot of "${key}" (${slotType} accepts: ${SLOT_RECIPES[slotType].join(', ')})`,
|
|
249
249
|
);
|
|
250
250
|
}
|
|
251
251
|
}
|
|
@@ -361,19 +361,40 @@ export const CANON_README =
|
|
|
361
361
|
"Per-project orchestration config: the recipe used at each step (slot) of each named activity. " +
|
|
362
362
|
"Easiest: tell the agent in plain language and run the `set-recipe` writer — it interprets your intent, " +
|
|
363
363
|
"previews the change, and writes valid JSON for you. You can still hand-edit this file directly whenever you " +
|
|
364
|
-
"prefer; that option never goes away.
|
|
365
|
-
"plan-
|
|
364
|
+
"prefer; that option never goes away. Three activities are configured independently, and so is each slot " +
|
|
365
|
+
"within them: 'plan-authoring' (slots author, review), 'plan-execution' (slots execute, review) and " +
|
|
366
|
+
"'routine' (slots carrier, parallel). A slot's value is a recipe: a 'review' slot accepts " +
|
|
366
367
|
"solo | reviewed | council (you self-review / one backend reviews / both review and you synthesize); an " +
|
|
367
|
-
"'execute' slot accepts solo | delegated (you implement / a backend runs a bounded sub-task
|
|
368
|
-
"
|
|
369
|
-
"
|
|
370
|
-
"
|
|
371
|
-
"
|
|
372
|
-
"
|
|
368
|
+
"'execute' slot accepts solo | delegated | subagent (you implement / a backend runs a bounded sub-task / a " +
|
|
369
|
+
"full-tool frontier subagent carries a bounded slice you verify); the carrier slots 'plan-authoring.author' " +
|
|
370
|
+
"and 'routine.carrier' accept solo | subagent. 'routine.parallel' is a flag rather than a recipe: it accepts " +
|
|
371
|
+
"on | off and decides whether file-disjoint subagent slices dispatch concurrently. The default below is " +
|
|
372
|
+
"'solo' for every recipe and carrier slot, and 'on' for the parallel switch — no execution backend required. " +
|
|
373
|
+
"Raise a slot to reviewed or council for a second " +
|
|
374
|
+
"opinion, or to delegated to hand off execution; those need an execution backend set up first. 'subagent' " +
|
|
375
|
+
"needs the executor vehicle placed in this project — the composition root's `agents` writer places it; without " +
|
|
376
|
+
"it the slot resolves to solo with the reason stated. Remove a slot's line, or a whole activity block (or " +
|
|
377
|
+
"run `set-recipe --unset <activity>.<slot>`), to fall back to the computed default: reviewed when a review " +
|
|
378
|
+
"backend is ready and otherwise solo for a review slot, solo for author, execute and carrier, on for " +
|
|
379
|
+
"parallel. Run the read-only procedures advisor to see an activity's steps plus the recipe resolved for " +
|
|
380
|
+
"your environment. Strict JSON — no comments.";
|
|
373
381
|
|
|
374
382
|
export const KNOWN_PRIOR_README = [
|
|
375
383
|
// v1 (pre-set-recipe) — the "Hand-edit this file — it is never written for you" note. APPEND-ONLY.
|
|
376
384
|
"Per-project orchestration config: the recipe used at each step (slot) of each named activity. Hand-edit this file — it is never written for you. Each activity is configured independently (e.g. plan-authoring, plan-execution), and so is each slot within it. A slot's value is a recipe: a 'review' slot accepts solo | reviewed | council (you self-review / one backend reviews / both review and you synthesize); an 'execute' slot accepts solo | delegated (you implement / a backend runs a bounded sub-task). The default below is 'solo' everywhere — no execution backend required. Raise a slot to reviewed or council for a second opinion, or to delegated to hand off execution; those need an execution backend set up first. Remove a slot's line to fall back to the computed default (reviewed when a review backend is ready, otherwise solo). Run the read-only procedures advisor to see an activity's steps plus the recipe resolved for your environment, and pass a per-run override to change one slot just once. Strict JSON — no comments.",
|
|
385
|
+
// v2 (two activities, an `execute` slot without a carrier) — the note that shipped before AD-124.
|
|
386
|
+
"Per-project orchestration config: the recipe used at each step (slot) of each named activity. Easiest: tell " +
|
|
387
|
+
"the agent in plain language and run the `set-recipe` writer — it interprets your intent, previews the change, " +
|
|
388
|
+
"and writes valid JSON for you. You can still hand-edit this file directly whenever you prefer; that option " +
|
|
389
|
+
"never goes away. Each activity is configured independently (e.g. plan-authoring, plan-execution), and so is " +
|
|
390
|
+
"each slot within it. A slot's value is a recipe: a 'review' slot accepts solo | reviewed | council (you " +
|
|
391
|
+
"self-review / one backend reviews / both review and you synthesize); an 'execute' slot accepts solo | " +
|
|
392
|
+
"delegated (you implement / a backend runs a bounded sub-task). The default below is 'solo' everywhere — no " +
|
|
393
|
+
"execution backend required. Raise a slot to reviewed or council for a second opinion, or to delegated to hand " +
|
|
394
|
+
"off execution; those need an execution backend set up first. Remove a slot's line (or run `set-recipe --unset " +
|
|
395
|
+
"<activity>.<slot>`) to fall back to the computed default (reviewed when a review backend is ready, otherwise " +
|
|
396
|
+
"solo). Run the read-only procedures advisor to see an activity's steps plus the recipe resolved for your " +
|
|
397
|
+
"environment. Strict JSON — no comments.",
|
|
377
398
|
];
|
|
378
399
|
|
|
379
400
|
// refreshReadme(config) → { config, changed }: refresh ONLY the `_README` value when it normalize-
|
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,7 +26,9 @@ 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.
|
|
@@ -34,19 +36,15 @@ 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
38
|
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.
|
|
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';
|
|
52
50
|
// The declared source-size practice (D-17 U1), read through the practice's PURE READ core — never
|
|
@@ -57,18 +55,17 @@ export { CONFIG_REL };
|
|
|
57
55
|
|
|
58
56
|
// ── argument + override parsing (usage errors → exit 2) ─────────────────────────────
|
|
59
57
|
|
|
60
|
-
// Parse the activity's --override <slot>=<
|
|
58
|
+
// Parse the activity's --override <slot>=<value> tokens into a { slot: recipe } map, validating each
|
|
61
59
|
// 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.
|
|
60
|
+
// op parser uses, drift-guarded). Every malformed token is a USAGE error (exit 2): a bare `<recipe>`, an
|
|
61
|
+
// unknown slot for the activity, an invalid recipe-for-slot, or a duplicate slot. (An override naming a
|
|
62
|
+
// recipe whose backend merely is not `ready` is NOT a usage error — it degrades loudly at resolution
|
|
63
|
+
// time, exit 0.) The grammar stays activity-SCOPED, unlike the writer's `--set <activity>.<slot>=<x>`.
|
|
67
64
|
const parseOverrides = (tokens, activity) => {
|
|
68
65
|
const overrides = {};
|
|
69
66
|
for (const tok of tokens) {
|
|
70
67
|
const eq = tok.indexOf('=');
|
|
71
|
-
if (eq <= 0) throw fail(2, `--override must be <slot>=<
|
|
68
|
+
if (eq <= 0) throw fail(2, `--override must be <slot>=<value> (got "${tok}")`);
|
|
72
69
|
const slot = tok.slice(0, eq);
|
|
73
70
|
const recipe = tok.slice(eq + 1);
|
|
74
71
|
assertSlotRecipe(activity, slot, recipe); // shared validity (unknown slot / invalid recipe → exit 2)
|
|
@@ -91,7 +88,7 @@ const parseArgs = (argv) => {
|
|
|
91
88
|
json = true;
|
|
92
89
|
} else if (a === '--override') {
|
|
93
90
|
const tok = argv[i + 1];
|
|
94
|
-
if (tok === undefined || tok.startsWith('--')) throw fail(2, '--override requires <slot>=<
|
|
91
|
+
if (tok === undefined || tok.startsWith('--')) throw fail(2, '--override requires <slot>=<value>');
|
|
95
92
|
overrideTokens.push(tok);
|
|
96
93
|
i += 1;
|
|
97
94
|
} else if (a.startsWith('--override=')) {
|
|
@@ -162,28 +159,36 @@ const resolveAllSlots = ({ activity, config, detection, overrides }) => {
|
|
|
162
159
|
}
|
|
163
160
|
})();
|
|
164
161
|
const knobsFor = (cmd) => [...registry.values()].filter((k) => (k.appliesTo ?? []).includes(cmd));
|
|
165
|
-
return Object.
|
|
162
|
+
return Object.entries(ACTIVITIES[activity].slots).map(([slot, slotType]) => {
|
|
166
163
|
const resolved = resolveActivityRecipe({ config: config ?? {}, readiness: detection, activity, slot, override: overrides[slot] });
|
|
164
|
+
if (isSwitchSlot(slotType)) return { slot, slotType, ...resolved, backends: [], contracts: [], vehicles: [] };
|
|
167
165
|
// The concrete wrapper set this slot's EFFECTIVE recipe dispatches (empty for solo). Reuse
|
|
168
166
|
// 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.
|
|
167
|
+
// manifest wrapper cmd via the bridge registry — no wrapper name is hand-composed here. A vehicle
|
|
168
|
+
// step is NOT a bridge: it carries its own state and is never looked up in a manifest.
|
|
170
169
|
const { dispatch } = planRecipe(resolved.recipe, detection);
|
|
171
|
-
const
|
|
170
|
+
const vehicles = dispatch.filter((d) => d.vehicle != null).map((d) => ({ backend: d.backend, state: d.vehicle }));
|
|
171
|
+
const bridged = dispatch.filter((d) => d.vehicle == null);
|
|
172
|
+
const backends = bridged.map((d) => wrapperCmdFor(d.backend, d.role)).filter(Boolean);
|
|
172
173
|
// The full DRIVING CONTRACT per dispatched (backend, role) — resolved HERE, on the raw dispatch
|
|
173
174
|
// pairs, BEFORE they are flattened to wrapper names (the name array cannot reconstruct the role).
|
|
174
175
|
// Every slot with a non-empty dispatch gets contracts — including execute=delegated; the contract
|
|
175
176
|
// is NEVER gated by REVIEW_RECIPES (that set gates only the review-loop economics block).
|
|
176
|
-
const contracts =
|
|
177
|
+
const contracts = bridged
|
|
177
178
|
.map((d) => ({ backend: d.backend, role: d.role, cmd: wrapperCmdFor(d.backend, d.role), contract: wrapperContractFor(d.backend, d.role) }))
|
|
178
179
|
.filter((c) => c.cmd && c.contract)
|
|
179
180
|
// `retired` rides along: without it this surface advertised a RETIRED key as an ordinary
|
|
180
181
|
// settable knob, while the writer refuses to set it — a driving contract that contradicts the
|
|
181
182
|
// tool it points at.
|
|
182
183
|
.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 };
|
|
184
|
+
return { slot, slotType, ...resolved, backends, contracts, vehicles };
|
|
184
185
|
});
|
|
185
186
|
};
|
|
186
187
|
|
|
188
|
+
// The routine switch reads against the EFFECTIVE carrier: the resolved recipe of the activity's
|
|
189
|
+
// carrier-typed slot (registry-driven — no slot name is spelled here), solo when it has none.
|
|
190
|
+
const effectiveCarrier = (slots) => slots.find((s) => s.slotType === 'carrier')?.recipe ?? 'solo';
|
|
191
|
+
|
|
187
192
|
// An unsatisfiable EXPLICIT override is the only "warning" (loud, flagged for the agent to relay). A
|
|
188
193
|
// graceful config/default degradation is reported as a per-slot reason, not a warning.
|
|
189
194
|
const collectWarnings = (slots) =>
|
|
@@ -410,12 +415,11 @@ const flowHalvesAdvice = (flow, probe) => {
|
|
|
410
415
|
// plan is being written. Composed from the project's live declaration, never from constants here.
|
|
411
416
|
// Each config state speaks as itself: ABSENT renders NOTHING (a project that declares no practice must
|
|
412
417
|
// not be handed invented limits); AUTHORED and INCOMPLETE render the declared caps plus the honest
|
|
413
|
-
// "nothing is recorded yet" line
|
|
414
|
-
//
|
|
418
|
+
// "nothing is recorded yet" line (treating INCOMPLETE as MINTED would report a half record as the
|
|
419
|
+
// whole tree's debt); MINTED renders the recorded counts too.
|
|
415
420
|
// 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.
|
|
421
|
+
// still completes: the exit code for a broken source-size config belongs to the practice's own checker,
|
|
422
|
+
// while THIS tool's exit contract is about its own config and the engine.
|
|
419
423
|
|
|
420
424
|
export const DECLARED_PRACTICE_HEADER = `Declared source-size practice (${SOURCE_SIZE_CONFIG_REL}) — known BEFORE the code is written:`;
|
|
421
425
|
|
|
@@ -486,12 +490,21 @@ const formatHuman = ({ activity, section, slots, warnings, plans, autonomy, flow
|
|
|
486
490
|
const lines = [
|
|
487
491
|
section,
|
|
488
492
|
'',
|
|
489
|
-
`resolved recipes for "${activity}" (read-only — the orchestrator runs the recipe via the bridge skills and owns any commit;
|
|
493
|
+
`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
494
|
];
|
|
495
|
+
const carrier = effectiveCarrier(slots);
|
|
491
496
|
for (const s of slots) {
|
|
492
497
|
const arrow = s.degradedFrom ? ` (requested ${s.degradedFrom} → degraded)` : '';
|
|
493
|
-
|
|
498
|
+
// A switch slot states what the flag DOES under the effective carrier; both keep the source suffix.
|
|
499
|
+
lines.push(isSwitchSlot(s.slotType)
|
|
500
|
+
? ` ${parallelLine({ value: s.recipe, carrier })} — ${SOURCE_LABEL[s.source]}`
|
|
501
|
+
: ` ${s.slot}: ${s.recipe} — ${SOURCE_LABEL[s.source]}${arrow}${backendSetLabel(s.backends)}`);
|
|
494
502
|
if (s.reason) lines.push(` ↳ ${s.reason}`);
|
|
503
|
+
// The form replaces the one-line vehicle mention: a carrier never told how to carry is a name,
|
|
504
|
+
// not an instruction. Indented like the driving contracts beside it.
|
|
505
|
+
for (const v of s.vehicles ?? []) {
|
|
506
|
+
for (const line of dispatchForm({ activity, slot: s.slot, state: v.state })) lines.push(` ${line}`);
|
|
507
|
+
}
|
|
495
508
|
for (const c of s.contracts ?? []) lines.push(...contractLines(c));
|
|
496
509
|
}
|
|
497
510
|
if ((flowHalves ?? []).length) lines.push('', ...flowHalves);
|
|
@@ -544,16 +557,17 @@ const buildJson = ({ activity, section, slots, configSource, warnings, plans, au
|
|
|
544
557
|
const HELP = `procedures — read-only activity-procedures advisor for the agent-workflow family.
|
|
545
558
|
|
|
546
559
|
Usage:
|
|
547
|
-
node procedures.mjs <activity> [--override <slot>=<
|
|
560
|
+
node procedures.mjs <activity> [--override <slot>=<value>]... [--json]
|
|
548
561
|
|
|
549
562
|
Activities: ${Object.keys(ACTIVITIES).join(', ')}
|
|
550
|
-
Slots:
|
|
551
|
-
|
|
563
|
+
Slots: ${Object.entries(ACTIVITIES).map(([a, d]) => `${a} → ${Object.keys(d.slots).join(', ')}`).join('; ')}
|
|
564
|
+
Accepted values: ${Object.entries(SLOT_RECIPES).map(([type, values]) => `${type} accepts ${values.join('|')}`).join('; ')}
|
|
552
565
|
|
|
553
566
|
Reads the activity's procedure steps LIVE from the installed agent-workflow-engine
|
|
554
567
|
(references/procedures.md), resolves the effective recipe per slot from
|
|
555
|
-
${CONFIG_REL} + the read-only backend detector, and prints
|
|
556
|
-
|
|
568
|
+
${CONFIG_REL} + the read-only backend detector plus the executor-vehicle survey, and prints
|
|
569
|
+
both. A per-run
|
|
570
|
+
--override <slot>=<value> (repeatable) overrides the configured/default recipe for that slot.
|
|
557
571
|
Read-only: never writes, never commits, never runs a subscription CLI.
|
|
558
572
|
|
|
559
573
|
Also prints the project's DECLARED source-size practice (${SOURCE_SIZE_CONFIG_REL}) when it declares
|
|
@@ -582,18 +596,17 @@ export const main = (argv, ctx = {}) => {
|
|
|
582
596
|
const { activity, overrides, json } = parseArgs(argv);
|
|
583
597
|
const { config, source: configSource } = loadConfig(cwd, readFile, lstat);
|
|
584
598
|
const section = extractSection(readProceduresCanon(env, home), activity);
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
//
|
|
599
|
+
// Readiness is a SECONDARY input — it only refines the recipe. A corrupt bridge must NOT fail
|
|
600
|
+
// activity resolution as a config/engine error: the detector-failure hook floors the bridge half
|
|
601
|
+
// at not-ready and warns (exit 0), while the surveyed executor vehicle survives untouched.
|
|
588
602
|
const detectWarnings = [];
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
}
|
|
603
|
+
const detection = composeReadiness(cwd, {
|
|
604
|
+
detect,
|
|
605
|
+
surveyVehicle: ctx.surveyVehicle,
|
|
606
|
+
onDetectError: (err) => detectWarnings.push(
|
|
607
|
+
`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).`,
|
|
608
|
+
),
|
|
609
|
+
});
|
|
597
610
|
const slots = resolveAllSlots({ activity, config, detection, overrides });
|
|
598
611
|
const warnings = [...detectWarnings, ...collectWarnings(slots)];
|
|
599
612
|
const plans = plansInFlight(cwd);
|