agentxchain 2.37.0 → 2.39.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 +11 -0
- package/bin/agentxchain.js +1 -0
- package/dashboard/components/timeline.js +13 -2
- package/package.json +1 -1
- package/src/commands/init.js +10 -7
- package/src/commands/restart.js +109 -5
- package/src/commands/status.js +15 -2
- package/src/commands/template-list.js +41 -0
- package/src/lib/continuity-status.js +112 -2
- package/src/lib/governed-state.js +12 -0
- package/src/lib/normalized-config.js +35 -26
- package/src/lib/session-checkpoint.js +59 -8
- package/src/lib/workflow-kit-phase-templates.js +102 -0
package/README.md
CHANGED
|
@@ -9,7 +9,9 @@ Legacy IDE-window coordination is still shipped as a compatibility mode for team
|
|
|
9
9
|
## Docs
|
|
10
10
|
|
|
11
11
|
- [Quickstart](https://agentxchain.dev/docs/quickstart/)
|
|
12
|
+
- [Getting Started](https://agentxchain.dev/docs/getting-started/)
|
|
12
13
|
- [CLI reference](https://agentxchain.dev/docs/cli/)
|
|
14
|
+
- [Templates](https://agentxchain.dev/docs/templates/)
|
|
13
15
|
- [Export schema reference](https://agentxchain.dev/docs/export-schema/)
|
|
14
16
|
- [Adapter reference](https://agentxchain.dev/docs/adapters/)
|
|
15
17
|
- [Protocol spec (v6)](https://agentxchain.dev/docs/protocol/)
|
|
@@ -90,6 +92,15 @@ Built-in governed templates:
|
|
|
90
92
|
- `web-app`: user flows, UI acceptance, browser support
|
|
91
93
|
- `enterprise-app`: enterprise planning artifacts plus blueprint-backed `architect` and `security_reviewer` phases
|
|
92
94
|
|
|
95
|
+
Inspect the shipped template surfaces instead of inferring them from docs:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
agentxchain template list
|
|
99
|
+
agentxchain template list --phase-templates
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`template list` enumerates governed project templates. `template list --phase-templates` enumerates the reusable workflow-kit phase-template bundles you can reference from `workflow_kit.phases.<phase>.template`.
|
|
103
|
+
|
|
93
104
|
`step` writes a turn-scoped bundle under `.agentxchain/dispatch/turns/<turn_id>/` and expects a staged result at `.agentxchain/staging/<turn_id>/turn-result.json`. Typical continuation:
|
|
94
105
|
|
|
95
106
|
```bash
|
package/bin/agentxchain.js
CHANGED
|
@@ -411,6 +411,7 @@ templateCmd
|
|
|
411
411
|
.command('list')
|
|
412
412
|
.description('List available governed templates')
|
|
413
413
|
.option('-j, --json', 'Output as JSON')
|
|
414
|
+
.option('--phase-templates', 'List workflow-kit phase templates instead of governed project templates')
|
|
414
415
|
.action(templateListCommand);
|
|
415
416
|
|
|
416
417
|
templateCmd
|
|
@@ -154,8 +154,19 @@ function renderContinuityPanel(continuity) {
|
|
|
154
154
|
html += `<div class="turn-detail"><span class="detail-label">Checkpoint:</span> No session checkpoint recorded</div>`;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
if (continuity.
|
|
158
|
-
html += `<div class="turn-detail"><span class="detail-label">
|
|
157
|
+
if (continuity.drift_detected === true && Array.isArray(continuity.drift_warnings) && continuity.drift_warnings.length > 0) {
|
|
158
|
+
html += `<div class="turn-detail risks"><span class="detail-label">Drift:</span><ul>`;
|
|
159
|
+
for (const warning of continuity.drift_warnings) {
|
|
160
|
+
html += `<li>${esc(warning)}</li>`;
|
|
161
|
+
}
|
|
162
|
+
html += `</ul></div>`;
|
|
163
|
+
} else if (continuity.drift_detected === false) {
|
|
164
|
+
html += `<div class="turn-detail"><span class="detail-label">Drift:</span> none detected since checkpoint</div>`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (continuity.recommended_command) {
|
|
168
|
+
const detail = continuity.recommended_detail ? ` (${continuity.recommended_detail})` : '';
|
|
169
|
+
html += `<div class="turn-detail"><span class="detail-label">Action:</span> <span class="mono">${esc(continuity.recommended_command)}</span>${esc(detail)}</div>`;
|
|
159
170
|
}
|
|
160
171
|
|
|
161
172
|
if (continuity.recovery_report_path) {
|
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -6,7 +6,7 @@ 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
8
|
import { loadGovernedTemplate, VALID_GOVERNED_TEMPLATE_IDS, buildSystemSpecContent } from '../lib/governed-templates.js';
|
|
9
|
-
import { VALID_PROMPT_TRANSPORTS } from '../lib/normalized-config.js';
|
|
9
|
+
import { normalizeWorkflowKit, VALID_PROMPT_TRANSPORTS } from '../lib/normalized-config.js';
|
|
10
10
|
|
|
11
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
12
|
const TEMPLATES_DIR = join(__dirname, '../templates');
|
|
@@ -628,6 +628,9 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
|
|
|
628
628
|
const { runtime: localDevRuntime } = resolveGovernedLocalDevRuntime(runtimeOptions);
|
|
629
629
|
const scaffoldConfig = buildScaffoldConfigFromTemplate(template, localDevRuntime, workflowKitConfig);
|
|
630
630
|
const { roles, runtimes, routing, gates, prompts, workflowKitConfig: effectiveWorkflowKitConfig } = scaffoldConfig;
|
|
631
|
+
const scaffoldWorkflowKitConfig = effectiveWorkflowKitConfig
|
|
632
|
+
? normalizeWorkflowKit(effectiveWorkflowKitConfig, Object.keys(routing))
|
|
633
|
+
: null;
|
|
631
634
|
const initialPhase = Object.keys(routing)[0] || 'planning';
|
|
632
635
|
const phaseGateStatus = Object.fromEntries(
|
|
633
636
|
[...new Set(
|
|
@@ -709,7 +712,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
|
|
|
709
712
|
const basePrompt = buildGovernedPrompt(roleId, role, {
|
|
710
713
|
routing,
|
|
711
714
|
gates,
|
|
712
|
-
workflowKitConfig:
|
|
715
|
+
workflowKitConfig: scaffoldWorkflowKitConfig,
|
|
713
716
|
});
|
|
714
717
|
const prompt = appendPromptOverride(basePrompt, template.prompt_overrides?.[roleId]);
|
|
715
718
|
writeFileSync(join(dir, '.agentxchain', 'prompts', `${roleId}.md`), prompt);
|
|
@@ -736,7 +739,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
|
|
|
736
739
|
|
|
737
740
|
// Workflow-kit custom artifacts — only scaffold files from explicit workflow_kit config
|
|
738
741
|
// that are not already handled by the default scaffold above
|
|
739
|
-
if (
|
|
742
|
+
if (scaffoldWorkflowKitConfig && scaffoldWorkflowKitConfig.phases && typeof scaffoldWorkflowKitConfig.phases === 'object') {
|
|
740
743
|
const defaultScaffoldPaths = new Set([
|
|
741
744
|
'.planning/PM_SIGNOFF.md',
|
|
742
745
|
'.planning/ROADMAP.md',
|
|
@@ -747,7 +750,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
|
|
|
747
750
|
'.planning/RELEASE_NOTES.md',
|
|
748
751
|
]);
|
|
749
752
|
|
|
750
|
-
for (const phaseConfig of Object.values(
|
|
753
|
+
for (const phaseConfig of Object.values(scaffoldWorkflowKitConfig.phases)) {
|
|
751
754
|
if (!Array.isArray(phaseConfig.artifacts)) continue;
|
|
752
755
|
for (const artifact of phaseConfig.artifacts) {
|
|
753
756
|
if (!artifact.path || defaultScaffoldPaths.has(artifact.path)) continue;
|
|
@@ -782,7 +785,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
|
|
|
782
785
|
}
|
|
783
786
|
}
|
|
784
787
|
|
|
785
|
-
return { config, state };
|
|
788
|
+
return { config, state, scaffoldWorkflowKitConfig };
|
|
786
789
|
}
|
|
787
790
|
|
|
788
791
|
async function initGoverned(opts) {
|
|
@@ -882,7 +885,7 @@ async function initGoverned(opts) {
|
|
|
882
885
|
}
|
|
883
886
|
}
|
|
884
887
|
|
|
885
|
-
const { config } = scaffoldGoverned(dir, projectName, projectId, templateId, opts, workflowKitConfig);
|
|
888
|
+
const { config, scaffoldWorkflowKitConfig } = scaffoldGoverned(dir, projectName, projectId, templateId, opts, workflowKitConfig);
|
|
886
889
|
|
|
887
890
|
console.log('');
|
|
888
891
|
console.log(chalk.green(` ✓ Created governed project ${chalk.bold(targetLabel)}/`));
|
|
@@ -897,7 +900,7 @@ async function initGoverned(opts) {
|
|
|
897
900
|
console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} reviews/`);
|
|
898
901
|
console.log(` ${chalk.dim('│')} ${chalk.dim('└──')} dispatch/`);
|
|
899
902
|
console.log(` ${chalk.dim('├──')} .planning/`);
|
|
900
|
-
const planningSummaryLines = buildPlanningSummaryLines(selectedTemplate,
|
|
903
|
+
const planningSummaryLines = buildPlanningSummaryLines(selectedTemplate, scaffoldWorkflowKitConfig);
|
|
901
904
|
for (const [index, line] of planningSummaryLines.entries()) {
|
|
902
905
|
const branch = index === planningSummaryLines.length - 1 ? '└──' : '├──';
|
|
903
906
|
console.log(` ${chalk.dim('│')} ${chalk.dim(branch)} ${line}`);
|
package/src/commands/restart.js
CHANGED
|
@@ -23,13 +23,13 @@ import {
|
|
|
23
23
|
HISTORY_PATH,
|
|
24
24
|
LEDGER_PATH,
|
|
25
25
|
} from '../lib/governed-state.js';
|
|
26
|
-
import { readSessionCheckpoint, SESSION_PATH } from '../lib/session-checkpoint.js';
|
|
26
|
+
import { readSessionCheckpoint, writeSessionCheckpoint, captureBaselineRef, SESSION_PATH } from '../lib/session-checkpoint.js';
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Generate a session recovery report summarizing the run state
|
|
30
30
|
* so a new agent session can orient quickly.
|
|
31
31
|
*/
|
|
32
|
-
function generateRecoveryReport(root, state, checkpoint) {
|
|
32
|
+
function generateRecoveryReport(root, state, checkpoint, driftWarnings = []) {
|
|
33
33
|
const lines = [
|
|
34
34
|
'# Session Recovery Report',
|
|
35
35
|
'',
|
|
@@ -102,7 +102,50 @@ function generateRecoveryReport(root, state, checkpoint) {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
// Pending gate / run completion surfacing
|
|
106
|
+
if (state.pending_phase_transition) {
|
|
107
|
+
const pt = state.pending_phase_transition;
|
|
108
|
+
lines.push(
|
|
109
|
+
'## Pending Phase Transition',
|
|
110
|
+
'',
|
|
111
|
+
`- **From**: ${pt.from}`,
|
|
112
|
+
`- **To**: ${pt.to}`,
|
|
113
|
+
`- **Gate**: ${pt.gate}`,
|
|
114
|
+
`- **Requested by**: ${pt.requested_by_turn || 'unknown'}`,
|
|
115
|
+
`- **Action**: Run \`agentxchain approve-transition\` to approve`,
|
|
116
|
+
'',
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (state.pending_run_completion) {
|
|
121
|
+
const pc = state.pending_run_completion;
|
|
122
|
+
lines.push(
|
|
123
|
+
'## Pending Run Completion',
|
|
124
|
+
'',
|
|
125
|
+
`- **Gate**: ${pc.gate}`,
|
|
126
|
+
`- **Requested by**: ${pc.requested_by_turn || 'unknown'}`,
|
|
127
|
+
`- **Action**: Run \`agentxchain approve-completion\` to approve`,
|
|
128
|
+
'',
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Repo-drift warnings
|
|
133
|
+
if (checkpoint?.baseline_ref && driftWarnings?.length > 0) {
|
|
134
|
+
lines.push('## Continuity Warnings', '');
|
|
135
|
+
for (const warning of driftWarnings) {
|
|
136
|
+
lines.push(`- ⚠ ${warning}`);
|
|
137
|
+
}
|
|
138
|
+
lines.push('');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
lines.push('## Next Steps', '');
|
|
142
|
+
if (state.pending_phase_transition) {
|
|
143
|
+
lines.push('A phase transition is pending approval. Run `agentxchain approve-transition` before assigning new turns.', '');
|
|
144
|
+
} else if (state.pending_run_completion) {
|
|
145
|
+
lines.push('A run completion is pending approval. Run `agentxchain approve-completion` to finalize.', '');
|
|
146
|
+
} else {
|
|
147
|
+
lines.push('The next turn has been assigned. Check the dispatch bundle for context.', '');
|
|
148
|
+
}
|
|
106
149
|
|
|
107
150
|
return lines.join('\n');
|
|
108
151
|
}
|
|
@@ -160,6 +203,42 @@ export async function restartCommand(opts) {
|
|
|
160
203
|
process.exit(1);
|
|
161
204
|
}
|
|
162
205
|
|
|
206
|
+
// ── Repo-drift detection ────────────────────────────────────────────────
|
|
207
|
+
const driftWarnings = [];
|
|
208
|
+
if (checkpoint?.baseline_ref) {
|
|
209
|
+
const currentBaseline = captureBaselineRef(root);
|
|
210
|
+
const prev = checkpoint.baseline_ref;
|
|
211
|
+
|
|
212
|
+
if (prev.git_head && currentBaseline.git_head && prev.git_head !== currentBaseline.git_head) {
|
|
213
|
+
driftWarnings.push(`Git HEAD has moved since checkpoint: ${prev.git_head.slice(0, 8)} → ${currentBaseline.git_head.slice(0, 8)}`);
|
|
214
|
+
}
|
|
215
|
+
if (prev.git_branch && currentBaseline.git_branch && prev.git_branch !== currentBaseline.git_branch) {
|
|
216
|
+
driftWarnings.push(`Branch changed since checkpoint: ${prev.git_branch} → ${currentBaseline.git_branch}`);
|
|
217
|
+
}
|
|
218
|
+
if (prev.workspace_dirty === false && currentBaseline.workspace_dirty === true) {
|
|
219
|
+
driftWarnings.push('Workspace was clean at checkpoint but is now dirty');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (driftWarnings.length > 0) {
|
|
224
|
+
for (const warning of driftWarnings) {
|
|
225
|
+
console.log(chalk.yellow(`⚠ ${warning}`));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── Pending gate / completion check ────────────────────────────────────
|
|
230
|
+
if (state.pending_phase_transition) {
|
|
231
|
+
const pt = state.pending_phase_transition;
|
|
232
|
+
console.log(chalk.yellow(`Pending phase transition: ${pt.from} → ${pt.to} (gate: ${pt.gate})`));
|
|
233
|
+
console.log(chalk.dim('Run `agentxchain approve-transition` to approve before assigning new turns.'));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (state.pending_run_completion) {
|
|
237
|
+
const pc = state.pending_run_completion;
|
|
238
|
+
console.log(chalk.yellow(`Pending run completion (gate: ${pc.gate})`));
|
|
239
|
+
console.log(chalk.dim('Run `agentxchain approve-completion` to finalize.'));
|
|
240
|
+
}
|
|
241
|
+
|
|
163
242
|
// Handle abandoned active turns (assigned but never completed)
|
|
164
243
|
const activeTurns = getActiveTurns(state);
|
|
165
244
|
const activeTurnCount = getActiveTurnCount(state);
|
|
@@ -167,9 +246,29 @@ export async function restartCommand(opts) {
|
|
|
167
246
|
const turnIds = Object.keys(activeTurns);
|
|
168
247
|
console.log(chalk.yellow(`Warning: ${activeTurnCount} turn(s) were assigned but never completed: ${turnIds.join(', ')}`));
|
|
169
248
|
console.log(chalk.dim('These turns will be available for the next agent to complete.'));
|
|
249
|
+
|
|
250
|
+
// Fail closed if retained turn + irreconcilable drift
|
|
251
|
+
if (driftWarnings.length > 0) {
|
|
252
|
+
console.log(chalk.yellow('Active turns exist with repo drift since checkpoint. Reconnecting with warnings.'));
|
|
253
|
+
console.log(chalk.dim('Inspect the drift before continuing work on the retained turns.'));
|
|
254
|
+
}
|
|
170
255
|
}
|
|
171
256
|
|
|
172
|
-
// If
|
|
257
|
+
// If pending gate/completion, do not bypass — surface and exit with recovery
|
|
258
|
+
if (state.pending_phase_transition || state.pending_run_completion) {
|
|
259
|
+
// Write checkpoint for the reconnect
|
|
260
|
+
writeSessionCheckpoint(root, state, 'restart_reconnect');
|
|
261
|
+
|
|
262
|
+
const recoveryReport = generateRecoveryReport(root, state, checkpoint, driftWarnings);
|
|
263
|
+
const recoveryPath = join(root, '.agentxchain/SESSION_RECOVERY.md');
|
|
264
|
+
const recoveryDir = dirname(recoveryPath);
|
|
265
|
+
if (!existsSync(recoveryDir)) mkdirSync(recoveryDir, { recursive: true });
|
|
266
|
+
writeFileSync(recoveryPath, recoveryReport);
|
|
267
|
+
console.log(chalk.dim(` Recovery report: .agentxchain/SESSION_RECOVERY.md`));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// If paused or idle without a pending approval gate, reactivate.
|
|
173
272
|
if (state.status === 'paused' || state.status === 'idle') {
|
|
174
273
|
const reactivated = reactivateGovernedRun(root, state, {
|
|
175
274
|
reason: 'session_restart',
|
|
@@ -198,6 +297,8 @@ export async function restartCommand(opts) {
|
|
|
198
297
|
process.exit(1);
|
|
199
298
|
}
|
|
200
299
|
|
|
300
|
+
// assignGovernedTurn already writes a checkpoint at turn_assigned
|
|
301
|
+
|
|
201
302
|
console.log(chalk.green(`✓ Restarted run ${state.run_id}`));
|
|
202
303
|
console.log(chalk.dim(` Phase: ${phase}`));
|
|
203
304
|
console.log(chalk.dim(` Turn: ${assignment.turn?.id || 'assigned'}`));
|
|
@@ -206,6 +307,9 @@ export async function restartCommand(opts) {
|
|
|
206
307
|
console.log(chalk.dim(` Last checkpoint: ${checkpoint.checkpoint_reason} at ${checkpoint.last_checkpoint_at}`));
|
|
207
308
|
}
|
|
208
309
|
} else {
|
|
310
|
+
// Reconnect to existing active turns — write checkpoint
|
|
311
|
+
writeSessionCheckpoint(root, state, 'restart_reconnect');
|
|
312
|
+
|
|
209
313
|
console.log(chalk.green(`✓ Reconnected to run ${state.run_id}`));
|
|
210
314
|
console.log(chalk.dim(` Phase: ${phase}`));
|
|
211
315
|
console.log(chalk.dim(` Active turns: ${Object.keys(activeTurns).join(', ')}`));
|
|
@@ -213,7 +317,7 @@ export async function restartCommand(opts) {
|
|
|
213
317
|
}
|
|
214
318
|
|
|
215
319
|
// Write session recovery report
|
|
216
|
-
const recoveryReport = generateRecoveryReport(root, state, checkpoint);
|
|
320
|
+
const recoveryReport = generateRecoveryReport(root, state, checkpoint, driftWarnings);
|
|
217
321
|
const recoveryPath = join(root, '.agentxchain/SESSION_RECOVERY.md');
|
|
218
322
|
const recoveryDir = dirname(recoveryPath);
|
|
219
323
|
if (!existsSync(recoveryDir)) mkdirSync(recoveryDir, { recursive: true });
|
package/src/commands/status.js
CHANGED
|
@@ -268,8 +268,21 @@ function renderContinuityStatus(continuity, state) {
|
|
|
268
268
|
console.log(` ${chalk.dim('Checkpoint:')} ${chalk.yellow('No session checkpoint recorded')}`);
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
if (continuity.
|
|
272
|
-
|
|
271
|
+
if (continuity.drift_detected === true) {
|
|
272
|
+
const [firstWarning, ...remainingWarnings] = continuity.drift_warnings || [];
|
|
273
|
+
if (firstWarning) {
|
|
274
|
+
console.log(` ${chalk.dim('Drift:')} ${chalk.yellow(firstWarning)}`);
|
|
275
|
+
}
|
|
276
|
+
for (const warning of remainingWarnings) {
|
|
277
|
+
console.log(` ${chalk.dim(' ')} ${chalk.yellow(warning)}`);
|
|
278
|
+
}
|
|
279
|
+
} else if (continuity.drift_detected === false) {
|
|
280
|
+
console.log(` ${chalk.dim('Drift:')} ${chalk.green('none detected since checkpoint')}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (continuity.recommended_command) {
|
|
284
|
+
const detail = continuity.recommended_detail ? ` (${continuity.recommended_detail})` : '';
|
|
285
|
+
console.log(` ${chalk.dim('Action:')} ${chalk.cyan(continuity.recommended_command)}${chalk.dim(detail)}`);
|
|
273
286
|
}
|
|
274
287
|
|
|
275
288
|
if (continuity.recovery_report_path) {
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { loadAllGovernedTemplates, VALID_GOVERNED_TEMPLATE_IDS } from '../lib/governed-templates.js';
|
|
3
|
+
import { listWorkflowKitPhaseTemplates } from '../lib/workflow-kit-phase-templates.js';
|
|
3
4
|
|
|
4
5
|
export function templateListCommand(opts) {
|
|
6
|
+
if (opts.phaseTemplates) {
|
|
7
|
+
return listPhaseTemplates(opts);
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
if (opts.json) {
|
|
6
11
|
const templates = loadAllGovernedTemplates();
|
|
7
12
|
const output = templates.map((t) => ({
|
|
@@ -34,4 +39,40 @@ export function templateListCommand(opts) {
|
|
|
34
39
|
console.log('');
|
|
35
40
|
}
|
|
36
41
|
console.log(chalk.dim(` Usage: agentxchain template set <id>\n`));
|
|
42
|
+
console.log(chalk.dim(` Tip: use --phase-templates to list workflow-kit phase templates.\n`));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function listPhaseTemplates(opts) {
|
|
46
|
+
const templates = listWorkflowKitPhaseTemplates();
|
|
47
|
+
|
|
48
|
+
if (opts.json) {
|
|
49
|
+
const output = templates.map((t) => ({
|
|
50
|
+
id: t.id,
|
|
51
|
+
description: t.description,
|
|
52
|
+
artifacts: t.artifacts.map((a) => ({
|
|
53
|
+
path: a.path,
|
|
54
|
+
semantics: a.semantics || null,
|
|
55
|
+
semantics_config: a.semantics_config || null,
|
|
56
|
+
required: a.required,
|
|
57
|
+
})),
|
|
58
|
+
}));
|
|
59
|
+
console.log(JSON.stringify(output, null, 2));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(chalk.bold('\n Workflow-kit phase templates:\n'));
|
|
64
|
+
for (const t of templates) {
|
|
65
|
+
console.log(` ${chalk.cyan(t.id)} — ${t.description}`);
|
|
66
|
+
for (const a of t.artifacts) {
|
|
67
|
+
const req = a.required ? chalk.green('required') : chalk.dim('optional');
|
|
68
|
+
const sem = a.semantics ? chalk.yellow(a.semantics) : chalk.dim('none');
|
|
69
|
+
console.log(` ${a.path} [${req}] [semantics: ${sem}]`);
|
|
70
|
+
if (a.semantics === 'section_check' && a.semantics_config?.required_sections) {
|
|
71
|
+
console.log(` sections: ${a.semantics_config.required_sections.join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
console.log('');
|
|
75
|
+
}
|
|
76
|
+
console.log(chalk.dim(' Usage in agentxchain.json:'));
|
|
77
|
+
console.log(chalk.dim(' "workflow_kit": { "phases": { "<phase>": { "template": "<id>" } } }\n'));
|
|
37
78
|
}
|
|
@@ -1,9 +1,111 @@
|
|
|
1
1
|
import { existsSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
import { readSessionCheckpoint } from './session-checkpoint.js';
|
|
3
|
+
import { captureBaselineRef, readSessionCheckpoint } from './session-checkpoint.js';
|
|
4
4
|
|
|
5
5
|
export const SESSION_RECOVERY_PATH = '.agentxchain/SESSION_RECOVERY.md';
|
|
6
6
|
|
|
7
|
+
function deriveRecommendedContinuityAction(state) {
|
|
8
|
+
if (!state) {
|
|
9
|
+
return {
|
|
10
|
+
recommended_command: null,
|
|
11
|
+
recommended_reason: 'no_state',
|
|
12
|
+
recommended_detail: null,
|
|
13
|
+
restart_recommended: false,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (state.pending_phase_transition) {
|
|
18
|
+
const pt = state.pending_phase_transition;
|
|
19
|
+
return {
|
|
20
|
+
recommended_command: 'agentxchain approve-transition',
|
|
21
|
+
recommended_reason: 'pending_phase_transition',
|
|
22
|
+
recommended_detail: `${pt.from || 'unknown'} -> ${pt.to || 'unknown'} (gate: ${pt.gate || 'unknown'})`,
|
|
23
|
+
restart_recommended: false,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (state.pending_run_completion) {
|
|
28
|
+
const pc = state.pending_run_completion;
|
|
29
|
+
return {
|
|
30
|
+
recommended_command: 'agentxchain approve-completion',
|
|
31
|
+
recommended_reason: 'pending_run_completion',
|
|
32
|
+
recommended_detail: pc.gate ? `gate: ${pc.gate}` : null,
|
|
33
|
+
restart_recommended: false,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!['blocked', 'completed', 'failed'].includes(state.status)) {
|
|
38
|
+
return {
|
|
39
|
+
recommended_command: 'agentxchain restart',
|
|
40
|
+
recommended_reason: 'restart_available',
|
|
41
|
+
recommended_detail: 'rebuild session context from disk',
|
|
42
|
+
restart_recommended: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
recommended_command: null,
|
|
48
|
+
recommended_reason: state.status === 'blocked' ? 'blocked' : 'terminal_state',
|
|
49
|
+
recommended_detail: null,
|
|
50
|
+
restart_recommended: false,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function deriveCheckpointDrift(root, checkpoint, staleCheckpoint) {
|
|
55
|
+
if (!checkpoint?.baseline_ref || staleCheckpoint) {
|
|
56
|
+
return {
|
|
57
|
+
drift_detected: null,
|
|
58
|
+
drift_warnings: [],
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const currentBaseline = captureBaselineRef(root);
|
|
63
|
+
const previousBaseline = checkpoint.baseline_ref;
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
currentBaseline.git_head == null
|
|
67
|
+
&& currentBaseline.git_branch == null
|
|
68
|
+
&& currentBaseline.workspace_dirty == null
|
|
69
|
+
) {
|
|
70
|
+
return {
|
|
71
|
+
drift_detected: null,
|
|
72
|
+
drift_warnings: [],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const driftWarnings = [];
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
previousBaseline.git_head
|
|
80
|
+
&& currentBaseline.git_head
|
|
81
|
+
&& previousBaseline.git_head !== currentBaseline.git_head
|
|
82
|
+
) {
|
|
83
|
+
driftWarnings.push(
|
|
84
|
+
`Git HEAD has moved since checkpoint: ${previousBaseline.git_head.slice(0, 8)} -> ${currentBaseline.git_head.slice(0, 8)}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
previousBaseline.git_branch
|
|
90
|
+
&& currentBaseline.git_branch
|
|
91
|
+
&& previousBaseline.git_branch !== currentBaseline.git_branch
|
|
92
|
+
) {
|
|
93
|
+
driftWarnings.push(`Branch changed since checkpoint: ${previousBaseline.git_branch} -> ${currentBaseline.git_branch}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
previousBaseline.workspace_dirty === false
|
|
98
|
+
&& currentBaseline.workspace_dirty === true
|
|
99
|
+
) {
|
|
100
|
+
driftWarnings.push('Workspace was clean at checkpoint but is now dirty');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
drift_detected: driftWarnings.length > 0,
|
|
105
|
+
drift_warnings: driftWarnings,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
7
109
|
export function getContinuityStatus(root, state) {
|
|
8
110
|
const checkpoint = readSessionCheckpoint(root);
|
|
9
111
|
const recoveryReportPath = existsSync(join(root, SESSION_RECOVERY_PATH))
|
|
@@ -18,10 +120,18 @@ export function getContinuityStatus(root, state) {
|
|
|
18
120
|
&& checkpoint.run_id !== state.run_id
|
|
19
121
|
);
|
|
20
122
|
|
|
123
|
+
const action = deriveRecommendedContinuityAction(state);
|
|
124
|
+
const drift = deriveCheckpointDrift(root, checkpoint, staleCheckpoint);
|
|
125
|
+
|
|
21
126
|
return {
|
|
22
127
|
checkpoint,
|
|
23
128
|
stale_checkpoint: staleCheckpoint,
|
|
24
129
|
recovery_report_path: recoveryReportPath,
|
|
25
|
-
restart_recommended:
|
|
130
|
+
restart_recommended: action.restart_recommended,
|
|
131
|
+
recommended_command: action.recommended_command,
|
|
132
|
+
recommended_reason: action.recommended_reason,
|
|
133
|
+
recommended_detail: action.recommended_detail,
|
|
134
|
+
drift_detected: drift.drift_detected,
|
|
135
|
+
drift_warnings: drift.drift_warnings,
|
|
26
136
|
};
|
|
27
137
|
}
|
|
@@ -1352,6 +1352,11 @@ export function markRunBlocked(root, details) {
|
|
|
1352
1352
|
|
|
1353
1353
|
writeState(root, updatedState);
|
|
1354
1354
|
|
|
1355
|
+
// Session checkpoint — non-fatal, written after blocked state is persisted
|
|
1356
|
+
writeSessionCheckpoint(root, updatedState, 'blocked', {
|
|
1357
|
+
role: turnId ? (getActiveTurns(updatedState)[turnId]?.assigned_role || null) : null,
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1355
1360
|
emitBlockedNotification(root, details.notificationConfig, updatedState, {
|
|
1356
1361
|
category: details.category,
|
|
1357
1362
|
blockedOn: details.blockedOn,
|
|
@@ -1753,6 +1758,13 @@ export function assignGovernedTurn(root, config, roleId) {
|
|
|
1753
1758
|
};
|
|
1754
1759
|
|
|
1755
1760
|
writeState(root, updatedState);
|
|
1761
|
+
|
|
1762
|
+
// Session checkpoint — non-fatal, written after every successful turn assignment
|
|
1763
|
+
writeSessionCheckpoint(root, updatedState, 'turn_assigned', {
|
|
1764
|
+
role: roleId,
|
|
1765
|
+
dispatch_dir: `.agentxchain/dispatch/turns/${turnId}`,
|
|
1766
|
+
});
|
|
1767
|
+
|
|
1756
1768
|
const assignedTurn = updatedState.active_turns[turnId];
|
|
1757
1769
|
const result = { ok: true, state: attachLegacyCurrentTurnAlias(updatedState), turn: assignedTurn };
|
|
1758
1770
|
if (warnings.length > 0) {
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
import { validateHooksConfig } from './hook-runner.js';
|
|
16
16
|
import { validateNotificationsConfig } from './notification-runner.js';
|
|
17
17
|
import { SUPPORTED_TOKEN_COUNTER_PROVIDERS } from './token-counter.js';
|
|
18
|
+
import {
|
|
19
|
+
buildDefaultWorkflowKitArtifactsForPhase,
|
|
20
|
+
expandWorkflowKitPhaseArtifacts,
|
|
21
|
+
isWorkflowKitPhaseTemplateId,
|
|
22
|
+
VALID_WORKFLOW_KIT_PHASE_TEMPLATE_IDS,
|
|
23
|
+
} from './workflow-kit-phase-templates.js';
|
|
18
24
|
|
|
19
25
|
const VALID_WRITE_AUTHORITIES = ['authoritative', 'proposed', 'review_only'];
|
|
20
26
|
const VALID_RUNTIME_TYPES = ['manual', 'local_cli', 'api_proxy', 'mcp', 'remote_agent'];
|
|
@@ -26,25 +32,6 @@ export { DEFAULT_PHASES };
|
|
|
26
32
|
const VALID_PHASE_NAME = /^[a-z][a-z0-9_-]*$/;
|
|
27
33
|
const VALID_SEMANTIC_IDS = ['pm_signoff', 'system_spec', 'implementation_notes', 'acceptance_matrix', 'ship_verdict', 'release_notes', 'section_check'];
|
|
28
34
|
|
|
29
|
-
/**
|
|
30
|
-
* Default artifact map for phases when workflow_kit is absent from config.
|
|
31
|
-
* Only phases present in this map get default artifacts.
|
|
32
|
-
*/
|
|
33
|
-
const DEFAULT_PHASE_ARTIFACTS = {
|
|
34
|
-
planning: [
|
|
35
|
-
{ path: '.planning/PM_SIGNOFF.md', semantics: 'pm_signoff', required: true },
|
|
36
|
-
{ path: '.planning/SYSTEM_SPEC.md', semantics: 'system_spec', required: true },
|
|
37
|
-
{ path: '.planning/ROADMAP.md', semantics: null, required: true },
|
|
38
|
-
],
|
|
39
|
-
implementation: [
|
|
40
|
-
{ path: '.planning/IMPLEMENTATION_NOTES.md', semantics: 'implementation_notes', required: true },
|
|
41
|
-
],
|
|
42
|
-
qa: [
|
|
43
|
-
{ path: '.planning/acceptance-matrix.md', semantics: 'acceptance_matrix', required: true },
|
|
44
|
-
{ path: '.planning/ship-verdict.md', semantics: 'ship_verdict', required: true },
|
|
45
|
-
{ path: '.planning/RELEASE_NOTES.md', semantics: 'release_notes', required: true },
|
|
46
|
-
],
|
|
47
|
-
};
|
|
48
35
|
const VALID_API_PROXY_RETRY_JITTER = ['none', 'full'];
|
|
49
36
|
const VALID_API_PROXY_RETRY_CLASSES = [
|
|
50
37
|
'rate_limited',
|
|
@@ -566,14 +553,35 @@ export function validateWorkflowKitConfig(wk, routing, roles) {
|
|
|
566
553
|
continue;
|
|
567
554
|
}
|
|
568
555
|
|
|
569
|
-
|
|
556
|
+
let templateValid = true;
|
|
557
|
+
if (phaseConfig.template !== undefined) {
|
|
558
|
+
if (typeof phaseConfig.template !== 'string' || !phaseConfig.template.trim()) {
|
|
559
|
+
errors.push(`workflow_kit.phases.${phase}.template must be a non-empty string`);
|
|
560
|
+
templateValid = false;
|
|
561
|
+
} else if (!isWorkflowKitPhaseTemplateId(phaseConfig.template)) {
|
|
562
|
+
errors.push(
|
|
563
|
+
`workflow_kit.phases.${phase}.template "${phaseConfig.template}" is unknown; valid values: ${VALID_WORKFLOW_KIT_PHASE_TEMPLATE_IDS.join(', ')}`,
|
|
564
|
+
);
|
|
565
|
+
templateValid = false;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (phaseConfig.artifacts !== undefined && !Array.isArray(phaseConfig.artifacts)) {
|
|
570
570
|
errors.push(`workflow_kit.phases.${phase}.artifacts must be an array`);
|
|
571
571
|
continue;
|
|
572
572
|
}
|
|
573
573
|
|
|
574
|
+
if (phaseConfig.template === undefined && phaseConfig.artifacts === undefined) {
|
|
575
|
+
errors.push(`workflow_kit.phases.${phase} must declare template, artifacts, or both`);
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
|
|
574
579
|
const seenPaths = new Set();
|
|
575
|
-
|
|
576
|
-
|
|
580
|
+
const expandedArtifacts = templateValid
|
|
581
|
+
? expandWorkflowKitPhaseArtifacts(phaseConfig)
|
|
582
|
+
: Array.isArray(phaseConfig.artifacts) ? phaseConfig.artifacts : [];
|
|
583
|
+
for (let i = 0; i < expandedArtifacts.length; i++) {
|
|
584
|
+
const artifact = expandedArtifacts[i];
|
|
577
585
|
const prefix = `workflow_kit.phases.${phase}.artifacts[${i}]`;
|
|
578
586
|
|
|
579
587
|
if (!artifact || typeof artifact !== 'object') {
|
|
@@ -845,7 +853,7 @@ export function getMaxConcurrentTurns(config, phase) {
|
|
|
845
853
|
|
|
846
854
|
/**
|
|
847
855
|
* Normalize workflow_kit config.
|
|
848
|
-
* When absent, builds defaults from routing phases using
|
|
856
|
+
* When absent, builds defaults from routing phases using the built-in phase templates.
|
|
849
857
|
* When present, normalizes artifact entries.
|
|
850
858
|
*/
|
|
851
859
|
export function normalizeWorkflowKit(raw, routingPhases) {
|
|
@@ -862,7 +870,7 @@ export function normalizeWorkflowKit(raw, routingPhases) {
|
|
|
862
870
|
if (raw.phases) {
|
|
863
871
|
for (const [phase, phaseConfig] of Object.entries(raw.phases)) {
|
|
864
872
|
phases[phase] = {
|
|
865
|
-
artifacts: (phaseConfig
|
|
873
|
+
artifacts: expandWorkflowKitPhaseArtifacts(phaseConfig).map(a => ({
|
|
866
874
|
path: a.path,
|
|
867
875
|
semantics: a.semantics || null,
|
|
868
876
|
semantics_config: a.semantics_config || null,
|
|
@@ -879,9 +887,10 @@ export function normalizeWorkflowKit(raw, routingPhases) {
|
|
|
879
887
|
function buildDefaultWorkflowKit(routingPhases) {
|
|
880
888
|
const phases = {};
|
|
881
889
|
for (const phase of routingPhases) {
|
|
882
|
-
|
|
890
|
+
const templateArtifacts = buildDefaultWorkflowKitArtifactsForPhase(phase);
|
|
891
|
+
if (templateArtifacts) {
|
|
883
892
|
phases[phase] = {
|
|
884
|
-
artifacts:
|
|
893
|
+
artifacts: templateArtifacts.map(a => ({ ...a, semantics_config: a.semantics_config || null })),
|
|
885
894
|
};
|
|
886
895
|
}
|
|
887
896
|
}
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Session checkpoint — automatic state markers for cross-session restart.
|
|
3
3
|
*
|
|
4
4
|
* Writes .agentxchain/session.json at every governance boundary
|
|
5
|
-
* (turn acceptance, phase transition,
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* (turn assignment, acceptance, phase transition, blocked state,
|
|
6
|
+
* gate approval, run completion, restart/reconnect) so that
|
|
7
|
+
* `agentxchain restart` can reconstruct dispatch context without
|
|
8
|
+
* any in-memory session state.
|
|
8
9
|
*
|
|
9
10
|
* Design rules:
|
|
10
11
|
* - Checkpoint writes are non-fatal: failures log a warning and do not
|
|
@@ -12,11 +13,13 @@
|
|
|
12
13
|
* - The file is always overwritten, not appended.
|
|
13
14
|
* - run_id in session.json must agree with state.json; mismatch is a
|
|
14
15
|
* corruption signal.
|
|
16
|
+
* - state.json is always authoritative; session.json is recovery metadata.
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
19
|
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
|
|
18
20
|
import { join, dirname } from 'path';
|
|
19
21
|
import { randomBytes } from 'crypto';
|
|
22
|
+
import { execSync as shellExec } from 'child_process';
|
|
20
23
|
|
|
21
24
|
const SESSION_PATH = '.agentxchain/session.json';
|
|
22
25
|
|
|
@@ -27,6 +30,25 @@ function generateSessionId() {
|
|
|
27
30
|
return `session_${randomBytes(8).toString('hex')}`;
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Capture git baseline ref for repo-drift detection.
|
|
35
|
+
* Non-fatal: returns partial/null fields on failure.
|
|
36
|
+
*/
|
|
37
|
+
export function captureBaselineRef(root) {
|
|
38
|
+
try {
|
|
39
|
+
const gitHead = shellExec('git rev-parse HEAD', { cwd: root, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
40
|
+
const gitBranch = shellExec('git rev-parse --abbrev-ref HEAD', { cwd: root, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
41
|
+
const statusOutput = shellExec('git status --porcelain', { cwd: root, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
42
|
+
return {
|
|
43
|
+
git_head: gitHead,
|
|
44
|
+
git_branch: gitBranch,
|
|
45
|
+
workspace_dirty: statusOutput.length > 0,
|
|
46
|
+
};
|
|
47
|
+
} catch {
|
|
48
|
+
return { git_head: null, git_branch: null, workspace_dirty: null };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
30
52
|
/**
|
|
31
53
|
* Read the current session checkpoint, or null if none exists.
|
|
32
54
|
*/
|
|
@@ -40,12 +62,21 @@ export function readSessionCheckpoint(root) {
|
|
|
40
62
|
}
|
|
41
63
|
}
|
|
42
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Extract active turn IDs from governed state.
|
|
67
|
+
*/
|
|
68
|
+
function getActiveTurnIds(state) {
|
|
69
|
+
const turns = state.active_turns || {};
|
|
70
|
+
return Object.keys(turns);
|
|
71
|
+
}
|
|
72
|
+
|
|
43
73
|
/**
|
|
44
74
|
* Write or update the session checkpoint.
|
|
45
75
|
*
|
|
46
76
|
* @param {string} root - project root
|
|
47
77
|
* @param {object} state - current governed state (from state.json)
|
|
48
|
-
* @param {string} reason - checkpoint reason (e.g. '
|
|
78
|
+
* @param {string} reason - checkpoint reason (e.g. 'turn_assigned', 'turn_accepted',
|
|
79
|
+
* 'phase_approved', 'run_completed', 'blocked', 'restart_reconnect')
|
|
49
80
|
* @param {object} [extra] - optional extra context fields
|
|
50
81
|
*/
|
|
51
82
|
export function writeSessionCheckpoint(root, state, reason, extra = {}) {
|
|
@@ -58,20 +89,40 @@ export function writeSessionCheckpoint(root, state, reason, extra = {}) {
|
|
|
58
89
|
: generateSessionId();
|
|
59
90
|
|
|
60
91
|
const currentTurn = state.current_turn || null;
|
|
61
|
-
const
|
|
92
|
+
const activeTurnIds = getActiveTurnIds(state);
|
|
93
|
+
const lastTurnId = currentTurn?.id || currentTurn?.turn_id
|
|
94
|
+
|| (activeTurnIds.length > 0 ? activeTurnIds[activeTurnIds.length - 1] : null)
|
|
95
|
+
|| state.last_completed_turn_id || null;
|
|
62
96
|
const lastRole = currentTurn?.role || currentTurn?.assigned_role || extra.role || null;
|
|
63
97
|
const lastPhase = state.current_phase || state.phase || null;
|
|
64
98
|
|
|
99
|
+
// Derive last_completed_turn_id from history or state
|
|
100
|
+
const lastCompletedTurnId = state.last_completed_turn_id || existing?.last_completed_turn_id || null;
|
|
101
|
+
|
|
102
|
+
// Derive pending gates
|
|
103
|
+
const pendingGate = state.pending_phase_transition?.gate || state.pending_transition?.gate || null;
|
|
104
|
+
const pendingRunCompletion = state.pending_run_completion?.gate || null;
|
|
105
|
+
|
|
106
|
+
// Capture git baseline for repo-drift detection
|
|
107
|
+
const baselineRef = extra.baseline_ref || captureBaselineRef(root);
|
|
108
|
+
|
|
65
109
|
const checkpoint = {
|
|
66
110
|
session_id: sessionId,
|
|
67
111
|
run_id: state.run_id,
|
|
68
112
|
started_at: existing?.started_at || new Date().toISOString(),
|
|
69
113
|
last_checkpoint_at: new Date().toISOString(),
|
|
114
|
+
checkpoint_reason: reason,
|
|
115
|
+
run_status: state.status || null,
|
|
116
|
+
phase: lastPhase,
|
|
117
|
+
last_phase: lastPhase, // backward compat alias for report.js consumers
|
|
70
118
|
last_turn_id: lastTurnId,
|
|
71
|
-
|
|
119
|
+
last_completed_turn_id: lastCompletedTurnId,
|
|
120
|
+
active_turn_ids: activeTurnIds,
|
|
72
121
|
last_role: lastRole,
|
|
73
|
-
|
|
74
|
-
|
|
122
|
+
pending_gate: pendingGate,
|
|
123
|
+
pending_run_completion: pendingRunCompletion ? true : null,
|
|
124
|
+
blocked: state.status === 'blocked',
|
|
125
|
+
baseline_ref: baselineRef,
|
|
75
126
|
agent_context: {
|
|
76
127
|
adapter: extra.adapter || null,
|
|
77
128
|
dispatch_dir: extra.dispatch_dir || null,
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
function cloneJsonCompatible(value) {
|
|
2
|
+
return value == null ? value : JSON.parse(JSON.stringify(value));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
const WORKFLOW_KIT_PHASE_TEMPLATE_REGISTRY = Object.freeze({
|
|
6
|
+
'planning-default': {
|
|
7
|
+
description: 'Core planning proof surface for governed repos.',
|
|
8
|
+
artifacts: [
|
|
9
|
+
{ path: '.planning/PM_SIGNOFF.md', semantics: 'pm_signoff', required: true },
|
|
10
|
+
{ path: '.planning/SYSTEM_SPEC.md', semantics: 'system_spec', required: true },
|
|
11
|
+
{ path: '.planning/ROADMAP.md', semantics: null, required: true },
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
'implementation-default': {
|
|
15
|
+
description: 'Implementation proof surface for governed repos.',
|
|
16
|
+
artifacts: [
|
|
17
|
+
{ path: '.planning/IMPLEMENTATION_NOTES.md', semantics: 'implementation_notes', required: true },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
'qa-default': {
|
|
21
|
+
description: 'QA and ship-verdict proof surface for governed repos.',
|
|
22
|
+
artifacts: [
|
|
23
|
+
{ path: '.planning/acceptance-matrix.md', semantics: 'acceptance_matrix', required: true },
|
|
24
|
+
{ path: '.planning/ship-verdict.md', semantics: 'ship_verdict', required: true },
|
|
25
|
+
{ path: '.planning/RELEASE_NOTES.md', semantics: 'release_notes', required: true },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
'architecture-review': {
|
|
29
|
+
description: 'Structured architecture-review document with required sections.',
|
|
30
|
+
artifacts: [
|
|
31
|
+
{
|
|
32
|
+
path: '.planning/ARCHITECTURE.md',
|
|
33
|
+
semantics: 'section_check',
|
|
34
|
+
semantics_config: {
|
|
35
|
+
required_sections: ['## Context', '## Proposed Design', '## Trade-offs', '## Risks'],
|
|
36
|
+
},
|
|
37
|
+
required: true,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
'security-review': {
|
|
42
|
+
description: 'Structured security-review document with required sections.',
|
|
43
|
+
artifacts: [
|
|
44
|
+
{
|
|
45
|
+
path: '.planning/SECURITY_REVIEW.md',
|
|
46
|
+
semantics: 'section_check',
|
|
47
|
+
semantics_config: {
|
|
48
|
+
required_sections: ['## Threat Model', '## Findings', '## Verdict'],
|
|
49
|
+
},
|
|
50
|
+
required: true,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const VALID_WORKFLOW_KIT_PHASE_TEMPLATE_IDS = Object.freeze(
|
|
57
|
+
Object.keys(WORKFLOW_KIT_PHASE_TEMPLATE_REGISTRY),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const DEFAULT_WORKFLOW_KIT_PHASE_TEMPLATE_BY_PHASE = Object.freeze({
|
|
61
|
+
planning: 'planning-default',
|
|
62
|
+
implementation: 'implementation-default',
|
|
63
|
+
qa: 'qa-default',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export function listWorkflowKitPhaseTemplates() {
|
|
67
|
+
return VALID_WORKFLOW_KIT_PHASE_TEMPLATE_IDS.map((id) => loadWorkflowKitPhaseTemplate(id));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function isWorkflowKitPhaseTemplateId(templateId) {
|
|
71
|
+
return VALID_WORKFLOW_KIT_PHASE_TEMPLATE_IDS.includes(templateId);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function loadWorkflowKitPhaseTemplate(templateId) {
|
|
75
|
+
if (!isWorkflowKitPhaseTemplateId(templateId)) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Unknown workflow-kit phase template "${templateId}". Valid templates: ${VALID_WORKFLOW_KIT_PHASE_TEMPLATE_IDS.join(', ')}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const template = WORKFLOW_KIT_PHASE_TEMPLATE_REGISTRY[templateId];
|
|
82
|
+
return {
|
|
83
|
+
id: templateId,
|
|
84
|
+
description: template.description,
|
|
85
|
+
artifacts: cloneJsonCompatible(template.artifacts),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function expandWorkflowKitPhaseArtifacts(phaseConfig = {}) {
|
|
90
|
+
const templateArtifacts = phaseConfig.template
|
|
91
|
+
? loadWorkflowKitPhaseTemplate(phaseConfig.template).artifacts
|
|
92
|
+
: [];
|
|
93
|
+
const explicitArtifacts = Array.isArray(phaseConfig.artifacts)
|
|
94
|
+
? cloneJsonCompatible(phaseConfig.artifacts)
|
|
95
|
+
: [];
|
|
96
|
+
return [...templateArtifacts, ...explicitArtifacts];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function buildDefaultWorkflowKitArtifactsForPhase(phase) {
|
|
100
|
+
const templateId = DEFAULT_WORKFLOW_KIT_PHASE_TEMPLATE_BY_PHASE[phase];
|
|
101
|
+
return templateId ? loadWorkflowKitPhaseTemplate(templateId).artifacts : null;
|
|
102
|
+
}
|