agentxchain 2.80.0 → 2.82.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 +34 -2
- package/bin/agentxchain.js +13 -1
- package/package.json +1 -1
- package/src/commands/doctor.js +121 -0
- package/src/commands/init.js +92 -25
- package/src/commands/multi.js +8 -0
- package/src/commands/replay.js +120 -0
- package/src/commands/resume.js +30 -10
- package/src/commands/step.js +14 -12
- package/src/lib/accepted-turn-history.js +84 -0
- package/src/lib/coordinator-recovery.js +9 -0
- package/src/lib/dispatch-bundle.js +34 -10
- package/src/lib/governed-state.js +9 -3
- package/src/lib/intake.js +32 -9
- 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
|
|---|---|
|
|
@@ -167,12 +170,41 @@ agentxchain step
|
|
|
167
170
|
| `approve-completion` | Approve a pending human-gated run completion |
|
|
168
171
|
| `validate` | Validate governed kickoff wiring, a staged turn, or both |
|
|
169
172
|
| `template validate` | Prove the template registry, workflow-kit scaffold contract, and planning artifact completeness (`--json` exposes a `workflow_kit` block) |
|
|
173
|
+
| `verify turn` | Replay a staged turn's declared machine-evidence commands to confirm reproducibility before acceptance |
|
|
174
|
+
| `replay turn` | Replay an accepted turn's machine-evidence commands from history for audit and drift detection |
|
|
170
175
|
| `verify protocol` | Run the shipped protocol conformance suite against a target implementation |
|
|
171
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
|
+
|---|---|
|
|
172
199
|
| `multi init\|status\|step\|resume\|approve-gate\|resync` | Run the multi-repo coordinator lifecycle, including blocked-state recovery via `multi resume` |
|
|
173
200
|
| `intake record\|triage\|approve\|plan\|start\|scan\|resolve` | Continuous-delivery intake: turn delivery signals into governed work items |
|
|
174
201
|
| `intake handoff` | Bridge a planned intake intent to a coordinator workstream for multi-repo execution |
|
|
175
|
-
| `
|
|
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` |
|
|
176
208
|
|
|
177
209
|
### Shared utilities
|
|
178
210
|
|
package/bin/agentxchain.js
CHANGED
|
@@ -60,6 +60,7 @@ import { doctorCommand } from '../src/commands/doctor.js';
|
|
|
60
60
|
import { superviseCommand } from '../src/commands/supervise.js';
|
|
61
61
|
import { validateCommand } from '../src/commands/validate.js';
|
|
62
62
|
import { verifyExportCommand, verifyProtocolCommand, verifyTurnCommand } from '../src/commands/verify.js';
|
|
63
|
+
import { replayTurnCommand } from '../src/commands/replay.js';
|
|
63
64
|
import { kickoffCommand } from '../src/commands/kickoff.js';
|
|
64
65
|
import { rebindCommand } from '../src/commands/rebind.js';
|
|
65
66
|
import { branchCommand } from '../src/commands/branch.js';
|
|
@@ -130,7 +131,7 @@ program
|
|
|
130
131
|
program
|
|
131
132
|
.command('init')
|
|
132
133
|
.description('Create a new AgentXchain project folder')
|
|
133
|
-
.option('-y, --yes', 'Skip prompts, use defaults')
|
|
134
|
+
.option('-y, --yes', 'Skip guided prompts, use defaults')
|
|
134
135
|
.option('--governed', 'Create a governed project (orchestrator-owned state)')
|
|
135
136
|
.option('--dir <path>', 'Scaffold target directory. Use "." for in-place bootstrap.')
|
|
136
137
|
.option('--template <id>', 'Governed scaffold template: generic, api-service, cli-tool, library, web-app, enterprise-app')
|
|
@@ -388,6 +389,17 @@ verifyCmd
|
|
|
388
389
|
.option('--format <format>', 'Output format: text or json', 'text')
|
|
389
390
|
.action(verifyExportCommand);
|
|
390
391
|
|
|
392
|
+
const replayCmd = program
|
|
393
|
+
.command('replay')
|
|
394
|
+
.description('Replay accepted governed evidence against the current workspace');
|
|
395
|
+
|
|
396
|
+
replayCmd
|
|
397
|
+
.command('turn [turn_id]')
|
|
398
|
+
.description('Replay an accepted turn\'s declared machine-evidence commands from history')
|
|
399
|
+
.option('-j, --json', 'Output as JSON')
|
|
400
|
+
.option('--timeout <ms>', 'Per-command replay timeout in milliseconds', '30000')
|
|
401
|
+
.action(replayTurnCommand);
|
|
402
|
+
|
|
391
403
|
program
|
|
392
404
|
.command('migrate')
|
|
393
405
|
.description('Migrate a legacy v3 project to governed format')
|
package/package.json
CHANGED
package/src/commands/doctor.js
CHANGED
|
@@ -8,6 +8,7 @@ import { getWatchPid } from './watch.js';
|
|
|
8
8
|
import { loadNormalizedConfig, detectConfigVersion } from '../lib/normalized-config.js';
|
|
9
9
|
import { readDaemonState, evaluateDaemonStatus } from '../lib/run-schedule.js';
|
|
10
10
|
import { getGovernedVersionSurface, formatGovernedVersionLabel } from '../lib/protocol-version.js';
|
|
11
|
+
import { PLUGIN_MANIFEST_FILE } from '../lib/plugins.js';
|
|
11
12
|
|
|
12
13
|
export async function doctorCommand(opts = {}) {
|
|
13
14
|
const root = findProjectRoot(process.cwd());
|
|
@@ -74,6 +75,7 @@ function governedDoctor(root, rawConfig, opts) {
|
|
|
74
75
|
const check = checkRuntimeReachable(rtId, rt);
|
|
75
76
|
checks.push(check);
|
|
76
77
|
}
|
|
78
|
+
const connectorProbe = getConnectorProbeRecommendation(runtimes);
|
|
77
79
|
|
|
78
80
|
// 4. State directory
|
|
79
81
|
const stateDir = join(root, '.agentxchain');
|
|
@@ -130,6 +132,90 @@ function governedDoctor(root, rawConfig, opts) {
|
|
|
130
132
|
}
|
|
131
133
|
}
|
|
132
134
|
|
|
135
|
+
// 8. Installed plugin health (only when plugins are installed)
|
|
136
|
+
const installedPlugins = rawConfig.plugins || {};
|
|
137
|
+
const pluginNames = Object.keys(installedPlugins);
|
|
138
|
+
if (pluginNames.length > 0) {
|
|
139
|
+
for (const pluginName of pluginNames) {
|
|
140
|
+
const meta = installedPlugins[pluginName];
|
|
141
|
+
const checkId = `plugin_${pluginName.replace(/[^a-z0-9_-]/gi, '_')}`;
|
|
142
|
+
|
|
143
|
+
// Check install path exists
|
|
144
|
+
if (!meta.install_path) {
|
|
145
|
+
checks.push({ id: checkId, name: `Plugin: ${pluginName}`, level: 'fail', detail: 'No install_path recorded', plugin_name: pluginName });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const installAbsPath = join(root, meta.install_path);
|
|
149
|
+
if (!existsSync(installAbsPath)) {
|
|
150
|
+
checks.push({ id: checkId, name: `Plugin: ${pluginName}`, level: 'fail', detail: `Install path missing: ${meta.install_path}`, plugin_name: pluginName });
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check manifest exists and is valid
|
|
155
|
+
const manifestPath = join(installAbsPath, PLUGIN_MANIFEST_FILE);
|
|
156
|
+
if (!existsSync(manifestPath)) {
|
|
157
|
+
checks.push({ id: checkId, name: `Plugin: ${pluginName}`, level: 'fail', detail: 'Manifest file missing', plugin_name: pluginName });
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
let manifest;
|
|
161
|
+
try {
|
|
162
|
+
manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
163
|
+
} catch (err) {
|
|
164
|
+
checks.push({ id: checkId, name: `Plugin: ${pluginName}`, level: 'fail', detail: `Manifest is corrupt JSON: ${err.message}`, plugin_name: pluginName });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check hook files exist
|
|
169
|
+
const hookErrors = [];
|
|
170
|
+
if (manifest.hooks && typeof manifest.hooks === 'object') {
|
|
171
|
+
for (const [hookName, hookDef] of Object.entries(manifest.hooks)) {
|
|
172
|
+
if (!hookDef) continue;
|
|
173
|
+
const commands = Array.isArray(hookDef) ? hookDef : (hookDef.command ? [hookDef] : []);
|
|
174
|
+
for (const cmd of commands) {
|
|
175
|
+
const cmdArgs = cmd.command || cmd;
|
|
176
|
+
if (Array.isArray(cmdArgs) && cmdArgs.length > 0) {
|
|
177
|
+
const firstArg = cmdArgs[0];
|
|
178
|
+
if (typeof firstArg === 'string' && (firstArg.startsWith('./') || firstArg.startsWith('../'))) {
|
|
179
|
+
const hookFilePath = join(installAbsPath, firstArg);
|
|
180
|
+
if (!existsSync(hookFilePath)) {
|
|
181
|
+
hookErrors.push(`${hookName}: ${firstArg}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (hookErrors.length > 0) {
|
|
189
|
+
checks.push({ id: checkId, name: `Plugin: ${pluginName}`, level: 'fail', detail: `Missing hook files: ${hookErrors.join(', ')}`, plugin_name: pluginName });
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check config env vars (warn only)
|
|
194
|
+
const envWarnings = [];
|
|
195
|
+
const pluginConfig = meta.config || {};
|
|
196
|
+
for (const [key, value] of Object.entries(pluginConfig)) {
|
|
197
|
+
if (typeof value === 'string' && value.startsWith('$')) {
|
|
198
|
+
const envVar = value.slice(1);
|
|
199
|
+
if (!process.env[envVar]) {
|
|
200
|
+
envWarnings.push(envVar);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Also check webhook_env pattern from config
|
|
205
|
+
if (pluginConfig.webhook_env && !process.env[pluginConfig.webhook_env]) {
|
|
206
|
+
if (!envWarnings.includes(pluginConfig.webhook_env)) {
|
|
207
|
+
envWarnings.push(pluginConfig.webhook_env);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (envWarnings.length > 0) {
|
|
212
|
+
checks.push({ id: checkId, name: `Plugin: ${pluginName}`, level: 'warn', detail: `Env var(s) not set: ${envWarnings.join(', ')}`, plugin_name: pluginName });
|
|
213
|
+
} else {
|
|
214
|
+
checks.push({ id: checkId, name: `Plugin: ${pluginName}`, level: 'pass', detail: `v${manifest.version || '?'}, ${Object.keys(manifest.hooks || {}).length} hooks`, plugin_name: pluginName });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
133
219
|
// Compute summary
|
|
134
220
|
const failCount = checks.filter(c => c.level === 'fail').length;
|
|
135
221
|
const warnCount = checks.filter(c => c.level === 'warn').length;
|
|
@@ -143,6 +229,9 @@ function governedDoctor(root, rawConfig, opts) {
|
|
|
143
229
|
...versionSurface,
|
|
144
230
|
config_version: versionSurface.config_generation,
|
|
145
231
|
overall,
|
|
232
|
+
connector_probe_recommended: connectorProbe.recommended,
|
|
233
|
+
connector_probe_runtime_ids: connectorProbe.runtimeIds,
|
|
234
|
+
connector_probe_detail: connectorProbe.detail,
|
|
146
235
|
checks,
|
|
147
236
|
fail_count: failCount,
|
|
148
237
|
warn_count: warnCount,
|
|
@@ -173,6 +262,9 @@ function governedDoctor(root, rawConfig, opts) {
|
|
|
173
262
|
} else {
|
|
174
263
|
console.log(chalk.red(` Not ready: ${failCount} failure${failCount > 1 ? 's' : ''}, ${warnCount} warning${warnCount > 1 ? 's' : ''}.`));
|
|
175
264
|
}
|
|
265
|
+
if (failCount === 0 && connectorProbe.recommended) {
|
|
266
|
+
console.log(chalk.dim(` Next: ${connectorProbe.detail}`));
|
|
267
|
+
}
|
|
176
268
|
console.log('');
|
|
177
269
|
}
|
|
178
270
|
|
|
@@ -236,6 +328,35 @@ function checkRuntimeReachable(rtId, rt) {
|
|
|
236
328
|
}
|
|
237
329
|
}
|
|
238
330
|
|
|
331
|
+
function getConnectorProbeRecommendation(runtimes) {
|
|
332
|
+
const runtimeIds = [];
|
|
333
|
+
|
|
334
|
+
for (const [rtId, rt] of Object.entries(runtimes || {})) {
|
|
335
|
+
if (!rt || typeof rt !== 'object') continue;
|
|
336
|
+
if (rt.type === 'api_proxy' || rt.type === 'remote_agent') {
|
|
337
|
+
runtimeIds.push(rtId);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (rt.type === 'mcp' && (rt.transport || 'stdio') === 'streamable_http') {
|
|
341
|
+
runtimeIds.push(rtId);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (runtimeIds.length === 0) {
|
|
346
|
+
return {
|
|
347
|
+
recommended: false,
|
|
348
|
+
runtimeIds: [],
|
|
349
|
+
detail: null,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
recommended: true,
|
|
355
|
+
runtimeIds,
|
|
356
|
+
detail: 'run `agentxchain connector check` to live-probe api / remote HTTP runtimes before the first governed turn.',
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
239
360
|
function getCurrentPhase(root) {
|
|
240
361
|
const statePath = join(root, '.agentxchain', 'state.json');
|
|
241
362
|
if (!existsSync(statePath)) return null;
|
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('');
|
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
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
import { loadProjectContext } from '../lib/config.js';
|
|
4
|
+
import { normalizeVerification } from '../lib/repo-observer.js';
|
|
5
|
+
import { resolveAcceptedTurnHistoryReference } from '../lib/accepted-turn-history.js';
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS,
|
|
8
|
+
replayVerificationMachineEvidence,
|
|
9
|
+
} from '../lib/verification-replay.js';
|
|
10
|
+
|
|
11
|
+
export async function replayTurnCommand(turnId, opts = {}) {
|
|
12
|
+
const context = loadProjectContext();
|
|
13
|
+
if (!context) {
|
|
14
|
+
console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
|
|
15
|
+
process.exit(2);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (context.config.protocol_mode !== 'governed' || context.version !== 4) {
|
|
19
|
+
console.log(chalk.red('replay turn is only available in governed v4 projects.'));
|
|
20
|
+
process.exit(2);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const timeoutMs = Number.parseInt(String(opts.timeout || String(DEFAULT_VERIFICATION_REPLAY_TIMEOUT_MS)), 10);
|
|
24
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
|
|
25
|
+
console.log(chalk.red('replay turn requires a positive integer --timeout in milliseconds.'));
|
|
26
|
+
process.exit(2);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { root, config } = context;
|
|
30
|
+
const resolved = resolveAcceptedTurnHistoryReference(root, turnId);
|
|
31
|
+
if (!resolved.ok) {
|
|
32
|
+
console.log(chalk.red(resolved.error));
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const entry = resolved.entry;
|
|
37
|
+
const runtimeType = config.runtimes?.[entry.runtime_id]?.type || 'unknown';
|
|
38
|
+
const payload = {
|
|
39
|
+
source: 'history',
|
|
40
|
+
match_kind: resolved.match_kind,
|
|
41
|
+
turn_id: entry.turn_id,
|
|
42
|
+
resolved_turn_id: resolved.resolved_ref,
|
|
43
|
+
run_id: entry.run_id || null,
|
|
44
|
+
role: entry.role || null,
|
|
45
|
+
phase: entry.phase || null,
|
|
46
|
+
runtime_id: entry.runtime_id || null,
|
|
47
|
+
runtime_type: runtimeType,
|
|
48
|
+
accepted_at: entry.accepted_at || null,
|
|
49
|
+
declared_status: entry.verification?.status || 'skipped',
|
|
50
|
+
normalized_status: normalizeVerification(entry.verification, runtimeType).status,
|
|
51
|
+
timeout_ms: timeoutMs,
|
|
52
|
+
prior_verification_replay: entry.verification_replay || null,
|
|
53
|
+
...replayVerificationMachineEvidence({
|
|
54
|
+
root,
|
|
55
|
+
verification: entry.verification,
|
|
56
|
+
timeoutMs,
|
|
57
|
+
}),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
emitReplayTurn(payload, opts.json);
|
|
61
|
+
process.exit(payload.overall === 'match' ? 0 : 1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function emitReplayTurn(payload, jsonMode) {
|
|
65
|
+
if (jsonMode) {
|
|
66
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log('');
|
|
71
|
+
console.log(chalk.bold(` Replay Turn: ${chalk.cyan(payload.turn_id)}`));
|
|
72
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
73
|
+
console.log(` ${chalk.dim('Source:')} accepted history (${payload.match_kind})`);
|
|
74
|
+
console.log(` ${chalk.dim('Run:')} ${payload.run_id || '—'}`);
|
|
75
|
+
console.log(` ${chalk.dim('Role:')} ${payload.role || '—'}`);
|
|
76
|
+
console.log(` ${chalk.dim('Phase:')} ${payload.phase || '—'}`);
|
|
77
|
+
console.log(` ${chalk.dim('Runtime:')} ${payload.runtime_id || '—'} (${payload.runtime_type})`);
|
|
78
|
+
console.log(` ${chalk.dim('Accepted:')} ${payload.accepted_at || '—'}`);
|
|
79
|
+
console.log(` ${chalk.dim('Declared:')} ${payload.declared_status}`);
|
|
80
|
+
console.log(` ${chalk.dim('Normalized:')} ${payload.normalized_status}`);
|
|
81
|
+
if (payload.prior_verification_replay) {
|
|
82
|
+
const prior = payload.prior_verification_replay;
|
|
83
|
+
const verifiedAt = prior.verified_at ? ` at ${prior.verified_at}` : '';
|
|
84
|
+
console.log(` ${chalk.dim('Prior replay:')} ${prior.overall} (${prior.matched_commands || 0}/${prior.replayed_commands || 0})${verifiedAt}`);
|
|
85
|
+
}
|
|
86
|
+
console.log(` ${chalk.dim('Outcome:')} ${formatOutcome(payload.overall)}`);
|
|
87
|
+
|
|
88
|
+
if (payload.reason) {
|
|
89
|
+
console.log(` ${chalk.dim('Reason:')} ${payload.reason}`);
|
|
90
|
+
console.log('');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log('');
|
|
95
|
+
for (const command of payload.commands || []) {
|
|
96
|
+
const marker = command.matched ? chalk.green('match') : chalk.red('mismatch');
|
|
97
|
+
console.log(` [${marker}] ${command.command}`);
|
|
98
|
+
console.log(` declared=${command.declared_exit_code} actual=${command.actual_exit_code == null ? 'null' : command.actual_exit_code}`);
|
|
99
|
+
if (command.signal) {
|
|
100
|
+
console.log(` signal=${command.signal}`);
|
|
101
|
+
}
|
|
102
|
+
if (command.timed_out) {
|
|
103
|
+
console.log(' timed_out=true');
|
|
104
|
+
}
|
|
105
|
+
if (command.error) {
|
|
106
|
+
console.log(` error=${command.error}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log(chalk.dim(' Replay uses the current workspace and shell environment. It verifies declared exit-code reproducibility, not historical stdout/stderr identity.'));
|
|
112
|
+
console.log('');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formatOutcome(outcome) {
|
|
116
|
+
if (outcome === 'match') return chalk.green('match');
|
|
117
|
+
if (outcome === 'mismatch') return chalk.red('mismatch');
|
|
118
|
+
return chalk.yellow('not_reproducible');
|
|
119
|
+
}
|
|
120
|
+
|
package/src/commands/resume.js
CHANGED
|
@@ -36,7 +36,6 @@ 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';
|
|
42
41
|
|
|
@@ -100,6 +99,11 @@ export async function resumeCommand(opts) {
|
|
|
100
99
|
process.exit(1);
|
|
101
100
|
}
|
|
102
101
|
|
|
102
|
+
if (state.pending_phase_transition || state.pending_run_completion) {
|
|
103
|
+
printRecoverySummary(state, 'This run is awaiting approval.');
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
103
107
|
// §47: paused + retained turn with failed/retrying status → re-dispatch same turn
|
|
104
108
|
if (state.status === 'paused' && activeCount > 0) {
|
|
105
109
|
// Resolve which turn to re-dispatch
|
|
@@ -129,10 +133,12 @@ export async function resumeCommand(opts) {
|
|
|
129
133
|
console.log(` Attempt: ${retainedTurn.attempt}`);
|
|
130
134
|
console.log('');
|
|
131
135
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
+
const reactivated = reactivateGovernedRun(root, state, { via: 'resume --turn', notificationConfig: config });
|
|
137
|
+
if (!reactivated.ok) {
|
|
138
|
+
console.log(chalk.red(`Failed to reactivate run: ${reactivated.error}`));
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
state = reactivated.state;
|
|
136
142
|
|
|
137
143
|
// Write dispatch bundle for the existing turn
|
|
138
144
|
const bundleResult = writeDispatchBundle(root, state, config);
|
|
@@ -236,11 +242,12 @@ export async function resumeCommand(opts) {
|
|
|
236
242
|
|
|
237
243
|
// §47: paused + run_id exists → resume same run
|
|
238
244
|
if (state.status === 'paused' && state.run_id) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
245
|
+
const reactivated = reactivateGovernedRun(root, state, { via: 'resume', notificationConfig: config });
|
|
246
|
+
if (!reactivated.ok) {
|
|
247
|
+
console.log(chalk.red(`Failed to reactivate run: ${reactivated.error}`));
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
state = reactivated.state;
|
|
244
251
|
console.log(chalk.green(`Resumed governed run: ${state.run_id}`));
|
|
245
252
|
}
|
|
246
253
|
|
|
@@ -422,6 +429,19 @@ function printAssignmentWarnings(assignResult) {
|
|
|
422
429
|
}
|
|
423
430
|
}
|
|
424
431
|
|
|
432
|
+
function printRecoverySummary(state, heading) {
|
|
433
|
+
const recovery = deriveRecoveryDescriptor(state);
|
|
434
|
+
console.log(chalk.yellow(heading));
|
|
435
|
+
if (!recovery) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
console.log(` ${chalk.dim('Reason:')} ${recovery.typed_reason}`);
|
|
439
|
+
console.log(` ${chalk.dim('Action:')} ${recovery.recovery_action}`);
|
|
440
|
+
if (recovery.detail) {
|
|
441
|
+
console.log(` ${chalk.dim('Detail:')} ${recovery.detail}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
425
445
|
function printAssignmentHookFailure(result, roleId) {
|
|
426
446
|
const recovery = deriveRecoveryDescriptor(result.state);
|
|
427
447
|
const hookName = result.hookResults?.blocker?.hook_name
|
package/src/commands/step.js
CHANGED
|
@@ -60,7 +60,6 @@ import {
|
|
|
60
60
|
getTurnStagingResultPath,
|
|
61
61
|
} from '../lib/turn-paths.js';
|
|
62
62
|
import { dispatchApiProxy } from '../lib/adapters/api-proxy-adapter.js';
|
|
63
|
-
import { safeWriteJson } from '../lib/safe-write.js';
|
|
64
63
|
import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
|
|
65
64
|
import { runHooks } from '../lib/hook-runner.js';
|
|
66
65
|
import { finalizeDispatchManifest, verifyDispatchManifest } from '../lib/dispatch-manifest.js';
|
|
@@ -165,8 +164,8 @@ export async function stepCommand(opts) {
|
|
|
165
164
|
}
|
|
166
165
|
|
|
167
166
|
if (!skipAssignment) {
|
|
168
|
-
if (state.
|
|
169
|
-
printRecoverySummary(state, 'This run is
|
|
167
|
+
if (state.pending_phase_transition || state.pending_run_completion) {
|
|
168
|
+
printRecoverySummary(state, 'This run is awaiting approval.');
|
|
170
169
|
process.exit(1);
|
|
171
170
|
}
|
|
172
171
|
|
|
@@ -231,10 +230,12 @@ export async function stepCommand(opts) {
|
|
|
231
230
|
const turnStatus = pausedTurn?.status;
|
|
232
231
|
if (turnStatus === 'failed' || turnStatus === 'retrying') {
|
|
233
232
|
console.log(chalk.yellow(`Re-dispatching failed turn: ${pausedTurn.turn_id}`));
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
233
|
+
const reactivated = reactivateGovernedRun(root, state, { via: 'step --resume', notificationConfig: config });
|
|
234
|
+
if (!reactivated.ok) {
|
|
235
|
+
console.log(chalk.red(`Failed to reactivate run: ${reactivated.error}`));
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
state = reactivated.state;
|
|
238
239
|
skipAssignment = true;
|
|
239
240
|
|
|
240
241
|
const bundleResult = writeDispatchBundle(root, state, config);
|
|
@@ -270,11 +271,12 @@ export async function stepCommand(opts) {
|
|
|
270
271
|
}
|
|
271
272
|
|
|
272
273
|
if (!skipAssignment && state.status === 'paused' && state.run_id) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
274
|
+
const reactivated = reactivateGovernedRun(root, state, { via: 'step', notificationConfig: config });
|
|
275
|
+
if (!reactivated.ok) {
|
|
276
|
+
console.log(chalk.red(`Failed to reactivate run: ${reactivated.error}`));
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
state = reactivated.state;
|
|
278
280
|
console.log(chalk.green(`Resumed governed run: ${state.run_id}`));
|
|
279
281
|
}
|
|
280
282
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
const HISTORY_PATH = '.agentxchain/history.jsonl';
|
|
5
|
+
|
|
6
|
+
export function queryAcceptedTurnHistory(root) {
|
|
7
|
+
const filePath = join(root, HISTORY_PATH);
|
|
8
|
+
if (!existsSync(filePath)) return [];
|
|
9
|
+
|
|
10
|
+
let content;
|
|
11
|
+
try {
|
|
12
|
+
content = readFileSync(filePath, 'utf8').trim();
|
|
13
|
+
} catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!content) return [];
|
|
18
|
+
|
|
19
|
+
return content
|
|
20
|
+
.split('\n')
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.map((line) => {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(line);
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
.filter((entry) => entry && typeof entry.turn_id === 'string')
|
|
30
|
+
.reverse();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveAcceptedTurnHistoryReference(root, ref) {
|
|
34
|
+
const entries = queryAcceptedTurnHistory(root);
|
|
35
|
+
|
|
36
|
+
if (entries.length === 0) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
error: 'No accepted turn history found. Accept at least one governed turn first.',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!ref) {
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
entry: entries[0],
|
|
47
|
+
resolved_ref: entries[0].turn_id,
|
|
48
|
+
match_kind: 'latest',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const exact = entries.find((entry) => entry.turn_id === ref);
|
|
53
|
+
if (exact) {
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
entry: exact,
|
|
57
|
+
resolved_ref: exact.turn_id,
|
|
58
|
+
match_kind: 'exact',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const prefixMatches = entries.filter((entry) => entry.turn_id.startsWith(ref));
|
|
63
|
+
if (prefixMatches.length === 1) {
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
entry: prefixMatches[0],
|
|
67
|
+
resolved_ref: prefixMatches[0].turn_id,
|
|
68
|
+
match_kind: 'prefix',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (prefixMatches.length > 1) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
error: `Turn reference "${ref}" is ambiguous. Matches: ${prefixMatches.map((entry) => entry.turn_id).join(', ')}`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
error: `Accepted turn ${ref} not found in history.`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
@@ -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
|
|
@@ -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
|
@@ -521,6 +521,20 @@ export function startIntent(root, intentId, options = {}) {
|
|
|
521
521
|
};
|
|
522
522
|
}
|
|
523
523
|
|
|
524
|
+
const loadedEvent = readEvent(root, intent.event_id);
|
|
525
|
+
if (!loadedEvent.ok) {
|
|
526
|
+
return loadedEvent;
|
|
527
|
+
}
|
|
528
|
+
const { event } = loadedEvent;
|
|
529
|
+
const intakeContext = {
|
|
530
|
+
intent_id: intent.intent_id,
|
|
531
|
+
event_id: intent.event_id,
|
|
532
|
+
source: event.source || null,
|
|
533
|
+
category: event.category || null,
|
|
534
|
+
charter: intent.charter || null,
|
|
535
|
+
acceptance_contract: Array.isArray(intent.acceptance_contract) ? intent.acceptance_contract : [],
|
|
536
|
+
};
|
|
537
|
+
|
|
524
538
|
// Load governed project context
|
|
525
539
|
const context = loadProjectContext(root);
|
|
526
540
|
if (!context) {
|
|
@@ -569,6 +583,10 @@ export function startIntent(root, intentId, options = {}) {
|
|
|
569
583
|
};
|
|
570
584
|
}
|
|
571
585
|
|
|
586
|
+
if (state.status === 'paused') {
|
|
587
|
+
return { ok: false, error: 'cannot start: run is paused (awaiting approval). Resolve the blocking gate before starting a new intake turn.', exitCode: 1 };
|
|
588
|
+
}
|
|
589
|
+
|
|
572
590
|
if (state.pending_phase_transition) {
|
|
573
591
|
return { ok: false, error: `cannot start: pending phase transition to "${state.pending_phase_transition}"`, exitCode: 1 };
|
|
574
592
|
}
|
|
@@ -579,21 +597,20 @@ export function startIntent(root, intentId, options = {}) {
|
|
|
579
597
|
|
|
580
598
|
// Bootstrap: idle with no run → initialize
|
|
581
599
|
if (state.status === 'idle' && !state.run_id) {
|
|
582
|
-
const initResult = initializeGovernedRun(root, config
|
|
600
|
+
const initResult = initializeGovernedRun(root, config, {
|
|
601
|
+
provenance: {
|
|
602
|
+
trigger: 'intake',
|
|
603
|
+
intake_intent_id: intent.intent_id,
|
|
604
|
+
trigger_reason: intent.charter || null,
|
|
605
|
+
created_by: 'operator',
|
|
606
|
+
},
|
|
607
|
+
});
|
|
583
608
|
if (!initResult.ok) {
|
|
584
609
|
return { ok: false, error: `run initialization failed: ${initResult.error}`, exitCode: 1 };
|
|
585
610
|
}
|
|
586
611
|
state = initResult.state;
|
|
587
612
|
}
|
|
588
613
|
|
|
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
614
|
// Resolve role
|
|
598
615
|
const roleId = resolveIntakeRole(options.role, state, config);
|
|
599
616
|
if (!roleId.ok) {
|
|
@@ -614,6 +631,12 @@ export function startIntent(root, intentId, options = {}) {
|
|
|
614
631
|
return { ok: false, error: 'turn assignment succeeded but turn not found in state', exitCode: 1 };
|
|
615
632
|
}
|
|
616
633
|
|
|
634
|
+
assignedTurn.intake_context = intakeContext;
|
|
635
|
+
if (state.active_turns?.[assignedTurn.turn_id]) {
|
|
636
|
+
state.active_turns[assignedTurn.turn_id].intake_context = intakeContext;
|
|
637
|
+
safeWriteJson(statePath, state);
|
|
638
|
+
}
|
|
639
|
+
|
|
617
640
|
// Write dispatch bundle
|
|
618
641
|
const bundleResult = writeDispatchBundle(root, state, config);
|
|
619
642
|
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
|
|