agentxchain 2.134.0 → 2.135.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/package.json +1 -1
- package/src/commands/doctor.js +3 -1
- package/src/commands/events.js +5 -1
- package/src/commands/mission.js +29 -1
- package/src/commands/restart.js +1 -1
- package/src/commands/resume.js +3 -1
- package/src/commands/status.js +55 -2
- package/src/commands/step.js +3 -1
- package/src/lib/continuous-run.js +2 -2
- package/src/lib/coordinator-warnings.js +31 -0
- package/src/lib/dashboard/plan-reader.js +9 -0
- package/src/lib/dispatch-bundle.js +20 -19
- package/src/lib/governed-state.js +100 -0
- package/src/lib/intake.js +121 -5
- package/src/lib/recent-event-summary.js +5 -0
- package/src/lib/run-events.js +1 -0
package/package.json
CHANGED
package/src/commands/doctor.js
CHANGED
|
@@ -61,6 +61,7 @@ export async function doctorCommand(opts = {}) {
|
|
|
61
61
|
function governedDoctor(root, rawConfig, opts) {
|
|
62
62
|
const checks = [];
|
|
63
63
|
const cliVersionHealth = getCliVersionHealth();
|
|
64
|
+
let stateRunId = null;
|
|
64
65
|
|
|
65
66
|
checks.push(buildCliVersionCheck(cliVersionHealth));
|
|
66
67
|
|
|
@@ -109,6 +110,7 @@ function governedDoctor(root, rawConfig, opts) {
|
|
|
109
110
|
if (existsSync(statePath)) {
|
|
110
111
|
try {
|
|
111
112
|
const stateData = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
113
|
+
stateRunId = stateData.run_id || null;
|
|
112
114
|
if (stateData.schema_version) {
|
|
113
115
|
checks.push({ id: 'state_health', name: 'State health', level: 'pass', detail: `schema_version: ${stateData.schema_version}, status: ${stateData.status || 'unknown'}` });
|
|
114
116
|
} else {
|
|
@@ -354,7 +356,7 @@ function governedDoctor(root, rawConfig, opts) {
|
|
|
354
356
|
|
|
355
357
|
// 11. Pending intake intents (BUG-15 — informational)
|
|
356
358
|
{
|
|
357
|
-
const pendingIntents = findPendingApprovedIntents(root);
|
|
359
|
+
const pendingIntents = findPendingApprovedIntents(root, { run_id: stateRunId });
|
|
358
360
|
if (pendingIntents.length > 0) {
|
|
359
361
|
const summary = pendingIntents.map(pi => `[${pi.priority}] ${pi.intent_id}`).join(', ');
|
|
360
362
|
checks.push({
|
package/src/commands/events.js
CHANGED
|
@@ -88,7 +88,10 @@ function printEvent(evt) {
|
|
|
88
88
|
const coordinatorRetryDetail = evt.event_type === 'coordinator_retry' && evt.payload
|
|
89
89
|
? ` — ws ${evt.payload.workstream_id || '?'} repo ${evt.payload.repo_id || '?'} (retry of ${evt.payload.failed_turn_id || '?'})`
|
|
90
90
|
: '';
|
|
91
|
-
|
|
91
|
+
const projectionWarningDetail = evt.event_type === 'coordinator_retry_projection_warning' && evt.payload
|
|
92
|
+
? ` — ws ${evt.payload.workstream_id || '?'} repo ${evt.payload.repo_id || '?'} (reconciliation required)`
|
|
93
|
+
: '';
|
|
94
|
+
console.log(`${chalk.dim(ts)} ${type} ${chalk.cyan(runId)} ${phase}${turnInfo}${intentInfo}${conflictDetail}${conflictResolvedDetail}${rejectionDetail}${acceptanceFailedDetail}${phaseTransitionDetail}${gateFailedDetail}${humanEscalationDetail}${coordinatorRetryDetail}${projectionWarningDetail}`);
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
function formatConflictDetail(evt) {
|
|
@@ -150,6 +153,7 @@ function colorEventType(type) {
|
|
|
150
153
|
gate_failed: chalk.red,
|
|
151
154
|
budget_exceeded_warn: chalk.yellowBright,
|
|
152
155
|
coordinator_retry: chalk.cyan.bold,
|
|
156
|
+
coordinator_retry_projection_warning: chalk.yellow.bold,
|
|
153
157
|
turn_checkpointed: chalk.green,
|
|
154
158
|
dispatch_progress: chalk.blue.dim,
|
|
155
159
|
};
|
package/src/commands/mission.js
CHANGED
|
@@ -38,6 +38,7 @@ import { executeGovernedRun } from './run.js';
|
|
|
38
38
|
import { dispatchCoordinatorTurn, selectAssignmentForWorkstream } from '../lib/coordinator-dispatch.js';
|
|
39
39
|
import { loadCoordinatorState } from '../lib/coordinator-state.js';
|
|
40
40
|
import { projectRepoAcceptance } from '../lib/coordinator-acceptance.js';
|
|
41
|
+
import { emitRunEvent } from '../lib/run-events.js';
|
|
41
42
|
|
|
42
43
|
export async function missionStartCommand(opts) {
|
|
43
44
|
const root = findProjectRoot(opts.dir || process.cwd());
|
|
@@ -182,6 +183,13 @@ function projectAcceptedCoordinatorTurn(workspacePath, coordinatorConfig, repoId
|
|
|
182
183
|
);
|
|
183
184
|
}
|
|
184
185
|
|
|
186
|
+
function buildCoordinatorProjectionWarning(message) {
|
|
187
|
+
return {
|
|
188
|
+
code: 'coordinator_acceptance_projection_incomplete',
|
|
189
|
+
message,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
185
193
|
export async function missionListCommand(opts) {
|
|
186
194
|
const root = findProjectRoot(opts.dir || process.cwd());
|
|
187
195
|
if (!root) {
|
|
@@ -634,6 +642,7 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
|
|
|
634
642
|
};
|
|
635
643
|
|
|
636
644
|
let execution;
|
|
645
|
+
const retryWarnings = [];
|
|
637
646
|
try {
|
|
638
647
|
execution = await executor(repoContext, runOpts);
|
|
639
648
|
} catch (error) {
|
|
@@ -669,7 +678,21 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
|
|
|
669
678
|
loadCoordinatorState(mission.coordinator.workspace_path),
|
|
670
679
|
);
|
|
671
680
|
if (!projection.ok) {
|
|
672
|
-
|
|
681
|
+
const warning = buildCoordinatorProjectionWarning(projection.error);
|
|
682
|
+
retryWarnings.push(warning);
|
|
683
|
+
console.error(chalk.yellow(`Coordinator retry projection warning: ${warning.message}`));
|
|
684
|
+
emitRunEvent(mission.coordinator.workspace_path, 'coordinator_retry_projection_warning', {
|
|
685
|
+
run_id: mission.coordinator.super_run_id,
|
|
686
|
+
phase: coordinatorConfigResult.config?.workstreams?.[opts.workstream]?.phase || null,
|
|
687
|
+
status: 'active',
|
|
688
|
+
payload: {
|
|
689
|
+
workstream_id: opts.workstream,
|
|
690
|
+
repo_id: retry.retryResult.repo_id,
|
|
691
|
+
reissued_turn_id: retry.retryResult.reissued_turn_id,
|
|
692
|
+
warning_code: warning.code,
|
|
693
|
+
warning_message: warning.message,
|
|
694
|
+
},
|
|
695
|
+
});
|
|
673
696
|
}
|
|
674
697
|
}
|
|
675
698
|
|
|
@@ -697,6 +720,8 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
|
|
|
697
720
|
workstream_status: retriedWorkstream?.launch_status || 'launched',
|
|
698
721
|
launch_record: retriedLaunchRecord,
|
|
699
722
|
exit_code: execution?.exitCode ?? 0,
|
|
723
|
+
warnings: retryWarnings,
|
|
724
|
+
reconciliation_required: retryWarnings.length > 0,
|
|
700
725
|
}, null, 2));
|
|
701
726
|
if ((execution?.exitCode ?? 0) !== 0) {
|
|
702
727
|
process.exit(execution.exitCode);
|
|
@@ -713,6 +738,9 @@ export async function missionPlanLaunchCommand(planTarget, opts) {
|
|
|
713
738
|
console.log(chalk.dim(` Old Turn: ${retry.retryResult.failed_turn_id}`));
|
|
714
739
|
console.log(chalk.dim(` New Turn: ${retry.retryResult.reissued_turn_id}`));
|
|
715
740
|
console.log(chalk.dim(` Workstream: ${retriedWorkstream?.launch_status || 'launched'}`));
|
|
741
|
+
if (retryWarnings.length > 0) {
|
|
742
|
+
console.log(chalk.yellow(` Warning: ${retryWarnings[0].message}`));
|
|
743
|
+
}
|
|
716
744
|
console.log('');
|
|
717
745
|
renderPlan(retriedPlan);
|
|
718
746
|
if ((execution?.exitCode ?? 0) !== 0) {
|
package/src/commands/restart.js
CHANGED
|
@@ -342,7 +342,7 @@ export async function restartCommand(opts) {
|
|
|
342
342
|
if (activeTurnCount === 0) {
|
|
343
343
|
// BUG-21 fix: consume approved intents (same as resume path) so intent_id
|
|
344
344
|
// propagates into turn metadata and all lifecycle events.
|
|
345
|
-
const consumed = consumeNextApprovedIntent(root, { role: roleId });
|
|
345
|
+
const consumed = consumeNextApprovedIntent(root, { role: roleId, run_id: state?.run_id || null });
|
|
346
346
|
let assignedState;
|
|
347
347
|
let turnId;
|
|
348
348
|
let assignedRole = roleId;
|
package/src/commands/resume.js
CHANGED
|
@@ -267,7 +267,9 @@ export async function resumeCommand(opts) {
|
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
const shouldBindIntent = opts.intent !== false;
|
|
270
|
-
const consumed = shouldBindIntent
|
|
270
|
+
const consumed = shouldBindIntent
|
|
271
|
+
? consumeNextApprovedIntent(root, { role: roleId, run_id: state?.run_id || null })
|
|
272
|
+
: { ok: false };
|
|
271
273
|
if (consumed.ok) {
|
|
272
274
|
state = loadProjectState(root, config);
|
|
273
275
|
if (!state) {
|
package/src/commands/status.js
CHANGED
|
@@ -24,6 +24,7 @@ import { getDashboardPid, getDashboardSession } from './dashboard.js';
|
|
|
24
24
|
import { readPreemptionMarker, findPendingApprovedIntents } from '../lib/intake.js';
|
|
25
25
|
import { readContinuousSession } from '../lib/continuous-run.js';
|
|
26
26
|
import { readAllDispatchProgress } from '../lib/dispatch-progress.js';
|
|
27
|
+
import { readCoordinatorWarnings } from '../lib/coordinator-warnings.js';
|
|
27
28
|
|
|
28
29
|
export async function statusCommand(opts) {
|
|
29
30
|
const context = loadStatusContext();
|
|
@@ -111,10 +112,22 @@ function loadStatusContext(dir = process.cwd()) {
|
|
|
111
112
|
return null;
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
const fallbackConfig = {
|
|
116
|
+
...rawConfig,
|
|
117
|
+
files: {
|
|
118
|
+
talk: rawConfig.files?.talk || 'TALK.md',
|
|
119
|
+
history: rawConfig.files?.history || '.agentxchain/history.jsonl',
|
|
120
|
+
state: rawConfig.files?.state || '.agentxchain/state.json',
|
|
121
|
+
log: Object.prototype.hasOwnProperty.call(rawConfig.files || {}, 'log')
|
|
122
|
+
? rawConfig.files.log
|
|
123
|
+
: null,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
114
127
|
return {
|
|
115
128
|
root,
|
|
116
129
|
rawConfig,
|
|
117
|
-
config:
|
|
130
|
+
config: fallbackConfig,
|
|
118
131
|
version: 4,
|
|
119
132
|
};
|
|
120
133
|
}
|
|
@@ -122,6 +135,7 @@ function loadStatusContext(dir = process.cwd()) {
|
|
|
122
135
|
function renderGovernedStatus(context, opts) {
|
|
123
136
|
const { root, config, version } = context;
|
|
124
137
|
const state = loadProjectState(root, config);
|
|
138
|
+
const stateRunId = state?.run_id || readRawStateRunId(root, config);
|
|
125
139
|
const continuity = getContinuityStatus(root, state);
|
|
126
140
|
const connectorHealth = getConnectorHealth(root, config, state);
|
|
127
141
|
const recovery = deriveRecoveryDescriptor(state, config);
|
|
@@ -133,7 +147,7 @@ function renderGovernedStatus(context, opts) {
|
|
|
133
147
|
const workflowKitArtifacts = deriveWorkflowKitArtifacts(root, config, state);
|
|
134
148
|
const humanEscalation = findCurrentHumanEscalation(root, state);
|
|
135
149
|
const preemptionMarker = readPreemptionMarker(root);
|
|
136
|
-
const pendingIntents = findPendingApprovedIntents(root);
|
|
150
|
+
const pendingIntents = findPendingApprovedIntents(root, { run_id: stateRunId || null });
|
|
137
151
|
const continuousSession = readContinuousSession(root);
|
|
138
152
|
const gateActionAttempt = state?.pending_phase_transition
|
|
139
153
|
? summarizeLatestGateActionAttempt(root, 'phase_transition', state.pending_phase_transition.gate)
|
|
@@ -147,6 +161,9 @@ function renderGovernedStatus(context, opts) {
|
|
|
147
161
|
const activeTurns = getActiveTurns(state);
|
|
148
162
|
const dispatchProgress = filterDispatchProgressForActiveTurns(readAllDispatchProgress(root), activeTurns);
|
|
149
163
|
|
|
164
|
+
// Coordinator warning surfacing — DEC-COORD-RETRY-PROJECTION-EVENT-001
|
|
165
|
+
const coordinatorWarnings = readCoordinatorWarnings(root, { runId: stateRunId || null });
|
|
166
|
+
|
|
150
167
|
if (opts.json) {
|
|
151
168
|
const dashPid = getDashboardPid(root);
|
|
152
169
|
const dashSession = getDashboardSession(root);
|
|
@@ -183,6 +200,7 @@ function renderGovernedStatus(context, opts) {
|
|
|
183
200
|
dashboard_session: dashboardSessionObj,
|
|
184
201
|
binding_drift: detectActiveTurnBindingDrift(state, config),
|
|
185
202
|
bundle_integrity: detectStateBundleDesync(root, state),
|
|
203
|
+
coordinator_warnings: coordinatorWarnings,
|
|
186
204
|
}, null, 2));
|
|
187
205
|
return;
|
|
188
206
|
}
|
|
@@ -284,6 +302,7 @@ function renderGovernedStatus(context, opts) {
|
|
|
284
302
|
renderContinuityStatus(continuity, state);
|
|
285
303
|
renderConnectorHealthStatus(connectorHealth);
|
|
286
304
|
renderRecentEventSummary(recentEventSummary);
|
|
305
|
+
renderCoordinatorWarnings(coordinatorWarnings);
|
|
287
306
|
|
|
288
307
|
// BUG-18: State/bundle integrity check
|
|
289
308
|
const desync = detectStateBundleDesync(root, state);
|
|
@@ -908,3 +927,37 @@ function formatUsd(value) {
|
|
|
908
927
|
if (typeof value !== 'number' || Number.isNaN(value)) return '0.00';
|
|
909
928
|
return value.toFixed(2);
|
|
910
929
|
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Read coordinator retry projection warnings from the event log.
|
|
933
|
+
* Returns a structured summary for both JSON and CLI output.
|
|
934
|
+
*/
|
|
935
|
+
function renderCoordinatorWarnings(warnings) {
|
|
936
|
+
if (!warnings || warnings.count === 0) return;
|
|
937
|
+
console.log(chalk.yellow.bold(` ⚠ Coordinator reconciliation required (${warnings.count} projection warning${warnings.count !== 1 ? 's' : ''})`));
|
|
938
|
+
for (const w of warnings.warnings) {
|
|
939
|
+
const wsLabel = w.workstream_id ? `ws:${w.workstream_id}` : '';
|
|
940
|
+
const repoLabel = w.repo_id ? `repo:${w.repo_id}` : '';
|
|
941
|
+
const parts = [wsLabel, repoLabel].filter(Boolean).join(' ');
|
|
942
|
+
console.log(` ${chalk.yellow('●')} ${parts} — ${w.warning_code}`);
|
|
943
|
+
}
|
|
944
|
+
console.log(chalk.dim(' Run `agentxchain mission plan show latest --json` to force plan sync and verify.'));
|
|
945
|
+
console.log('');
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function readRawStateRunId(root, config) {
|
|
949
|
+
const relPath = config?.files?.state || '.agentxchain/state.json';
|
|
950
|
+
const filePath = join(root, relPath);
|
|
951
|
+
if (!existsSync(filePath)) {
|
|
952
|
+
return null;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
try {
|
|
956
|
+
const parsed = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
957
|
+
return typeof parsed?.run_id === 'string' && parsed.run_id.trim().length > 0
|
|
958
|
+
? parsed.run_id.trim()
|
|
959
|
+
: null;
|
|
960
|
+
} catch {
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
}
|
package/src/commands/step.js
CHANGED
|
@@ -316,7 +316,9 @@ export async function stepCommand(opts) {
|
|
|
316
316
|
}
|
|
317
317
|
|
|
318
318
|
const shouldBindIntent = opts.intent !== false;
|
|
319
|
-
const consumed = shouldBindIntent
|
|
319
|
+
const consumed = shouldBindIntent
|
|
320
|
+
? consumeNextApprovedIntent(root, { role: roleId, run_id: state?.run_id || null })
|
|
321
|
+
: { ok: false };
|
|
320
322
|
if (consumed.ok) {
|
|
321
323
|
state = loadProjectState(root, config);
|
|
322
324
|
if (!state) {
|
|
@@ -350,8 +350,8 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
350
350
|
return { ok: false, status: 'failed', action: 'vision_missing', stop_reason: `VISION.md not found at ${absVisionPath}` };
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
-
// Step 1: Check intake queue for pending work
|
|
354
|
-
const queued = findNextDispatchableIntent(root);
|
|
353
|
+
// Step 1: Check intake queue for pending work (BUG-34: scope to current run)
|
|
354
|
+
const queued = findNextDispatchableIntent(root, { run_id: session.current_run_id });
|
|
355
355
|
let targetIntentId = null;
|
|
356
356
|
let visionObjective = null;
|
|
357
357
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readRunEvents } from './run-events.js';
|
|
2
|
+
|
|
3
|
+
function normalizeCoordinatorWarning(event) {
|
|
4
|
+
return {
|
|
5
|
+
event_id: event.event_id,
|
|
6
|
+
timestamp: event.timestamp,
|
|
7
|
+
run_id: event.run_id || null,
|
|
8
|
+
workstream_id: event.payload?.workstream_id || null,
|
|
9
|
+
repo_id: event.payload?.repo_id || null,
|
|
10
|
+
reissued_turn_id: event.payload?.reissued_turn_id || null,
|
|
11
|
+
warning_code: event.payload?.warning_code || 'coordinator_acceptance_projection_incomplete',
|
|
12
|
+
warning_message: event.payload?.warning_message || null,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function readCoordinatorWarnings(root, { runId = null } = {}) {
|
|
17
|
+
const events = readRunEvents(root, { type: 'coordinator_retry_projection_warning' });
|
|
18
|
+
const filtered = runId
|
|
19
|
+
? events.filter((event) => event.run_id === runId)
|
|
20
|
+
: events;
|
|
21
|
+
|
|
22
|
+
if (filtered.length === 0) {
|
|
23
|
+
return { count: 0, reconciliation_required: false, warnings: [] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
count: filtered.length,
|
|
28
|
+
reconciliation_required: true,
|
|
29
|
+
warnings: filtered.map(normalizeCoordinatorWarning),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import { buildPlanProgressSummary, loadAllPlans } from '../mission-plans.js';
|
|
9
9
|
import { loadAllMissionArtifacts } from '../missions.js';
|
|
10
|
+
import { loadCoordinatorState } from '../coordinator-state.js';
|
|
11
|
+
import { readCoordinatorWarnings } from '../coordinator-warnings.js';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Build a dashboard-ready plan snapshot across all missions.
|
|
@@ -49,12 +51,19 @@ export function readPlanSnapshot(workspacePath, { limit, missionId } = {}) {
|
|
|
49
51
|
latestSummary = buildPlanSummary(latest);
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
// Surface coordinator projection warnings for dashboard consumers
|
|
55
|
+
const coordinatorState = loadCoordinatorState(workspacePath);
|
|
56
|
+
const coordinatorWarnings = readCoordinatorWarnings(workspacePath, {
|
|
57
|
+
runId: coordinatorState?.super_run_id || null,
|
|
58
|
+
});
|
|
59
|
+
|
|
52
60
|
return {
|
|
53
61
|
ok: true,
|
|
54
62
|
status: 200,
|
|
55
63
|
body: {
|
|
56
64
|
latest: latestSummary,
|
|
57
65
|
plans: plans.map(buildPlanSummary),
|
|
66
|
+
coordinator_warnings: coordinatorWarnings,
|
|
58
67
|
},
|
|
59
68
|
};
|
|
60
69
|
}
|
|
@@ -332,25 +332,8 @@ function renderPrompt(role, roleId, turn, state, config, root) {
|
|
|
332
332
|
lines.push('');
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
lines.push('');
|
|
338
|
-
if (turn.intake_context.charter) {
|
|
339
|
-
lines.push(turn.intake_context.charter);
|
|
340
|
-
lines.push('');
|
|
341
|
-
}
|
|
342
|
-
if (Array.isArray(turn.intake_context.acceptance_contract) && turn.intake_context.acceptance_contract.length > 0) {
|
|
343
|
-
lines.push('Acceptance contract:');
|
|
344
|
-
turn.intake_context.acceptance_contract.forEach((requirement, index) => {
|
|
345
|
-
lines.push(`${index + 1}. ${requirement}`);
|
|
346
|
-
});
|
|
347
|
-
lines.push('');
|
|
348
|
-
}
|
|
349
|
-
lines.push('You must explicitly address every acceptance item in your turn summary, artifacts, or verification evidence. Do not treat this as background context.');
|
|
350
|
-
lines.push('');
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Retry context
|
|
335
|
+
// BUG-35: retry context must appear BEFORE the injected intent so the agent
|
|
336
|
+
// sees the blocker (gate failure) first and the repair guidance (intent) second.
|
|
354
337
|
if (turn.attempt > 1 && turn.last_rejection) {
|
|
355
338
|
lines.push('## Previous Attempt Failed');
|
|
356
339
|
lines.push('');
|
|
@@ -369,6 +352,24 @@ function renderPrompt(role, roleId, turn, state, config, root) {
|
|
|
369
352
|
lines.push('');
|
|
370
353
|
}
|
|
371
354
|
|
|
355
|
+
if (turn.intake_context) {
|
|
356
|
+
lines.push('### Active Injected Intent — respond to this as your primary charter');
|
|
357
|
+
lines.push('');
|
|
358
|
+
if (turn.intake_context.charter) {
|
|
359
|
+
lines.push(turn.intake_context.charter);
|
|
360
|
+
lines.push('');
|
|
361
|
+
}
|
|
362
|
+
if (Array.isArray(turn.intake_context.acceptance_contract) && turn.intake_context.acceptance_contract.length > 0) {
|
|
363
|
+
lines.push('Acceptance contract:');
|
|
364
|
+
turn.intake_context.acceptance_contract.forEach((requirement, index) => {
|
|
365
|
+
lines.push(`${index + 1}. ${requirement}`);
|
|
366
|
+
});
|
|
367
|
+
lines.push('');
|
|
368
|
+
}
|
|
369
|
+
lines.push('You must explicitly address every acceptance item in your turn summary, artifacts, or verification evidence. Do not treat this as background context.');
|
|
370
|
+
lines.push('');
|
|
371
|
+
}
|
|
372
|
+
|
|
372
373
|
if (turn.conflict_context) {
|
|
373
374
|
lines.push('## File Conflict - Retry Required');
|
|
374
375
|
lines.push('');
|
|
@@ -2154,6 +2154,42 @@ export function initializeGovernedRun(root, config, options = {}) {
|
|
|
2154
2154
|
};
|
|
2155
2155
|
|
|
2156
2156
|
writeState(root, updatedState);
|
|
2157
|
+
|
|
2158
|
+
// BUG-34: retroactive migration — archive stale intents from prior runs.
|
|
2159
|
+
// Intents with an approved_run_id from a DIFFERENT run are archived.
|
|
2160
|
+
// Intents with no approved_run_id are adopted into the current run
|
|
2161
|
+
// (they were created while the project was idle or pre-run).
|
|
2162
|
+
try {
|
|
2163
|
+
const intentsDir = join(root, '.agentxchain', 'intake', 'intents');
|
|
2164
|
+
if (existsSync(intentsDir)) {
|
|
2165
|
+
const DISPATCHABLE = new Set(['planned', 'approved']);
|
|
2166
|
+
const intNow = new Date().toISOString();
|
|
2167
|
+
for (const f of readdirSync(intentsDir).filter(x => x.endsWith('.json') && !x.startsWith('.tmp-'))) {
|
|
2168
|
+
const ip = join(intentsDir, f);
|
|
2169
|
+
try {
|
|
2170
|
+
const intent = JSON.parse(readFileSync(ip, 'utf8'));
|
|
2171
|
+
if (!intent || !DISPATCHABLE.has(intent.status)) continue;
|
|
2172
|
+
if (intent.cross_run_durable === true) continue;
|
|
2173
|
+
if (intent.approved_run_id === runId) continue;
|
|
2174
|
+
|
|
2175
|
+
if (intent.approved_run_id && intent.approved_run_id !== runId) {
|
|
2176
|
+
// Intent from a different run — archive it
|
|
2177
|
+
intent.status = 'suppressed';
|
|
2178
|
+
intent.updated_at = intNow;
|
|
2179
|
+
intent.archived_reason = `stale: approved under run ${intent.approved_run_id}, archived on run ${runId} initialization`;
|
|
2180
|
+
if (!intent.history) intent.history = [];
|
|
2181
|
+
intent.history.push({ from: 'approved', to: 'suppressed', at: intNow, reason: intent.archived_reason });
|
|
2182
|
+
} else if (!intent.approved_run_id) {
|
|
2183
|
+
// Legacy intent with no run binding — adopt into current run
|
|
2184
|
+
intent.approved_run_id = runId;
|
|
2185
|
+
intent.updated_at = intNow;
|
|
2186
|
+
}
|
|
2187
|
+
safeWriteJson(ip, intent);
|
|
2188
|
+
} catch { /* non-fatal per-intent */ }
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
} catch { /* non-fatal — intent migration is best-effort */ }
|
|
2192
|
+
|
|
2157
2193
|
emitRunEvent(root, 'run_started', {
|
|
2158
2194
|
run_id: runId,
|
|
2159
2195
|
phase: updatedState.phase,
|
|
@@ -3107,6 +3143,70 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
3107
3143
|
}
|
|
3108
3144
|
}
|
|
3109
3145
|
|
|
3146
|
+
// ── Gate semantic coverage validation (BUG-36) ────────────────────────────
|
|
3147
|
+
// When a turn proposes a phase transition, pre-evaluate the gate. If the gate
|
|
3148
|
+
// would fail AND the failing files are not in files_changed, reject the turn
|
|
3149
|
+
// early — the agent didn't do the work required for the transition.
|
|
3150
|
+
if (turnResult.phase_transition_request) {
|
|
3151
|
+
const preGateResult = evaluatePhaseExit({
|
|
3152
|
+
state,
|
|
3153
|
+
config,
|
|
3154
|
+
acceptedTurn: turnResult,
|
|
3155
|
+
root,
|
|
3156
|
+
});
|
|
3157
|
+
|
|
3158
|
+
if (preGateResult.action === 'gate_failed') {
|
|
3159
|
+
// Gate is failing. Check if any of the failing reasons reference files
|
|
3160
|
+
// that this turn didn't modify.
|
|
3161
|
+
const declaredFiles = new Set((turnResult.files_changed || []).map(f => f.replace(/^\.\//, '')));
|
|
3162
|
+
const exitGateId = preGateResult.gate_id || 'unknown_gate';
|
|
3163
|
+
|
|
3164
|
+
// Extract file paths from gate failure reasons and missing_files
|
|
3165
|
+
const failingFiles = [
|
|
3166
|
+
...(preGateResult.missing_files || []),
|
|
3167
|
+
];
|
|
3168
|
+
|
|
3169
|
+
// Also extract file paths from failure reasons (e.g., ".planning/IMPLEMENTATION_NOTES.md: ...")
|
|
3170
|
+
for (const reason of (preGateResult.reasons || [])) {
|
|
3171
|
+
const fileMatch = reason.match(/(?:Required file missing|file): ([^\s,]+)/);
|
|
3172
|
+
if (fileMatch) failingFiles.push(fileMatch[1]);
|
|
3173
|
+
// Also catch paths at the start of semantic failure messages
|
|
3174
|
+
const semanticMatch = reason.match(/^([^\s:]+\.md):/);
|
|
3175
|
+
if (semanticMatch) failingFiles.push(semanticMatch[1]);
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
const uniqueFailingFiles = [...new Set(failingFiles.map(f => f.replace(/^\.\//, '')))];
|
|
3179
|
+
const uncoveredFiles = uniqueFailingFiles.filter(f => !declaredFiles.has(f));
|
|
3180
|
+
|
|
3181
|
+
const gateSemanticMode = config.gate_semantic_coverage_mode || 'strict';
|
|
3182
|
+
if (uncoveredFiles.length > 0 && gateSemanticMode === 'strict') {
|
|
3183
|
+
const coverageError = `Gate "${exitGateId}" is failing on ${uncoveredFiles.join(', ')}. Your turn did not modify ${uncoveredFiles.length === 1 ? 'that file' : 'those files'}. Either edit the file(s) to satisfy the gate, or remove the phase transition request.`;
|
|
3184
|
+
transitionToFailedAcceptance(root, state, currentTurn, coverageError, {
|
|
3185
|
+
error_code: 'gate_semantic_coverage',
|
|
3186
|
+
stage: 'gate_semantic_coverage',
|
|
3187
|
+
extra: {
|
|
3188
|
+
gate_id: exitGateId,
|
|
3189
|
+
uncovered_files: uncoveredFiles,
|
|
3190
|
+
declared_files: [...declaredFiles],
|
|
3191
|
+
gate_reasons: preGateResult.reasons,
|
|
3192
|
+
},
|
|
3193
|
+
});
|
|
3194
|
+
return {
|
|
3195
|
+
ok: false,
|
|
3196
|
+
error: coverageError,
|
|
3197
|
+
validation: {
|
|
3198
|
+
...validation,
|
|
3199
|
+
ok: false,
|
|
3200
|
+
stage: 'gate_semantic_coverage',
|
|
3201
|
+
error_class: 'gate_coverage_error',
|
|
3202
|
+
errors: uncoveredFiles.map(f => `Gate "${exitGateId}" is failing on "${f}". Your turn did not modify that file.`),
|
|
3203
|
+
warnings: [],
|
|
3204
|
+
},
|
|
3205
|
+
};
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3110
3210
|
const observedArtifact = buildObservedArtifact(observation, baseline);
|
|
3111
3211
|
const normalizedVerification = normalizeVerification(turnResult.verification, runtimeType);
|
|
3112
3212
|
const artifactType = turnResult.artifact?.type || 'review';
|
package/src/lib/intake.js
CHANGED
|
@@ -504,15 +504,35 @@ export function intakeStatus(root, intentId) {
|
|
|
504
504
|
return { ok: true, summary, exitCode: 0 };
|
|
505
505
|
}
|
|
506
506
|
|
|
507
|
-
export function findNextDispatchableIntent(root) {
|
|
507
|
+
export function findNextDispatchableIntent(root, options = {}) {
|
|
508
508
|
const dirs = intakeDirs(root);
|
|
509
509
|
if (!existsSync(dirs.intents)) {
|
|
510
510
|
return { ok: false, error: 'no intents directory' };
|
|
511
511
|
}
|
|
512
512
|
|
|
513
|
-
const
|
|
513
|
+
const scopeRunId = options.run_id || null;
|
|
514
|
+
|
|
515
|
+
let intents = readJsonDir(dirs.intents)
|
|
514
516
|
.filter((intent) => intent && DISPATCHABLE_STATUSES.has(intent.status));
|
|
515
517
|
|
|
518
|
+
// BUG-34: when run_id scoping is active, filter out intents that belong to
|
|
519
|
+
// a different run. An intent belongs to the current run if:
|
|
520
|
+
// (a) it has approved_run_id matching the current run, OR
|
|
521
|
+
// (b) it has no approved_run_id AND is marked cross_run_durable, OR
|
|
522
|
+
// (c) it was injected in the current run (approved_run_id matches)
|
|
523
|
+
// Legacy intents (no approved_run_id, no cross_run_durable) are excluded
|
|
524
|
+
// because they are stale leftovers from prior runs.
|
|
525
|
+
if (scopeRunId) {
|
|
526
|
+
intents = intents.filter((intent) => {
|
|
527
|
+
if (intent.approved_run_id === scopeRunId) return true;
|
|
528
|
+
if (intent.cross_run_durable === true) return true;
|
|
529
|
+
// Legacy intent with no run binding — stale, skip it
|
|
530
|
+
if (!intent.approved_run_id) return false;
|
|
531
|
+
// Intent bound to a different run — stale, skip it
|
|
532
|
+
return false;
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
516
536
|
if (intents.length === 0) {
|
|
517
537
|
return { ok: false, error: 'no dispatchable intents' };
|
|
518
538
|
}
|
|
@@ -550,12 +570,23 @@ export function findNextDispatchableIntent(root) {
|
|
|
550
570
|
* Return all approved-but-unconsumed intents sorted by priority (BUG-15).
|
|
551
571
|
* Used by `status` to surface the pending intent queue.
|
|
552
572
|
*/
|
|
553
|
-
export function findPendingApprovedIntents(root) {
|
|
573
|
+
export function findPendingApprovedIntents(root, options = {}) {
|
|
554
574
|
const dirs = intakeDirs(root);
|
|
555
575
|
if (!existsSync(dirs.intents)) return [];
|
|
556
576
|
|
|
577
|
+
const scopeRunId = options.run_id || null;
|
|
578
|
+
|
|
557
579
|
return readJsonDir(dirs.intents)
|
|
558
|
-
.filter((intent) =>
|
|
580
|
+
.filter((intent) => {
|
|
581
|
+
if (!intent || intent.status !== 'approved') return false;
|
|
582
|
+
// BUG-34: run_id scoping — same logic as findNextDispatchableIntent
|
|
583
|
+
if (scopeRunId) {
|
|
584
|
+
if (intent.approved_run_id === scopeRunId) return true;
|
|
585
|
+
if (intent.cross_run_durable === true) return true;
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
return true;
|
|
589
|
+
})
|
|
559
590
|
.sort((a, b) => {
|
|
560
591
|
const aPriority = PRIORITY_RANK[a.priority] ?? Number.MAX_SAFE_INTEGER;
|
|
561
592
|
const bPriority = PRIORITY_RANK[b.priority] ?? Number.MAX_SAFE_INTEGER;
|
|
@@ -574,6 +605,59 @@ export function findPendingApprovedIntents(root) {
|
|
|
574
605
|
}));
|
|
575
606
|
}
|
|
576
607
|
|
|
608
|
+
/**
|
|
609
|
+
* BUG-34: Archive stale intents from prior runs.
|
|
610
|
+
* Called during run initialization to prevent cross-run intent leakage.
|
|
611
|
+
* Transitions approved/planned intents that don't belong to the new run into
|
|
612
|
+
* 'suppressed' status with an archival reason.
|
|
613
|
+
*
|
|
614
|
+
* @param {string} root
|
|
615
|
+
* @param {string} newRunId - the run_id of the newly initialized run
|
|
616
|
+
* @returns {{ archived: number }}
|
|
617
|
+
*/
|
|
618
|
+
export function archiveStaleIntents(root, newRunId) {
|
|
619
|
+
const dirs = intakeDirs(root);
|
|
620
|
+
if (!existsSync(dirs.intents)) return { archived: 0, adopted: 0 };
|
|
621
|
+
|
|
622
|
+
const now = nowISO();
|
|
623
|
+
let archived = 0;
|
|
624
|
+
let adopted = 0;
|
|
625
|
+
|
|
626
|
+
const files = readdirSync(dirs.intents).filter(f => f.endsWith('.json') && !f.startsWith('.tmp-'));
|
|
627
|
+
for (const file of files) {
|
|
628
|
+
const intentPath = join(dirs.intents, file);
|
|
629
|
+
let intent;
|
|
630
|
+
try {
|
|
631
|
+
intent = JSON.parse(readFileSync(intentPath, 'utf8'));
|
|
632
|
+
} catch {
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (!intent || !DISPATCHABLE_STATUSES.has(intent.status)) continue;
|
|
637
|
+
if (intent.cross_run_durable === true) continue;
|
|
638
|
+
if (intent.approved_run_id === newRunId) continue;
|
|
639
|
+
|
|
640
|
+
if (intent.approved_run_id && intent.approved_run_id !== newRunId) {
|
|
641
|
+
// Intent from a different run — archive it
|
|
642
|
+
intent.status = 'suppressed';
|
|
643
|
+
intent.updated_at = now;
|
|
644
|
+
intent.archived_reason = `stale: approved under run ${intent.approved_run_id}, archived on run ${newRunId} initialization`;
|
|
645
|
+
if (!intent.history) intent.history = [];
|
|
646
|
+
intent.history.push({ from: 'approved', to: 'suppressed', at: now, reason: intent.archived_reason });
|
|
647
|
+
safeWriteJson(intentPath, intent);
|
|
648
|
+
archived++;
|
|
649
|
+
} else if (!intent.approved_run_id) {
|
|
650
|
+
// Legacy intent with no run binding — adopt into current run
|
|
651
|
+
intent.approved_run_id = newRunId;
|
|
652
|
+
intent.updated_at = now;
|
|
653
|
+
safeWriteJson(intentPath, intent);
|
|
654
|
+
adopted++;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return { archived, adopted };
|
|
659
|
+
}
|
|
660
|
+
|
|
577
661
|
/**
|
|
578
662
|
* Unified intent consumption entry point (BUG-16).
|
|
579
663
|
* Both manual (resume/step --resume) and continuous/scheduler paths should call
|
|
@@ -584,7 +668,22 @@ export function findPendingApprovedIntents(root) {
|
|
|
584
668
|
* @returns {{ ok: boolean, intentId?: string, intent?: object, error?: string }}
|
|
585
669
|
*/
|
|
586
670
|
export function consumeNextApprovedIntent(root, options = {}) {
|
|
587
|
-
|
|
671
|
+
let runId = options.run_id || null;
|
|
672
|
+
if (!runId) {
|
|
673
|
+
try {
|
|
674
|
+
const context = loadProjectContext(root);
|
|
675
|
+
const state = context ? loadProjectState(root, context.config) : null;
|
|
676
|
+
runId = state?.run_id || null;
|
|
677
|
+
} catch {
|
|
678
|
+
runId = null;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (runId && options.auto_archive_stale !== false) {
|
|
683
|
+
archiveStaleIntents(root, runId);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const queued = findNextDispatchableIntent(root, { run_id: runId });
|
|
588
687
|
if (!queued.ok) {
|
|
589
688
|
return { ok: false, error: queued.error || 'no dispatchable intents' };
|
|
590
689
|
}
|
|
@@ -717,6 +816,23 @@ export function approveIntent(root, intentId, options = {}) {
|
|
|
717
816
|
const reason = options.reason || (previousStatus === 'blocked' ? 're-approved after block resolution' : 'approved for planning');
|
|
718
817
|
const now = nowISO();
|
|
719
818
|
|
|
819
|
+
// BUG-34: stamp the current run_id on approval so the intent is scoped to
|
|
820
|
+
// the run that approved it. Intents without approved_run_id are treated as
|
|
821
|
+
// legacy/unbound and filtered out by run-scoped queries.
|
|
822
|
+
if (!intent.approved_run_id) {
|
|
823
|
+
const statePath = join(root, '.agentxchain', 'state.json');
|
|
824
|
+
if (existsSync(statePath)) {
|
|
825
|
+
try {
|
|
826
|
+
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
827
|
+
if (state.run_id) {
|
|
828
|
+
intent.approved_run_id = state.run_id;
|
|
829
|
+
}
|
|
830
|
+
} catch {
|
|
831
|
+
// non-fatal — stamp is best-effort during approval
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
720
836
|
intent.status = 'approved';
|
|
721
837
|
intent.approved_by = approver;
|
|
722
838
|
intent.updated_at = now;
|
|
@@ -47,6 +47,11 @@ function describeEvent(eventType, entry) {
|
|
|
47
47
|
const retryRepo = trimToNull(entry.payload?.repo_id);
|
|
48
48
|
return `${prefix}${eventType}${wsId ? ` ${wsId}` : ''}${retryRepo ? ` (${retryRepo})` : ''}`;
|
|
49
49
|
}
|
|
50
|
+
case 'coordinator_retry_projection_warning': {
|
|
51
|
+
const wsIdWarn = trimToNull(entry.payload?.workstream_id);
|
|
52
|
+
const warnRepo = trimToNull(entry.payload?.repo_id);
|
|
53
|
+
return `${prefix}${eventType}${wsIdWarn ? ` ${wsIdWarn}` : ''}${warnRepo ? ` (${warnRepo})` : ''} — reconciliation required`;
|
|
54
|
+
}
|
|
50
55
|
case 'turn_checkpointed':
|
|
51
56
|
return `${prefix}${eventType}${roleId ? ` [${roleId}]` : ''}`;
|
|
52
57
|
case 'dispatch_progress':
|