agentxchain 2.139.0 → 2.140.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.
@@ -1031,6 +1031,7 @@ intakeCmd
1031
1031
  .command('resolve')
1032
1032
  .description('Resolve an executing intent by reading the governed run outcome')
1033
1033
  .option('--intent <id>', 'Intent ID to resolve')
1034
+ .option('--outcome <status>', 'Force transition to this status (e.g., "completed")')
1034
1035
  .option('-j, --json', 'Output as JSON')
1035
1036
  .action(intakeResolveCommand);
1036
1037
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.139.0",
3
+ "version": "2.140.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,11 @@ export async function intakeResolveCommand(opts) {
15
15
  process.exit(1);
16
16
  }
17
17
 
18
- const result = resolveIntent(root, opts.intent);
18
+ const resolveOpts = {};
19
+ if (opts.outcome) {
20
+ resolveOpts.outcome = opts.outcome;
21
+ }
22
+ const result = resolveIntent(root, opts.intent, resolveOpts);
19
23
 
20
24
  if (opts.json) {
21
25
  console.log(JSON.stringify(result, null, 2));
@@ -115,6 +115,50 @@ function listIntakeIntentFiles(root) {
115
115
  .map((name) => join(intentsDir, name));
116
116
  }
117
117
 
118
+ // BUG-45: Reconcile embedded intake_context against the live intent file.
119
+ // The embedded copy is historical state; the live intent file is authoritative.
120
+ // If the live intent cannot be read, acceptance must fail closed instead of
121
+ // silently reusing the stale embedded contract.
122
+ const INTENT_TERMINAL_STATES = ['completed', 'satisfied', 'superseded', 'suppressed', 'failed', 'rejected'];
123
+
124
+ function reconcileIntakeContext(root, intakeCtx) {
125
+ if (!intakeCtx || !intakeCtx.intent_id) {
126
+ return { ok: true, intakeCtx };
127
+ }
128
+
129
+ try {
130
+ const intentPath = join(root, INTAKE_INTENTS_DIR, `${intakeCtx.intent_id}.json`);
131
+ if (!existsSync(intentPath)) {
132
+ return {
133
+ ok: false,
134
+ error: `Intent reconciliation failed: live intent ${intakeCtx.intent_id} not found at ${INTAKE_INTENTS_DIR}/${intakeCtx.intent_id}.json`,
135
+ };
136
+ }
137
+
138
+ const liveIntent = JSON.parse(readFileSync(intentPath, 'utf8'));
139
+
140
+ // If the intent has reached a terminal state, skip coverage enforcement
141
+ if (INTENT_TERMINAL_STATES.includes(liveIntent.status)) {
142
+ return { ok: true, intakeCtx: null };
143
+ }
144
+
145
+ // Intent is still active — use the CURRENT acceptance_contract from disk
146
+ if (Array.isArray(liveIntent.acceptance_contract)) {
147
+ return {
148
+ ok: true,
149
+ intakeCtx: { ...intakeCtx, acceptance_contract: liveIntent.acceptance_contract },
150
+ };
151
+ }
152
+
153
+ return { ok: true, intakeCtx };
154
+ } catch (error) {
155
+ return {
156
+ ok: false,
157
+ error: `Intent reconciliation failed: could not read live intent ${intakeCtx.intent_id}: ${error.message}`,
158
+ };
159
+ }
160
+ }
161
+
118
162
  function retireApprovedPhaseScopedIntents(root, state, config, exitedPhase, now) {
119
163
  const retired = [];
120
164
 
@@ -3222,10 +3266,33 @@ function _acceptGovernedTurnLocked(root, config, opts) {
3222
3266
  };
3223
3267
  }
3224
3268
 
3225
- // ── Intent coverage validation (BUG-14) ──────────────────────────────────
3226
- // When a turn is bound to an injected intent, verify the turn result
3227
- // addresses each acceptance item. Default: strict for p0, lenient for others.
3228
- const intakeCtx = currentTurn.intake_context;
3269
+ // ── BUG-45: Reconcile intake_context against live intent state ──────────
3270
+ // The embedded intake_context is a snapshot from dispatch time. The intent
3271
+ // may have since been completed, satisfied, or had its contract updated.
3272
+ // Re-read the live intent file and reconcile before evaluating coverage.
3273
+ const intakeReconciliation = reconcileIntakeContext(root, currentTurn.intake_context);
3274
+ if (!intakeReconciliation.ok) {
3275
+ transitionToFailedAcceptance(root, state, currentTurn, intakeReconciliation.error, {
3276
+ error_code: 'intent_reconciliation_failed',
3277
+ stage: 'intent_reconciliation',
3278
+ extra: {
3279
+ intent_id: currentTurn.intake_context?.intent_id || null,
3280
+ },
3281
+ });
3282
+ return {
3283
+ ok: false,
3284
+ error: intakeReconciliation.error,
3285
+ validation: {
3286
+ ...validation,
3287
+ ok: false,
3288
+ stage: 'intent_reconciliation',
3289
+ error_class: 'intent_reconciliation_error',
3290
+ errors: [intakeReconciliation.error],
3291
+ warnings: validation.warnings,
3292
+ },
3293
+ };
3294
+ }
3295
+ const intakeCtx = intakeReconciliation.intakeCtx;
3229
3296
  if (intakeCtx && Array.isArray(intakeCtx.acceptance_contract) && intakeCtx.acceptance_contract.length > 0) {
3230
3297
  const intentCoverage = evaluateIntentCoverage(turnResult, intakeCtx, {
3231
3298
  state,
package/src/lib/intake.js CHANGED
@@ -1270,7 +1270,7 @@ export function handoffIntent(root, intentId, options = {}) {
1270
1270
  // Resolve — execution exit and intent closure linkage (V3-S5)
1271
1271
  // ---------------------------------------------------------------------------
1272
1272
 
1273
- export function resolveIntent(root, intentId) {
1273
+ export function resolveIntent(root, intentId, opts = {}) {
1274
1274
  const loadedIntent = readIntent(root, intentId);
1275
1275
  if (!loadedIntent.ok) {
1276
1276
  return loadedIntent;
@@ -1298,6 +1298,38 @@ export function resolveIntent(root, intentId) {
1298
1298
  };
1299
1299
  }
1300
1300
 
1301
+ // BUG-45: operator-forced completion — transition executing → completed
1302
+ // when the operator knows the work is done but the framework hasn't retired
1303
+ // the intent automatically (e.g., retained-turn deadlock recovery).
1304
+ if (opts.outcome === 'completed' && intent.status === 'executing') {
1305
+ const now = nowISO();
1306
+ const previousStatus = intent.status;
1307
+ intent.status = 'completed';
1308
+ intent.completed_at = now;
1309
+ intent.updated_at = now;
1310
+ if (!Array.isArray(intent.history)) intent.history = [];
1311
+ intent.history.push({
1312
+ from: previousStatus,
1313
+ to: 'completed',
1314
+ at: now,
1315
+ reason: opts.reason || 'operator-resolved: intent marked completed via intake resolve --outcome completed',
1316
+ });
1317
+
1318
+ const obsDir = join(dirs.base, 'observations', intent.intent_id);
1319
+ mkdirSync(obsDir, { recursive: true });
1320
+
1321
+ safeWriteJson(intentPath, intent);
1322
+ return {
1323
+ ok: true,
1324
+ intent,
1325
+ previous_status: previousStatus,
1326
+ new_status: 'completed',
1327
+ run_outcome: 'completed',
1328
+ no_change: false,
1329
+ exitCode: 0,
1330
+ };
1331
+ }
1332
+
1301
1333
  if (intent.target_workstream) {
1302
1334
  return resolveCoordinatorBackedIntent(root, intentPath, dirs, intent);
1303
1335
  }
@@ -50,6 +50,7 @@ const ORCHESTRATOR_STATE_FILES = [
50
50
  '.agentxchain/human-escalations.jsonl',
51
51
  '.agentxchain/sla-reminders.json',
52
52
  'TALK.md',
53
+ 'HUMAN_TASKS.md',
53
54
  ];
54
55
 
55
56
  // Evidence paths may legitimately remain dirty across turns without blocking the