@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
|
@@ -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 })
|
package/tools/fold-scope.mjs
CHANGED
|
@@ -22,10 +22,13 @@
|
|
|
22
22
|
// Dependency-free, Node >= 22.
|
|
23
23
|
|
|
24
24
|
import { tokenizeMarkdown } from '../references/scripts/markdown-blocks.mjs';
|
|
25
|
+
import { PLAN_HEADINGS, bulletBlocks } from './plan-shape.mjs';
|
|
26
|
+
|
|
27
|
+
export { bulletBlocks };
|
|
25
28
|
|
|
26
29
|
export const CLASSES = ['in-scope', 'new-invariant', 'blocking'];
|
|
27
30
|
export const ROW_FIELDS = ['invariant', 'origin', 'narrow fix', 'proof', 'residual exposure'];
|
|
28
|
-
export const ACCEPTANCE_HEADING =
|
|
31
|
+
export const ACCEPTANCE_HEADING = PLAN_HEADINGS[2];
|
|
29
32
|
// The canon says a deferral row carries "the origin `file:line`". Anchored at the start of the value
|
|
30
33
|
// and a POSITIVE line number, so "file.mjs:12junk" and "file.mjs:0" are not one; trailing context
|
|
31
34
|
// after the token is fine, because the canon asks the row to CARRY a file:line, not to carry nothing
|
|
@@ -35,68 +38,10 @@ const ORIGIN_SHAPE = /^\S+:[1-9]\d*(\s|$)/;
|
|
|
35
38
|
// literals refuses fail-closed; the general per-project status grammar is queued, not guessed here.
|
|
36
39
|
const CLOSED_MARKERS = ['DONE', 'CLOSED'];
|
|
37
40
|
const ORIGIN_MISSING = 'origin (the canon requires a file:line)';
|
|
38
|
-
const BULLET = /^-\s+\S/;
|
|
39
41
|
|
|
40
42
|
const normalize = (s) => String(s ?? '').replace(/\r/g, '').replace(/\s+/g, ' ').trim();
|
|
41
43
|
const contains = (haystack, needle) => normalize(haystack).toLowerCase().includes(needle);
|
|
42
44
|
|
|
43
|
-
// The ONE bullet scan both readers use, over the block model's lines. A fenced region is a quotation
|
|
44
|
-
// AND a boundary: it closes the block it interrupts, so text past a fence can never join the bullet
|
|
45
|
-
// before it (which would let a far-side literal satisfy a near-side claim). A `-` plus any whitespace
|
|
46
|
-
// run opens a block; a blank or indented line continues it; any other unindented line closes it.
|
|
47
|
-
// Blocks are returned RAW (their own lines) — the queue reader needs the field lines inside them —
|
|
48
|
-
// each carrying the body index it OPENS at, because a second reader (queue-audit.mjs) reports rows by
|
|
49
|
-
// file line and a scan that dropped the index would have to re-derive it against a different grammar.
|
|
50
|
-
//
|
|
51
|
-
// `fenceContinues` is the SECOND reader's question, and it is a different one. A deferral row asks
|
|
52
|
-
// what a bullet CLAIMS, so a fence must cut it. A queue row asks what a bullet COSTS and whether it
|
|
53
|
-
// is still work, and there the fence-as-boundary is a hole: measured, a row carrying a code block
|
|
54
|
-
// reported ONE line and its `**DONE 2026-01-01:**` two lines further down was invisible, so the
|
|
55
|
-
// per-row cap could be walked straight past and a closure went unseen.
|
|
56
|
-
//
|
|
57
|
-
// Under the option only a NESTED fence continues an open block — one whose opening line is indented,
|
|
58
|
-
// which is what makes it part of the list item at all. A fence opening at column 0 is a
|
|
59
|
-
// DOCUMENT-level block and still closes the row, exactly as an unindented line does; absorbing it
|
|
60
|
-
// charged a one-line row for six. The run is decided ONCE, at its opening line, so a content line
|
|
61
|
-
// inside it cannot re-decide the question.
|
|
62
|
-
//
|
|
63
|
-
// The absorbed lines never enter `lines` — a marker inside a quotation is not a status — so the
|
|
64
|
-
// block records where they were: `span` is the row's PHYSICAL extent, and `gaps` holds the `lines`
|
|
65
|
-
// indices a fence run follows, so a reader assembling a multi-line span cannot join text from both
|
|
66
|
-
// sides of a code block into one claim.
|
|
67
|
-
export const bulletBlocks = (lines, fencedLines, from, to, { fenceContinues = false } = {}) => {
|
|
68
|
-
const blocks = [];
|
|
69
|
-
let current = null;
|
|
70
|
-
let absorbing = null;
|
|
71
|
-
const close = () => {
|
|
72
|
-
if (current) blocks.push(current);
|
|
73
|
-
current = null;
|
|
74
|
-
};
|
|
75
|
-
for (let index = from; index < to; index += 1) {
|
|
76
|
-
if (fencedLines.has(index)) {
|
|
77
|
-
if (absorbing === null) absorbing = Boolean(fenceContinues && current && /^\s+\S/.test(lines[index]));
|
|
78
|
-
if (!absorbing) close();
|
|
79
|
-
else {
|
|
80
|
-
current.span += 1;
|
|
81
|
-
current.gaps.add(current.lines.length - 1);
|
|
82
|
-
}
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
absorbing = null;
|
|
86
|
-
const line = lines[index];
|
|
87
|
-
if (BULLET.test(line)) {
|
|
88
|
-
close();
|
|
89
|
-
current = { start: index, lines: [line], span: 1, gaps: new Set() };
|
|
90
|
-
} else if (current && (line.trim() === '' || /^\s+\S/.test(line))) {
|
|
91
|
-
current.lines.push(line);
|
|
92
|
-
current.span += 1;
|
|
93
|
-
} else {
|
|
94
|
-
close();
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
close();
|
|
98
|
-
return blocks;
|
|
99
|
-
};
|
|
100
45
|
|
|
101
46
|
// extractAcceptance(planText) -> the top-level bullets under `## Verification`, each collapsed to one
|
|
102
47
|
// line. Per the planning canon those bullets ARE the acceptance criteria and they are the WHOLE list.
|
|
@@ -131,7 +76,7 @@ const parseFields = (block) => {
|
|
|
131
76
|
if (match) {
|
|
132
77
|
open = match[1].toLowerCase().replace(/\s+/g, ' ');
|
|
133
78
|
values[open] = [...(values[open] ?? []), match[2].trim()];
|
|
134
|
-
} else if (open && /^\s+\S/.test(line) &&
|
|
79
|
+
} else if (open && /^\s+\S/.test(line) && !/^-\s+\S/.test(line.trim())) {
|
|
135
80
|
values[open][values[open].length - 1] += ` ${line.trim()}`;
|
|
136
81
|
} else {
|
|
137
82
|
open = null;
|
package/tools/grounding.mjs
CHANGED
|
@@ -36,7 +36,7 @@ import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
|
|
|
36
36
|
// (f) --autonomy (AD-044 Plan 3): the effective per-project autonomy policy for the facts payload.
|
|
37
37
|
// READ core only — never autonomy-write.mjs (the import-split invariant).
|
|
38
38
|
import { AUTONOMY_REL, loadAutonomy, resolveAutonomy, isSparseSeedConfig } from './autonomy-config.mjs';
|
|
39
|
-
|
|
39
|
+
import { PLAN_HEADINGS } from './plan-shape.mjs';
|
|
40
40
|
const PLAN_EXECUTION = 'plan-execution';
|
|
41
41
|
|
|
42
42
|
// The agy single-argv byte contract (mirrors agy-review.sh — the wrapper is the enforcement home).
|
|
@@ -44,7 +44,7 @@ export const DEFAULT_MAX_PROMPT_BYTES = 120000;
|
|
|
44
44
|
export const ARGV_HARD_MAX = 131000;
|
|
45
45
|
|
|
46
46
|
export const CONSTRAINTS_HEADING = /^## .*Hard Constraints$/;
|
|
47
|
-
export const PLAN_SECTIONS =
|
|
47
|
+
export const PLAN_SECTIONS = PLAN_HEADINGS.slice(0, 3);
|
|
48
48
|
|
|
49
49
|
// ── pure section slicing (exactly-one-match; the inject-methodology discipline) ────────
|
|
50
50
|
|
|
@@ -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.
|
|
@@ -22,6 +22,16 @@ import { readFileSync, lstatSync } from 'node:fs';
|
|
|
22
22
|
import { join } from 'node:path';
|
|
23
23
|
import { ACTIVITIES, SLOT_RECIPES } from './recipes.mjs';
|
|
24
24
|
import { refuseDirectRun } from './direct-run.mjs';
|
|
25
|
+
import { validateRoster } from './review-roster.mjs';
|
|
26
|
+
import {
|
|
27
|
+
CANON_README,
|
|
28
|
+
KNOWN_PRIOR_README,
|
|
29
|
+
normalizeCanonical,
|
|
30
|
+
refreshIfCanonical,
|
|
31
|
+
refreshReadme,
|
|
32
|
+
} from './orchestration-readme.mjs';
|
|
33
|
+
|
|
34
|
+
export { CANON_README, KNOWN_PRIOR_README, normalizeCanonical, refreshIfCanonical, refreshReadme };
|
|
25
35
|
|
|
26
36
|
// The hand-editable / agent-writable, per-project config (strict JSON). cwd-relative — the error prefix
|
|
27
37
|
// uses this rel path so a user sees a path they can open, never an absolute temp/host path.
|
|
@@ -68,7 +78,7 @@ export const assertSlotRecipe = (activity, slot, recipe, exitCode = 2) => {
|
|
|
68
78
|
if (!(SLOT_RECIPES[slotType] ?? []).includes(recipe)) {
|
|
69
79
|
throw fail(
|
|
70
80
|
exitCode,
|
|
71
|
-
`invalid
|
|
81
|
+
`invalid value "${recipe}" for ${slotType} slot of "${activity}" (${slotType} accepts: ${SLOT_RECIPES[slotType].join(', ')})`,
|
|
72
82
|
);
|
|
73
83
|
}
|
|
74
84
|
return slotType;
|
|
@@ -98,14 +108,14 @@ const parseQualified = (lhs, flag) => {
|
|
|
98
108
|
export const parseOp = (kind, token) => {
|
|
99
109
|
if (kind === 'set') {
|
|
100
110
|
const eq = token.indexOf('=');
|
|
101
|
-
if (eq <= 0) throw fail(2, `--set must be <activity>.<slot>=<
|
|
111
|
+
if (eq <= 0) throw fail(2, `--set must be <activity>.<slot>=<value> (got "${token}")`);
|
|
102
112
|
const recipe = token.slice(eq + 1);
|
|
103
|
-
if (!recipe) throw fail(2, `--set must be <activity>.<slot>=<
|
|
113
|
+
if (!recipe) throw fail(2, `--set must be <activity>.<slot>=<value> (got "${token}")`);
|
|
104
114
|
const { activity, slot } = parseQualified(token.slice(0, eq), '--set');
|
|
105
115
|
assertSlotRecipe(activity, slot, recipe);
|
|
106
116
|
return { kind: 'set', activity, slot, recipe };
|
|
107
117
|
}
|
|
108
|
-
if (token.includes('=')) throw fail(2, `--unset takes <activity>.<slot> without a
|
|
118
|
+
if (token.includes('=')) throw fail(2, `--unset takes <activity>.<slot> without a value (got "${token}")`);
|
|
109
119
|
const { activity, slot } = parseQualified(token, '--unset');
|
|
110
120
|
assertSlot(activity, slot);
|
|
111
121
|
return { kind: 'unset', activity, slot };
|
|
@@ -242,10 +252,18 @@ export const validateConfig = (config) => {
|
|
|
242
252
|
`${CONFIG_REL}: unknown slot "${slot}" for activity "${key}" (${key} slots: ${Object.keys(activityDef.slots).join(', ')})`,
|
|
243
253
|
);
|
|
244
254
|
}
|
|
255
|
+
if (Array.isArray(recipe) && slotType === 'review') {
|
|
256
|
+
try {
|
|
257
|
+
validateRoster(recipe);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
throw fail(1, `${CONFIG_REL}: invalid review roster for "${key}.${slot}" (${error.message})`);
|
|
260
|
+
}
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
245
263
|
if (typeof recipe !== 'string' || !(SLOT_RECIPES[slotType] ?? []).includes(recipe)) {
|
|
246
264
|
throw fail(
|
|
247
265
|
1,
|
|
248
|
-
`${CONFIG_REL}: invalid
|
|
266
|
+
`${CONFIG_REL}: invalid value ${JSON.stringify(recipe)} for ${slotType} slot of "${key}" (${slotType} accepts: ${SLOT_RECIPES[slotType].join(', ')})`,
|
|
249
267
|
);
|
|
250
268
|
}
|
|
251
269
|
}
|
|
@@ -338,62 +356,6 @@ export const serializeConfig = (config) => {
|
|
|
338
356
|
return `${JSON.stringify(ordered, null, 2)}\n`;
|
|
339
357
|
};
|
|
340
358
|
|
|
341
|
-
// ── canonical-refresh (shared by the _README refresh + the injected-slot refresh) ───
|
|
342
|
-
// normalizeCanonical: trim + LF-normalize (handles the CRLF / trailing-whitespace trap) so a byte-noisy
|
|
343
|
-
// copy of a canonical string still matches. refreshIfCanonical: replace `current` with `next` IFF it
|
|
344
|
-
// normalize-equals ANY known prior canonical; otherwise return `current` UNCHANGED (preserve a
|
|
345
|
-
// customization). Pure; no fs. Used for the orchestration `_README` and the two injected pointers.
|
|
346
|
-
|
|
347
|
-
export const normalizeCanonical = (s) => String(s).replace(/\r\n/g, '\n').trim();
|
|
348
|
-
|
|
349
|
-
export const refreshIfCanonical = (current, knownPriorCanonicals, next) => {
|
|
350
|
-
const cur = normalizeCanonical(current);
|
|
351
|
-
return knownPriorCanonicals.some((prior) => normalizeCanonical(prior) === cur) ? next : current;
|
|
352
|
-
};
|
|
353
|
-
|
|
354
|
-
// ── canonical `_README` (drift-guarded, append-only known-prior set) ─────────────────
|
|
355
|
-
// CANON_README is the CURRENT onboarding note — what the templates ship + what a refresh installs. It
|
|
356
|
-
// frames hand-edit as a still-available option AND points at the set-recipe writer (no "never written
|
|
357
|
-
// for you"). KNOWN_PRIOR_README is the APPEND-ONLY set of every PREVIOUS canonical note: any release
|
|
358
|
-
// that changes CANON_README must FIRST append the outgoing string here, so an immediately-previous
|
|
359
|
-
// deployment still normalize-matches and gets refreshed (a customized note never matches → preserved).
|
|
360
|
-
export const CANON_README =
|
|
361
|
-
"Per-project orchestration config: the recipe used at each step (slot) of each named activity. " +
|
|
362
|
-
"Easiest: tell the agent in plain language and run the `set-recipe` writer — it interprets your intent, " +
|
|
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. Each activity is configured independently (e.g. plan-authoring, " +
|
|
365
|
-
"plan-execution), and so is each slot within it. A slot's value is a recipe: a 'review' slot accepts " +
|
|
366
|
-
"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). The default " +
|
|
368
|
-
"below is 'solo' everywhere — no execution backend required. Raise a slot to reviewed or council for a second " +
|
|
369
|
-
"opinion, or to delegated to hand off execution; those need an execution backend set up first. Remove a slot's " +
|
|
370
|
-
"line (or run `set-recipe --unset <activity>.<slot>`) to fall back to the computed default (reviewed when a " +
|
|
371
|
-
"review backend is ready, otherwise solo). Run the read-only procedures advisor to see an activity's steps " +
|
|
372
|
-
"plus the recipe resolved for your environment. Strict JSON — no comments.";
|
|
373
|
-
|
|
374
|
-
export const KNOWN_PRIOR_README = [
|
|
375
|
-
// v1 (pre-set-recipe) — the "Hand-edit this file — it is never written for you" note. APPEND-ONLY.
|
|
376
|
-
"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.",
|
|
377
|
-
];
|
|
378
|
-
|
|
379
|
-
// refreshReadme(config) → { config, changed }: refresh ONLY the `_README` value when it normalize-
|
|
380
|
-
// matches a known prior canonical (preserve a customized note untouched); seed it when absent. The
|
|
381
|
-
// stamp-independent config-ensure (kit fallback + memory delegated upgrade paths) uses this so an
|
|
382
|
-
// install-base deployment gains the new note without a migration file — never clobbering a customization.
|
|
383
|
-
export const refreshReadme = (config) => {
|
|
384
|
-
if (config == null || typeof config !== 'object' || Array.isArray(config)) {
|
|
385
|
-
return { config, changed: false };
|
|
386
|
-
}
|
|
387
|
-
const had = config._README;
|
|
388
|
-
const nextReadme = had === undefined ? CANON_README : refreshIfCanonical(had, KNOWN_PRIOR_README, CANON_README);
|
|
389
|
-
if (nextReadme === had) return { config, changed: false };
|
|
390
|
-
const next = { _README: nextReadme };
|
|
391
|
-
for (const [k, v] of Object.entries(config)) {
|
|
392
|
-
if (k !== '_README') next[k] = v;
|
|
393
|
-
}
|
|
394
|
-
return { config: next, changed: true };
|
|
395
|
-
};
|
|
396
|
-
|
|
397
359
|
// The canonical seed file body (what `init` deploys + what serializeConfig round-trips byte-identically).
|
|
398
360
|
export const SEED_CONFIG = { _README: CANON_README, 'plan-authoring': { review: 'solo' }, 'plan-execution': { execute: 'solo', review: 'solo' } };
|
|
399
361
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export const normalizeCanonical = (value) => String(value).replace(/\r\n/gu, '\n').trim();
|
|
2
|
+
|
|
3
|
+
export const refreshIfCanonical = (current, knownPriorCanonicals, next) => {
|
|
4
|
+
const normalized = normalizeCanonical(current);
|
|
5
|
+
return knownPriorCanonicals.some((prior) => normalizeCanonical(prior) === normalized) ? next : current;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const V1_README =
|
|
9
|
+
"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). " +
|
|
10
|
+
"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.";
|
|
11
|
+
|
|
12
|
+
const V2_README =
|
|
13
|
+
"Per-project orchestration config: the recipe used at each step (slot) of each named activity. Easiest: tell " +
|
|
14
|
+
"the agent in plain language and run the `set-recipe` writer — it interprets your intent, previews the change, " +
|
|
15
|
+
"and writes valid JSON for you. You can still hand-edit this file directly whenever you prefer; that option " +
|
|
16
|
+
"never goes away. Each activity is configured independently (e.g. plan-authoring, plan-execution), and so is " +
|
|
17
|
+
"each slot within it. A slot's value is a recipe: a 'review' slot accepts solo | reviewed | council (you " +
|
|
18
|
+
"self-review / one backend reviews / both review and you synthesize); an 'execute' slot accepts solo | " +
|
|
19
|
+
"delegated (you implement / a backend runs a bounded sub-task). The default below is 'solo' everywhere — no " +
|
|
20
|
+
"execution backend required. Raise a slot to reviewed or council for a second opinion, or to delegated to hand " +
|
|
21
|
+
"off execution; those need an execution backend set up first. Remove a slot's line (or run `set-recipe --unset " +
|
|
22
|
+
"<activity>.<slot>`) to fall back to the computed default (reviewed when a review backend is ready, otherwise " +
|
|
23
|
+
"solo). Run the read-only procedures advisor to see an activity's steps plus the recipe resolved for your " +
|
|
24
|
+
"environment. Strict JSON — no comments.";
|
|
25
|
+
|
|
26
|
+
const V3_README =
|
|
27
|
+
"Per-project orchestration config: the recipe used at each step (slot) of each named activity. " +
|
|
28
|
+
"Easiest: tell the agent in plain language and run the `set-recipe` writer — it interprets your intent, " +
|
|
29
|
+
"previews the change, and writes valid JSON for you. You can still hand-edit this file directly whenever you " +
|
|
30
|
+
"prefer; that option never goes away. Three activities are configured independently, and so is each slot " +
|
|
31
|
+
"within them: 'plan-authoring' (slots author, review), 'plan-execution' (slots execute, review) and " +
|
|
32
|
+
"'routine' (slots carrier, parallel). A slot's value is a recipe: a 'review' slot accepts " +
|
|
33
|
+
"solo | reviewed | council (you self-review / one backend reviews / both review and you synthesize); an " +
|
|
34
|
+
"'execute' slot accepts solo | delegated | subagent (you implement / a backend runs a bounded sub-task / a " +
|
|
35
|
+
"full-tool frontier subagent carries a bounded slice you verify); the carrier slots 'plan-authoring.author' " +
|
|
36
|
+
"and 'routine.carrier' accept solo | subagent. 'routine.parallel' is a flag rather than a recipe: it accepts " +
|
|
37
|
+
"on | off and decides whether file-disjoint subagent slices dispatch concurrently. The default below is " +
|
|
38
|
+
"'solo' for every recipe and carrier slot, and 'on' for the parallel switch — no execution backend required. " +
|
|
39
|
+
"Raise a slot to reviewed or council for a second " +
|
|
40
|
+
"opinion, or to delegated to hand off execution; those need an execution backend set up first. 'subagent' " +
|
|
41
|
+
"needs the executor vehicle placed in this project — the composition root's `agents` writer places it; without " +
|
|
42
|
+
"it the slot resolves to solo with the reason stated. Remove a slot's line, or a whole activity block (or " +
|
|
43
|
+
"run `set-recipe --unset <activity>.<slot>`), to fall back to the computed default: reviewed when a review " +
|
|
44
|
+
"backend is ready and otherwise solo for a review slot, solo for author, execute and carrier, on for " +
|
|
45
|
+
"parallel. Run the read-only procedures advisor to see an activity's steps plus the recipe resolved for " +
|
|
46
|
+
"your environment. Strict JSON — no comments.";
|
|
47
|
+
|
|
48
|
+
const ROSTER_README = V3_README.replace(
|
|
49
|
+
"a 'review' slot accepts solo | reviewed | council (you self-review / one backend reviews / both review and you synthesize)",
|
|
50
|
+
"a 'review' slot accepts solo | reviewed | council (you self-review / one backend reviews / both review and you synthesize), or an explicit roster array such as [\"codex-review\", \"agy-review\", \"review-lens\"] in hand-edit form",
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
export const CANON_README = ROSTER_README
|
|
54
|
+
.replace("'plan-authoring' (slots author, review)", "'plan-authoring' (slots author, fold, review)")
|
|
55
|
+
.replace("the carrier slots 'plan-authoring.author' and 'routine.carrier'", "the carrier slots 'plan-authoring.author', 'plan-authoring.fold' and 'routine.carrier'")
|
|
56
|
+
.replace('solo for author, execute and carrier', 'solo for author, fold, execute and carrier');
|
|
57
|
+
|
|
58
|
+
export const KNOWN_PRIOR_README = Object.freeze([V1_README, V2_README, V3_README, ROSTER_README]);
|
|
59
|
+
|
|
60
|
+
export const refreshReadme = (config) => {
|
|
61
|
+
if (config == null || typeof config !== 'object' || Array.isArray(config)) return { config, changed: false };
|
|
62
|
+
const current = config._README;
|
|
63
|
+
const readme = current === undefined
|
|
64
|
+
? CANON_README
|
|
65
|
+
: refreshIfCanonical(current, KNOWN_PRIOR_README, CANON_README);
|
|
66
|
+
if (readme === current) return { config, changed: false };
|
|
67
|
+
const next = { _README: readme };
|
|
68
|
+
for (const [key, value] of Object.entries(config)) if (key !== '_README') next[key] = value;
|
|
69
|
+
return { config: next, changed: true };
|
|
70
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readdirSync } from 'node:fs';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { tokenizeMarkdown } from '../references/scripts/markdown-blocks.mjs';
|
|
6
|
+
import { isDirectRun } from './direct-run.mjs';
|
|
7
|
+
import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
|
|
8
|
+
import { plansInFlight, PLANS_REL } from './plan-files.mjs';
|
|
9
|
+
import { buildFacts, openRepo } from './plan-shape-facts.mjs';
|
|
10
|
+
import { checkPlan, checkPlanStructure, formatFindings, parseLedger, PLAN_TITLE_PREFIX, verifyPlan } from './plan-shape.mjs';
|
|
11
|
+
|
|
12
|
+
const USAGE = `Usage:
|
|
13
|
+
node plan-shape-cli.mjs --check <plan>
|
|
14
|
+
node plan-shape-cli.mjs --verify <plan>
|
|
15
|
+
node plan-shape-cli.mjs --check --in-flight
|
|
16
|
+
node plan-shape-cli.mjs --verify --in-flight`;
|
|
17
|
+
|
|
18
|
+
const readPlan = (path) => {
|
|
19
|
+
const result = readRegularFileNoFollow(path);
|
|
20
|
+
if (result.outcome !== 'ok') throw new Error(`${path} must be a readable regular plan file (${result.className ?? result.code ?? result.outcome})`);
|
|
21
|
+
return result.content;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const readPlanEntries = (cwd) => {
|
|
25
|
+
try {
|
|
26
|
+
return readdirSync(join(cwd, PLANS_REL), { withFileTypes: true });
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error?.code === 'ENOENT') return [];
|
|
29
|
+
throw new Error(`${PLANS_REL} could not be read (${error?.code ?? 'fs error'})`);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const getPaths = (text) => {
|
|
34
|
+
try {
|
|
35
|
+
const rows = parseLedger(text).rows.filter((row) => row.valid);
|
|
36
|
+
return [...rows.map((row) => row.path), ...rows.map((row) => row.anchorPath).filter(Boolean)];
|
|
37
|
+
} catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const isPlanShape = (text) => {
|
|
43
|
+
try {
|
|
44
|
+
const first = tokenizeMarkdown(text, 'the in-flight document').headings[0];
|
|
45
|
+
return Boolean(first && first.level === 1 && first.text.startsWith(PLAN_TITLE_PREFIX));
|
|
46
|
+
} catch {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const writeLine = (write, line) => write(`${line}\n`);
|
|
52
|
+
|
|
53
|
+
// A facts failure scoped to the plan's own paths is that plan's listed finding, so the other plans
|
|
54
|
+
// are still judged; a repository-level refusal (the practice, a package, its pins) keeps its usage class.
|
|
55
|
+
const buildPlanFacts = (cwd, repo, text) => {
|
|
56
|
+
try {
|
|
57
|
+
return { facts: buildFacts(cwd, { paths: getPaths(text), repo }) };
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error?.scope !== 'plan') throw error;
|
|
60
|
+
return { findings: [{ line: 1, code: 'facts', message: error.message, rowId: null }], skips: [] };
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const judgeWith = (cwd, repo, text, rules) => {
|
|
65
|
+
const built = buildPlanFacts(cwd, repo, text);
|
|
66
|
+
return built.facts ? rules(text, built.facts) : built;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const runExplicit = (cwd, arm, operand, write) => {
|
|
70
|
+
const text = readPlan(resolve(cwd, operand));
|
|
71
|
+
const result = judgeWith(cwd, openRepo(cwd), text, arm === '--check' ? checkPlan : verifyPlan);
|
|
72
|
+
writeLine(write, formatFindings(result, operand));
|
|
73
|
+
return result.findings.length === 0 ? 0 : 1;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const runInFlight = (cwd, write) => {
|
|
77
|
+
const entries = readPlanEntries(cwd);
|
|
78
|
+
const names = plansInFlight(cwd, () => entries);
|
|
79
|
+
const documents = names.map((name) => {
|
|
80
|
+
const label = `${PLANS_REL}/${name}`;
|
|
81
|
+
return { label, text: readPlan(resolve(cwd, label)) };
|
|
82
|
+
});
|
|
83
|
+
const judged = documents.filter((document) => isPlanShape(document.text));
|
|
84
|
+
const repo = openRepo(cwd);
|
|
85
|
+
const results = judged.map((document) => {
|
|
86
|
+
const result = judgeWith(cwd, repo, document.text, checkPlanStructure);
|
|
87
|
+
writeLine(write, formatFindings(result, document.label));
|
|
88
|
+
return result;
|
|
89
|
+
});
|
|
90
|
+
writeLine(write, `plan-shape: judged plans: ${judged.length}`);
|
|
91
|
+
writeLine(write, `plan-shape: skipped by shape: ${documents.length - judged.length}`);
|
|
92
|
+
return results.some((result) => result.findings.length > 0) ? 1 : 0;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const main = (argv = process.argv.slice(2), io = {}) => {
|
|
96
|
+
const cwd = io.cwd ?? process.cwd();
|
|
97
|
+
const stdout = io.stdout ?? ((text) => process.stdout.write(text));
|
|
98
|
+
const stderr = io.stderr ?? ((text) => process.stderr.write(text));
|
|
99
|
+
const [arm, operand, ...rest] = argv;
|
|
100
|
+
if (!['--check', '--verify'].includes(arm) || !operand || rest.length > 0) {
|
|
101
|
+
writeLine(stderr, USAGE);
|
|
102
|
+
return 2;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
return operand === '--in-flight' ? runInFlight(cwd, stdout) : runExplicit(cwd, arm, operand, stdout);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
writeLine(stderr, `plan-shape: ${error.message}`);
|
|
108
|
+
return 2;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (isDirectRun(import.meta.url)) process.exitCode = main();
|