@fitlab-ai/agent-infra 0.9.4 → 0.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +13 -3
- package/dist/lib/agent-clients/adapter.js +2 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +492 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +175 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +255 -0
- package/dist/lib/agent-clients/adapters/codex.js +3 -3
- package/dist/lib/git/workflow.js +0 -1
- package/dist/lib/git/worktree-identity.js +64 -0
- package/dist/lib/internal/codex-lifecycle.js +300 -0
- package/dist/lib/internal/release-workflow.js +108 -11
- package/dist/lib/internal/sandbox-control.js +43 -6
- package/dist/lib/internal/task-create.js +56 -0
- package/dist/lib/internal/task-orchestration.js +71 -31
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +16 -16
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +318 -89
- package/dist/lib/sandbox/control/client.js +152 -22
- package/dist/lib/sandbox/control/executor.js +136 -0
- package/dist/lib/sandbox/control/lifecycle.js +348 -0
- package/dist/lib/sandbox/control/protocol.js +73 -18
- package/dist/lib/sandbox/control/server.js +235 -80
- package/dist/lib/sandbox/control/state.js +181 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +35 -24
- package/dist/lib/sandbox/recovery.js +262 -45
- package/dist/lib/sandbox/workspace-view.js +80 -11
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/process-control.js +6 -0
- package/dist/lib/task/activity-log.js +32 -5
- package/dist/lib/task/artifact-lifecycle.js +5 -4
- package/dist/lib/task/codex-orchestration.js +161 -0
- package/dist/lib/task/commands/status.js +3 -0
- package/dist/lib/task/commands/validate.js +289 -0
- package/dist/lib/task/commit-finalization.js +50 -12
- package/dist/lib/task/create-service.js +137 -0
- package/dist/lib/task/create.js +333 -0
- package/dist/lib/task/delegation-receipts.js +25 -0
- package/dist/lib/task/events.js +10 -4
- package/dist/lib/task/finalization-artifacts.js +2 -1
- package/dist/lib/task/implementation-inputs.js +1 -7
- package/dist/lib/task/index.js +9 -0
- package/dist/lib/task/orchestration.js +283 -18
- package/dist/lib/task/review-fingerprint.js +4 -0
- package/dist/lib/task/verification-engine.js +38 -6
- package/dist/lib/task/verification.js +1 -0
- package/dist/lib/task/workspace-snapshot.js +78 -3
- package/lib/agent-clients/adapter.ts +4 -4
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +569 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +323 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +298 -0
- package/lib/agent-clients/adapters/codex.ts +3 -3
- package/lib/git/workflow.ts +0 -1
- package/lib/git/worktree-identity.ts +86 -0
- package/lib/internal/codex-lifecycle.ts +308 -0
- package/lib/internal/release-workflow.ts +107 -11
- package/lib/internal/sandbox-control.ts +38 -6
- package/lib/internal/task-create.ts +61 -0
- package/lib/internal/task-orchestration.ts +57 -31
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +18 -21
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +368 -103
- package/lib/sandbox/control/client.ts +173 -25
- package/lib/sandbox/control/executor.ts +163 -0
- package/lib/sandbox/control/lifecycle.ts +342 -0
- package/lib/sandbox/control/protocol.ts +110 -48
- package/lib/sandbox/control/server.ts +252 -85
- package/lib/sandbox/control/state.ts +196 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +31 -13
- package/lib/sandbox/recovery.ts +301 -60
- package/lib/sandbox/workspace-view.ts +89 -12
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/process-control.ts +11 -0
- package/lib/task/activity-log.ts +46 -7
- package/lib/task/artifact-lifecycle.ts +5 -4
- package/lib/task/codex-orchestration.ts +233 -0
- package/lib/task/commands/status.ts +2 -0
- package/lib/task/commands/validate.ts +282 -0
- package/lib/task/commit-finalization.ts +75 -12
- package/lib/task/create-service.ts +171 -0
- package/lib/task/create.ts +372 -0
- package/lib/task/delegation-receipts.ts +52 -3
- package/lib/task/events.ts +9 -4
- package/lib/task/finalization-artifacts.ts +2 -1
- package/lib/task/implementation-inputs.ts +1 -5
- package/lib/task/index.ts +8 -0
- package/lib/task/orchestration.ts +381 -22
- package/lib/task/review-fingerprint.ts +9 -0
- package/lib/task/verification-engine.ts +42 -8
- package/lib/task/verification.ts +2 -1
- package/lib/task/workspace-snapshot.ts +98 -4
- package/package.json +7 -7
- package/templates/.agents/hooks/lifecycle-delegation.js +88 -8
- package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
- package/templates/.agents/rules/task-management.en.md +1 -0
- package/templates/.agents/rules/task-management.zh-CN.md +1 -0
- package/templates/.agents/skills/commit/SKILL.en.md +3 -9
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -10
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +4 -3
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +5 -4
- package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
- package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +44 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +44 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -3
- package/templates/.claude/commands/run-manual-validation.en.md +9 -0
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
- package/templates/.codex/hooks.json +50 -0
- package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
|
@@ -13,10 +13,12 @@ import { inspectPlatformPullRequest } from '../platform/pull-requests.js';
|
|
|
13
13
|
import { getAgentClientCapability, getAgentClientDelegationEvidence, getAgentClientModelSelection } from '../agent-clients/registry.js';
|
|
14
14
|
import { isAgentClientId } from '../agent-clients/types.js';
|
|
15
15
|
import { captureRepositorySnapshot, captureWorkspaceSnapshot, diffWorkspaceSnapshots } from './workspace-snapshot.js';
|
|
16
|
+
import { assertGitRepositoryBinding } from '../git/worktree-identity.js';
|
|
16
17
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
|
|
17
18
|
import { CommitIntentError, commitIntentPath, createCommitIntent, digest, readCommitIntent, removeCommitIntent, removeCommitIntentByDigest, serialize, updateCommitIntent } from './commit-intent.js';
|
|
18
19
|
import { inspectCommitFinalization, planCommitTaskFinalization } from './commit-finalization.js';
|
|
19
20
|
import { captureTaskWriteMetadata, writeTask } from './write.js';
|
|
21
|
+
import { appendActivityEntry, commitAttemptStartedNote, locateActivityLog, pairEntries } from './activity-log.js';
|
|
20
22
|
function supportsLifecycleDelegation(client) {
|
|
21
23
|
return getAgentClientCapability(client, 'subagents').level !== 'unsupported'
|
|
22
24
|
&& getAgentClientCapability(client, 'orchestration').level !== 'unsupported'
|
|
@@ -71,6 +73,11 @@ function repositoryHead(repoRoot) {
|
|
|
71
73
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
72
74
|
}).trim();
|
|
73
75
|
}
|
|
76
|
+
function gitRootFor(stateRoot, options) {
|
|
77
|
+
if (!options.gitWorktreeRoot)
|
|
78
|
+
return stateRoot;
|
|
79
|
+
return assertGitRepositoryBinding(stateRoot, options.gitWorktreeRoot).worktreeRoot;
|
|
80
|
+
}
|
|
74
81
|
function isAncestor(repoRoot, ancestor, descendant) {
|
|
75
82
|
return spawnSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
|
|
76
83
|
cwd: repoRoot,
|
|
@@ -97,7 +104,8 @@ function commitFinalizationView(inspection) {
|
|
|
97
104
|
currentHead: inspection.currentHead,
|
|
98
105
|
committedHead: inspection.committedHead,
|
|
99
106
|
needsAnchor: inspection.needsAnchor,
|
|
100
|
-
needsLog: inspection.needsLog
|
|
107
|
+
needsLog: inspection.needsLog,
|
|
108
|
+
attempt: inspection.attempt
|
|
101
109
|
};
|
|
102
110
|
}
|
|
103
111
|
function validCommitRun(run, taskId) {
|
|
@@ -151,6 +159,22 @@ function resolveProjectPolicy(repoRoot, client) {
|
|
|
151
159
|
const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
152
160
|
return normalizeAgentClients(raw).state[client].orchestration;
|
|
153
161
|
}
|
|
162
|
+
function canRecoverCodexUnsupportedPause(run, taskDir) {
|
|
163
|
+
return run.schemaVersion === 2
|
|
164
|
+
&& run.status === 'paused'
|
|
165
|
+
&& run.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
|
|
166
|
+
&& run.modelPolicySource?.client === 'codex'
|
|
167
|
+
&& run.stepCount === 0
|
|
168
|
+
&& run.nextStage === null
|
|
169
|
+
&& run.baseline === ''
|
|
170
|
+
&& run.pendingDelegation === null
|
|
171
|
+
&& run.receipts.length === 0
|
|
172
|
+
&& run.commitAuthorization?.issuedAt === null
|
|
173
|
+
&& run.commitAuthorization?.consumedAt === null
|
|
174
|
+
&& run.completionEvidence == null
|
|
175
|
+
&& !fs.existsSync(commitIntentPath(taskDir))
|
|
176
|
+
&& (run.recoveryHistory ?? []).every((entry) => entry.code === 'MODEL_POLICY_SUPPLEMENTED');
|
|
177
|
+
}
|
|
154
178
|
function beginOrResumeOrchestration(taskRef, options = {}) {
|
|
155
179
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
156
180
|
if (!resolved.ok)
|
|
@@ -196,6 +220,39 @@ function beginOrResumeOrchestration(taskRef, options = {}) {
|
|
|
196
220
|
return failed('ORCHESTRATION_MODEL_POLICY_MISMATCH', 'provided model policy does not match the persisted run policy', resolved.taskId);
|
|
197
221
|
}
|
|
198
222
|
}
|
|
223
|
+
if (existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED') {
|
|
224
|
+
if (!canRecoverCodexUnsupportedPause(existing, resolved.taskDir)) {
|
|
225
|
+
return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
226
|
+
}
|
|
227
|
+
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
228
|
+
const previousPause = existing.pause;
|
|
229
|
+
const recovery = {
|
|
230
|
+
code: 'CLIENT_CAPABILITY_ENABLED',
|
|
231
|
+
recoveredAt: now,
|
|
232
|
+
previousSchemaVersion: 2,
|
|
233
|
+
previousStatus: 'paused',
|
|
234
|
+
previousPause,
|
|
235
|
+
client: 'codex',
|
|
236
|
+
guards: {
|
|
237
|
+
stepCount: 0,
|
|
238
|
+
nextStage: null,
|
|
239
|
+
baselineEmpty: true,
|
|
240
|
+
receiptCount: 0,
|
|
241
|
+
pendingDelegation: false,
|
|
242
|
+
commitAuthorizationUnused: true,
|
|
243
|
+
completionEvidenceAbsent: true,
|
|
244
|
+
commitIntentAbsent: true
|
|
245
|
+
},
|
|
246
|
+
resultingStatus: 'running'
|
|
247
|
+
};
|
|
248
|
+
const resumed = withUpdatedRun(existing, {
|
|
249
|
+
status: 'running',
|
|
250
|
+
pause: null,
|
|
251
|
+
recoveryHistory: Object.freeze([...(existing.recoveryHistory ?? []), recovery])
|
|
252
|
+
}, () => now);
|
|
253
|
+
saveRun(resolved.taskDir, resumed);
|
|
254
|
+
return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
|
|
255
|
+
}
|
|
199
256
|
if (existing.status === 'paused' && existing.pause?.recoverable && existing.pendingDelegation === null) {
|
|
200
257
|
const resumed = withUpdatedRun(existing, { status: 'running', pause: null });
|
|
201
258
|
saveRun(resolved.taskDir, resumed);
|
|
@@ -399,6 +456,13 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
399
456
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
400
457
|
if (!resolved.ok)
|
|
401
458
|
return failed(resolved.code, resolved.message, resolved.taskId);
|
|
459
|
+
let gitRoot;
|
|
460
|
+
try {
|
|
461
|
+
gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
return failed('ORCHESTRATION_WORKTREE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
465
|
+
}
|
|
402
466
|
let content;
|
|
403
467
|
try {
|
|
404
468
|
content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
@@ -469,7 +533,7 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
469
533
|
const capture = options.captureRepository ?? captureRepositorySnapshot;
|
|
470
534
|
let before;
|
|
471
535
|
try {
|
|
472
|
-
before = capture(
|
|
536
|
+
before = capture(gitRoot);
|
|
473
537
|
}
|
|
474
538
|
catch (error) {
|
|
475
539
|
return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
@@ -484,7 +548,7 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
484
548
|
const inspect = options.inspectPullRequest ?? inspectPlatformPullRequest;
|
|
485
549
|
let inspection;
|
|
486
550
|
try {
|
|
487
|
-
inspection = inspect(resolved.taskId, { cwd:
|
|
551
|
+
inspection = inspect(resolved.taskId, { cwd: gitRoot });
|
|
488
552
|
}
|
|
489
553
|
catch (error) {
|
|
490
554
|
return failed('ORCHESTRATION_PR_INSPECTION_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
@@ -505,7 +569,7 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
505
569
|
}
|
|
506
570
|
let after;
|
|
507
571
|
try {
|
|
508
|
-
after = capture(
|
|
572
|
+
after = capture(gitRoot);
|
|
509
573
|
}
|
|
510
574
|
catch (error) {
|
|
511
575
|
return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
@@ -538,13 +602,101 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
538
602
|
}
|
|
539
603
|
return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
|
|
540
604
|
}
|
|
605
|
+
function appendCommitActivity(resolved, step, agent, note) {
|
|
606
|
+
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
607
|
+
const activity = locateActivityLog(taskContent);
|
|
608
|
+
if (!activity)
|
|
609
|
+
return { code: 'ORCHESTRATION_TASK_INVALID', message: 'task activity log is missing or ambiguous' };
|
|
610
|
+
const metadata = captureTaskWriteMetadata();
|
|
611
|
+
const written = writeTask({
|
|
612
|
+
taskRef: resolved.taskId,
|
|
613
|
+
expectedState: 'active',
|
|
614
|
+
mutations: [{
|
|
615
|
+
kind: 'section',
|
|
616
|
+
aliases: ['活动日志', 'Activity Log'],
|
|
617
|
+
heading: activity.heading,
|
|
618
|
+
body: appendActivityEntry(activity, { time: metadata.timestamp, step, agent, note })
|
|
619
|
+
}]
|
|
620
|
+
}, { repoRoot: resolved.repoRoot, metadataProvider: () => metadata });
|
|
621
|
+
return written.status === 'failed' ? written.error : null;
|
|
622
|
+
}
|
|
623
|
+
function startCommitAttempt(taskRef, input, options = {}) {
|
|
624
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
625
|
+
if (!resolved.ok)
|
|
626
|
+
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
627
|
+
try {
|
|
628
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
629
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.start', () => {
|
|
630
|
+
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
631
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
|
|
632
|
+
}
|
|
633
|
+
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
634
|
+
const activity = locateActivityLog(taskContent);
|
|
635
|
+
if (!activity)
|
|
636
|
+
return commitIntentFailed('ORCHESTRATION_TASK_INVALID', 'task activity log is missing or ambiguous', resolved.taskId);
|
|
637
|
+
const open = pairEntries(activity.entries).filter((row) => row.step === 'Commit' && row.started !== '' && row.done === '');
|
|
638
|
+
if (open.length > 0) {
|
|
639
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_ATTEMPT_BUSY', 'an open Commit activity entry already exists', resolved.taskId);
|
|
640
|
+
}
|
|
641
|
+
const currentHead = repositoryHead(gitRoot);
|
|
642
|
+
const attempt = (options.id ?? randomUUID)();
|
|
643
|
+
const error = appendCommitActivity(resolved, 'Commit [started]', input.agent, commitAttemptStartedNote({
|
|
644
|
+
attempt,
|
|
645
|
+
baseline: currentHead,
|
|
646
|
+
agent: input.agent
|
|
647
|
+
}));
|
|
648
|
+
if (error)
|
|
649
|
+
return commitIntentFailed(error.code, error.message, resolved.taskId);
|
|
650
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
651
|
+
return {
|
|
652
|
+
status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
|
|
653
|
+
finalization: commitFinalizationView(inspection), error: null
|
|
654
|
+
};
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
catch (error) {
|
|
658
|
+
return mapCommitIntentError(error, resolved.taskId);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
function terminateCommitAttempt(taskRef, input, options = {}) {
|
|
662
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
663
|
+
if (!resolved.ok)
|
|
664
|
+
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
665
|
+
try {
|
|
666
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
667
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.terminate', () => {
|
|
668
|
+
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
669
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'an active commit intent must be aborted or recovered first', resolved.taskId);
|
|
670
|
+
}
|
|
671
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
672
|
+
if (inspection.disposition !== 'retryable-start'
|
|
673
|
+
|| inspection.attempt?.attempt !== input.attempt
|
|
674
|
+
|| inspection.attempt.agent !== input.agent
|
|
675
|
+
|| inspection.attempt.baseline !== inspection.currentHead) {
|
|
676
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit attempt identity or repository HEAD has drifted', resolved.taskId);
|
|
677
|
+
}
|
|
678
|
+
const error = appendCommitActivity(resolved, 'Commit [aborted]', input.agent, `aborted; attempt=${input.attempt}; code=${input.code}`);
|
|
679
|
+
if (error)
|
|
680
|
+
return commitIntentFailed(error.code, error.message, resolved.taskId);
|
|
681
|
+
return {
|
|
682
|
+
status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
|
|
683
|
+
finalization: commitFinalizationView(inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId)),
|
|
684
|
+
error: null
|
|
685
|
+
};
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
return mapCommitIntentError(error, resolved.taskId);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
541
692
|
function beginCommitIntent(taskRef, input, options = {}) {
|
|
542
693
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
543
694
|
if (!resolved.ok)
|
|
544
695
|
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
545
696
|
try {
|
|
697
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
546
698
|
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.begin', () => {
|
|
547
|
-
const currentHead = repositoryHead(
|
|
699
|
+
const currentHead = repositoryHead(gitRoot);
|
|
548
700
|
if (currentHead !== input.baselineHead) {
|
|
549
701
|
return commitIntentFailed('ORCHESTRATION_COMMIT_BASELINE_MISMATCH', 'baseline HEAD does not match the repository HEAD', resolved.taskId);
|
|
550
702
|
}
|
|
@@ -557,6 +709,13 @@ function beginCommitIntent(taskRef, input, options = {}) {
|
|
|
557
709
|
}
|
|
558
710
|
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
|
|
559
711
|
}
|
|
712
|
+
const attemptInspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
713
|
+
if (attemptInspection.disposition !== 'retryable-start'
|
|
714
|
+
|| attemptInspection.attempt?.attempt !== input.attempt
|
|
715
|
+
|| attemptInspection.attempt.agent !== input.agent
|
|
716
|
+
|| attemptInspection.attempt.baseline !== input.baselineHead) {
|
|
717
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_ATTEMPT_MISMATCH', 'commit begin requires one matching retryable Commit attempt', resolved.taskId);
|
|
718
|
+
}
|
|
560
719
|
const runFile = orchestrationPath(resolved.taskDir);
|
|
561
720
|
let run = null;
|
|
562
721
|
let sourceRunBytes = null;
|
|
@@ -646,13 +805,14 @@ function checkpointCommitIntent(taskRef, input, options = {}) {
|
|
|
646
805
|
if (!resolved.ok)
|
|
647
806
|
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
648
807
|
try {
|
|
808
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
649
809
|
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.checkpoint', () => {
|
|
650
810
|
const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
651
|
-
const currentHead = repositoryHead(
|
|
811
|
+
const currentHead = repositoryHead(gitRoot);
|
|
652
812
|
if (currentHead !== input.head) {
|
|
653
813
|
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'checkpoint HEAD does not match the repository HEAD', resolved.taskId);
|
|
654
814
|
}
|
|
655
|
-
if (input.kind === 'committed' && !isAncestor(
|
|
815
|
+
if (input.kind === 'committed' && !isAncestor(gitRoot, intent.baselineHead, input.head)) {
|
|
656
816
|
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'committed HEAD does not descend from the baseline', resolved.taskId);
|
|
657
817
|
}
|
|
658
818
|
if (input.kind === 'pushed') {
|
|
@@ -750,9 +910,10 @@ function completeCommitIntent(taskRef, input, options = {}) {
|
|
|
750
910
|
if (!resolved.ok)
|
|
751
911
|
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
752
912
|
try {
|
|
913
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
753
914
|
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.complete', () => {
|
|
754
915
|
readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
755
|
-
const inspection = inspectCommitFinalization(resolved.taskDir,
|
|
916
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
756
917
|
return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
|
|
757
918
|
removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
758
919
|
});
|
|
@@ -767,8 +928,15 @@ function recoverCommitIntent(taskRef, input, options = {}) {
|
|
|
767
928
|
if (!resolved.ok)
|
|
768
929
|
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
769
930
|
try {
|
|
931
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
770
932
|
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.recover', () => {
|
|
771
|
-
const inspection = inspectCommitFinalization(resolved.taskDir,
|
|
933
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
934
|
+
if (inspection.disposition === 'retryable-start') {
|
|
935
|
+
return {
|
|
936
|
+
status: 'ready', changed: false, taskId: resolved.taskId, intent: null,
|
|
937
|
+
finalization: commitFinalizationView(inspection), error: null
|
|
938
|
+
};
|
|
939
|
+
}
|
|
772
940
|
if (inspection.disposition === 'prepared' && inspection.intentDigest) {
|
|
773
941
|
removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest);
|
|
774
942
|
return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
|
|
@@ -790,9 +958,10 @@ function abortCommitIntent(taskRef, input, options = {}) {
|
|
|
790
958
|
if (!resolved.ok)
|
|
791
959
|
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
792
960
|
try {
|
|
961
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
793
962
|
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.abort', () => {
|
|
794
963
|
const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
795
|
-
const currentHead = repositoryHead(
|
|
964
|
+
const currentHead = repositoryHead(gitRoot);
|
|
796
965
|
if (intent.phase !== 'prepared'
|
|
797
966
|
|| intent.pushEvidence !== null
|
|
798
967
|
|| input.expectedHead !== intent.baselineHead
|
|
@@ -811,7 +980,14 @@ function statusCommitIntent(taskRef, options = {}) {
|
|
|
811
980
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
812
981
|
if (!resolved.ok)
|
|
813
982
|
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
814
|
-
|
|
983
|
+
let gitRoot;
|
|
984
|
+
try {
|
|
985
|
+
gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
986
|
+
}
|
|
987
|
+
catch (error) {
|
|
988
|
+
return commitIntentFailed('ORCHESTRATION_WORKTREE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
989
|
+
}
|
|
990
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
815
991
|
return {
|
|
816
992
|
status: 'ready',
|
|
817
993
|
changed: false,
|
|
@@ -878,7 +1054,11 @@ function prepareOrchestrationDelegationUnlocked(taskRef, input, options = {}) {
|
|
|
878
1054
|
}
|
|
879
1055
|
let beforeFingerprint;
|
|
880
1056
|
try {
|
|
881
|
-
beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)(
|
|
1057
|
+
beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
|
|
1058
|
+
gitRoot: gitRootFor(resolved.repoRoot, options),
|
|
1059
|
+
stateRoot: resolved.repoRoot,
|
|
1060
|
+
taskId: resolved.taskId
|
|
1061
|
+
});
|
|
882
1062
|
}
|
|
883
1063
|
catch (error) {
|
|
884
1064
|
return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
@@ -928,7 +1108,7 @@ function matchingDelegations(predicate, options) {
|
|
|
928
1108
|
.filter((entry) => entry.isDirectory())
|
|
929
1109
|
.flatMap((entry) => {
|
|
930
1110
|
const run = readRun(path.join(activeRoot, entry.name));
|
|
931
|
-
if (!run
|
|
1111
|
+
if (run?.status !== 'running' || !run.pendingDelegation)
|
|
932
1112
|
return [];
|
|
933
1113
|
const receipt = run.pendingDelegation;
|
|
934
1114
|
return predicate(receipt)
|
|
@@ -949,13 +1129,36 @@ function activateMatchingOrchestrationDelegation(client, event, options = {}) {
|
|
|
949
1129
|
if (!role)
|
|
950
1130
|
return failed('DELEGATION_IGNORED', `subagent '${event.nativeAgent}' is not lifecycle-managed`);
|
|
951
1131
|
const matched = uniqueMatchingDelegation(client, (receipt) => receipt.status === 'prepared', options);
|
|
952
|
-
if ('status' in matched)
|
|
953
|
-
|
|
1132
|
+
if ('status' in matched) {
|
|
1133
|
+
if (matched.error?.code !== 'ORCHESTRATION_DELEGATION_MISSING')
|
|
1134
|
+
return matched;
|
|
1135
|
+
const replay = uniqueMatchingDelegation(client, (receipt) => receipt.childId === event.childId && ['activated', 'stage-completed'].includes(receipt.status), options);
|
|
1136
|
+
if ('status' in replay)
|
|
1137
|
+
return matched;
|
|
1138
|
+
const receipt = replay.run.pendingDelegation;
|
|
1139
|
+
const sameEvidence = receipt.parentId === event.parentId
|
|
1140
|
+
&& receipt.spawnMode === event.spawnMode
|
|
1141
|
+
&& receipt.actualModel === event.actualModel
|
|
1142
|
+
&& receipt.actualReasoningEffort === event.actualReasoningEffort
|
|
1143
|
+
&& receipt.modelFallbackReason === (event.modelFallbackReason ?? null)
|
|
1144
|
+
&& receipt.reasoningEffortFallbackReason === (event.reasoningEffortFallbackReason ?? null)
|
|
1145
|
+
&& receipt.hostEvidence?.hookDefinitionHash === event.hostEvidence?.hookDefinitionHash;
|
|
1146
|
+
if (!sameEvidence) {
|
|
1147
|
+
return pauseOrchestration(replay.taskId, 'DELEGATION_REPLAY_CONFLICT', 'replayed native start evidence conflicts with the active receipt', true, options);
|
|
1148
|
+
}
|
|
1149
|
+
return { status: replay.run.status, changed: false, taskId: replay.taskId, run: replay.run, next: null, error: null };
|
|
1150
|
+
}
|
|
954
1151
|
if (matched.run.receipts.some((receipt) => receipt.childId === event.childId)) {
|
|
955
1152
|
return pauseOrchestration(matched.taskId, 'DELEGATION_IDENTITY_REUSED', 'native child identity was already used by this run', true, options);
|
|
956
1153
|
}
|
|
957
1154
|
return activateOrchestrationDelegation(matched.taskId, event, options);
|
|
958
1155
|
}
|
|
1156
|
+
function pauseMatchingOrchestrationDelegation(client, code, message, options = {}) {
|
|
1157
|
+
const matched = uniqueMatchingDelegation(client, () => true, options);
|
|
1158
|
+
if ('status' in matched)
|
|
1159
|
+
return matched;
|
|
1160
|
+
return pauseOrchestration(matched.taskId, code, message, true, options);
|
|
1161
|
+
}
|
|
959
1162
|
function sealMatchingOrchestrationDelegation(client, event, options = {}) {
|
|
960
1163
|
const role = managedDelegationRole(event.nativeAgent);
|
|
961
1164
|
if (!role)
|
|
@@ -969,9 +1172,14 @@ function sealMatchingOrchestrationDelegation(client, event, options = {}) {
|
|
|
969
1172
|
}
|
|
970
1173
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
971
1174
|
try {
|
|
1175
|
+
const gitRoot = gitRootFor(repoRoot, options);
|
|
972
1176
|
const snapshotTaskId = receipt.workspaceSnapshotScope === 'task' ? receipt.taskId : null;
|
|
973
|
-
const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)(
|
|
974
|
-
|
|
1177
|
+
const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
|
|
1178
|
+
gitRoot,
|
|
1179
|
+
stateRoot: repoRoot,
|
|
1180
|
+
taskId: snapshotTaskId
|
|
1181
|
+
});
|
|
1182
|
+
const changedPaths = (options.diffWorkspace ?? diffWorkspaceSnapshots)(gitRoot, receipt.beforeFingerprint, afterFingerprint);
|
|
975
1183
|
return sealOrchestrationDelegation(matched.taskId, {
|
|
976
1184
|
childId: event.childId,
|
|
977
1185
|
exitCode: 0,
|
|
@@ -983,6 +1191,63 @@ function sealMatchingOrchestrationDelegation(client, event, options = {}) {
|
|
|
983
1191
|
return pauseOrchestration(matched.taskId, 'ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), true, options);
|
|
984
1192
|
}
|
|
985
1193
|
}
|
|
1194
|
+
function sealMatchingOrchestrationDelegationWithHostEvidence(client, event, consumeEvidence, options = {}) {
|
|
1195
|
+
const role = managedDelegationRole(event.nativeAgent);
|
|
1196
|
+
if (!role)
|
|
1197
|
+
return failed('DELEGATION_IGNORED', `subagent '${event.nativeAgent}' is not lifecycle-managed`);
|
|
1198
|
+
const matched = uniqueMatchingDelegation(client, (receipt) => ((receipt.status === 'stage-completed' || receipt.status === 'sealed')
|
|
1199
|
+
&& receipt.childId === event.childId), options);
|
|
1200
|
+
if ('status' in matched)
|
|
1201
|
+
return matched;
|
|
1202
|
+
const receipt = matched.run.pendingDelegation;
|
|
1203
|
+
if (receipt.status === 'sealed') {
|
|
1204
|
+
return reconcileMatchingOrchestrationDelegation(client, event.childId, options);
|
|
1205
|
+
}
|
|
1206
|
+
if (receipt.role !== role) {
|
|
1207
|
+
return pauseOrchestration(matched.taskId, 'DELEGATION_ROLE_MISMATCH', `managed role ${role} does not match ${receipt.role}`, true, options);
|
|
1208
|
+
}
|
|
1209
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
1210
|
+
try {
|
|
1211
|
+
const gitRoot = gitRootFor(repoRoot, options);
|
|
1212
|
+
const snapshotTaskId = receipt.workspaceSnapshotScope === 'task' ? receipt.taskId : null;
|
|
1213
|
+
const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
|
|
1214
|
+
gitRoot,
|
|
1215
|
+
stateRoot: repoRoot,
|
|
1216
|
+
taskId: snapshotTaskId
|
|
1217
|
+
});
|
|
1218
|
+
const changedPaths = (options.diffWorkspace ?? diffWorkspaceSnapshots)(gitRoot, receipt.beforeFingerprint, afterFingerprint);
|
|
1219
|
+
const baseEvent = { childId: event.childId, exitCode: 0, afterFingerprint, changedPaths };
|
|
1220
|
+
const validated = sealDelegation(receipt, baseEvent, { now: options.now });
|
|
1221
|
+
if (!validated.ok) {
|
|
1222
|
+
return pauseOrchestration(matched.taskId, validated.code, validated.message, true, options);
|
|
1223
|
+
}
|
|
1224
|
+
const hostEvidence = consumeEvidence(receipt);
|
|
1225
|
+
const sealed = sealDelegation(receipt, { ...baseEvent, hostEvidence }, { now: options.now });
|
|
1226
|
+
if (!sealed.ok)
|
|
1227
|
+
return pauseOrchestration(matched.taskId, sealed.code, sealed.message, true, options);
|
|
1228
|
+
const resolved = resolveTaskRef(matched.taskId, { repoRoot });
|
|
1229
|
+
if (!resolved.ok)
|
|
1230
|
+
return failed(resolved.code, resolved.message, resolved.taskId);
|
|
1231
|
+
const updated = withUpdatedRun(matched.run, { pendingDelegation: sealed.receipt }, options.now);
|
|
1232
|
+
saveRun(resolved.taskDir, updated);
|
|
1233
|
+
return { status: 'running', changed: true, taskId: matched.taskId, run: updated, next: null, error: null };
|
|
1234
|
+
}
|
|
1235
|
+
catch (error) {
|
|
1236
|
+
return pauseOrchestration(matched.taskId, 'ORCHESTRATION_CODEX_EVIDENCE_FAILED', error instanceof Error ? error.message : String(error), true, options);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
function reconcileMatchingOrchestrationDelegation(client, childId, options = {}) {
|
|
1240
|
+
const matched = uniqueMatchingDelegation(client, (receipt) => receipt.childId === childId, options);
|
|
1241
|
+
if ('status' in matched)
|
|
1242
|
+
return matched;
|
|
1243
|
+
const receipt = matched.run.pendingDelegation;
|
|
1244
|
+
if (receipt.status !== 'sealed'
|
|
1245
|
+
|| receipt.hostEvidence?.consumer !== receipt.id
|
|
1246
|
+
|| receipt.hostEvidence.consumedAt === null) {
|
|
1247
|
+
return pauseOrchestration(matched.taskId, 'ORCHESTRATION_CODEX_RECONCILIATION_FAILED', 'Codex native spawn did not produce one sealed receipt with consumed host evidence', true, options);
|
|
1248
|
+
}
|
|
1249
|
+
return { status: matched.run.status, changed: false, taskId: matched.taskId, run: matched.run, next: null, error: null };
|
|
1250
|
+
}
|
|
986
1251
|
function activateOrchestrationDelegation(taskRef, event, options = {}) {
|
|
987
1252
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
988
1253
|
if (!resolved.ok)
|
|
@@ -1115,5 +1380,5 @@ function pauseOrchestration(taskRef, code, message, recoverable, options = {}) {
|
|
|
1115
1380
|
saveRun(resolved.taskDir, updated);
|
|
1116
1381
|
return { status: 'paused', changed: true, taskId: resolved.taskId, run: updated, next: null, error: null };
|
|
1117
1382
|
}
|
|
1118
|
-
export { activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, advanceOrchestration, abortCommitIntent, beginCommitIntent, beginOrResumeOrchestration, checkpointCommitIntent, commitOrchestrationStageCompletion, completeCommitIntent, completeOrchestrationStage, inspectOrchestrationStage, orchestrationPath, pauseOrchestration, planOrchestrationStageCompletion, prepareOrchestrationDelegation, readRun, recoverCommitIntent, routeOrchestration, sealMatchingOrchestrationDelegation, sealOrchestrationDelegation, statusCommitIntent, statusOrchestration };
|
|
1383
|
+
export { activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, advanceOrchestration, abortCommitIntent, beginCommitIntent, beginOrResumeOrchestration, checkpointCommitIntent, commitOrchestrationStageCompletion, completeCommitIntent, completeOrchestrationStage, inspectOrchestrationStage, orchestrationPath, pauseMatchingOrchestrationDelegation, pauseOrchestration, planOrchestrationStageCompletion, prepareOrchestrationDelegation, readRun, reconcileMatchingOrchestrationDelegation, recoverCommitIntent, routeOrchestration, sealMatchingOrchestrationDelegation, sealMatchingOrchestrationDelegationWithHostEvidence, sealOrchestrationDelegation, startCommitAttempt, statusCommitIntent, statusOrchestration, terminateCommitAttempt };
|
|
1119
1384
|
//# sourceMappingURL=orchestration.js.map
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { artifactName, maxRound } from "./review-artifacts.js";
|
|
4
|
+
const REVIEWED_TREE_RE = /^[a-f0-9]{40,64}$/;
|
|
5
|
+
export function parseReviewedGitTree(value) {
|
|
6
|
+
return REVIEWED_TREE_RE.test(value) ? value : null;
|
|
7
|
+
}
|
|
4
8
|
// Paths excluded from the post-review coverage gate. Fail-closed: the gate
|
|
5
9
|
// covers ALL tracked changes by default. This default denylist is EMPTY —
|
|
6
10
|
// projects opt into exclusions explicitly via review.post_review_exclude_globs.
|
|
@@ -239,12 +239,29 @@ function checkOrchestrationEvidence({ taskDir }) {
|
|
|
239
239
|
return failResult('orchestration-evidence', 'Run model policy source or recovery history is invalid');
|
|
240
240
|
}
|
|
241
241
|
for (const recovery of run.recoveryHistory) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
242
|
+
const validModelRecovery = recovery.code === 'MODEL_POLICY_SUPPLEMENTED'
|
|
243
|
+
&& recovery.previousSchemaVersion === 1
|
|
244
|
+
&& recovery.receiptCount === 0
|
|
245
|
+
&& recovery.pendingDelegation === false
|
|
246
|
+
&& exactText(recovery.recoveredAt)
|
|
247
|
+
&& recovery.policySource;
|
|
248
|
+
const guards = recovery.guards;
|
|
249
|
+
const validCapabilityRecovery = recovery.code === 'CLIENT_CAPABILITY_ENABLED'
|
|
250
|
+
&& recovery.previousSchemaVersion === 2
|
|
251
|
+
&& recovery.previousStatus === 'paused'
|
|
252
|
+
&& recovery.previousPause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
|
|
253
|
+
&& recovery.client === 'codex'
|
|
254
|
+
&& recovery.resultingStatus === 'running'
|
|
255
|
+
&& exactText(recovery.recoveredAt)
|
|
256
|
+
&& guards?.stepCount === 0
|
|
257
|
+
&& guards?.nextStage === null
|
|
258
|
+
&& guards?.baselineEmpty === true
|
|
259
|
+
&& guards?.receiptCount === 0
|
|
260
|
+
&& guards?.pendingDelegation === false
|
|
261
|
+
&& guards?.commitAuthorizationUnused === true
|
|
262
|
+
&& guards?.completionEvidenceAbsent === true
|
|
263
|
+
&& guards?.commitIntentAbsent === true;
|
|
264
|
+
if (!validModelRecovery && !validCapabilityRecovery) {
|
|
248
265
|
return failResult('orchestration-evidence', 'Run recovery history contains invalid provenance');
|
|
249
266
|
}
|
|
250
267
|
}
|
|
@@ -298,6 +315,21 @@ function checkOrchestrationEvidence({ taskDir }) {
|
|
|
298
315
|
&& receipt.reasoningEffortFallbackReason !== null) {
|
|
299
316
|
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated reasoning-effort fallback reason`);
|
|
300
317
|
}
|
|
318
|
+
if (receipt.client === 'codex') {
|
|
319
|
+
const host = receipt.hostEvidence;
|
|
320
|
+
if (host?.kind !== 'codex-lifecycle-v1'
|
|
321
|
+
|| !exactText(host.hookDefinitionHash)
|
|
322
|
+
|| !Number.isSafeInteger(host.startRevision)
|
|
323
|
+
|| host.startRevision < 1) {
|
|
324
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid Codex start evidence`);
|
|
325
|
+
}
|
|
326
|
+
if (['sealed', 'consumed'].includes(receipt.status) && (!Number.isSafeInteger(host.stopRevision)
|
|
327
|
+
|| host.stopRevision <= host.startRevision
|
|
328
|
+
|| host.consumer !== receipt.id
|
|
329
|
+
|| !exactText(host.consumedAt))) {
|
|
330
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid Codex consumed stop evidence`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
301
333
|
}
|
|
302
334
|
}
|
|
303
335
|
if (run.receipts.some((receipt) => receipt.status !== 'consumed')) {
|
|
@@ -16,6 +16,7 @@ const VERIFICATION_CATALOG = {
|
|
|
16
16
|
'code.completed': gate('code-task', 'active', 'code'),
|
|
17
17
|
'review-code.completed': gate('review-code', 'active', 'review-code'),
|
|
18
18
|
'manual-validation.completed': gate('complete-manual-validation', 'active', 'manual-validation'),
|
|
19
|
+
'validation-run.completed': gate('run-manual-validation', 'active', 'validation-run'),
|
|
19
20
|
'block-task.completed': gate('block-task', 'blocked'),
|
|
20
21
|
'cancel-task.completed': gate('cancel-task', 'completed'),
|
|
21
22
|
'commit.completed': gate('commit', 'active'),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -25,12 +26,70 @@ function captureWorktreeTree(repoRoot, forcedPath) {
|
|
|
25
26
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
|
-
function
|
|
29
|
+
function captureLegacyWorkspaceSnapshot(repoRoot, taskId) {
|
|
29
30
|
const workspaceRelative = taskId
|
|
30
31
|
? `.agents/workspace/active/${taskId}`
|
|
31
32
|
: '.agents/workspace/active';
|
|
32
33
|
return captureWorktreeTree(repoRoot, workspaceRelative);
|
|
33
34
|
}
|
|
35
|
+
function taskFileFingerprints(stateRoot, taskId) {
|
|
36
|
+
const activeRoot = path.join(stateRoot, '.agents', 'workspace', 'active');
|
|
37
|
+
const roots = taskId ? [path.join(activeRoot, taskId)] : [activeRoot];
|
|
38
|
+
const records = [];
|
|
39
|
+
const visit = (absolute, relative) => {
|
|
40
|
+
const stat = fs.lstatSync(absolute);
|
|
41
|
+
if (stat.isDirectory()) {
|
|
42
|
+
for (const name of fs.readdirSync(absolute).sort()) {
|
|
43
|
+
visit(path.join(absolute, name), path.posix.join(relative, name));
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!stat.isFile() && !stat.isSymbolicLink())
|
|
48
|
+
return;
|
|
49
|
+
const content = stat.isSymbolicLink()
|
|
50
|
+
? Buffer.from(fs.readlinkSync(absolute))
|
|
51
|
+
: fs.readFileSync(absolute);
|
|
52
|
+
records.push({
|
|
53
|
+
path: relative,
|
|
54
|
+
mode: stat.mode & 0o777,
|
|
55
|
+
kind: stat.isSymbolicLink() ? 'symlink' : 'file',
|
|
56
|
+
sha256: createHash('sha256').update(content).digest('hex')
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
for (const root of roots) {
|
|
60
|
+
if (!fs.existsSync(root))
|
|
61
|
+
continue;
|
|
62
|
+
const relative = path.posix.join('.agents/workspace/active', path.relative(activeRoot, root).split(path.sep).join('/'));
|
|
63
|
+
visit(root, relative.replace(/\/$/, ''));
|
|
64
|
+
}
|
|
65
|
+
return records.sort((left, right) => left.path.localeCompare(right.path));
|
|
66
|
+
}
|
|
67
|
+
function encodeWorkspaceFingerprint(value) {
|
|
68
|
+
return `ws2:${Buffer.from(JSON.stringify(value)).toString('base64url')}`;
|
|
69
|
+
}
|
|
70
|
+
function decodeWorkspaceFingerprint(value) {
|
|
71
|
+
if (!value.startsWith('ws2:'))
|
|
72
|
+
return null;
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(Buffer.from(value.slice(4), 'base64url').toString('utf8'));
|
|
75
|
+
if (parsed.version !== 2 || !/^[a-f0-9]{40,64}$/.test(parsed.gitTree) || !Array.isArray(parsed.taskFiles)) {
|
|
76
|
+
throw new Error('invalid workspace fingerprint fields');
|
|
77
|
+
}
|
|
78
|
+
return parsed;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throw new Error(`ORCHESTRATION_SNAPSHOT_FAILED: ${error instanceof Error ? error.message : String(error)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function captureWorkspaceSnapshot(contextOrRoot, taskId) {
|
|
85
|
+
if (typeof contextOrRoot === 'string')
|
|
86
|
+
return captureLegacyWorkspaceSnapshot(contextOrRoot, taskId ?? null);
|
|
87
|
+
return encodeWorkspaceFingerprint({
|
|
88
|
+
version: 2,
|
|
89
|
+
gitTree: captureWorktreeTree(contextOrRoot.gitRoot, null),
|
|
90
|
+
taskFiles: taskFileFingerprints(contextOrRoot.stateRoot, contextOrRoot.taskId)
|
|
91
|
+
});
|
|
92
|
+
}
|
|
34
93
|
function captureRepositorySnapshot(repoRoot) {
|
|
35
94
|
return {
|
|
36
95
|
head: git(repoRoot, null, ['rev-parse', 'HEAD']).trim(),
|
|
@@ -39,8 +98,24 @@ function captureRepositorySnapshot(repoRoot) {
|
|
|
39
98
|
};
|
|
40
99
|
}
|
|
41
100
|
function diffWorkspaceSnapshots(repoRoot, before, after) {
|
|
42
|
-
const
|
|
43
|
-
|
|
101
|
+
const beforeV2 = decodeWorkspaceFingerprint(before);
|
|
102
|
+
const afterV2 = decodeWorkspaceFingerprint(after);
|
|
103
|
+
if ((beforeV2 === null) !== (afterV2 === null)) {
|
|
104
|
+
throw new Error('ORCHESTRATION_SNAPSHOT_FAILED: workspace fingerprint versions cannot be mixed');
|
|
105
|
+
}
|
|
106
|
+
if (!beforeV2 || !afterV2) {
|
|
107
|
+
const output = git(repoRoot, null, ['diff', '--name-only', '-z', before, after]);
|
|
108
|
+
return output.split('\0').filter(Boolean);
|
|
109
|
+
}
|
|
110
|
+
const output = git(repoRoot, null, ['diff', '--name-only', '-z', beforeV2.gitTree, afterV2.gitTree]);
|
|
111
|
+
const changed = new Set(output.split('\0').filter(Boolean));
|
|
112
|
+
const beforeFiles = new Map(beforeV2.taskFiles.map((record) => [record.path, JSON.stringify(record)]));
|
|
113
|
+
const afterFiles = new Map(afterV2.taskFiles.map((record) => [record.path, JSON.stringify(record)]));
|
|
114
|
+
for (const filePath of new Set([...beforeFiles.keys(), ...afterFiles.keys()])) {
|
|
115
|
+
if (beforeFiles.get(filePath) !== afterFiles.get(filePath))
|
|
116
|
+
changed.add(filePath);
|
|
117
|
+
}
|
|
118
|
+
return [...changed].sort();
|
|
44
119
|
}
|
|
45
120
|
export { captureRepositorySnapshot, captureWorkspaceSnapshot, diffWorkspaceSnapshots };
|
|
46
121
|
//# sourceMappingURL=workspace-snapshot.js.map
|