@fitlab-ai/agent-infra 0.9.7 → 0.9.8
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/dist/bin/internal-cli.js +5 -0
- package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +2 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +2 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +30 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
- package/dist/lib/decide.js +68 -14
- package/dist/lib/init.js +1 -1
- package/dist/lib/internal/release-workflow.js +1 -3
- package/dist/lib/internal/task-activity.js +57 -7
- package/dist/lib/internal/task-event.js +57 -4
- package/dist/lib/internal/task-ledger.js +59 -5
- package/dist/lib/internal/task-lifecycle.js +59 -8
- package/dist/lib/internal/task-orchestration.js +11 -3
- package/dist/lib/internal/task-override.js +131 -0
- package/dist/lib/internal/task-review.js +64 -6
- package/dist/lib/internal/task-verify.js +65 -3
- package/dist/lib/internal/task-warning.js +57 -4
- package/dist/lib/platform/pr-summary.js +5 -3
- package/dist/lib/runtime/agent-runtime.js +56 -0
- package/dist/lib/sandbox/commands/create.js +389 -311
- package/dist/lib/sandbox/control/container-identity.js +1 -1
- package/dist/lib/sandbox/control/executor.js +10 -2
- package/dist/lib/sandbox/control/lifecycle.js +392 -72
- package/dist/lib/sandbox/control/state.js +27 -11
- package/dist/lib/sandbox/engine.js +14 -7
- package/dist/lib/sandbox/mounts.js +20 -5
- package/dist/lib/sandbox/recovery.js +78 -10
- package/dist/lib/sandbox/workspace-view.js +62 -40
- package/dist/lib/server/process-state.js +51 -1
- package/dist/lib/task/activity-intent.js +4 -1
- package/dist/lib/task/decision-intents.js +4 -2
- package/dist/lib/task/delegation-receipts.js +37 -16
- package/dist/lib/task/events.js +9 -5
- package/dist/lib/task/guard-override.js +31 -0
- package/dist/lib/task/human-override.js +1240 -0
- package/dist/lib/task/ledger-intents.js +6 -3
- package/dist/lib/task/lifecycle.js +69 -14
- package/dist/lib/task/orchestration.js +60 -6
- package/dist/lib/task/review-finalization.js +3 -2
- package/dist/lib/task/verification-engine.js +40 -14
- package/dist/lib/task/verification.js +13 -1
- package/dist/lib/task/workflow-warning-intents.js +4 -2
- package/dist/lib/task/write.js +2 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/claude-code.ts +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +2 -1
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +2 -1
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +29 -2
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
- package/lib/decide.ts +59 -14
- package/lib/init.ts +1 -1
- package/lib/internal/release-workflow.ts +1 -3
- package/lib/internal/task-activity.ts +46 -6
- package/lib/internal/task-event.ts +53 -4
- package/lib/internal/task-ledger.ts +48 -4
- package/lib/internal/task-lifecycle.ts +56 -12
- package/lib/internal/task-orchestration.ts +12 -4
- package/lib/internal/task-override.ts +138 -0
- package/lib/internal/task-review.ts +51 -6
- package/lib/internal/task-verify.ts +52 -3
- package/lib/internal/task-warning.ts +44 -3
- package/lib/platform/pr-summary.ts +10 -3
- package/lib/runtime/agent-runtime.ts +76 -0
- package/lib/sandbox/commands/create.ts +88 -4
- package/lib/sandbox/config.ts +2 -1
- package/lib/sandbox/control/container-identity.ts +3 -3
- package/lib/sandbox/control/executor.ts +10 -2
- package/lib/sandbox/control/lifecycle.ts +463 -76
- package/lib/sandbox/control/protocol.ts +5 -2
- package/lib/sandbox/control/state.ts +35 -18
- package/lib/sandbox/engine.ts +20 -9
- package/lib/sandbox/mounts.ts +23 -5
- package/lib/sandbox/recovery.ts +90 -9
- package/lib/sandbox/workspace-view.ts +70 -40
- package/lib/server/process-state.ts +48 -1
- package/lib/task/activity-intent.ts +4 -2
- package/lib/task/decision-intents.ts +4 -2
- package/lib/task/delegation-receipts.ts +46 -18
- package/lib/task/events.ts +10 -5
- package/lib/task/guard-override.ts +50 -0
- package/lib/task/human-override.ts +1503 -0
- package/lib/task/ledger-intents.ts +6 -3
- package/lib/task/lifecycle.ts +71 -14
- package/lib/task/orchestration.ts +89 -7
- package/lib/task/review-finalization.ts +6 -2
- package/lib/task/verification-engine.ts +42 -16
- package/lib/task/verification.ts +15 -1
- package/lib/task/workflow-warning-intents.ts +4 -2
- package/lib/task/write.ts +4 -1
- package/package.json +1 -1
- package/templates/.agents/hooks/lifecycle-delegation.js +8 -3
- package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -2
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
|
@@ -12,6 +12,7 @@ import type { LedgerRow, ReviewStage } from './ledger.ts';
|
|
|
12
12
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
13
13
|
import { writeTask } from './write.ts';
|
|
14
14
|
import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
|
|
15
|
+
import { allowsManualOverride } from './guard-override.ts';
|
|
15
16
|
|
|
16
17
|
type ReviewSeverity = 'blocker' | 'major' | 'minor';
|
|
17
18
|
type ExecutorResponse = 'accepted' | 'adjusted' | 'refuted' | 'cannot-judge';
|
|
@@ -120,7 +121,8 @@ function mapWrite(intent: LedgerIntent, entityId: string, before: LedgerRow | nu
|
|
|
120
121
|
function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {}): LedgerIntentResult {
|
|
121
122
|
const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
|
|
122
123
|
if (!resolved.ok) return failed(intent, resolved.code, resolved.message, resolved.taskId);
|
|
123
|
-
|
|
124
|
+
const stateOverride = allowsManualOverride(options.manualOverride, 'ledger-intent', 'TASK_STATE_MISMATCH');
|
|
125
|
+
if (resolved.state !== 'active' && !stateOverride) return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
124
126
|
let content: string;
|
|
125
127
|
let rows: LedgerRow[];
|
|
126
128
|
try {
|
|
@@ -196,7 +198,8 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
|
|
|
196
198
|
? new Set(['confirmed', 'open', 'needs-human-decision'])
|
|
197
199
|
: before.status === 'cannot-judge' ? new Set(['open', 'needs-human-decision']) : new Set<string>();
|
|
198
200
|
const sameRoundMinorClose = before.status === 'open' && before.severity === 'minor' && intent.status === 'closed';
|
|
199
|
-
if ((!sameRoundMinorClose && !allowed.has(intent.status)) || (intent.status === 'open' && Number(before.round) >= maxHandshakeRounds(resolved.repoRoot)))
|
|
201
|
+
if (((!sameRoundMinorClose && !allowed.has(intent.status)) || (intent.status === 'open' && Number(before.round) >= maxHandshakeRounds(resolved.repoRoot)))
|
|
202
|
+
&& !allowsManualOverride(options.manualOverride, 'ledger-intent', 'LEDGER_TRANSITION_INVALID')) {
|
|
200
203
|
return failed(intent, 'LEDGER_TRANSITION_INVALID', `finding '${intent.id}' cannot transition from ${before.status} to ${intent.status}`, resolved.taskId, intent.id);
|
|
201
204
|
}
|
|
202
205
|
after = { ...before, status: intent.status, evidence };
|
|
@@ -230,7 +233,7 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
|
|
|
230
233
|
}
|
|
231
234
|
|
|
232
235
|
const result = writeTask({
|
|
233
|
-
taskRef: intent.taskRef, expectedState: 'active',
|
|
236
|
+
taskRef: intent.taskRef, expectedState: stateOverride ? resolved.state : 'active',
|
|
234
237
|
mutations: [...ledgerSectionMutation(content), rowMutation(after), ...(implementationMutation ? [implementationMutation] : [])],
|
|
235
238
|
dryRun: 'dryRun' in intent ? intent.dryRun : false
|
|
236
239
|
}, { ...options, taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state } });
|
package/lib/task/lifecycle.ts
CHANGED
|
@@ -16,6 +16,33 @@ import type { TaskFileSystem, TaskMutation, TaskOperationSummary, TaskWriteMetad
|
|
|
16
16
|
const lifecycleIntentCatalog = [
|
|
17
17
|
'block', 'activate', 'cancel', 'complete', 'close-codescan', 'close-dependabot', 'restore'
|
|
18
18
|
] as const;
|
|
19
|
+
const lifecycleFailureCatalog = [
|
|
20
|
+
'LIFECYCLE_DIRECTORY_RENAME_FAILED',
|
|
21
|
+
'LIFECYCLE_DOCUMENT_INVALID',
|
|
22
|
+
'LIFECYCLE_FINAL_STATE_INVALID',
|
|
23
|
+
'LIFECYCLE_IDENTITY_CONFLICT',
|
|
24
|
+
'LIFECYCLE_IDENTITY_INVALID',
|
|
25
|
+
'LIFECYCLE_INTENT_CONFLICT',
|
|
26
|
+
'LIFECYCLE_JOURNAL_CLEANUP_FAILED',
|
|
27
|
+
'LIFECYCLE_JOURNAL_INVALID',
|
|
28
|
+
'LIFECYCLE_JOURNAL_WRITE_FAILED',
|
|
29
|
+
'LIFECYCLE_LOG_MISSING',
|
|
30
|
+
'LIFECYCLE_METADATA_FAILED',
|
|
31
|
+
'LIFECYCLE_PAYLOAD_INVALID',
|
|
32
|
+
'LIFECYCLE_SHORT_ID_FAILED',
|
|
33
|
+
'LIFECYCLE_SHORT_ID_PRECONDITION',
|
|
34
|
+
'LIFECYCLE_SOURCE_INVALID',
|
|
35
|
+
'LIFECYCLE_STAGING_IDENTITY_INVALID',
|
|
36
|
+
'LIFECYCLE_STAGING_INVALID',
|
|
37
|
+
'LIFECYCLE_TARGET_CONFLICT',
|
|
38
|
+
'LIFECYCLE_TASK_NOT_FOUND',
|
|
39
|
+
'SHORT_ID_CAPACITY_EXCEEDED'
|
|
40
|
+
] as const;
|
|
41
|
+
const lifecycleProducerCatalog = lifecycleFailureCatalog.map((code) => ({
|
|
42
|
+
producerId: 'lifecycle.apply' as const,
|
|
43
|
+
guardId: 'G-03' as const,
|
|
44
|
+
code
|
|
45
|
+
}));
|
|
19
46
|
type TaskLifecycleIntent = (typeof lifecycleIntentCatalog)[number];
|
|
20
47
|
type TaskLifecycleRequest =
|
|
21
48
|
| { taskRef: string; intent: 'block'; agent: string; reason: string; unblockCondition: string; dryRun?: boolean }
|
|
@@ -39,6 +66,7 @@ type LifecycleJournal = {
|
|
|
39
66
|
targetState: HotState;
|
|
40
67
|
metadata: TaskWriteMetadata;
|
|
41
68
|
completedSteps: LifecycleStep[];
|
|
69
|
+
failure?: LifecycleError;
|
|
42
70
|
};
|
|
43
71
|
type TaskLifecycleResult = {
|
|
44
72
|
status: 'planned' | 'applied' | 'no-op' | 'failed';
|
|
@@ -72,8 +100,13 @@ type TaskLifecycleOptions = {
|
|
|
72
100
|
fileSystem?: Partial<LifecycleFileSystem>;
|
|
73
101
|
taskFileSystem?: Partial<TaskFileSystem>;
|
|
74
102
|
directoryRenameSync?: (source: string, target: string) => void;
|
|
103
|
+
manualOverride?: Readonly<{ failureId: string; operator: string; reason: string }>;
|
|
75
104
|
};
|
|
76
105
|
|
|
106
|
+
function allowsManualOverride(options: TaskLifecycleOptions, code: string): boolean {
|
|
107
|
+
return options.manualOverride?.failureId === `lifecycle.apply:${code}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
77
110
|
const STEPS: readonly LifecycleStep[] = ['task-written', 'directory-moved', 'registry-committed'];
|
|
78
111
|
const DEFAULT_IO: LifecycleFileSystem = {
|
|
79
112
|
renameSync: (source, target) => fs.renameSync(source, target),
|
|
@@ -172,10 +205,16 @@ function matchingCompletion(content: string, request: TaskLifecycleRequest, rest
|
|
|
172
205
|
}
|
|
173
206
|
|
|
174
207
|
function validateRestoreStaging(request: Extract<TaskLifecycleRequest, { intent: 'restore' }>, repoRoot: string, taskId: string): LifecycleError | { stagingDir: string; fileCount: number } {
|
|
175
|
-
|
|
176
|
-
|
|
208
|
+
let workspace: string;
|
|
209
|
+
let stagingDir: string;
|
|
210
|
+
try {
|
|
211
|
+
workspace = fs.realpathSync.native(path.resolve(repoRoot, '.agents', 'workspace'));
|
|
212
|
+
stagingDir = fs.realpathSync.native(path.resolve(request.stagingDir));
|
|
213
|
+
} catch (error) {
|
|
214
|
+
return { code: 'LIFECYCLE_STAGING_INVALID', message: String(error) };
|
|
215
|
+
}
|
|
177
216
|
const relative = path.relative(workspace, stagingDir);
|
|
178
|
-
if (!relative || relative.startsWith(
|
|
217
|
+
if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative) || path.basename(stagingDir) === taskId) {
|
|
179
218
|
return { code: 'LIFECYCLE_STAGING_INVALID', message: 'restore staging must be a non-final directory inside .agents/workspace' };
|
|
180
219
|
}
|
|
181
220
|
let entries: fs.Dirent[];
|
|
@@ -213,8 +252,8 @@ function validateRestoreStaging(request: Extract<TaskLifecycleRequest, { intent:
|
|
|
213
252
|
return { stagingDir, fileCount: entries.filter((entry) => !entry.name.startsWith('.')).length };
|
|
214
253
|
}
|
|
215
254
|
|
|
216
|
-
function mutationsFor(request: TaskLifecycleRequest, content: string, metadata: TaskWriteMetadata, restoredFiles: number): TaskMutation[] | LifecycleError {
|
|
217
|
-
const section = locateActivityLog(content);
|
|
255
|
+
function mutationsFor(request: TaskLifecycleRequest, content: string, metadata: TaskWriteMetadata, restoredFiles: number, allowMissingActivityLog = false): TaskMutation[] | LifecycleError {
|
|
256
|
+
const section = locateActivityLog(content) ?? (allowMissingActivityLog ? { heading: 'Activity Log', body: '', entries: [] } : null);
|
|
218
257
|
if (!section) return { code: 'LIFECYCLE_LOG_MISSING', message: 'task has no unique Activity Log section' };
|
|
219
258
|
const identity = actionAndNote(request, restoredFiles);
|
|
220
259
|
let body = appendActivityEntry(section, { time: metadata.timestamp, step: `${identity.action} [started]`, agent: request.agent, note: 'started' });
|
|
@@ -261,6 +300,14 @@ function writeJournal(file: string, journal: LifecycleJournal, io: LifecycleFile
|
|
|
261
300
|
}
|
|
262
301
|
}
|
|
263
302
|
|
|
303
|
+
function rememberJournalFailure(file: string, journal: LifecycleJournal, io: LifecycleFileSystem, failure: LifecycleError): void {
|
|
304
|
+
try {
|
|
305
|
+
writeJournal(file, { ...journal, failure }, io);
|
|
306
|
+
} catch {
|
|
307
|
+
// Preserve the original lifecycle failure when the diagnostic receipt cannot be updated.
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
264
311
|
function parseJournal(content: string): LifecycleJournal {
|
|
265
312
|
const value = JSON.parse(content) as Partial<LifecycleJournal>;
|
|
266
313
|
if (
|
|
@@ -326,7 +373,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
326
373
|
}
|
|
327
374
|
return failed(request, { code: 'LIFECYCLE_INTENT_CONFLICT', message: `task ${taskId} is already ${spec.target} with a different lifecycle intent` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
328
375
|
}
|
|
329
|
-
if (!spec.sources.includes(sourceState) && !existingJournal) {
|
|
376
|
+
if (!spec.sources.includes(sourceState) && !existingJournal && !allowsManualOverride(options, 'LIFECYCLE_SOURCE_INVALID')) {
|
|
330
377
|
return failed(request, { code: 'LIFECYCLE_SOURCE_INVALID', message: `${request.intent} is not allowed from ${sourceState}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
331
378
|
}
|
|
332
379
|
if (sourcePath !== targetPath && fs.existsSync(targetPath)) {
|
|
@@ -337,7 +384,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
337
384
|
try { frontmatter = parseTypedTaskFrontmatter(content); }
|
|
338
385
|
catch (error) { return failed(request, { code: 'LIFECYCLE_DOCUMENT_INVALID', message: error instanceof Error ? error.message : String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath }); }
|
|
339
386
|
if (frontmatter.id !== taskId) return failed(request, { code: 'LIFECYCLE_IDENTITY_INVALID', message: 'task.md id does not match its lifecycle task id' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
340
|
-
if (!locateActivityLog(content)) return failed(request, { code: 'LIFECYCLE_LOG_MISSING', message: 'task has no unique Activity Log section' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
387
|
+
if (!locateActivityLog(content) && !allowsManualOverride(options, 'LIFECYCLE_LOG_MISSING')) return failed(request, { code: 'LIFECYCLE_LOG_MISSING', message: 'task has no unique Activity Log section' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
341
388
|
const registryRequest = executeShortIdCommand({
|
|
342
389
|
operation: 'list', activeDir: path.join(repoRoot, '.agents', 'workspace', 'active'),
|
|
343
390
|
shortIdLength: configuredShortIdLength(repoRoot)
|
|
@@ -347,10 +394,10 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
347
394
|
}
|
|
348
395
|
const registry = JSON.parse(registryRequest.output || '{"version":1,"ids":{}}') as { ids: Record<string, string> };
|
|
349
396
|
const registeredKey = Object.entries(registry.ids).find(([, candidate]) => candidate === taskId)?.[0];
|
|
350
|
-
if (spec.registry === 'release' && sourceState === 'active' && !registeredKey) {
|
|
397
|
+
if (spec.registry === 'release' && sourceState === 'active' && !registeredKey && !allowsManualOverride(options, 'LIFECYCLE_SHORT_ID_PRECONDITION')) {
|
|
351
398
|
return failed(request, { code: 'LIFECYCLE_SHORT_ID_PRECONDITION', message: 'active source has no short-id registry entry' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
352
399
|
}
|
|
353
|
-
if (spec.registry === 'alloc' && !registeredKey && Object.keys(registry.ids).length >= 10 ** configuredShortIdLength(repoRoot) - 1) {
|
|
400
|
+
if (spec.registry === 'alloc' && !registeredKey && Object.keys(registry.ids).length >= 10 ** configuredShortIdLength(repoRoot) - 1 && !allowsManualOverride(options, 'SHORT_ID_CAPACITY_EXCEEDED')) {
|
|
354
401
|
return failed(request, { code: 'SHORT_ID_CAPACITY_EXCEEDED', message: 'short-id registry has no free slot' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
355
402
|
}
|
|
356
403
|
const digest = intentIdentity(request, taskId);
|
|
@@ -360,14 +407,20 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
360
407
|
if (fs.existsSync(initialJournalPath)) {
|
|
361
408
|
try { journal = parseJournal(io.readFileSync(initialJournalPath)); }
|
|
362
409
|
catch (error) { return failed(request, { code: 'LIFECYCLE_JOURNAL_INVALID', message: String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath }); }
|
|
363
|
-
if (journal.
|
|
410
|
+
if (journal.taskId !== taskId || journal.intent !== request.intent) return failed(request, { code: 'LIFECYCLE_INTENT_CONFLICT', message: 'an in-progress lifecycle journal belongs to a different request' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
|
|
411
|
+
if (request.dryRun) {
|
|
412
|
+
if (!journal.failure) return failed(request, { code: 'LIFECYCLE_JOURNAL_INVALID', message: 'existing lifecycle journal has no durable failure evidence for a read-only probe' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
|
|
413
|
+
return failed(request, journal.failure, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath, completedSteps: journal.completedSteps, pendingSteps: STEPS.filter((step) => !journal.completedSteps.includes(step)) });
|
|
414
|
+
}
|
|
415
|
+
const overrideResumesRecordedFailure = journal.failure && allowsManualOverride(options, journal.failure.code);
|
|
416
|
+
if (journal.intentDigest !== digest && !overrideResumesRecordedFailure) return failed(request, { code: 'LIFECYCLE_INTENT_CONFLICT', message: 'an in-progress lifecycle journal belongs to a different request' }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath: initialJournalPath });
|
|
364
417
|
} else {
|
|
365
418
|
let metadata: TaskWriteMetadata;
|
|
366
419
|
try { metadata = (options.metadataProvider ?? captureTaskWriteMetadata)(); }
|
|
367
420
|
catch (error) { return failed(request, { code: 'LIFECYCLE_METADATA_FAILED', message: String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath }); }
|
|
368
421
|
journal = { version: 1, taskId, intent: request.intent, intentDigest: digest, sourceState, targetState: spec.target, metadata, completedSteps: [] };
|
|
369
422
|
if (request.dryRun) {
|
|
370
|
-
const plannedMutations = mutationsFor(request, content, metadata, restoredFiles);
|
|
423
|
+
const plannedMutations = mutationsFor(request, content, metadata, restoredFiles, allowsManualOverride(options, 'LIFECYCLE_LOG_MISSING'));
|
|
371
424
|
if ('code' in plannedMutations) return failed(request, plannedMutations, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath });
|
|
372
425
|
const taskPlan = writeTask(
|
|
373
426
|
{ taskRef: taskId, expectedState: sourceState === 'blocked' ? 'blocked' : 'active', mutations: plannedMutations, dryRun: true },
|
|
@@ -403,7 +456,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
403
456
|
if (matchingCompletion(io.readFileSync(path.join(sourcePath, 'task.md')), request, restoredFiles)) completed.add('task-written');
|
|
404
457
|
if (!completed.has('task-written')) {
|
|
405
458
|
const current = io.readFileSync(path.join(sourcePath, 'task.md'));
|
|
406
|
-
const mutations = mutationsFor(request, current, journal.metadata, restoredFiles);
|
|
459
|
+
const mutations = mutationsFor(request, current, journal.metadata, restoredFiles, allowsManualOverride(options, 'LIFECYCLE_LOG_MISSING'));
|
|
407
460
|
if ('code' in mutations) return failed(request, mutations, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, completedSteps: [...completed], changed: completed.size > 0 });
|
|
408
461
|
const writeResult = writeTask(
|
|
409
462
|
{ taskRef: taskId, expectedState: sourceState === 'blocked' ? 'blocked' : 'active', mutations },
|
|
@@ -424,7 +477,11 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
424
477
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
425
478
|
if (fs.existsSync(targetPath)) return failed(request, { code: 'LIFECYCLE_TARGET_CONFLICT', message: `target directory already exists: ${targetPath}` }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion });
|
|
426
479
|
try { (options.directoryRenameSync ?? io.renameSync)(sourcePath, targetPath); }
|
|
427
|
-
catch (error) {
|
|
480
|
+
catch (error) {
|
|
481
|
+
const failure = { code: 'LIFECYCLE_DIRECTORY_RENAME_FAILED', message: String(error) } as const;
|
|
482
|
+
rememberJournalFailure(journalPath, journal, io, failure);
|
|
483
|
+
return failed(request, failure, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion });
|
|
484
|
+
}
|
|
428
485
|
completed.add('directory-moved'); journal.completedSteps = [...completed]; journalPath = path.join(targetPath, '.task-lifecycle.json');
|
|
429
486
|
try { writeJournal(journalPath, journal, io); }
|
|
430
487
|
catch (error) { return failed(request, { code: 'LIFECYCLE_JOURNAL_WRITE_FAILED', message: String(error) }, { taskId, sourceState, targetState: spec.target, sourcePath, targetPath, journalPath, task: { operations: taskOperations }, directory: { effect: 'move', changed: true }, changed: true, completedSteps: [...completed], timestamp: journal.metadata.timestamp, agentInfraVersion: journal.metadata.agentInfraVersion }); }
|
|
@@ -451,7 +508,7 @@ function applyTaskLifecycle(requestInput: TaskLifecycleRequest, options: TaskLif
|
|
|
451
508
|
};
|
|
452
509
|
}
|
|
453
510
|
|
|
454
|
-
export { lifecycleIntentCatalog, applyTaskLifecycle };
|
|
511
|
+
export { lifecycleIntentCatalog, lifecycleFailureCatalog, lifecycleProducerCatalog, applyTaskLifecycle };
|
|
455
512
|
export type {
|
|
456
513
|
TaskLifecycleIntent, TaskLifecycleRequest, TaskLifecycleResult, TaskLifecycleOptions,
|
|
457
514
|
LifecycleError, LifecycleJournal, LifecycleStep
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
completeDelegationStage,
|
|
15
15
|
consumeDelegation,
|
|
16
16
|
dispatchDelegation,
|
|
17
|
+
foldBlankToNull,
|
|
17
18
|
managedDelegationRole,
|
|
18
19
|
prepareDelegation,
|
|
19
20
|
sealDelegation
|
|
@@ -25,6 +26,7 @@ import type {
|
|
|
25
26
|
DelegationStage
|
|
26
27
|
} from './delegation-receipts.ts';
|
|
27
28
|
import { normalizeAgentClients } from '../agent-clients/config.ts';
|
|
29
|
+
import { resolveAgentRuntimeStoreRoot } from '../runtime/agent-runtime.ts';
|
|
28
30
|
import { inspectPlatformPullRequest } from '../platform/pull-requests.ts';
|
|
29
31
|
import {
|
|
30
32
|
getAgentClientCapability,
|
|
@@ -112,6 +114,25 @@ type ClientCapabilityRecovery = Readonly<{
|
|
|
112
114
|
}>;
|
|
113
115
|
resultingStatus: 'running';
|
|
114
116
|
}>;
|
|
117
|
+
type ClaudeCodeCapabilityRecovery = Readonly<{
|
|
118
|
+
code: 'CLIENT_CAPABILITY_ENABLED_NO_MIGRATION';
|
|
119
|
+
recoveredAt: string;
|
|
120
|
+
previousSchemaVersion: 3;
|
|
121
|
+
previousStatus: 'paused';
|
|
122
|
+
previousPause: Readonly<{ code: 'ORCHESTRATION_CLIENT_UNSUPPORTED'; message: string; recoverable: boolean }>;
|
|
123
|
+
client: 'claude-code';
|
|
124
|
+
guards: Readonly<{
|
|
125
|
+
stepCount: 0;
|
|
126
|
+
nextStage: null;
|
|
127
|
+
baselineEmpty: true;
|
|
128
|
+
receiptCount: 0;
|
|
129
|
+
pendingDelegation: false;
|
|
130
|
+
commitAuthorizationUnused: true;
|
|
131
|
+
completionEvidenceAbsent: true;
|
|
132
|
+
commitIntentAbsent: true;
|
|
133
|
+
}>;
|
|
134
|
+
resultingStatus: 'running';
|
|
135
|
+
}>;
|
|
115
136
|
type SchemaMigrationRecovery = Readonly<{
|
|
116
137
|
code: 'SCHEMA_V3_MIGRATED';
|
|
117
138
|
recoveredAt: string;
|
|
@@ -130,7 +151,7 @@ type SchemaMigrationRecovery = Readonly<{
|
|
|
130
151
|
}>;
|
|
131
152
|
resultingStatus: 'running';
|
|
132
153
|
}>;
|
|
133
|
-
type OrchestrationRecovery = ModelPolicyRecovery | ClientCapabilityRecovery | SchemaMigrationRecovery;
|
|
154
|
+
type OrchestrationRecovery = ModelPolicyRecovery | ClientCapabilityRecovery | ClaudeCodeCapabilityRecovery | SchemaMigrationRecovery;
|
|
134
155
|
type CleanCompletionEvidence = Readonly<{
|
|
135
156
|
kind: 'reviewed-head-clean';
|
|
136
157
|
observedAt: string;
|
|
@@ -435,6 +456,23 @@ function canRecoverCodexUnsupportedPause(run: OrchestrationRun, taskDir: string)
|
|
|
435
456
|
&& (run.recoveryHistory ?? []).every((entry) => entry.code === 'MODEL_POLICY_SUPPLEMENTED');
|
|
436
457
|
}
|
|
437
458
|
|
|
459
|
+
function canRecoverClaudeCodeUnsupportedPause(run: OrchestrationRun, taskDir: string): boolean {
|
|
460
|
+
return run.schemaVersion === 3
|
|
461
|
+
&& run.status === 'paused'
|
|
462
|
+
&& run.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
|
|
463
|
+
&& run.modelPolicySource?.client === 'claude-code'
|
|
464
|
+
&& run.stepCount === 0
|
|
465
|
+
&& run.nextStage === null
|
|
466
|
+
&& run.baseline === ''
|
|
467
|
+
&& run.pendingDelegation === null
|
|
468
|
+
&& run.receipts.length === 0
|
|
469
|
+
&& run.commitAuthorization?.issuedAt === null
|
|
470
|
+
&& run.commitAuthorization?.consumedAt === null
|
|
471
|
+
&& run.completionEvidence == null
|
|
472
|
+
&& !fs.existsSync(commitIntentPath(taskDir))
|
|
473
|
+
&& (run.recoveryHistory ?? []).length === 0;
|
|
474
|
+
}
|
|
475
|
+
|
|
438
476
|
function canMigrateRecoverableV2Pause(run: OrchestrationRun, taskDir: string): boolean {
|
|
439
477
|
return run.schemaVersion === 2
|
|
440
478
|
&& run.status === 'paused'
|
|
@@ -504,6 +542,39 @@ function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptio
|
|
|
504
542
|
}
|
|
505
543
|
}
|
|
506
544
|
if (existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED') {
|
|
545
|
+
if (existing.modelPolicySource.client === 'claude-code') {
|
|
546
|
+
if (!canRecoverClaudeCodeUnsupportedPause(existing, resolved.taskDir)) {
|
|
547
|
+
return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
548
|
+
}
|
|
549
|
+
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
550
|
+
const previousPause = existing.pause as ClaudeCodeCapabilityRecovery['previousPause'];
|
|
551
|
+
const recovery: ClaudeCodeCapabilityRecovery = {
|
|
552
|
+
code: 'CLIENT_CAPABILITY_ENABLED_NO_MIGRATION',
|
|
553
|
+
recoveredAt: now,
|
|
554
|
+
previousSchemaVersion: 3,
|
|
555
|
+
previousStatus: 'paused',
|
|
556
|
+
previousPause,
|
|
557
|
+
client: 'claude-code',
|
|
558
|
+
guards: {
|
|
559
|
+
stepCount: 0,
|
|
560
|
+
nextStage: null,
|
|
561
|
+
baselineEmpty: true,
|
|
562
|
+
receiptCount: 0,
|
|
563
|
+
pendingDelegation: false,
|
|
564
|
+
commitAuthorizationUnused: true,
|
|
565
|
+
completionEvidenceAbsent: true,
|
|
566
|
+
commitIntentAbsent: true
|
|
567
|
+
},
|
|
568
|
+
resultingStatus: 'running'
|
|
569
|
+
};
|
|
570
|
+
const resumed = withUpdatedRun(existing, {
|
|
571
|
+
status: 'running',
|
|
572
|
+
pause: null,
|
|
573
|
+
recoveryHistory: Object.freeze([...(existing.recoveryHistory ?? []), recovery])
|
|
574
|
+
}, () => now);
|
|
575
|
+
saveRun(resolved.taskDir, resumed);
|
|
576
|
+
return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
|
|
577
|
+
}
|
|
507
578
|
if (!canRecoverCodexUnsupportedPause(existing, resolved.taskDir)) {
|
|
508
579
|
return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
509
580
|
}
|
|
@@ -1667,9 +1738,9 @@ function activateMatchingOrchestrationDelegation(
|
|
|
1667
1738
|
if ('status' in replay) return matched;
|
|
1668
1739
|
const receipt = replay.run.pendingDelegation!;
|
|
1669
1740
|
const sameEvidence = receipt.parentId === event.parentId
|
|
1670
|
-
&& receipt.spawnMode === event.spawnMode
|
|
1671
|
-
&& receipt.actualModel === event.actualModel
|
|
1672
|
-
&& receipt.actualReasoningEffort === event.actualReasoningEffort
|
|
1741
|
+
&& (receipt.spawnMode ?? null) === (event.spawnMode ?? null)
|
|
1742
|
+
&& (receipt.actualModel ?? null) === foldBlankToNull(event.actualModel)
|
|
1743
|
+
&& (receipt.actualReasoningEffort ?? null) === foldBlankToNull(event.actualReasoningEffort)
|
|
1673
1744
|
&& receipt.modelFallbackReason === (event.modelFallbackReason ?? null)
|
|
1674
1745
|
&& receipt.reasoningEffortFallbackReason === (event.reasoningEffortFallbackReason ?? null)
|
|
1675
1746
|
&& receipt.hostEvidence?.hookDefinitionHash === event.hostEvidence?.hookDefinitionHash;
|
|
@@ -1697,7 +1768,14 @@ function pauseMatchingOrchestrationDelegation(
|
|
|
1697
1768
|
|
|
1698
1769
|
function sealMatchingOrchestrationDelegation(
|
|
1699
1770
|
client: AgentClientId,
|
|
1700
|
-
event: Readonly<{
|
|
1771
|
+
event: Readonly<{
|
|
1772
|
+
nativeAgent: string;
|
|
1773
|
+
childId: string;
|
|
1774
|
+
actualModel?: string;
|
|
1775
|
+
actualReasoningEffort?: string;
|
|
1776
|
+
modelFallbackReason?: string;
|
|
1777
|
+
reasoningEffortFallbackReason?: string;
|
|
1778
|
+
}>,
|
|
1701
1779
|
options: OrchestrationOptions = {}
|
|
1702
1780
|
): OrchestrationResult {
|
|
1703
1781
|
const role = managedDelegationRole(event.nativeAgent);
|
|
@@ -1726,7 +1804,11 @@ function sealMatchingOrchestrationDelegation(
|
|
|
1726
1804
|
childId: event.childId,
|
|
1727
1805
|
exitCode: 0,
|
|
1728
1806
|
afterFingerprint,
|
|
1729
|
-
changedPaths
|
|
1807
|
+
changedPaths,
|
|
1808
|
+
actualModel: event.actualModel,
|
|
1809
|
+
actualReasoningEffort: event.actualReasoningEffort,
|
|
1810
|
+
modelFallbackReason: event.modelFallbackReason,
|
|
1811
|
+
reasoningEffortFallbackReason: event.reasoningEffortFallbackReason
|
|
1730
1812
|
}, options);
|
|
1731
1813
|
} catch (error) {
|
|
1732
1814
|
return pauseOrchestration(
|
|
@@ -1955,7 +2037,7 @@ function recoverPreparedOrchestrationDelegation(
|
|
|
1955
2037
|
receipt.client === 'codex'
|
|
1956
2038
|
&& receipt.lifecycleProvenance
|
|
1957
2039
|
&& hasActiveCodexLifecycleEvidence(
|
|
1958
|
-
|
|
2040
|
+
resolveAgentRuntimeStoreRoot({ repoRoot: resolved.repoRoot, store: 'lifecycle' }),
|
|
1959
2041
|
{
|
|
1960
2042
|
nativeAgent: `agent-infra-lifecycle-${receipt.role}`,
|
|
1961
2043
|
hookDefinitionHash: receipt.lifecycleProvenance.hookDefinitionHash
|
|
@@ -11,6 +11,8 @@ import { resolveTaskRef } from './resolve-ref.ts';
|
|
|
11
11
|
import { validateLifecycleExecution } from './lifecycle-execution.ts';
|
|
12
12
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
13
13
|
import type { ResolveTaskRefErrorCode } from './resolve-ref.ts';
|
|
14
|
+
import { allowsManualOverride } from './guard-override.ts';
|
|
15
|
+
import type { ManualOverrideCapability } from './guard-override.ts';
|
|
14
16
|
|
|
15
17
|
type ReviewFinalizationErrorCode =
|
|
16
18
|
| ResolveTaskRefErrorCode
|
|
@@ -62,6 +64,8 @@ type ReviewFinalizationOptions = {
|
|
|
62
64
|
repoRoot?: string;
|
|
63
65
|
randomSuffix?: () => string;
|
|
64
66
|
fileSystem?: Partial<ReviewFileSystem>;
|
|
67
|
+
manualOverride?: ManualOverrideCapability;
|
|
68
|
+
lockAlreadyHeld?: boolean;
|
|
65
69
|
};
|
|
66
70
|
|
|
67
71
|
const STAGES: Record<ReviewStage, { family: 'review-analysis' | 'review-plan' | 'review-code'; action: string }> = {
|
|
@@ -127,7 +131,7 @@ function finalizeReviewSummaryUnlocked(
|
|
|
127
131
|
}
|
|
128
132
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
129
133
|
if (!resolved.ok) return failed(request, resolved.code, resolved.message, resolved.taskId);
|
|
130
|
-
if (resolved.state !== 'active') {
|
|
134
|
+
if (resolved.state !== 'active' && !allowsManualOverride(options.manualOverride, 'review-finalization', 'TASK_STATE_MISMATCH')) {
|
|
131
135
|
return failed(request, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
132
136
|
}
|
|
133
137
|
const parsedArtifact = parseArtifactName(request.artifact);
|
|
@@ -275,7 +279,7 @@ function finalizeReviewSummary(
|
|
|
275
279
|
request: ReviewFinalizationRequest,
|
|
276
280
|
options: ReviewFinalizationOptions = {}
|
|
277
281
|
): ReviewFinalizationResult {
|
|
278
|
-
if (request.dryRun) return finalizeReviewSummaryUnlocked(request, options);
|
|
282
|
+
if (request.dryRun || options.lockAlreadyHeld) return finalizeReviewSummaryUnlocked(request, options);
|
|
279
283
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
280
284
|
if (!resolved.ok) return finalizeReviewSummaryUnlocked(request, options);
|
|
281
285
|
try {
|
|
@@ -291,7 +291,22 @@ function checkOrchestrationEvidence({ taskDir }: any): any {
|
|
|
291
291
|
&& guards?.commitAuthorizationUnused === true
|
|
292
292
|
&& guards?.completionEvidenceAbsent === true
|
|
293
293
|
&& guards?.commitIntentAbsent === true;
|
|
294
|
-
|
|
294
|
+
const validClaudeCodeRecovery = recovery.code === 'CLIENT_CAPABILITY_ENABLED_NO_MIGRATION'
|
|
295
|
+
&& recovery.previousSchemaVersion === 3
|
|
296
|
+
&& recovery.previousStatus === 'paused'
|
|
297
|
+
&& recovery.previousPause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
|
|
298
|
+
&& recovery.client === 'claude-code'
|
|
299
|
+
&& recovery.resultingStatus === 'running'
|
|
300
|
+
&& exactText(recovery.recoveredAt)
|
|
301
|
+
&& guards?.stepCount === 0
|
|
302
|
+
&& guards?.nextStage === null
|
|
303
|
+
&& guards?.baselineEmpty === true
|
|
304
|
+
&& guards?.receiptCount === 0
|
|
305
|
+
&& guards?.pendingDelegation === false
|
|
306
|
+
&& guards?.commitAuthorizationUnused === true
|
|
307
|
+
&& guards?.completionEvidenceAbsent === true
|
|
308
|
+
&& guards?.commitIntentAbsent === true;
|
|
309
|
+
if (!validModelRecovery && !validCapabilityRecovery && !validClaudeCodeRecovery) {
|
|
295
310
|
return failResult('orchestration-evidence', 'Run recovery history contains invalid provenance');
|
|
296
311
|
}
|
|
297
312
|
}
|
|
@@ -322,29 +337,40 @@ function checkOrchestrationEvidence({ taskDir }: any): any {
|
|
|
322
337
|
}
|
|
323
338
|
const activated = ['activated', 'stage-completed', 'sealed', 'consumed'].includes(receipt.status);
|
|
324
339
|
if (activated) {
|
|
325
|
-
|
|
326
|
-
|
|
340
|
+
const isClaudeCode = receipt.client === 'claude-code';
|
|
341
|
+
if (!isClaudeCode) {
|
|
342
|
+
if (!exactText(receipt.actualModel)) {
|
|
343
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual model`);
|
|
344
|
+
}
|
|
345
|
+
if (!exactText(receipt.actualReasoningEffort)) {
|
|
346
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has no host-observed actual reasoning effort`);
|
|
347
|
+
}
|
|
327
348
|
}
|
|
328
|
-
|
|
329
|
-
|
|
349
|
+
// parentId/childId 校验对所有 client 保持统一(不新增分支);spawnMode 检查按 client 判断
|
|
350
|
+
if (!exactText(receipt.parentId) || !exactText(receipt.childId) || receipt.parentId === receipt.childId) {
|
|
351
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid delegation identity`);
|
|
330
352
|
}
|
|
331
|
-
if (!
|
|
353
|
+
if (!isClaudeCode && receipt.spawnMode !== 'fresh') {
|
|
332
354
|
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid fresh delegation identity`);
|
|
333
355
|
}
|
|
334
|
-
if (receipt.actualModel
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated model fallback reason`);
|
|
356
|
+
if (receipt.actualModel != null && receipt.actualModel !== receipt.requestedModel) {
|
|
357
|
+
if (!isClaudeCode && !exactText(receipt.modelFallbackReason)) {
|
|
358
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' model fallback is not justified`);
|
|
359
|
+
}
|
|
339
360
|
}
|
|
340
361
|
if (
|
|
341
|
-
receipt.
|
|
342
|
-
&&
|
|
362
|
+
(receipt.actualModel == null || receipt.actualModel === receipt.requestedModel)
|
|
363
|
+
&& receipt.modelFallbackReason !== null
|
|
343
364
|
) {
|
|
344
|
-
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}'
|
|
365
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated model fallback reason`);
|
|
366
|
+
}
|
|
367
|
+
if (receipt.actualReasoningEffort != null && receipt.actualReasoningEffort !== receipt.requestedReasoningEffort) {
|
|
368
|
+
if (!isClaudeCode && !exactText(receipt.reasoningEffortFallbackReason)) {
|
|
369
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' reasoning-effort fallback is not justified`);
|
|
370
|
+
}
|
|
345
371
|
}
|
|
346
372
|
if (
|
|
347
|
-
receipt.actualReasoningEffort === receipt.requestedReasoningEffort
|
|
373
|
+
(receipt.actualReasoningEffort == null || receipt.actualReasoningEffort === receipt.requestedReasoningEffort)
|
|
348
374
|
&& receipt.reasoningEffortFallbackReason !== null
|
|
349
375
|
) {
|
|
350
376
|
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated reasoning-effort fallback reason`);
|
|
@@ -902,7 +928,7 @@ function checkReviewLedger({ taskDir, config }: any): any {
|
|
|
902
928
|
}
|
|
903
929
|
|
|
904
930
|
const stageScope = Array.isArray(config.stage_scope) ? config.stage_scope : null;
|
|
905
|
-
const maxRounds = Number(resolveReviewSetting(config, "
|
|
931
|
+
const maxRounds = Number(resolveReviewSetting(config, "maxHandshakeRounds", DEFAULT_MAX_HANDSHAKE_ROUNDS));
|
|
906
932
|
const problems = [];
|
|
907
933
|
let inScopeCount = 0;
|
|
908
934
|
|
package/lib/task/verification.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { parseArtifactName } from './artifact-lifecycle.ts';
|
|
|
7
7
|
import type { ArtifactFamily } from './artifact-lifecycle.ts';
|
|
8
8
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
9
9
|
import type { TaskWorkspaceState } from './resolve-ref.ts';
|
|
10
|
+
import { allowsManualOverride } from './guard-override.ts';
|
|
11
|
+
import type { ManualOverrideCapability } from './guard-override.ts';
|
|
10
12
|
|
|
11
13
|
type VerificationEvent =
|
|
12
14
|
| 'analyze.awaiting-input' | 'analyze.completed'
|
|
@@ -48,6 +50,7 @@ type TaskVerificationResult = {
|
|
|
48
50
|
type VerificationOptions = {
|
|
49
51
|
repoRoot?: string;
|
|
50
52
|
engine?: typeof verifyInProcess;
|
|
53
|
+
manualOverride?: ManualOverrideCapability;
|
|
51
54
|
};
|
|
52
55
|
|
|
53
56
|
const gate = (skill: string, expectedState: VerificationSpec['expectedState'], artifactFamily?: ArtifactFamily): VerificationSpec => ({
|
|
@@ -93,7 +96,7 @@ function verifyTaskEvent(request: { taskRef: string; event: string; artifact?: s
|
|
|
93
96
|
const resolved = resolveTaskRef(request.taskRef, { repoRoot: options.repoRoot });
|
|
94
97
|
if (!resolved.ok) return failure(request, resolved.code, resolved.message, { taskId: resolved.taskId, skill: spec.skill, mode: spec.mode });
|
|
95
98
|
const identity = { taskId: resolved.taskId, taskDir: resolved.taskDir, taskState: resolved.state, skill: spec.skill, mode: spec.mode } as const;
|
|
96
|
-
if (resolved.state !== spec.expectedState) {
|
|
99
|
+
if (resolved.state !== spec.expectedState && !allowsManualOverride(options.manualOverride, 'task-verify', 'VERIFY_TASK_STATE_MISMATCH')) {
|
|
97
100
|
return failure(request, 'VERIFY_TASK_STATE_MISMATCH', `event '${request.event}' requires workspace '${spec.expectedState}', received '${resolved.state}'`, identity);
|
|
98
101
|
}
|
|
99
102
|
if (spec.artifactFamily) {
|
|
@@ -111,11 +114,22 @@ function verifyTaskEvent(request: { taskRef: string; event: string; artifact?: s
|
|
|
111
114
|
if (spec.mode === 'gate') {
|
|
112
115
|
const payload = engine({ mode: 'gate', skillName: spec.skill, taskDir: resolved.taskDir, artifactFile: request.artifact, checks: [], repositoryRoot: resolved.repoRoot }) as Record<string, unknown>;
|
|
113
116
|
const status = payload.gate as 'pass' | 'fail' | 'blocked';
|
|
117
|
+
if (status !== 'pass' && allowsManualOverride(options.manualOverride, 'verification-engine', status === 'blocked' ? 'CHECK_BLOCKED' : 'CHECK_FAILED')) {
|
|
118
|
+
invocations.push({ status: 'pass', exitCode: 0, payload: { ...payload, gate: 'pass', humanOverride: 'human-approved', originalGate: status } });
|
|
119
|
+
return {
|
|
120
|
+
status: 'pass', changed: false, event: request.event, requestRef: request.taskRef,
|
|
121
|
+
...identity, artifact: request.artifact ?? null, invocations, error: null
|
|
122
|
+
};
|
|
123
|
+
}
|
|
114
124
|
invocations.push({ status, exitCode: ({ pass: 0, fail: 1, blocked: 2 } as const)[status], payload });
|
|
115
125
|
} else {
|
|
116
126
|
for (const check of spec.checks ?? []) {
|
|
117
127
|
const payload = engine({ mode: 'checks', skillName: spec.skill, taskDir: resolved.taskDir, artifactFile: request.artifact, checks: [check], repositoryRoot: resolved.repoRoot }) as Record<string, unknown>;
|
|
118
128
|
const status = payload.status as 'pass' | 'fail' | 'blocked';
|
|
129
|
+
if (status !== 'pass' && allowsManualOverride(options.manualOverride, 'verification-engine', status === 'blocked' ? 'CHECK_BLOCKED' : 'CHECK_FAILED')) {
|
|
130
|
+
invocations.push({ status: 'pass', exitCode: 0, payload: { ...payload, status: 'pass', humanOverride: 'human-approved', originalStatus: status } });
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
119
133
|
invocations.push({ status, exitCode: ({ pass: 0, fail: 1, blocked: 2 } as const)[status], payload });
|
|
120
134
|
if (status !== 'pass') break;
|
|
121
135
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import type { WorkflowWarning } from './workflow-warnings.ts';
|
|
12
12
|
import { captureTaskWriteMetadata, writeTask } from './write.ts';
|
|
13
13
|
import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
|
|
14
|
+
import { allowsManualOverride } from './guard-override.ts';
|
|
14
15
|
|
|
15
16
|
type WarningSeverity = 'IMPORTANT' | 'ACTION_REQUIRED';
|
|
16
17
|
type WarningStatus = 'open' | 'resolved' | 'ignored';
|
|
@@ -79,7 +80,8 @@ function rowMutation(row: WorkflowWarning): TaskMutation {
|
|
|
79
80
|
function applyWorkflowWarningIntent(intent: WorkflowWarningIntent, options: TaskWriteOptions = {}): WorkflowWarningIntentResult {
|
|
80
81
|
const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
|
|
81
82
|
if (!resolved.ok) return failed(intent, resolved.code, resolved.message, resolved.taskId);
|
|
82
|
-
|
|
83
|
+
const stateOverride = allowsManualOverride(options.manualOverride, 'workflow-warning', 'TASK_STATE_MISMATCH');
|
|
84
|
+
if (resolved.state !== 'active' && !stateOverride) return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
|
|
83
85
|
let content: string;
|
|
84
86
|
let rows: WorkflowWarning[];
|
|
85
87
|
try {
|
|
@@ -125,7 +127,7 @@ function applyWorkflowWarningIntent(intent: WorkflowWarningIntent, options: Task
|
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
const writeResult = writeTask({
|
|
128
|
-
taskRef: intent.taskRef, expectedState: 'active', mutations: [...sectionMutation(content), rowMutation(after)], dryRun: intent.dryRun
|
|
130
|
+
taskRef: intent.taskRef, expectedState: stateOverride ? resolved.state : 'active', mutations: [...sectionMutation(content), rowMutation(after)], dryRun: intent.dryRun
|
|
129
131
|
}, {
|
|
130
132
|
...options,
|
|
131
133
|
taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state },
|
package/lib/task/write.ts
CHANGED
|
@@ -12,6 +12,8 @@ import type {
|
|
|
12
12
|
ResolveTaskRefErrorCode,
|
|
13
13
|
TaskWorkspaceState
|
|
14
14
|
} from './resolve-ref.ts';
|
|
15
|
+
import { allowsManualOverride } from './guard-override.ts';
|
|
16
|
+
import type { ManualOverrideCapability } from './guard-override.ts';
|
|
15
17
|
import { mutateTableRow, upsertSection } from './sections.ts';
|
|
16
18
|
import type {
|
|
17
19
|
TableRowDeleteMutation,
|
|
@@ -156,6 +158,7 @@ type TaskWriteOptions = {
|
|
|
156
158
|
metadataProvider?: () => TaskWriteMetadata;
|
|
157
159
|
randomSuffix?: () => string;
|
|
158
160
|
fileSystem?: Partial<TaskFileSystem>;
|
|
161
|
+
manualOverride?: ManualOverrideCapability;
|
|
159
162
|
};
|
|
160
163
|
|
|
161
164
|
const DEFAULT_FILE_SYSTEM: TaskFileSystem = {
|
|
@@ -241,7 +244,7 @@ function writeTask(request: TaskWriteRequest, options: TaskWriteOptions = {}): T
|
|
|
241
244
|
taskMdPath: resolved.taskMdPath,
|
|
242
245
|
actualState: resolved.state
|
|
243
246
|
};
|
|
244
|
-
if (resolved.state !== request.expectedState) {
|
|
247
|
+
if (resolved.state !== request.expectedState && !allowsManualOverride(options.manualOverride, 'task.write', 'TASK_STATE_MISMATCH')) {
|
|
245
248
|
return failure(
|
|
246
249
|
request,
|
|
247
250
|
identity,
|
package/package.json
CHANGED