agentxchain 2.81.0 → 2.83.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/README.md +32 -2
- package/bin/agentxchain.js +1 -1
- package/package.json +1 -1
- package/src/commands/init.js +92 -25
- package/src/commands/intake-status.js +21 -3
- package/src/commands/multi.js +8 -0
- package/src/commands/restart.js +1 -1
- package/src/commands/resume.js +83 -10
- package/src/commands/run.js +19 -0
- package/src/commands/status.js +23 -1
- package/src/commands/step.js +64 -12
- package/src/lib/coordinator-recovery.js +9 -0
- package/src/lib/dispatch-bundle.js +34 -10
- package/src/lib/governed-state.js +10 -4
- package/src/lib/intake.js +156 -10
- package/src/lib/turn-result-validator.js +59 -0
- package/src/lib/workflow-gate-semantics.js +65 -0
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ Legacy IDE-window coordination is still shipped as a compatibility mode for team
|
|
|
13
13
|
- [Quickstart](https://agentxchain.dev/docs/quickstart/)
|
|
14
14
|
- [Getting Started](https://agentxchain.dev/docs/getting-started/)
|
|
15
15
|
- [CLI reference](https://agentxchain.dev/docs/cli/)
|
|
16
|
+
- [Lights-Out Scheduling](https://agentxchain.dev/docs/lights-out-scheduling/)
|
|
16
17
|
- [Templates](https://agentxchain.dev/docs/templates/)
|
|
17
18
|
- [Export schema reference](https://agentxchain.dev/docs/export-schema/)
|
|
18
19
|
- [Adapter reference](https://agentxchain.dev/docs/adapters/)
|
|
@@ -71,6 +72,8 @@ Duplicate execution remains intentional for the current 36-file slice until a la
|
|
|
71
72
|
|
|
72
73
|
### Governed workflow
|
|
73
74
|
|
|
75
|
+
Run `agentxchain init --governed` for the guided scaffold. Use the explicit non-interactive form below for scripts, CI, or copy-paste onboarding:
|
|
76
|
+
|
|
74
77
|
```bash
|
|
75
78
|
agentxchain init --governed --goal "Build an API change planner for release teams" --dir my-agentxchain-project -y
|
|
76
79
|
cd my-agentxchain-project
|
|
@@ -151,7 +154,7 @@ agentxchain step
|
|
|
151
154
|
|
|
152
155
|
## Command Sets
|
|
153
156
|
|
|
154
|
-
### Governed
|
|
157
|
+
### Governed lifecycle and execution
|
|
155
158
|
|
|
156
159
|
| Command | What it does |
|
|
157
160
|
|---|---|
|
|
@@ -171,10 +174,37 @@ agentxchain step
|
|
|
171
174
|
| `replay turn` | Replay an accepted turn's machine-evidence commands from history for audit and drift detection |
|
|
172
175
|
| `verify protocol` | Run the shipped protocol conformance suite against a target implementation |
|
|
173
176
|
| `dashboard` | Open the local governance dashboard in your browser for repo-local runs or multi-repo coordinator initiatives, including pending gate approvals |
|
|
177
|
+
| `run [--auto-approve] [--max-turns N] [--dry-run]` | Drive a governed run from start to completion — dispatches turns, handles gates, manages rejection/retry |
|
|
178
|
+
|
|
179
|
+
### Governed proof and inspection
|
|
180
|
+
|
|
181
|
+
| Command | What it does |
|
|
182
|
+
|---|---|
|
|
183
|
+
| `audit [--format json]` | Live governance audit report with cost summary, decision history, and artifact inventory |
|
|
184
|
+
| `diff <left> <right>` | Compare two governed runs side by side (phase, decisions, artifacts, timing) |
|
|
185
|
+
| `report` | Generate a governance report for the current run |
|
|
186
|
+
| `events [--type <type>] [--limit N]` | Inspect the lifecycle event stream (turns, phases, gates, governance events) |
|
|
187
|
+
| `history [--limit N] [--role <role>]` | Query accepted-turn history from append-only JSONL |
|
|
188
|
+
| `role list\|show` | List all configured roles or inspect a single role's charter, runtime, and phase assignment |
|
|
189
|
+
| `turn show` | Inspect the active turn in detail (assignment, artifacts, timing, verification) |
|
|
190
|
+
| `phase list\|show` | List configured phases or inspect a single phase's gate requirements and state |
|
|
191
|
+
| `gate list\|show [--evaluate]` | List configured gates or evaluate a gate's current pass/fail state |
|
|
192
|
+
| `doctor [--json]` | Governed project health check: config, roles, runtimes, state, schedules, plugins, workflow-kit, connector handoff |
|
|
193
|
+
| `connector check [--json]` | Live health probes for all configured connectors (api_proxy, remote_agent, MCP stdio/streamable_http) |
|
|
194
|
+
|
|
195
|
+
### Governed automation, plugins, and continuity
|
|
196
|
+
|
|
197
|
+
| Command | What it does |
|
|
198
|
+
|---|---|
|
|
174
199
|
| `multi init\|status\|step\|resume\|approve-gate\|resync` | Run the multi-repo coordinator lifecycle, including blocked-state recovery via `multi resume` |
|
|
175
200
|
| `intake record\|triage\|approve\|plan\|start\|scan\|resolve` | Continuous-delivery intake: turn delivery signals into governed work items |
|
|
176
201
|
| `intake handoff` | Bridge a planned intake intent to a coordinator workstream for multi-repo execution |
|
|
177
|
-
| `
|
|
202
|
+
| `schedule list\|run-due\|daemon\|status` | Run repo-local lights-out scheduling: inspect schedules, execute due runs, poll in a local daemon loop, or check daemon heartbeat |
|
|
203
|
+
| `plugin install\|list\|remove` | Install, inspect, or remove governed hook plugins under `.agentxchain/plugins/` |
|
|
204
|
+
| `plugin list-available` | List bundled built-in plugins installable by short name |
|
|
205
|
+
| `export [--output <path>]` | Export run state for cross-machine continuity |
|
|
206
|
+
| `restore --input <path>` | Restore run state from a prior export on a same-repo, same-commit checkout |
|
|
207
|
+
| `restart` | Rebuild lost session context from `.agentxchain/session.json` |
|
|
178
208
|
|
|
179
209
|
### Shared utilities
|
|
180
210
|
|
package/bin/agentxchain.js
CHANGED
|
@@ -131,7 +131,7 @@ program
|
|
|
131
131
|
program
|
|
132
132
|
.command('init')
|
|
133
133
|
.description('Create a new AgentXchain project folder')
|
|
134
|
-
.option('-y, --yes', 'Skip prompts, use defaults')
|
|
134
|
+
.option('-y, --yes', 'Skip guided prompts, use defaults')
|
|
135
135
|
.option('--governed', 'Create a governed project (orchestrator-owned state)')
|
|
136
136
|
.option('--dir <path>', 'Scaffold target directory. Use "." for in-place bootstrap.')
|
|
137
137
|
.option('--template <id>', 'Governed scaffold template: generic, api-service, cli-tool, library, web-app, enterprise-app')
|
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -5,7 +5,7 @@ import chalk from 'chalk';
|
|
|
5
5
|
import inquirer from 'inquirer';
|
|
6
6
|
import { CONFIG_FILE, LOCK_FILE, STATE_FILE } from '../lib/config.js';
|
|
7
7
|
import { generateVSCodeFiles } from '../lib/generate-vscode.js';
|
|
8
|
-
import { loadGovernedTemplate, VALID_GOVERNED_TEMPLATE_IDS, buildSystemSpecContent } from '../lib/governed-templates.js';
|
|
8
|
+
import { loadAllGovernedTemplates, loadGovernedTemplate, VALID_GOVERNED_TEMPLATE_IDS, buildSystemSpecContent } from '../lib/governed-templates.js';
|
|
9
9
|
import { normalizeWorkflowKit, VALID_PROMPT_TRANSPORTS } from '../lib/normalized-config.js';
|
|
10
10
|
|
|
11
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -527,6 +527,76 @@ function formatInitTarget(dir) {
|
|
|
527
527
|
return dir;
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
function normalizeOptionalGoal(value) {
|
|
531
|
+
if (typeof value !== 'string') return undefined;
|
|
532
|
+
const trimmed = value.trim();
|
|
533
|
+
return trimmed ? trimmed : undefined;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export function buildGovernedTemplateChoices(templates = loadAllGovernedTemplates()) {
|
|
537
|
+
return templates.map((template) => ({
|
|
538
|
+
name: `${chalk.cyan(template.display_name)} (${template.id}) — ${template.description}`,
|
|
539
|
+
value: template.id,
|
|
540
|
+
short: template.id,
|
|
541
|
+
}));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export async function resolveGovernedInitAnswers(opts, prompt = (questions) => inquirer.prompt(questions)) {
|
|
545
|
+
const explicitDir = resolveInitDirOption(opts.dir);
|
|
546
|
+
let templateId = opts.template || null;
|
|
547
|
+
|
|
548
|
+
if (!templateId) {
|
|
549
|
+
const { template } = await prompt([{
|
|
550
|
+
type: 'list',
|
|
551
|
+
name: 'template',
|
|
552
|
+
message: 'Governed template:',
|
|
553
|
+
choices: buildGovernedTemplateChoices(),
|
|
554
|
+
default: 'generic',
|
|
555
|
+
}]);
|
|
556
|
+
templateId = template;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const { name } = await prompt([{
|
|
560
|
+
type: 'input',
|
|
561
|
+
name: 'name',
|
|
562
|
+
message: 'Project name:',
|
|
563
|
+
default: explicitDir
|
|
564
|
+
? inferProjectNameFromTarget(explicitDir, 'My AgentXchain Project')
|
|
565
|
+
: 'My AgentXchain Project',
|
|
566
|
+
}]);
|
|
567
|
+
const projectName = name;
|
|
568
|
+
let folderName = explicitDir || slugify(projectName);
|
|
569
|
+
|
|
570
|
+
let projectGoal = normalizeOptionalGoal(opts.goal);
|
|
571
|
+
if (!projectGoal) {
|
|
572
|
+
const { goal } = await prompt([{
|
|
573
|
+
type: 'input',
|
|
574
|
+
name: 'goal',
|
|
575
|
+
message: 'Project goal (recommended; shown to every agent turn):',
|
|
576
|
+
default: '',
|
|
577
|
+
}]);
|
|
578
|
+
projectGoal = normalizeOptionalGoal(goal);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (!explicitDir) {
|
|
582
|
+
const { folder } = await prompt([{
|
|
583
|
+
type: 'input',
|
|
584
|
+
name: 'folder',
|
|
585
|
+
message: 'Folder name:',
|
|
586
|
+
default: folderName,
|
|
587
|
+
}]);
|
|
588
|
+
folderName = folder;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return {
|
|
592
|
+
explicitDir,
|
|
593
|
+
templateId,
|
|
594
|
+
projectName,
|
|
595
|
+
folderName,
|
|
596
|
+
goal: projectGoal,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
530
600
|
function generateWorkflowKitPlaceholder(artifact, projectName) {
|
|
531
601
|
const filename = basename(artifact.path);
|
|
532
602
|
const title = filename.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
|
|
@@ -813,12 +883,24 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
|
|
|
813
883
|
|
|
814
884
|
async function initGoverned(opts) {
|
|
815
885
|
let projectName, folderName;
|
|
816
|
-
|
|
886
|
+
let templateId;
|
|
817
887
|
let selectedTemplate;
|
|
818
888
|
let explicitDir;
|
|
889
|
+
let projectGoal;
|
|
819
890
|
|
|
820
891
|
try {
|
|
821
|
-
|
|
892
|
+
if (opts.yes) {
|
|
893
|
+
explicitDir = resolveInitDirOption(opts.dir);
|
|
894
|
+
templateId = opts.template || 'generic';
|
|
895
|
+
projectGoal = normalizeOptionalGoal(opts.goal);
|
|
896
|
+
} else {
|
|
897
|
+
const answers = await resolveGovernedInitAnswers(opts);
|
|
898
|
+
explicitDir = answers.explicitDir;
|
|
899
|
+
templateId = answers.templateId;
|
|
900
|
+
projectName = answers.projectName;
|
|
901
|
+
folderName = answers.folderName;
|
|
902
|
+
projectGoal = answers.goal;
|
|
903
|
+
}
|
|
822
904
|
} catch (err) {
|
|
823
905
|
console.error(chalk.red(` Error: ${err.message}`));
|
|
824
906
|
process.exit(1);
|
|
@@ -842,27 +924,6 @@ async function initGoverned(opts) {
|
|
|
842
924
|
? inferProjectNameFromTarget(explicitDir, 'My AgentXchain Project')
|
|
843
925
|
: 'My AgentXchain Project';
|
|
844
926
|
folderName = explicitDir || slugify(projectName);
|
|
845
|
-
} else {
|
|
846
|
-
const { name } = await inquirer.prompt([{
|
|
847
|
-
type: 'input',
|
|
848
|
-
name: 'name',
|
|
849
|
-
message: 'Project name:',
|
|
850
|
-
default: explicitDir
|
|
851
|
-
? inferProjectNameFromTarget(explicitDir, 'My AgentXchain Project')
|
|
852
|
-
: 'My AgentXchain Project'
|
|
853
|
-
}]);
|
|
854
|
-
projectName = name;
|
|
855
|
-
folderName = explicitDir || slugify(projectName);
|
|
856
|
-
|
|
857
|
-
if (!explicitDir) {
|
|
858
|
-
const { folder } = await inquirer.prompt([{
|
|
859
|
-
type: 'input',
|
|
860
|
-
name: 'folder',
|
|
861
|
-
message: 'Folder name:',
|
|
862
|
-
default: folderName
|
|
863
|
-
}]);
|
|
864
|
-
folderName = folder;
|
|
865
|
-
}
|
|
866
927
|
}
|
|
867
928
|
|
|
868
929
|
const dir = resolve(process.cwd(), folderName);
|
|
@@ -908,7 +969,10 @@ async function initGoverned(opts) {
|
|
|
908
969
|
}
|
|
909
970
|
}
|
|
910
971
|
|
|
911
|
-
const
|
|
972
|
+
const scaffoldOptions = projectGoal
|
|
973
|
+
? { ...opts, goal: projectGoal }
|
|
974
|
+
: { ...opts };
|
|
975
|
+
const { config, scaffoldWorkflowKitConfig } = scaffoldGoverned(dir, projectName, projectId, templateId, scaffoldOptions, workflowKitConfig);
|
|
912
976
|
|
|
913
977
|
console.log('');
|
|
914
978
|
console.log(chalk.green(` ✓ Created governed project ${chalk.bold(targetLabel)}/`));
|
|
@@ -933,6 +997,9 @@ async function initGoverned(opts) {
|
|
|
933
997
|
console.log(` ${chalk.dim('Roles:')} ${promptRoleIds.join(', ')}`);
|
|
934
998
|
console.log(` ${chalk.dim('Phases:')} ${phaseNames.join(' → ')} ${chalk.dim(selectedTemplate.scaffold_blueprint ? '(template-defined; edit routing in agentxchain.json to customize)' : '(default; extend via routing in agentxchain.json)')}`);
|
|
935
999
|
console.log(` ${chalk.dim('Template:')} ${templateId}`);
|
|
1000
|
+
if (config.project?.goal) {
|
|
1001
|
+
console.log(` ${chalk.dim('Goal:')} ${config.project.goal}`);
|
|
1002
|
+
}
|
|
936
1003
|
console.log(` ${chalk.dim('Dev runtime:')} ${formatGovernedRuntimeCommand(localDevRuntime)} ${chalk.dim(`(${localDevRuntime.prompt_transport})`)}`);
|
|
937
1004
|
console.log(` ${chalk.dim('Protocol:')} governed convergence`);
|
|
938
1005
|
console.log('');
|
|
@@ -19,7 +19,7 @@ export async function intakeStatusCommand(opts) {
|
|
|
19
19
|
|
|
20
20
|
// Detail mode: single intent
|
|
21
21
|
if (result.intent) {
|
|
22
|
-
printIntentDetail(result.intent, result.event);
|
|
22
|
+
printIntentDetail(result.intent, result.event, result.next_action);
|
|
23
23
|
process.exit(0);
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -46,14 +46,17 @@ function printSummary(summary) {
|
|
|
46
46
|
const pri = i.priority ? i.priority.padEnd(3) : '---';
|
|
47
47
|
const tpl = (i.template || '---').padEnd(12);
|
|
48
48
|
const st = statusColor(i.status);
|
|
49
|
-
|
|
49
|
+
const actionHint = i.next_action?.action_required && i.next_action?.label !== 'none'
|
|
50
|
+
? ` ${chalk.dim(`→ ${i.next_action.label}`)}`
|
|
51
|
+
: '';
|
|
52
|
+
console.log(` ${chalk.dim(i.intent_id)} ${pri} ${tpl} ${st} ${chalk.dim(i.updated_at)}${actionHint}`);
|
|
50
53
|
}
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
console.log('');
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
function printIntentDetail(intent, event) {
|
|
59
|
+
function printIntentDetail(intent, event, nextAction) {
|
|
57
60
|
console.log('');
|
|
58
61
|
console.log(chalk.bold(` Intent: ${intent.intent_id}`));
|
|
59
62
|
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
@@ -93,6 +96,21 @@ function printIntentDetail(intent, event) {
|
|
|
93
96
|
console.log(` ${chalk.dim('Signal:')} ${JSON.stringify(event.signal)}`);
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
if (nextAction) {
|
|
100
|
+
console.log('');
|
|
101
|
+
console.log(chalk.dim(' Next Action:'));
|
|
102
|
+
console.log(` ${nextAction.summary}`);
|
|
103
|
+
if (nextAction.command) {
|
|
104
|
+
console.log(` ${chalk.dim('Command:')} ${nextAction.command}`);
|
|
105
|
+
}
|
|
106
|
+
for (const alternative of nextAction.alternatives || []) {
|
|
107
|
+
console.log(` ${chalk.dim('Alternative:')} ${alternative}`);
|
|
108
|
+
}
|
|
109
|
+
if (nextAction.recovery) {
|
|
110
|
+
console.log(` ${chalk.dim('Recovery:')} ${nextAction.recovery}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
96
114
|
console.log('');
|
|
97
115
|
}
|
|
98
116
|
|
package/src/commands/multi.js
CHANGED
|
@@ -235,6 +235,14 @@ export async function multiStepCommand(options) {
|
|
|
235
235
|
// Fire on_escalation for the blocked resync
|
|
236
236
|
fireEscalationHook(workspacePath, configResult.config, state, resync.blocked_reason || 'resync failure');
|
|
237
237
|
console.error(`Coordinator resync entered blocked state: ${resync.blocked_reason || 'unknown reason'}`);
|
|
238
|
+
for (const mismatch of resync.mismatch_details || []) {
|
|
239
|
+
const codeTag = mismatch.code ? `[${mismatch.code}] ` : '';
|
|
240
|
+
console.error(` - ${codeTag}${mismatch.message}`);
|
|
241
|
+
if (mismatch.code === 'repo_run_id_mismatch') {
|
|
242
|
+
console.error(` expected: ${mismatch.expected_run_id}`);
|
|
243
|
+
console.error(` actual: ${mismatch.actual_run_id}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
238
246
|
process.exitCode = 1;
|
|
239
247
|
return;
|
|
240
248
|
}
|
package/src/commands/restart.js
CHANGED
|
@@ -167,7 +167,7 @@ export async function restartCommand(opts) {
|
|
|
167
167
|
// Load state
|
|
168
168
|
const statePath = join(root, STATE_PATH);
|
|
169
169
|
if (!existsSync(statePath)) {
|
|
170
|
-
console.log(chalk.red('No governed run found. Use `agentxchain
|
|
170
|
+
console.log(chalk.red('No governed run found. Use `agentxchain run` to start a governed run.'));
|
|
171
171
|
process.exit(1);
|
|
172
172
|
}
|
|
173
173
|
|
package/src/commands/resume.js
CHANGED
|
@@ -36,9 +36,9 @@ import {
|
|
|
36
36
|
getDispatchEffectiveContextPath,
|
|
37
37
|
getDispatchPromptPath,
|
|
38
38
|
} from '../lib/turn-paths.js';
|
|
39
|
-
import { safeWriteJson } from '../lib/safe-write.js';
|
|
40
39
|
import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
|
|
41
40
|
import { runHooks } from '../lib/hook-runner.js';
|
|
41
|
+
import { summarizeRunProvenance } from '../lib/run-provenance.js';
|
|
42
42
|
|
|
43
43
|
export async function resumeCommand(opts) {
|
|
44
44
|
const context = loadProjectContext();
|
|
@@ -100,6 +100,11 @@ export async function resumeCommand(opts) {
|
|
|
100
100
|
process.exit(1);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
if (state.pending_phase_transition || state.pending_run_completion) {
|
|
104
|
+
printRecoverySummary(state, 'This run is awaiting approval.');
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
103
108
|
// §47: paused + retained turn with failed/retrying status → re-dispatch same turn
|
|
104
109
|
if (state.status === 'paused' && activeCount > 0) {
|
|
105
110
|
// Resolve which turn to re-dispatch
|
|
@@ -124,15 +129,18 @@ export async function resumeCommand(opts) {
|
|
|
124
129
|
|
|
125
130
|
const turnStatus = retainedTurn.status;
|
|
126
131
|
if (turnStatus === 'failed' || turnStatus === 'retrying') {
|
|
132
|
+
printResumeRunContext({ root, state, config });
|
|
127
133
|
console.log(chalk.yellow(`Re-dispatching failed turn: ${retainedTurn.turn_id}`));
|
|
128
134
|
console.log(` Role: ${retainedTurn.assigned_role}`);
|
|
129
135
|
console.log(` Attempt: ${retainedTurn.attempt}`);
|
|
130
136
|
console.log('');
|
|
131
137
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
138
|
+
const reactivated = reactivateGovernedRun(root, state, { via: 'resume --turn', notificationConfig: config });
|
|
139
|
+
if (!reactivated.ok) {
|
|
140
|
+
console.log(chalk.red(`Failed to reactivate run: ${reactivated.error}`));
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
state = reactivated.state;
|
|
136
144
|
|
|
137
145
|
// Write dispatch bundle for the existing turn
|
|
138
146
|
const bundleResult = writeDispatchBundle(root, state, config);
|
|
@@ -181,6 +189,7 @@ export async function resumeCommand(opts) {
|
|
|
181
189
|
process.exit(1);
|
|
182
190
|
}
|
|
183
191
|
|
|
192
|
+
printResumeRunContext({ root, state, config });
|
|
184
193
|
console.log(chalk.yellow(`Re-dispatching blocked turn: ${retainedTurn.turn_id}`));
|
|
185
194
|
console.log(` Role: ${retainedTurn.assigned_role}`);
|
|
186
195
|
console.log(` Attempt: ${retainedTurn.attempt}`);
|
|
@@ -236,14 +245,18 @@ export async function resumeCommand(opts) {
|
|
|
236
245
|
|
|
237
246
|
// §47: paused + run_id exists → resume same run
|
|
238
247
|
if (state.status === 'paused' && state.run_id) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
248
|
+
const reactivated = reactivateGovernedRun(root, state, { via: 'resume', notificationConfig: config });
|
|
249
|
+
if (!reactivated.ok) {
|
|
250
|
+
console.log(chalk.red(`Failed to reactivate run: ${reactivated.error}`));
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
state = reactivated.state;
|
|
244
254
|
console.log(chalk.green(`Resumed governed run: ${state.run_id}`));
|
|
245
255
|
}
|
|
246
256
|
|
|
257
|
+
// Print run-context header before dispatch
|
|
258
|
+
printResumeRunContext({ root, state, config });
|
|
259
|
+
|
|
247
260
|
// Resolve target role
|
|
248
261
|
const roleId = resolveTargetRole(opts, state, config);
|
|
249
262
|
if (!roleId) {
|
|
@@ -295,6 +308,53 @@ export async function resumeCommand(opts) {
|
|
|
295
308
|
|
|
296
309
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
297
310
|
|
|
311
|
+
function printResumeRunContext({ root, state, config }) {
|
|
312
|
+
console.log('');
|
|
313
|
+
console.log(chalk.cyan.bold('agentxchain resume'));
|
|
314
|
+
console.log(` ${chalk.dim('Run:')} ${state?.run_id || '(uninitialized)'}`);
|
|
315
|
+
console.log(` ${chalk.dim('Phase:')} ${state?.phase || '(unknown)'}`);
|
|
316
|
+
|
|
317
|
+
const provenanceSummary = summarizeRunProvenance(state?.provenance);
|
|
318
|
+
if (provenanceSummary) {
|
|
319
|
+
console.log(` ${chalk.dim('Origin:')} ${chalk.magenta(provenanceSummary)}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (state?.inherited_context?.parent_run_id) {
|
|
323
|
+
console.log(
|
|
324
|
+
` ${chalk.dim('Inherits:')} ${chalk.magenta(
|
|
325
|
+
`parent ${state.inherited_context.parent_run_id} (${state.inherited_context.parent_status || 'unknown'})`
|
|
326
|
+
)}`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const activeGate = config?.routing?.[state?.phase]?.exit_gate || null;
|
|
331
|
+
if (activeGate) {
|
|
332
|
+
const gateStatus = state?.phase_gate_status?.[activeGate] || 'pending';
|
|
333
|
+
console.log(` ${chalk.dim('Gate:')} ${activeGate} (${gateStatus})`);
|
|
334
|
+
|
|
335
|
+
if (gateStatus !== 'passed') {
|
|
336
|
+
const gateDef = config?.gates?.[activeGate];
|
|
337
|
+
if (Array.isArray(gateDef?.requires_files) && gateDef.requires_files.length > 0) {
|
|
338
|
+
const fileChecks = gateDef.requires_files.map((filePath) => {
|
|
339
|
+
const exists = existsSync(join(root, filePath));
|
|
340
|
+
const shortPath = filePath.replace(/^\.planning\//, '');
|
|
341
|
+
return exists ? chalk.green(shortPath) : chalk.red(shortPath);
|
|
342
|
+
});
|
|
343
|
+
console.log(` ${chalk.dim('Files:')} ${fileChecks.join(chalk.dim(', '))}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const requirements = [];
|
|
347
|
+
if (gateDef?.requires_human_approval) requirements.push('human approval');
|
|
348
|
+
if (gateDef?.requires_verification_pass) requirements.push('verification pass');
|
|
349
|
+
if (requirements.length > 0) {
|
|
350
|
+
console.log(` ${chalk.dim('Needs:')} ${requirements.join(', ')}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
console.log('');
|
|
356
|
+
}
|
|
357
|
+
|
|
298
358
|
function resolveTargetRole(opts, state, config) {
|
|
299
359
|
const phase = state.phase;
|
|
300
360
|
const routing = config.routing?.[phase];
|
|
@@ -422,6 +482,19 @@ function printAssignmentWarnings(assignResult) {
|
|
|
422
482
|
}
|
|
423
483
|
}
|
|
424
484
|
|
|
485
|
+
function printRecoverySummary(state, heading) {
|
|
486
|
+
const recovery = deriveRecoveryDescriptor(state);
|
|
487
|
+
console.log(chalk.yellow(heading));
|
|
488
|
+
if (!recovery) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
console.log(` ${chalk.dim('Reason:')} ${recovery.typed_reason}`);
|
|
492
|
+
console.log(` ${chalk.dim('Action:')} ${recovery.recovery_action}`);
|
|
493
|
+
if (recovery.detail) {
|
|
494
|
+
console.log(` ${chalk.dim('Detail:')} ${recovery.detail}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
425
498
|
function printAssignmentHookFailure(result, roleId) {
|
|
426
499
|
const recovery = deriveRecoveryDescriptor(result.state);
|
|
427
500
|
const hookName = result.hookResults?.blocker?.hook_name
|
package/src/commands/run.js
CHANGED
|
@@ -32,6 +32,7 @@ import { dispatchRemoteAgent, describeRemoteAgentTarget } from '../lib/adapters/
|
|
|
32
32
|
import { runHooks } from '../lib/hook-runner.js';
|
|
33
33
|
import { finalizeDispatchManifest } from '../lib/dispatch-manifest.js';
|
|
34
34
|
import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
|
|
35
|
+
import { summarizeRunProvenance } from '../lib/run-provenance.js';
|
|
35
36
|
import { resolveGovernedRole } from '../lib/role-resolution.js';
|
|
36
37
|
import { buildInheritedContext } from '../lib/run-context-inheritance.js';
|
|
37
38
|
import {
|
|
@@ -179,6 +180,24 @@ export async function executeGovernedRun(context, opts = {}) {
|
|
|
179
180
|
// ── Run header ──────────────────────────────────────────────────────────
|
|
180
181
|
log(chalk.cyan.bold('agentxchain run'));
|
|
181
182
|
log(chalk.dim(` Max turns: ${maxTurns} Gate mode: ${autoApprove ? 'auto-approve' : 'interactive'}`));
|
|
183
|
+
if (provenance) {
|
|
184
|
+
const provenanceSummary = summarizeRunProvenance(provenance);
|
|
185
|
+
if (provenanceSummary) {
|
|
186
|
+
log(` ${chalk.dim('Origin:')} ${chalk.magenta(provenanceSummary)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (inheritedContext) {
|
|
190
|
+
const ic = inheritedContext;
|
|
191
|
+
const phasesCount = ic.parent_phases_completed?.length || 0;
|
|
192
|
+
const decisionsCount = ic.recent_decisions?.length || 0;
|
|
193
|
+
const turnsCount = ic.recent_accepted_turns?.length || 0;
|
|
194
|
+
const parts = [];
|
|
195
|
+
if (phasesCount) parts.push(`${phasesCount} phase${phasesCount !== 1 ? 's' : ''}`);
|
|
196
|
+
if (decisionsCount) parts.push(`${decisionsCount} decision${decisionsCount !== 1 ? 's' : ''}`);
|
|
197
|
+
if (turnsCount) parts.push(`${turnsCount} turn${turnsCount !== 1 ? 's' : ''}`);
|
|
198
|
+
const detail = parts.length ? ` — ${parts.join(', ')}` : '';
|
|
199
|
+
log(` ${chalk.dim('Inherits:')} ${chalk.magenta(`parent ${ic.parent_run_id} (${ic.parent_status || 'unknown'})${detail}`)}`);
|
|
200
|
+
}
|
|
182
201
|
log('');
|
|
183
202
|
|
|
184
203
|
// ── Track first-call for --role override ────────────────────────────────
|
package/src/commands/status.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
2
4
|
import { loadConfig, loadLock, loadProjectContext, loadProjectState, loadState } from '../lib/config.js';
|
|
3
5
|
import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
|
|
4
6
|
import { getActiveTurn, getActiveTurnCount, getActiveTurns } from '../lib/governed-state.js';
|
|
@@ -267,9 +269,29 @@ function renderGovernedStatus(context, opts) {
|
|
|
267
269
|
if (state?.phase_gate_status) {
|
|
268
270
|
console.log('');
|
|
269
271
|
console.log(` ${chalk.dim('Gates:')}`);
|
|
272
|
+
const activePhase = state.phase;
|
|
273
|
+
const activeRouting = config.routing?.[activePhase];
|
|
274
|
+
const activeExitGate = activeRouting?.exit_gate || null;
|
|
270
275
|
for (const [gate, status] of Object.entries(state.phase_gate_status)) {
|
|
271
276
|
const icon = status === 'passed' ? chalk.green('✓') : chalk.dim('○');
|
|
272
277
|
console.log(` ${icon} ${gate}: ${status}`);
|
|
278
|
+
if (status !== 'passed' && gate === activeExitGate && config.gates?.[gate]) {
|
|
279
|
+
const gateDef = config.gates[gate];
|
|
280
|
+
if (Array.isArray(gateDef.requires_files) && gateDef.requires_files.length > 0) {
|
|
281
|
+
const fileChecks = gateDef.requires_files.map(f => {
|
|
282
|
+
const exists = existsSync(join(root, f));
|
|
283
|
+
const short = f.replace(/^\.planning\//, '');
|
|
284
|
+
return exists ? chalk.green(short) : chalk.red(short);
|
|
285
|
+
});
|
|
286
|
+
console.log(` ${chalk.dim('Files:')} ${fileChecks.join(chalk.dim(', '))}`);
|
|
287
|
+
}
|
|
288
|
+
const reqs = [];
|
|
289
|
+
if (gateDef.requires_human_approval) reqs.push('human approval');
|
|
290
|
+
if (gateDef.requires_verification_pass) reqs.push('verification pass');
|
|
291
|
+
if (reqs.length > 0) {
|
|
292
|
+
console.log(` ${chalk.dim('Needs:')} ${reqs.join(', ')}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
273
295
|
}
|
|
274
296
|
}
|
|
275
297
|
|
|
@@ -425,7 +447,7 @@ function renderWorkflowKitArtifactsSection(wkData) {
|
|
|
425
447
|
|
|
426
448
|
function renderLastGateFailure(failure, config) {
|
|
427
449
|
const entryRole = config?.routing?.[failure.phase]?.entry_role || null;
|
|
428
|
-
const suggestedCommand = entryRole ? `agentxchain
|
|
450
|
+
const suggestedCommand = entryRole ? `agentxchain step --role ${entryRole}` : 'agentxchain step --role <role>';
|
|
429
451
|
const requestLabel = failure.gate_type === 'run_completion'
|
|
430
452
|
? 'Run completion'
|
|
431
453
|
: `${failure.from_phase || failure.phase} -> ${failure.to_phase || 'unknown'}`;
|
package/src/commands/step.js
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
} from '../lib/adapters/local-cli-adapter.js';
|
|
52
52
|
import { describeMcpRuntimeTarget, dispatchMcp, resolveMcpTransport } from '../lib/adapters/mcp-adapter.js';
|
|
53
53
|
import { dispatchRemoteAgent, describeRemoteAgentTarget } from '../lib/adapters/remote-agent-adapter.js';
|
|
54
|
+
import { summarizeRunProvenance } from '../lib/run-provenance.js';
|
|
54
55
|
import {
|
|
55
56
|
getDispatchAssignmentPath,
|
|
56
57
|
getDispatchContextPath,
|
|
@@ -60,7 +61,6 @@ import {
|
|
|
60
61
|
getTurnStagingResultPath,
|
|
61
62
|
} from '../lib/turn-paths.js';
|
|
62
63
|
import { dispatchApiProxy } from '../lib/adapters/api-proxy-adapter.js';
|
|
63
|
-
import { safeWriteJson } from '../lib/safe-write.js';
|
|
64
64
|
import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
|
|
65
65
|
import { runHooks } from '../lib/hook-runner.js';
|
|
66
66
|
import { finalizeDispatchManifest, verifyDispatchManifest } from '../lib/dispatch-manifest.js';
|
|
@@ -165,8 +165,8 @@ export async function stepCommand(opts) {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
if (!skipAssignment) {
|
|
168
|
-
if (state.
|
|
169
|
-
printRecoverySummary(state, 'This run is
|
|
168
|
+
if (state.pending_phase_transition || state.pending_run_completion) {
|
|
169
|
+
printRecoverySummary(state, 'This run is awaiting approval.');
|
|
170
170
|
process.exit(1);
|
|
171
171
|
}
|
|
172
172
|
|
|
@@ -231,10 +231,12 @@ export async function stepCommand(opts) {
|
|
|
231
231
|
const turnStatus = pausedTurn?.status;
|
|
232
232
|
if (turnStatus === 'failed' || turnStatus === 'retrying') {
|
|
233
233
|
console.log(chalk.yellow(`Re-dispatching failed turn: ${pausedTurn.turn_id}`));
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
234
|
+
const reactivated = reactivateGovernedRun(root, state, { via: 'step --resume', notificationConfig: config });
|
|
235
|
+
if (!reactivated.ok) {
|
|
236
|
+
console.log(chalk.red(`Failed to reactivate run: ${reactivated.error}`));
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
state = reactivated.state;
|
|
238
240
|
skipAssignment = true;
|
|
239
241
|
|
|
240
242
|
const bundleResult = writeDispatchBundle(root, state, config);
|
|
@@ -270,11 +272,12 @@ export async function stepCommand(opts) {
|
|
|
270
272
|
}
|
|
271
273
|
|
|
272
274
|
if (!skipAssignment && state.status === 'paused' && state.run_id) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
275
|
+
const reactivated = reactivateGovernedRun(root, state, { via: 'step', notificationConfig: config });
|
|
276
|
+
if (!reactivated.ok) {
|
|
277
|
+
console.log(chalk.red(`Failed to reactivate run: ${reactivated.error}`));
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
state = reactivated.state;
|
|
278
281
|
console.log(chalk.green(`Resumed governed run: ${state.run_id}`));
|
|
279
282
|
}
|
|
280
283
|
|
|
@@ -324,6 +327,8 @@ export async function stepCommand(opts) {
|
|
|
324
327
|
const runtimeType = runtime?.type || role?.runtime_class || 'manual';
|
|
325
328
|
const hooksConfig = config.hooks || {};
|
|
326
329
|
|
|
330
|
+
printStepRunContext({ root, state, config });
|
|
331
|
+
|
|
327
332
|
if (bundleWritten && hooksConfig.after_dispatch?.length > 0) {
|
|
328
333
|
const afterDispatchHooks = runHooks(root, hooksConfig, 'after_dispatch', {
|
|
329
334
|
turn_id: turn.turn_id,
|
|
@@ -943,6 +948,53 @@ function printRecoverySummary(state, heading) {
|
|
|
943
948
|
}
|
|
944
949
|
}
|
|
945
950
|
|
|
951
|
+
function printStepRunContext({ root, state, config }) {
|
|
952
|
+
console.log('');
|
|
953
|
+
console.log(chalk.cyan.bold('agentxchain step'));
|
|
954
|
+
console.log(` ${chalk.dim('Run:')} ${state?.run_id || '(uninitialized)'}`);
|
|
955
|
+
console.log(` ${chalk.dim('Phase:')} ${state?.phase || '(unknown)'}`);
|
|
956
|
+
|
|
957
|
+
const provenanceSummary = summarizeRunProvenance(state?.provenance);
|
|
958
|
+
if (provenanceSummary) {
|
|
959
|
+
console.log(` ${chalk.dim('Origin:')} ${chalk.magenta(provenanceSummary)}`);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (state?.inherited_context?.parent_run_id) {
|
|
963
|
+
console.log(
|
|
964
|
+
` ${chalk.dim('Inherits:')} ${chalk.magenta(
|
|
965
|
+
`parent ${state.inherited_context.parent_run_id} (${state.inherited_context.parent_status || 'unknown'})`
|
|
966
|
+
)}`
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const activeGate = config?.routing?.[state?.phase]?.exit_gate || null;
|
|
971
|
+
if (activeGate) {
|
|
972
|
+
const gateStatus = state?.phase_gate_status?.[activeGate] || 'pending';
|
|
973
|
+
console.log(` ${chalk.dim('Gate:')} ${activeGate} (${gateStatus})`);
|
|
974
|
+
|
|
975
|
+
if (gateStatus !== 'passed') {
|
|
976
|
+
const gateDef = config?.gates?.[activeGate];
|
|
977
|
+
if (Array.isArray(gateDef?.requires_files) && gateDef.requires_files.length > 0) {
|
|
978
|
+
const fileChecks = gateDef.requires_files.map((filePath) => {
|
|
979
|
+
const exists = existsSync(join(root, filePath));
|
|
980
|
+
const shortPath = filePath.replace(/^\.planning\//, '');
|
|
981
|
+
return exists ? chalk.green(shortPath) : chalk.red(shortPath);
|
|
982
|
+
});
|
|
983
|
+
console.log(` ${chalk.dim('Files:')} ${fileChecks.join(chalk.dim(', '))}`);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const requirements = [];
|
|
987
|
+
if (gateDef?.requires_human_approval) requirements.push('human approval');
|
|
988
|
+
if (gateDef?.requires_verification_pass) requirements.push('verification pass');
|
|
989
|
+
if (requirements.length > 0) {
|
|
990
|
+
console.log(` ${chalk.dim('Needs:')} ${requirements.join(', ')}`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
console.log('');
|
|
996
|
+
}
|
|
997
|
+
|
|
946
998
|
function printDispatchBundleWarnings(bundleResult) {
|
|
947
999
|
for (const warning of bundleResult.warnings || []) {
|
|
948
1000
|
console.log(chalk.yellow(`Dispatch bundle warning: ${warning}`));
|
|
@@ -239,6 +239,7 @@ export function resyncFromRepoAuthority(workspacePath, state, config) {
|
|
|
239
239
|
const resyncedRepos = [];
|
|
240
240
|
const projectedAcceptances = [];
|
|
241
241
|
const barrierChanges = [];
|
|
242
|
+
const mismatchDetails = [];
|
|
242
243
|
|
|
243
244
|
// Step 1: Refresh repo_runs from repo-local authority
|
|
244
245
|
const updatedRepoRuns = { ...state.repo_runs };
|
|
@@ -262,6 +263,13 @@ export function resyncFromRepoAuthority(workspacePath, state, config) {
|
|
|
262
263
|
if (repoRun.run_id && repoRun.run_id !== (repoState.run_id ?? null)) {
|
|
263
264
|
const reason = buildRunIdMismatchReason(repoId, repoRun.run_id, repoState.run_id ?? null);
|
|
264
265
|
runIdMismatches.push(reason);
|
|
266
|
+
mismatchDetails.push({
|
|
267
|
+
code: 'repo_run_id_mismatch',
|
|
268
|
+
repo_id: repoId,
|
|
269
|
+
expected_run_id: repoRun.run_id,
|
|
270
|
+
actual_run_id: repoState.run_id ?? null,
|
|
271
|
+
message: reason,
|
|
272
|
+
});
|
|
265
273
|
errors.push(reason);
|
|
266
274
|
continue;
|
|
267
275
|
}
|
|
@@ -447,6 +455,7 @@ export function resyncFromRepoAuthority(workspacePath, state, config) {
|
|
|
447
455
|
resynced_repos: [...new Set(resyncedRepos)],
|
|
448
456
|
projected_acceptances: projectedAcceptances,
|
|
449
457
|
barrier_changes: barrierChanges,
|
|
458
|
+
mismatch_details: mismatchDetails,
|
|
450
459
|
errors,
|
|
451
460
|
blocked_reason: blockedReason || undefined,
|
|
452
461
|
};
|
|
@@ -118,6 +118,9 @@ export function writeDispatchBundle(root, state, config, opts = {}) {
|
|
|
118
118
|
budget_reservation_usd: state.budget_reservations?.[turn.turn_id]?.reserved_usd ?? null,
|
|
119
119
|
active_siblings: activeSiblings,
|
|
120
120
|
};
|
|
121
|
+
if (turn.intake_context) {
|
|
122
|
+
assignment.intake_context = turn.intake_context;
|
|
123
|
+
}
|
|
121
124
|
if (turn.conflict_context) {
|
|
122
125
|
assignment.conflict_context = turn.conflict_context;
|
|
123
126
|
}
|
|
@@ -523,6 +526,27 @@ function renderContext(state, config, root, turn, role) {
|
|
|
523
526
|
lines.push('');
|
|
524
527
|
}
|
|
525
528
|
|
|
529
|
+
if (turn.intake_context) {
|
|
530
|
+
lines.push('## Intake Intent');
|
|
531
|
+
lines.push('');
|
|
532
|
+
lines.push(`- **Intent:** ${turn.intake_context.intent_id || 'unknown'}`);
|
|
533
|
+
lines.push(`- **Event:** ${turn.intake_context.event_id || 'unknown'}`);
|
|
534
|
+
lines.push(`- **Source:** ${turn.intake_context.source || 'unknown'}`);
|
|
535
|
+
if (turn.intake_context.category) {
|
|
536
|
+
lines.push(`- **Category:** ${turn.intake_context.category}`);
|
|
537
|
+
}
|
|
538
|
+
if (turn.intake_context.charter) {
|
|
539
|
+
lines.push(`- **Charter:** ${turn.intake_context.charter}`);
|
|
540
|
+
}
|
|
541
|
+
if (Array.isArray(turn.intake_context.acceptance_contract) && turn.intake_context.acceptance_contract.length > 0) {
|
|
542
|
+
lines.push('- **Acceptance Contract:**');
|
|
543
|
+
for (const requirement of turn.intake_context.acceptance_contract) {
|
|
544
|
+
lines.push(` - ${requirement}`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
lines.push('');
|
|
548
|
+
}
|
|
549
|
+
|
|
526
550
|
// Inherited context from parent run (when --inherit-context was used)
|
|
527
551
|
if (state.inherited_context) {
|
|
528
552
|
// First turn gets the full rendering; subsequent turns get compact
|
|
@@ -1197,13 +1221,13 @@ function buildTurnResultTemplate(state, turn, roleId, role) {
|
|
|
1197
1221
|
role: roleId,
|
|
1198
1222
|
runtime_id: turn.runtime_id,
|
|
1199
1223
|
status: 'completed',
|
|
1200
|
-
summary: '
|
|
1224
|
+
summary: '<one-line summary of what you accomplished>',
|
|
1201
1225
|
decisions: [
|
|
1202
1226
|
{
|
|
1203
1227
|
id: 'DEC-001',
|
|
1204
1228
|
category: 'implementation',
|
|
1205
|
-
statement: '
|
|
1206
|
-
rationale: '
|
|
1229
|
+
statement: '<what was decided and why it matters>',
|
|
1230
|
+
rationale: '<reasoning behind this decision>',
|
|
1207
1231
|
},
|
|
1208
1232
|
],
|
|
1209
1233
|
objections: isReviewOnly
|
|
@@ -1211,29 +1235,29 @@ function buildTurnResultTemplate(state, turn, roleId, role) {
|
|
|
1211
1235
|
{
|
|
1212
1236
|
id: 'OBJ-001',
|
|
1213
1237
|
severity: 'medium',
|
|
1214
|
-
against_turn_id: state.last_completed_turn_id || '
|
|
1215
|
-
statement: '
|
|
1238
|
+
against_turn_id: state.last_completed_turn_id || '<turn_id of the turn you are reviewing>',
|
|
1239
|
+
statement: '<specific objection to the previous turn — required for review_only roles>',
|
|
1216
1240
|
status: 'raised',
|
|
1217
1241
|
},
|
|
1218
1242
|
]
|
|
1219
1243
|
: [],
|
|
1220
|
-
files_changed: isReviewOnly ? [] : ['
|
|
1244
|
+
files_changed: isReviewOnly ? [] : ['<path/to/modified/file>'],
|
|
1221
1245
|
artifacts_created: [],
|
|
1222
1246
|
verification: {
|
|
1223
1247
|
status: isReviewOnly ? 'skipped' : 'pass',
|
|
1224
|
-
commands: isReviewOnly ? [] : ['
|
|
1248
|
+
commands: isReviewOnly ? [] : ['<command you ran to verify>'],
|
|
1225
1249
|
evidence_summary: isReviewOnly
|
|
1226
1250
|
? 'Review turn — no verification commands required.'
|
|
1227
|
-
: '
|
|
1251
|
+
: '<what you verified and how>',
|
|
1228
1252
|
machine_evidence: isReviewOnly
|
|
1229
1253
|
? []
|
|
1230
|
-
: [{ command: '
|
|
1254
|
+
: [{ command: '<exact command that was run>', exit_code: 0 }],
|
|
1231
1255
|
},
|
|
1232
1256
|
artifact: {
|
|
1233
1257
|
type: isReviewOnly ? 'review' : 'workspace',
|
|
1234
1258
|
ref: isReviewOnly ? null : 'git:dirty',
|
|
1235
1259
|
},
|
|
1236
|
-
proposed_next_role: '
|
|
1260
|
+
proposed_next_role: '<role_id that should act next>',
|
|
1237
1261
|
phase_transition_request: null,
|
|
1238
1262
|
run_completion_request: null,
|
|
1239
1263
|
needs_human_reason: null,
|
|
@@ -1774,6 +1774,12 @@ export function reactivateGovernedRun(root, state, details = {}) {
|
|
|
1774
1774
|
if (!state || typeof state !== 'object') {
|
|
1775
1775
|
return { ok: false, error: 'State is required.' };
|
|
1776
1776
|
}
|
|
1777
|
+
if (state.status !== 'blocked' && state.status !== 'paused') {
|
|
1778
|
+
return { ok: false, error: `Cannot reactivate run: status is "${state.status}", expected "blocked" or "paused".` };
|
|
1779
|
+
}
|
|
1780
|
+
if (state.pending_phase_transition || state.pending_run_completion) {
|
|
1781
|
+
return { ok: false, error: 'Cannot reactivate run: this run is awaiting approval. Use approve-transition or approve-completion.' };
|
|
1782
|
+
}
|
|
1777
1783
|
|
|
1778
1784
|
const now = new Date().toISOString();
|
|
1779
1785
|
const wasEscalation = state.status === 'blocked' && typeof state.blocked_on === 'string' && state.blocked_on.startsWith('escalation:');
|
|
@@ -1819,7 +1825,7 @@ export function reactivateGovernedRun(root, state, details = {}) {
|
|
|
1819
1825
|
// ── Core Operations ──────────────────────────────────────────────────────────
|
|
1820
1826
|
|
|
1821
1827
|
/**
|
|
1822
|
-
* Initialize a governed run from
|
|
1828
|
+
* Initialize a governed run from bootstrap state.
|
|
1823
1829
|
* Creates a run_id and sets status to 'active'.
|
|
1824
1830
|
*
|
|
1825
1831
|
* @param {string} root - project root directory
|
|
@@ -1829,7 +1835,7 @@ export function reactivateGovernedRun(root, state, details = {}) {
|
|
|
1829
1835
|
export function initializeGovernedRun(root, config, options = {}) {
|
|
1830
1836
|
let state = readState(root);
|
|
1831
1837
|
if (!state) {
|
|
1832
|
-
|
|
1838
|
+
state = buildFreshIdleStateForNewRun(null, config);
|
|
1833
1839
|
}
|
|
1834
1840
|
const allowTerminalRestart = options.allow_terminal_restart === true
|
|
1835
1841
|
&& (state.status === 'completed' || state.status === 'blocked');
|
|
@@ -1837,8 +1843,8 @@ export function initializeGovernedRun(root, config, options = {}) {
|
|
|
1837
1843
|
return { ok: false, error: 'Cannot initialize run: this run is already completed. Start a new project or reset state.' };
|
|
1838
1844
|
}
|
|
1839
1845
|
const allowBlockedBootstrap = state.status === 'blocked' && state.run_id === null && getActiveTurnCount(state) === 0;
|
|
1840
|
-
if (state.status !== 'idle' &&
|
|
1841
|
-
return { ok: false, error: `Cannot initialize run: status is "${state.status}", expected "idle"
|
|
1846
|
+
if (state.status !== 'idle' && !allowBlockedBootstrap && !allowTerminalRestart) {
|
|
1847
|
+
return { ok: false, error: `Cannot initialize run: status is "${state.status}", expected "idle" or pre-run "blocked"` };
|
|
1842
1848
|
}
|
|
1843
1849
|
if (allowTerminalRestart) {
|
|
1844
1850
|
state = buildFreshIdleStateForNewRun(state, config);
|
package/src/lib/intake.js
CHANGED
|
@@ -120,6 +120,122 @@ function readJsonDir(dirPath) {
|
|
|
120
120
|
.filter(Boolean);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
function buildIntakeNextAction(intent) {
|
|
124
|
+
const intentId = intent?.intent_id || '<intent_id>';
|
|
125
|
+
const status = intent?.status || 'unknown';
|
|
126
|
+
const resolveCommand = `agentxchain intake resolve --intent ${intentId}`;
|
|
127
|
+
|
|
128
|
+
switch (status) {
|
|
129
|
+
case 'detected':
|
|
130
|
+
return {
|
|
131
|
+
label: 'triage',
|
|
132
|
+
summary: 'Triage this detected intent so it can enter governed delivery.',
|
|
133
|
+
command: `agentxchain intake triage --intent ${intentId} --priority <p0-p3> --template <template_id> --charter "<charter>" --acceptance "<criterion>"`,
|
|
134
|
+
alternatives: [
|
|
135
|
+
`agentxchain intake triage --intent ${intentId} --suppress --reason "<reason>"`,
|
|
136
|
+
],
|
|
137
|
+
recovery: null,
|
|
138
|
+
action_required: true,
|
|
139
|
+
};
|
|
140
|
+
case 'triaged':
|
|
141
|
+
return {
|
|
142
|
+
label: 'approve',
|
|
143
|
+
summary: 'Approve this triaged intent for planning or reject it explicitly.',
|
|
144
|
+
command: `agentxchain intake approve --intent ${intentId}`,
|
|
145
|
+
alternatives: [
|
|
146
|
+
`agentxchain intake triage --intent ${intentId} --reject --reason "<reason>"`,
|
|
147
|
+
],
|
|
148
|
+
recovery: null,
|
|
149
|
+
action_required: true,
|
|
150
|
+
};
|
|
151
|
+
case 'approved':
|
|
152
|
+
return {
|
|
153
|
+
label: 'plan',
|
|
154
|
+
summary: 'Generate planning artifacts for this approved intent.',
|
|
155
|
+
command: `agentxchain intake plan --intent ${intentId}`,
|
|
156
|
+
alternatives: [],
|
|
157
|
+
recovery: null,
|
|
158
|
+
action_required: true,
|
|
159
|
+
};
|
|
160
|
+
case 'planned':
|
|
161
|
+
return {
|
|
162
|
+
label: 'start',
|
|
163
|
+
summary: 'Start repo-local execution or hand the intent off to a coordinator workstream.',
|
|
164
|
+
command: `agentxchain intake start --intent ${intentId}`,
|
|
165
|
+
alternatives: [
|
|
166
|
+
`agentxchain intake handoff --intent ${intentId} --coordinator-root <path> --workstream <id>`,
|
|
167
|
+
],
|
|
168
|
+
recovery: null,
|
|
169
|
+
action_required: true,
|
|
170
|
+
};
|
|
171
|
+
case 'executing':
|
|
172
|
+
return {
|
|
173
|
+
label: 'resolve',
|
|
174
|
+
summary: intent?.target_workstream
|
|
175
|
+
? 'Re-check the coordinator workstream outcome for this intent.'
|
|
176
|
+
: 'Re-check the governed run outcome for this intent.',
|
|
177
|
+
command: resolveCommand,
|
|
178
|
+
alternatives: [],
|
|
179
|
+
recovery: null,
|
|
180
|
+
action_required: true,
|
|
181
|
+
};
|
|
182
|
+
case 'blocked':
|
|
183
|
+
return {
|
|
184
|
+
label: 'recover',
|
|
185
|
+
summary: 'Resolve the linked run blockage, then re-check intake resolution.',
|
|
186
|
+
command: resolveCommand,
|
|
187
|
+
alternatives: [],
|
|
188
|
+
recovery: intent?.run_blocked_recovery || null,
|
|
189
|
+
action_required: true,
|
|
190
|
+
};
|
|
191
|
+
case 'completed':
|
|
192
|
+
return {
|
|
193
|
+
label: 'none',
|
|
194
|
+
summary: 'No action required. This intent completed successfully.',
|
|
195
|
+
command: null,
|
|
196
|
+
alternatives: [],
|
|
197
|
+
recovery: null,
|
|
198
|
+
action_required: false,
|
|
199
|
+
};
|
|
200
|
+
case 'suppressed':
|
|
201
|
+
return {
|
|
202
|
+
label: 'none',
|
|
203
|
+
summary: 'No action required. This intent was suppressed.',
|
|
204
|
+
command: null,
|
|
205
|
+
alternatives: [],
|
|
206
|
+
recovery: null,
|
|
207
|
+
action_required: false,
|
|
208
|
+
};
|
|
209
|
+
case 'rejected':
|
|
210
|
+
return {
|
|
211
|
+
label: 'none',
|
|
212
|
+
summary: 'No action required. This intent was rejected.',
|
|
213
|
+
command: null,
|
|
214
|
+
alternatives: [],
|
|
215
|
+
recovery: null,
|
|
216
|
+
action_required: false,
|
|
217
|
+
};
|
|
218
|
+
case 'failed':
|
|
219
|
+
return {
|
|
220
|
+
label: 'inspect',
|
|
221
|
+
summary: 'Manual inspection required. This intent is in a reserved failed state.',
|
|
222
|
+
command: null,
|
|
223
|
+
alternatives: [],
|
|
224
|
+
recovery: 'Inspect the linked intent and run artifacts manually before continuing.',
|
|
225
|
+
action_required: true,
|
|
226
|
+
};
|
|
227
|
+
default:
|
|
228
|
+
return {
|
|
229
|
+
label: 'inspect',
|
|
230
|
+
summary: 'Manual inspection required. The intent state is not recognized by the current intake surface.',
|
|
231
|
+
command: null,
|
|
232
|
+
alternatives: [],
|
|
233
|
+
recovery: null,
|
|
234
|
+
action_required: true,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
123
239
|
// ---------------------------------------------------------------------------
|
|
124
240
|
// Validation
|
|
125
241
|
// ---------------------------------------------------------------------------
|
|
@@ -329,7 +445,13 @@ export function intakeStatus(root, intentId) {
|
|
|
329
445
|
const intent = JSON.parse(readFileSync(intentPath, 'utf8'));
|
|
330
446
|
const eventPath = join(dirs.events, `${intent.event_id}.json`);
|
|
331
447
|
const event = existsSync(eventPath) ? JSON.parse(readFileSync(eventPath, 'utf8')) : null;
|
|
332
|
-
return {
|
|
448
|
+
return {
|
|
449
|
+
ok: true,
|
|
450
|
+
intent,
|
|
451
|
+
event,
|
|
452
|
+
next_action: buildIntakeNextAction(intent),
|
|
453
|
+
exitCode: 0,
|
|
454
|
+
};
|
|
333
455
|
}
|
|
334
456
|
|
|
335
457
|
const events = readJsonDir(dirs.events);
|
|
@@ -354,6 +476,7 @@ export function intakeStatus(root, intentId) {
|
|
|
354
476
|
template: i.template,
|
|
355
477
|
status: i.status,
|
|
356
478
|
updated_at: i.updated_at,
|
|
479
|
+
next_action: buildIntakeNextAction(i),
|
|
357
480
|
})),
|
|
358
481
|
};
|
|
359
482
|
|
|
@@ -521,6 +644,20 @@ export function startIntent(root, intentId, options = {}) {
|
|
|
521
644
|
};
|
|
522
645
|
}
|
|
523
646
|
|
|
647
|
+
const loadedEvent = readEvent(root, intent.event_id);
|
|
648
|
+
if (!loadedEvent.ok) {
|
|
649
|
+
return loadedEvent;
|
|
650
|
+
}
|
|
651
|
+
const { event } = loadedEvent;
|
|
652
|
+
const intakeContext = {
|
|
653
|
+
intent_id: intent.intent_id,
|
|
654
|
+
event_id: intent.event_id,
|
|
655
|
+
source: event.source || null,
|
|
656
|
+
category: event.category || null,
|
|
657
|
+
charter: intent.charter || null,
|
|
658
|
+
acceptance_contract: Array.isArray(intent.acceptance_contract) ? intent.acceptance_contract : [],
|
|
659
|
+
};
|
|
660
|
+
|
|
524
661
|
// Load governed project context
|
|
525
662
|
const context = loadProjectContext(root);
|
|
526
663
|
if (!context) {
|
|
@@ -569,6 +706,10 @@ export function startIntent(root, intentId, options = {}) {
|
|
|
569
706
|
};
|
|
570
707
|
}
|
|
571
708
|
|
|
709
|
+
if (state.status === 'paused') {
|
|
710
|
+
return { ok: false, error: 'cannot start: run is paused (awaiting approval). Resolve the blocking gate before starting a new intake turn.', exitCode: 1 };
|
|
711
|
+
}
|
|
712
|
+
|
|
572
713
|
if (state.pending_phase_transition) {
|
|
573
714
|
return { ok: false, error: `cannot start: pending phase transition to "${state.pending_phase_transition}"`, exitCode: 1 };
|
|
574
715
|
}
|
|
@@ -579,21 +720,20 @@ export function startIntent(root, intentId, options = {}) {
|
|
|
579
720
|
|
|
580
721
|
// Bootstrap: idle with no run → initialize
|
|
581
722
|
if (state.status === 'idle' && !state.run_id) {
|
|
582
|
-
const initResult = initializeGovernedRun(root, config
|
|
723
|
+
const initResult = initializeGovernedRun(root, config, {
|
|
724
|
+
provenance: {
|
|
725
|
+
trigger: 'intake',
|
|
726
|
+
intake_intent_id: intent.intent_id,
|
|
727
|
+
trigger_reason: intent.charter || null,
|
|
728
|
+
created_by: 'operator',
|
|
729
|
+
},
|
|
730
|
+
});
|
|
583
731
|
if (!initResult.ok) {
|
|
584
732
|
return { ok: false, error: `run initialization failed: ${initResult.error}`, exitCode: 1 };
|
|
585
733
|
}
|
|
586
734
|
state = initResult.state;
|
|
587
735
|
}
|
|
588
736
|
|
|
589
|
-
// Resume: paused with no active turns → reactivate
|
|
590
|
-
if (state.status === 'paused' && state.run_id) {
|
|
591
|
-
state.status = 'active';
|
|
592
|
-
state.blocked_on = null;
|
|
593
|
-
state.escalation = null;
|
|
594
|
-
safeWriteJson(statePath, state);
|
|
595
|
-
}
|
|
596
|
-
|
|
597
737
|
// Resolve role
|
|
598
738
|
const roleId = resolveIntakeRole(options.role, state, config);
|
|
599
739
|
if (!roleId.ok) {
|
|
@@ -614,6 +754,12 @@ export function startIntent(root, intentId, options = {}) {
|
|
|
614
754
|
return { ok: false, error: 'turn assignment succeeded but turn not found in state', exitCode: 1 };
|
|
615
755
|
}
|
|
616
756
|
|
|
757
|
+
assignedTurn.intake_context = intakeContext;
|
|
758
|
+
if (state.active_turns?.[assignedTurn.turn_id]) {
|
|
759
|
+
state.active_turns[assignedTurn.turn_id].intake_context = intakeContext;
|
|
760
|
+
safeWriteJson(statePath, state);
|
|
761
|
+
}
|
|
762
|
+
|
|
617
763
|
// Write dispatch bundle
|
|
618
764
|
const bundleResult = writeDispatchBundle(root, state, config);
|
|
619
765
|
if (!bundleResult.ok) {
|
|
@@ -263,9 +263,68 @@ function validateSchema(tr) {
|
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
errors.push(...collectUnfilledTemplatePlaceholderErrors(tr));
|
|
267
|
+
|
|
266
268
|
return errors;
|
|
267
269
|
}
|
|
268
270
|
|
|
271
|
+
function collectUnfilledTemplatePlaceholderErrors(tr) {
|
|
272
|
+
const errors = [];
|
|
273
|
+
|
|
274
|
+
checkPlaceholder(errors, 'summary', tr.summary);
|
|
275
|
+
checkPlaceholder(errors, 'proposed_next_role', tr.proposed_next_role);
|
|
276
|
+
|
|
277
|
+
if (Array.isArray(tr.decisions)) {
|
|
278
|
+
for (let i = 0; i < tr.decisions.length; i++) {
|
|
279
|
+
const decision = tr.decisions[i];
|
|
280
|
+
checkPlaceholder(errors, `decisions[${i}].statement`, decision?.statement);
|
|
281
|
+
checkPlaceholder(errors, `decisions[${i}].rationale`, decision?.rationale);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (Array.isArray(tr.objections)) {
|
|
286
|
+
for (let i = 0; i < tr.objections.length; i++) {
|
|
287
|
+
const objection = tr.objections[i];
|
|
288
|
+
checkPlaceholder(errors, `objections[${i}].against_turn_id`, objection?.against_turn_id);
|
|
289
|
+
checkPlaceholder(errors, `objections[${i}].statement`, objection?.statement);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (Array.isArray(tr.files_changed)) {
|
|
294
|
+
for (let i = 0; i < tr.files_changed.length; i++) {
|
|
295
|
+
checkPlaceholder(errors, `files_changed[${i}]`, tr.files_changed[i]);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const verification = tr.verification;
|
|
300
|
+
if (verification && typeof verification === 'object' && !Array.isArray(verification)) {
|
|
301
|
+
if (Array.isArray(verification.commands)) {
|
|
302
|
+
for (let i = 0; i < verification.commands.length; i++) {
|
|
303
|
+
checkPlaceholder(errors, `verification.commands[${i}]`, verification.commands[i]);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
checkPlaceholder(errors, 'verification.evidence_summary', verification.evidence_summary);
|
|
307
|
+
|
|
308
|
+
if (Array.isArray(verification.machine_evidence)) {
|
|
309
|
+
for (let i = 0; i < verification.machine_evidence.length; i++) {
|
|
310
|
+
checkPlaceholder(
|
|
311
|
+
errors,
|
|
312
|
+
`verification.machine_evidence[${i}].command`,
|
|
313
|
+
verification.machine_evidence[i]?.command
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return errors;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function checkPlaceholder(errors, fieldPath, value) {
|
|
323
|
+
if (typeof value === 'string' && /^<[^>]+>$/.test(value)) {
|
|
324
|
+
errors.push(`${fieldPath} contains an unfilled template placeholder: "${value}".`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
269
328
|
function validateDecision(dec, index) {
|
|
270
329
|
const errors = [];
|
|
271
330
|
const prefix = `decisions[${index}]`;
|
|
@@ -56,6 +56,31 @@ function evaluatePmSignoff(content) {
|
|
|
56
56
|
return { ok: true };
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
const SYSTEM_SPEC_SCAFFOLD_PLACEHOLDER = /^\(.*\)$/;
|
|
60
|
+
const SYSTEM_SPEC_ACCEPTANCE_SCAFFOLD = /^- \[ \] Name the executable checks/;
|
|
61
|
+
|
|
62
|
+
function isSystemSpecPlaceholderLine(line) {
|
|
63
|
+
return SYSTEM_SPEC_SCAFFOLD_PLACEHOLDER.test(line) || SYSTEM_SPEC_ACCEPTANCE_SCAFFOLD.test(line);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function hasSectionRealContent(content, sectionHeader, isPlaceholderFn) {
|
|
67
|
+
const lines = content.split(/\r?\n/);
|
|
68
|
+
const headerIndex = lines.findIndex((line) => line.trim().startsWith(sectionHeader));
|
|
69
|
+
if (headerIndex === -1) {
|
|
70
|
+
return { found: false, hasContent: false };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (let i = headerIndex + 1; i < lines.length; i++) {
|
|
74
|
+
const line = lines[i].trim();
|
|
75
|
+
if (line.startsWith('## ')) break;
|
|
76
|
+
if (!line) continue;
|
|
77
|
+
if (isPlaceholderFn(line)) continue;
|
|
78
|
+
return { found: true, hasContent: true };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { found: true, hasContent: false };
|
|
82
|
+
}
|
|
83
|
+
|
|
59
84
|
function evaluateSystemSpec(content) {
|
|
60
85
|
const requiredSections = ['## Purpose', '## Interface', '## Acceptance Tests'];
|
|
61
86
|
const missingSections = requiredSections.filter((section) => !content.includes(section));
|
|
@@ -67,6 +92,21 @@ function evaluateSystemSpec(content) {
|
|
|
67
92
|
};
|
|
68
93
|
}
|
|
69
94
|
|
|
95
|
+
const placeholderSections = [];
|
|
96
|
+
for (const section of requiredSections) {
|
|
97
|
+
const result = hasSectionRealContent(content, section, isSystemSpecPlaceholderLine);
|
|
98
|
+
if (result.found && !result.hasContent) {
|
|
99
|
+
placeholderSections.push(section);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (placeholderSections.length > 0) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
reason: `${placeholderSections.join(' and ')} in .planning/SYSTEM_SPEC.md still contains only scaffold placeholder text. Replace placeholder content with real spec content before planning can exit.`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
70
110
|
return { ok: true };
|
|
71
111
|
}
|
|
72
112
|
|
|
@@ -362,6 +402,15 @@ function evaluateShipVerdict(content) {
|
|
|
362
402
|
return { ok: true };
|
|
363
403
|
}
|
|
364
404
|
|
|
405
|
+
const SECTION_CHECK_SCAFFOLD_PLACEHOLDERS = [
|
|
406
|
+
/^\(Content here\.\)$/i,
|
|
407
|
+
/^\(Operator fills this in\.\)$/i,
|
|
408
|
+
];
|
|
409
|
+
|
|
410
|
+
function isSectionCheckPlaceholderLine(line) {
|
|
411
|
+
return SECTION_CHECK_SCAFFOLD_PLACEHOLDERS.some((re) => re.test(line));
|
|
412
|
+
}
|
|
413
|
+
|
|
365
414
|
function evaluateSectionCheck(content, config) {
|
|
366
415
|
if (!config?.required_sections?.length) {
|
|
367
416
|
return { ok: true };
|
|
@@ -375,6 +424,22 @@ function evaluateSectionCheck(content, config) {
|
|
|
375
424
|
reason: `Document must contain sections: ${missing.join(', ')}`,
|
|
376
425
|
};
|
|
377
426
|
}
|
|
427
|
+
|
|
428
|
+
const placeholderSections = [];
|
|
429
|
+
for (const section of config.required_sections) {
|
|
430
|
+
const result = hasSectionRealContent(content, section, isSectionCheckPlaceholderLine);
|
|
431
|
+
if (result.found && !result.hasContent) {
|
|
432
|
+
placeholderSections.push(section);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (placeholderSections.length > 0) {
|
|
437
|
+
return {
|
|
438
|
+
ok: false,
|
|
439
|
+
reason: `Sections still contain only scaffold placeholder text: ${placeholderSections.join(', ')}. Replace placeholder content before this gate can pass.`,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
378
443
|
return { ok: true };
|
|
379
444
|
}
|
|
380
445
|
|