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.
package/bin/agentxchain.js
CHANGED
|
@@ -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
|
@@ -15,7 +15,11 @@ export async function intakeResolveCommand(opts) {
|
|
|
15
15
|
process.exit(1);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const
|
|
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
|
-
// ──
|
|
3226
|
-
//
|
|
3227
|
-
//
|
|
3228
|
-
|
|
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
|
}
|
package/src/lib/repo-observer.js
CHANGED