@fitlab-ai/agent-infra 0.9.7 → 0.9.9
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 +11 -1
- package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +60 -22
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +175 -11
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +86 -69
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
- package/dist/lib/decide.js +68 -14
- package/dist/lib/git/workflow.js +10 -1
- package/dist/lib/init.js +1 -1
- package/dist/lib/internal/codex-lifecycle.js +19 -5
- package/dist/lib/internal/codex-sandbox-controller.js +6 -13
- package/dist/lib/internal/git-workflow.js +5 -7
- package/dist/lib/internal/platform-pr.js +20 -7
- package/dist/lib/internal/release-workflow.js +3 -4
- package/dist/lib/internal/sandbox-control.js +79 -2
- package/dist/lib/internal/task-activity.js +57 -7
- package/dist/lib/internal/task-event.js +57 -4
- package/dist/lib/internal/task-finalization.js +54 -0
- 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 +161 -288
- 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 +53 -13
- package/dist/lib/platform/pull-requests.js +205 -41
- package/dist/lib/runtime/agent-runtime.js +56 -0
- package/dist/lib/sandbox/commands/create.js +389 -311
- package/dist/lib/sandbox/commands/rm.js +316 -55
- package/dist/lib/sandbox/control/client.js +190 -6
- package/dist/lib/sandbox/control/container-identity.js +1 -1
- package/dist/lib/sandbox/control/controller-registration.js +273 -0
- package/dist/lib/sandbox/control/executor.js +145 -5
- package/dist/lib/sandbox/control/lifecycle.js +395 -73
- package/dist/lib/sandbox/control/protocol.js +67 -6
- package/dist/lib/sandbox/control/state.js +27 -11
- package/dist/lib/sandbox/engine.js +14 -7
- package/dist/lib/sandbox/index.js +5 -3
- package/dist/lib/sandbox/managed-fs.js +5 -0
- package/dist/lib/sandbox/mounts.js +20 -5
- package/dist/lib/sandbox/recovery.js +78 -10
- package/dist/lib/sandbox/task-resolver.js +50 -6
- package/dist/lib/sandbox/workspace-identity.js +27 -0
- package/dist/lib/sandbox/workspace-view.js +62 -40
- package/dist/lib/sandbox/worktree-safety.js +215 -2
- package/dist/lib/server/process-state.js +51 -1
- package/dist/lib/task/activity-intent.js +4 -1
- package/dist/lib/task/codex-orchestration.js +158 -33
- package/dist/lib/task/commands/status.js +59 -1
- package/dist/lib/task/commit-operation.js +335 -0
- package/dist/lib/task/commit-policy.js +18 -0
- package/dist/lib/task/decision-intents.js +4 -2
- package/dist/lib/task/delegation-receipts.js +279 -17
- package/dist/lib/task/events.js +9 -5
- package/dist/lib/task/finalization.js +567 -0
- 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/operation-outcome.js +33 -0
- package/dist/lib/task/orchestration.js +199 -719
- package/dist/lib/task/review-finalization.js +3 -2
- package/dist/lib/task/short-id.js +59 -1
- package/dist/lib/task/task-execution-lock.js +6 -1
- package/dist/lib/task/verification-engine.js +138 -59
- package/dist/lib/task/verification.js +15 -2
- package/dist/lib/task/workflow-warning-intents.js +54 -3
- 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 +63 -20
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +278 -13
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +112 -93
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
- package/lib/decide.ts +59 -14
- package/lib/git/workflow.ts +11 -2
- package/lib/init.ts +1 -1
- package/lib/internal/codex-lifecycle.ts +18 -5
- package/lib/internal/codex-sandbox-controller.ts +6 -12
- package/lib/internal/git-workflow.ts +5 -6
- package/lib/internal/platform-pr.ts +14 -7
- package/lib/internal/release-workflow.ts +3 -4
- package/lib/internal/sandbox-control.ts +92 -2
- package/lib/internal/task-activity.ts +46 -6
- package/lib/internal/task-event.ts +53 -4
- package/lib/internal/task-finalization.ts +68 -0
- package/lib/internal/task-ledger.ts +48 -4
- package/lib/internal/task-lifecycle.ts +56 -12
- package/lib/internal/task-orchestration.ts +32 -107
- 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 +65 -14
- package/lib/platform/pull-requests.ts +211 -41
- package/lib/runtime/agent-runtime.ts +76 -0
- package/lib/sandbox/commands/create.ts +88 -4
- package/lib/sandbox/commands/rm.ts +413 -61
- package/lib/sandbox/config.ts +2 -1
- package/lib/sandbox/control/client.ts +255 -6
- package/lib/sandbox/control/container-identity.ts +3 -3
- package/lib/sandbox/control/controller-registration.ts +347 -0
- package/lib/sandbox/control/executor.ts +172 -6
- package/lib/sandbox/control/lifecycle.ts +465 -77
- package/lib/sandbox/control/protocol.ts +85 -9
- package/lib/sandbox/control/state.ts +35 -18
- package/lib/sandbox/engine.ts +20 -9
- package/lib/sandbox/index.ts +5 -3
- package/lib/sandbox/managed-fs.ts +5 -0
- package/lib/sandbox/mounts.ts +23 -5
- package/lib/sandbox/recovery.ts +90 -9
- package/lib/sandbox/task-resolver.ts +58 -7
- package/lib/sandbox/workspace-identity.ts +50 -2
- package/lib/sandbox/workspace-view.ts +73 -43
- package/lib/sandbox/worktree-safety.ts +236 -2
- package/lib/server/process-state.ts +48 -1
- package/lib/task/activity-intent.ts +4 -2
- package/lib/task/codex-orchestration.ts +167 -33
- package/lib/task/commands/status.ts +78 -1
- package/lib/task/commit-operation.ts +430 -0
- package/lib/task/commit-policy.ts +30 -0
- package/lib/task/decision-intents.ts +4 -2
- package/lib/task/delegation-receipts.ts +304 -19
- package/lib/task/events.ts +10 -5
- package/lib/task/finalization.ts +719 -0
- 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/operation-outcome.ts +48 -0
- package/lib/task/orchestration.ts +239 -922
- package/lib/task/review-finalization.ts +6 -2
- package/lib/task/short-id.ts +65 -1
- package/lib/task/task-execution-lock.ts +10 -1
- package/lib/task/verification-engine.ts +145 -64
- package/lib/task/verification.ts +18 -3
- package/lib/task/workflow-warning-intents.ts +61 -4
- package/lib/task/write.ts +4 -1
- package/package.json +2 -2
- package/templates/.agents/QUICKSTART.en.md +2 -2
- package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
- package/templates/.agents/hooks/lifecycle-delegation.js +9 -24
- package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -3
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -3
- package/templates/.agents/rules/task-management.en.md +2 -2
- package/templates/.agents/rules/task-management.zh-CN.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/commit/SKILL.en.md +41 -125
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
- package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
- package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
- package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
- 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
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
- package/templates/.codex/hooks.json +4 -4
- package/dist/lib/task/commit-finalization.js +0 -305
- package/dist/lib/task/commit-intent.js +0 -165
- package/lib/task/commit-finalization.ts +0 -419
- package/lib/task/commit-intent.ts +0 -230
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { execFileSync, spawnSync } from 'node:child_process';
|
|
5
4
|
import { parseTypedTaskFrontmatter } from './frontmatter.js';
|
|
6
5
|
import { parseReviewSummary } from './review-artifacts.js';
|
|
7
6
|
import { inspectArtifactDirectory } from './artifact-lifecycle.js';
|
|
8
7
|
import { parseLedger, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
|
|
9
8
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
10
|
-
import { activateDelegation, abortPreparedDelegation, completeDelegationStage, consumeDelegation, dispatchDelegation, managedDelegationRole, prepareDelegation, sealDelegation } from './delegation-receipts.js';
|
|
9
|
+
import { activateDelegation, abortPreparedDelegation, completeDelegationStage, consumeDelegation, dispatchDelegation, foldBlankToNull, isDelegationReceipt, managedDelegationRole, prepareDelegation, sealDelegation } from './delegation-receipts.js';
|
|
11
10
|
import { normalizeAgentClients } from '../agent-clients/config.js';
|
|
11
|
+
import { resolveAgentRuntimeStoreRoot } from '../runtime/agent-runtime.js';
|
|
12
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';
|
|
@@ -16,10 +16,6 @@ import { captureRepositorySnapshot, captureWorkspaceSnapshot, diffWorkspaceSnaps
|
|
|
16
16
|
import { assertGitRepositoryBinding } from '../git/worktree-identity.js';
|
|
17
17
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
|
|
18
18
|
import { hasActiveCodexLifecycleEvidence } from '../agent-clients/adapters/codex-lifecycle/store.js';
|
|
19
|
-
import { CommitIntentError, commitIntentPath, createCommitIntent, digest, readCommitIntent, removeCommitIntent, removeCommitIntentByDigest, serialize, updateCommitIntent } from './commit-intent.js';
|
|
20
|
-
import { inspectCommitFinalization, planCommitTaskFinalization } from './commit-finalization.js';
|
|
21
|
-
import { captureTaskWriteMetadata, writeTask } from './write.js';
|
|
22
|
-
import { appendActivityEntry, commitAttemptStartedNote, locateActivityLog, pairEntries } from './activity-log.js';
|
|
23
19
|
function supportsLifecycleDelegation(client) {
|
|
24
20
|
return getAgentClientCapability(client, 'subagents').level !== 'unsupported'
|
|
25
21
|
&& getAgentClientCapability(client, 'orchestration').level !== 'unsupported'
|
|
@@ -29,26 +25,138 @@ function supportsLifecycleDelegation(client) {
|
|
|
29
25
|
function orchestrationPath(taskDir) {
|
|
30
26
|
return path.join(taskDir, 'orchestration.json');
|
|
31
27
|
}
|
|
28
|
+
const ORCHESTRATION_STATE_INVALID_MESSAGE = 'orchestration.json does not match the current runtime structure; finish or clear active runs before upgrading';
|
|
29
|
+
class OrchestrationStateError extends Error {
|
|
30
|
+
code = 'ORCHESTRATION_STATE_INVALID';
|
|
31
|
+
taskId;
|
|
32
|
+
constructor(taskId = null) {
|
|
33
|
+
super(ORCHESTRATION_STATE_INVALID_MESSAGE);
|
|
34
|
+
this.name = 'OrchestrationStateError';
|
|
35
|
+
this.taskId = taskId;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function isRecord(value) {
|
|
39
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
function hasExactKeys(value, keys) {
|
|
42
|
+
if (!isRecord(value))
|
|
43
|
+
return false;
|
|
44
|
+
const actual = Object.keys(value);
|
|
45
|
+
return actual.length === keys.length && keys.every((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
46
|
+
}
|
|
47
|
+
function exactText(value) {
|
|
48
|
+
return typeof value === 'string' && value.length > 0 && value.trim() === value;
|
|
49
|
+
}
|
|
50
|
+
function nullableText(value) {
|
|
51
|
+
return value === null || exactText(value);
|
|
52
|
+
}
|
|
53
|
+
function isPause(value) {
|
|
54
|
+
return hasExactKeys(value, ['code', 'message', 'recoverable'])
|
|
55
|
+
&& exactText(value.code)
|
|
56
|
+
&& exactText(value.message)
|
|
57
|
+
&& typeof value.recoverable === 'boolean';
|
|
58
|
+
}
|
|
59
|
+
function isModelPolicySource(value) {
|
|
60
|
+
return hasExactKeys(value, ['kind', 'client', 'resolvedAt'])
|
|
61
|
+
&& ['explicit', 'project-config'].includes(value.kind)
|
|
62
|
+
&& isAgentClientId(value.client)
|
|
63
|
+
&& exactText(value.resolvedAt);
|
|
64
|
+
}
|
|
65
|
+
function isModelPolicy(value) {
|
|
66
|
+
if (!hasExactKeys(value, ['executor', 'reviewer']))
|
|
67
|
+
return false;
|
|
68
|
+
return [value.executor, value.reviewer].every((role) => hasExactKeys(role, ['model', 'reasoningEffort'])
|
|
69
|
+
&& exactText(role.model)
|
|
70
|
+
&& exactText(role.reasoningEffort));
|
|
71
|
+
}
|
|
72
|
+
const RECOVERY_GUARD_KEYS = [
|
|
73
|
+
'stepCount', 'nextStage', 'baselineEmpty', 'receiptCount', 'pendingDelegation',
|
|
74
|
+
'commitAuthorizationUnused', 'completionEvidenceAbsent'
|
|
75
|
+
];
|
|
76
|
+
function isRecovery(value) {
|
|
77
|
+
if (!hasExactKeys(value, [
|
|
78
|
+
'code', 'recoveredAt', 'previousStatus', 'previousPause', 'client', 'guards', 'resultingStatus'
|
|
79
|
+
]))
|
|
80
|
+
return false;
|
|
81
|
+
const guards = value.guards;
|
|
82
|
+
return value.code === 'CLIENT_CAPABILITY_ENABLED'
|
|
83
|
+
&& exactText(value.recoveredAt)
|
|
84
|
+
&& value.previousStatus === 'paused'
|
|
85
|
+
&& isPause(value.previousPause)
|
|
86
|
+
&& value.previousPause.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
|
|
87
|
+
&& value.client === 'claude-code'
|
|
88
|
+
&& hasExactKeys(guards, RECOVERY_GUARD_KEYS)
|
|
89
|
+
&& guards.stepCount === 0
|
|
90
|
+
&& guards.nextStage === null
|
|
91
|
+
&& guards.baselineEmpty === true
|
|
92
|
+
&& guards.receiptCount === 0
|
|
93
|
+
&& guards.pendingDelegation === false
|
|
94
|
+
&& guards.commitAuthorizationUnused === true
|
|
95
|
+
&& guards.completionEvidenceAbsent === true
|
|
96
|
+
&& value.resultingStatus === 'running';
|
|
97
|
+
}
|
|
98
|
+
function isCompletionEvidence(value) {
|
|
99
|
+
return hasExactKeys(value, [
|
|
100
|
+
'kind', 'observedAt', 'head', 'headTree', 'worktreeTree', 'lastReviewedCommit', 'prNumber', 'prHead'
|
|
101
|
+
])
|
|
102
|
+
&& value.kind === 'reviewed-head-clean'
|
|
103
|
+
&& exactText(value.observedAt)
|
|
104
|
+
&& exactText(value.head)
|
|
105
|
+
&& exactText(value.headTree)
|
|
106
|
+
&& exactText(value.worktreeTree)
|
|
107
|
+
&& exactText(value.lastReviewedCommit)
|
|
108
|
+
&& Number.isSafeInteger(value.prNumber)
|
|
109
|
+
&& value.prNumber > 0
|
|
110
|
+
&& exactText(value.prHead);
|
|
111
|
+
}
|
|
112
|
+
const ORCHESTRATION_RUN_KEYS = [
|
|
113
|
+
'taskId', 'runId', 'status', 'nextStage', 'stepCount', 'maxSteps', 'modelPolicy',
|
|
114
|
+
'modelPolicySource', 'recoveryHistory', 'baseline', 'pendingDelegation', 'receipts',
|
|
115
|
+
'pause', 'commitAuthorization', 'completionEvidence', 'createdAt', 'updatedAt'
|
|
116
|
+
];
|
|
117
|
+
function parseOrchestrationRun(value, expectedTaskId) {
|
|
118
|
+
if (!hasExactKeys(value, ORCHESTRATION_RUN_KEYS)
|
|
119
|
+
|| !exactText(value.taskId)
|
|
120
|
+
|| (expectedTaskId !== undefined && value.taskId !== expectedTaskId)
|
|
121
|
+
|| !exactText(value.runId)
|
|
122
|
+
|| !['running', 'paused', 'completed'].includes(value.status)
|
|
123
|
+
|| !(value.nextStage === null || ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code', 'commit'].includes(value.nextStage))
|
|
124
|
+
|| !Number.isSafeInteger(value.stepCount) || value.stepCount < 0
|
|
125
|
+
|| !Number.isSafeInteger(value.maxSteps) || value.maxSteps < 1
|
|
126
|
+
|| !isModelPolicy(value.modelPolicy)
|
|
127
|
+
|| !isModelPolicySource(value.modelPolicySource)
|
|
128
|
+
|| !Array.isArray(value.recoveryHistory) || !value.recoveryHistory.every(isRecovery)
|
|
129
|
+
|| typeof value.baseline !== 'string'
|
|
130
|
+
|| !(value.pendingDelegation === null || isDelegationReceipt(value.pendingDelegation))
|
|
131
|
+
|| !Array.isArray(value.receipts) || !value.receipts.every(isDelegationReceipt)
|
|
132
|
+
|| !(value.pause === null || isPause(value.pause))
|
|
133
|
+
|| !hasExactKeys(value.commitAuthorization, ['issuedAt', 'consumedAt'])
|
|
134
|
+
|| !nullableText(value.commitAuthorization.issuedAt)
|
|
135
|
+
|| !nullableText(value.commitAuthorization.consumedAt)
|
|
136
|
+
|| !(value.completionEvidence === null || isCompletionEvidence(value.completionEvidence))
|
|
137
|
+
|| !exactText(value.createdAt)
|
|
138
|
+
|| !exactText(value.updatedAt)) {
|
|
139
|
+
throw new OrchestrationStateError();
|
|
140
|
+
}
|
|
141
|
+
const receipts = [...value.receipts, ...(value.pendingDelegation ? [value.pendingDelegation] : [])];
|
|
142
|
+
if (receipts.some((receipt) => receipt.taskId !== value.taskId || receipt.runId !== value.runId)) {
|
|
143
|
+
throw new OrchestrationStateError();
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
32
147
|
function readRun(taskDir) {
|
|
33
148
|
const file = orchestrationPath(taskDir);
|
|
34
149
|
if (!fs.existsSync(file))
|
|
35
150
|
return null;
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
},
|
|
46
|
-
reviewer: {
|
|
47
|
-
model: run.modelPolicy.reviewer.model,
|
|
48
|
-
reasoningEffort: run.modelPolicy.reviewer.reasoningEffort
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
};
|
|
151
|
+
const taskId = path.basename(taskDir);
|
|
152
|
+
try {
|
|
153
|
+
return parseOrchestrationRun(JSON.parse(fs.readFileSync(file, 'utf8')), taskId);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (error instanceof OrchestrationStateError)
|
|
157
|
+
throw new OrchestrationStateError(taskId);
|
|
158
|
+
throw new OrchestrationStateError(taskId);
|
|
159
|
+
}
|
|
52
160
|
}
|
|
53
161
|
function atomicWrite(file, value) {
|
|
54
162
|
const temp = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
@@ -64,70 +172,11 @@ function withUpdatedRun(run, updates, now) {
|
|
|
64
172
|
function failed(code, message, taskId = null, details = {}) {
|
|
65
173
|
return { status: 'failed', changed: false, taskId, run: null, next: null, error: { code, message, ...details } };
|
|
66
174
|
}
|
|
67
|
-
function commitIntentFailed(code, message, taskId = null) {
|
|
68
|
-
return { status: 'failed', changed: false, taskId, intent: null, error: { code, message } };
|
|
69
|
-
}
|
|
70
|
-
function repositoryHead(repoRoot) {
|
|
71
|
-
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
72
|
-
cwd: repoRoot,
|
|
73
|
-
encoding: 'utf8',
|
|
74
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
75
|
-
}).trim();
|
|
76
|
-
}
|
|
77
175
|
function gitRootFor(stateRoot, options) {
|
|
78
176
|
if (!options.gitWorktreeRoot)
|
|
79
177
|
return stateRoot;
|
|
80
178
|
return assertGitRepositoryBinding(stateRoot, options.gitWorktreeRoot).worktreeRoot;
|
|
81
179
|
}
|
|
82
|
-
function isAncestor(repoRoot, ancestor, descendant) {
|
|
83
|
-
return spawnSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
|
|
84
|
-
cwd: repoRoot,
|
|
85
|
-
stdio: 'ignore'
|
|
86
|
-
}).status === 0;
|
|
87
|
-
}
|
|
88
|
-
function commitIntentView(intent, currentHead) {
|
|
89
|
-
return {
|
|
90
|
-
mode: intent.mode,
|
|
91
|
-
phase: intent.phase,
|
|
92
|
-
baselineHead: intent.baselineHead,
|
|
93
|
-
currentHead,
|
|
94
|
-
committedHead: intent.committedHead,
|
|
95
|
-
pushEvidence: intent.pushEvidence,
|
|
96
|
-
runId: intent.orchestration?.runId ?? null,
|
|
97
|
-
receiptId: intent.orchestration?.receiptId ?? null
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
function commitFinalizationView(inspection) {
|
|
101
|
-
return {
|
|
102
|
-
disposition: inspection.disposition,
|
|
103
|
-
code: inspection.code,
|
|
104
|
-
message: inspection.message,
|
|
105
|
-
currentHead: inspection.currentHead,
|
|
106
|
-
committedHead: inspection.committedHead,
|
|
107
|
-
needsAnchor: inspection.needsAnchor,
|
|
108
|
-
needsLog: inspection.needsLog,
|
|
109
|
-
attempt: inspection.attempt
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
function validCommitRun(run, taskId) {
|
|
113
|
-
return run !== null
|
|
114
|
-
&& [1, 2, 3].includes(run.schemaVersion)
|
|
115
|
-
&& run.taskId === taskId
|
|
116
|
-
&& ['running', 'paused', 'completed'].includes(run.status)
|
|
117
|
-
&& Array.isArray(run.receipts)
|
|
118
|
-
&& Object.prototype.hasOwnProperty.call(run, 'pendingDelegation')
|
|
119
|
-
&& typeof run.commitAuthorization === 'object'
|
|
120
|
-
&& run.commitAuthorization !== null
|
|
121
|
-
&& Object.prototype.hasOwnProperty.call(run.commitAuthorization, 'issuedAt')
|
|
122
|
-
&& Object.prototype.hasOwnProperty.call(run.commitAuthorization, 'consumedAt');
|
|
123
|
-
}
|
|
124
|
-
function mapCommitIntentError(error, taskId) {
|
|
125
|
-
if (error instanceof CommitIntentError)
|
|
126
|
-
return commitIntentFailed(error.code, error.message, taskId);
|
|
127
|
-
if (error instanceof TaskExecutionLockError)
|
|
128
|
-
return commitIntentFailed(error.code, error.message, taskId);
|
|
129
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_INVALID', error instanceof Error ? error.message : String(error), taskId);
|
|
130
|
-
}
|
|
131
180
|
function validModel(value) {
|
|
132
181
|
return typeof value === 'string' && value.length > 0 && value.trim() === value;
|
|
133
182
|
}
|
|
@@ -147,11 +196,8 @@ function sameModelPolicy(left, right) {
|
|
|
147
196
|
&& left.reviewer.model === right.reviewer.model
|
|
148
197
|
&& left.reviewer.reasoningEffort === right.reviewer.reasoningEffort;
|
|
149
198
|
}
|
|
150
|
-
function isV2Policy(policy) {
|
|
151
|
-
return typeof policy?.executor === 'object' && typeof policy?.reviewer === 'object';
|
|
152
|
-
}
|
|
153
199
|
function rolePolicy(run, role) {
|
|
154
|
-
return
|
|
200
|
+
return run.modelPolicy[role];
|
|
155
201
|
}
|
|
156
202
|
function resolveProjectPolicy(repoRoot, client) {
|
|
157
203
|
const configPath = path.join(repoRoot, '.agents', '.airc.json');
|
|
@@ -160,26 +206,10 @@ function resolveProjectPolicy(repoRoot, client) {
|
|
|
160
206
|
const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
161
207
|
return normalizeAgentClients(raw).state[client].orchestration;
|
|
162
208
|
}
|
|
163
|
-
function
|
|
164
|
-
return run.
|
|
165
|
-
&& run.status === 'paused'
|
|
209
|
+
function canRecoverClaudeCodeUnsupportedPause(run, taskDir) {
|
|
210
|
+
return run.status === 'paused'
|
|
166
211
|
&& run.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
|
|
167
|
-
&& run.modelPolicySource?.client === '
|
|
168
|
-
&& run.stepCount === 0
|
|
169
|
-
&& run.nextStage === null
|
|
170
|
-
&& run.baseline === ''
|
|
171
|
-
&& run.pendingDelegation === null
|
|
172
|
-
&& run.receipts.length === 0
|
|
173
|
-
&& run.commitAuthorization?.issuedAt === null
|
|
174
|
-
&& run.commitAuthorization?.consumedAt === null
|
|
175
|
-
&& run.completionEvidence == null
|
|
176
|
-
&& !fs.existsSync(commitIntentPath(taskDir))
|
|
177
|
-
&& (run.recoveryHistory ?? []).every((entry) => entry.code === 'MODEL_POLICY_SUPPLEMENTED');
|
|
178
|
-
}
|
|
179
|
-
function canMigrateRecoverableV2Pause(run, taskDir) {
|
|
180
|
-
return run.schemaVersion === 2
|
|
181
|
-
&& run.status === 'paused'
|
|
182
|
-
&& run.pause?.recoverable === true
|
|
212
|
+
&& run.modelPolicySource?.client === 'claude-code'
|
|
183
213
|
&& run.stepCount === 0
|
|
184
214
|
&& run.nextStage === null
|
|
185
215
|
&& run.baseline === ''
|
|
@@ -188,7 +218,7 @@ function canMigrateRecoverableV2Pause(run, taskDir) {
|
|
|
188
218
|
&& run.commitAuthorization?.issuedAt === null
|
|
189
219
|
&& run.commitAuthorization?.consumedAt === null
|
|
190
220
|
&& run.completionEvidence == null
|
|
191
|
-
&&
|
|
221
|
+
&& run.recoveryHistory.length === 0;
|
|
192
222
|
}
|
|
193
223
|
function beginOrResumeOrchestration(taskRef, options = {}) {
|
|
194
224
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
@@ -205,193 +235,57 @@ function beginOrResumeOrchestration(taskRef, options = {}) {
|
|
|
205
235
|
return failed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
206
236
|
}
|
|
207
237
|
if (existing) {
|
|
208
|
-
if (
|
|
209
|
-
|| !Array.isArray(existing.receipts)
|
|
210
|
-
|| !Object.prototype.hasOwnProperty.call(existing, 'pendingDelegation')) {
|
|
211
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'orchestration state has an invalid schema', resolved.taskId);
|
|
212
|
-
}
|
|
213
|
-
if (existing.status === 'completed' && existing.schemaVersion === 1) {
|
|
238
|
+
if (existing.status === 'completed') {
|
|
214
239
|
return { status: 'completed', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
215
240
|
}
|
|
216
|
-
if (existing.
|
|
217
|
-
|
|
218
|
-
if (policyError
|
|
219
|
-
|| !existing.modelPolicySource
|
|
220
|
-
|| !isAgentClientId(existing.modelPolicySource.client)
|
|
221
|
-
|| !Array.isArray(existing.recoveryHistory)) {
|
|
222
|
-
return failed('ORCHESTRATION_STATE_INVALID', `schemaVersion ${existing.schemaVersion} orchestration state is incomplete`, resolved.taskId);
|
|
223
|
-
}
|
|
224
|
-
if (existing.status === 'completed') {
|
|
225
|
-
return { status: 'completed', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
226
|
-
}
|
|
227
|
-
if (existing.schemaVersion === 2 && existing.status === 'running') {
|
|
228
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'active schemaVersion 2 runs cannot be advanced by lifecycle protocol v3', resolved.taskId);
|
|
229
|
-
}
|
|
230
|
-
if (existing.modelPolicySource.client !== options.client) {
|
|
231
|
-
return failed('ORCHESTRATION_CLIENT_MISMATCH', 'provided client does not match the persisted run policy source', resolved.taskId);
|
|
232
|
-
}
|
|
233
|
-
if (options.modelPolicy) {
|
|
234
|
-
const suppliedError = validateModelPolicy(options.modelPolicy);
|
|
235
|
-
if (suppliedError)
|
|
236
|
-
return failed(suppliedError.code, suppliedError.message, resolved.taskId);
|
|
237
|
-
if (!sameModelPolicy(existing.modelPolicy, options.modelPolicy)) {
|
|
238
|
-
return failed('ORCHESTRATION_MODEL_POLICY_MISMATCH', 'provided model policy does not match the persisted run policy', resolved.taskId);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
if (existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED') {
|
|
242
|
-
if (!canRecoverCodexUnsupportedPause(existing, resolved.taskDir)) {
|
|
243
|
-
return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
244
|
-
}
|
|
245
|
-
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
246
|
-
const previousPause = existing.pause;
|
|
247
|
-
const recovery = {
|
|
248
|
-
code: 'CLIENT_CAPABILITY_ENABLED',
|
|
249
|
-
recoveredAt: now,
|
|
250
|
-
previousSchemaVersion: 2,
|
|
251
|
-
previousStatus: 'paused',
|
|
252
|
-
previousPause,
|
|
253
|
-
client: 'codex',
|
|
254
|
-
guards: {
|
|
255
|
-
stepCount: 0,
|
|
256
|
-
nextStage: null,
|
|
257
|
-
baselineEmpty: true,
|
|
258
|
-
receiptCount: 0,
|
|
259
|
-
pendingDelegation: false,
|
|
260
|
-
commitAuthorizationUnused: true,
|
|
261
|
-
completionEvidenceAbsent: true,
|
|
262
|
-
commitIntentAbsent: true
|
|
263
|
-
},
|
|
264
|
-
resultingStatus: 'running'
|
|
265
|
-
};
|
|
266
|
-
const resumed = withUpdatedRun(existing, {
|
|
267
|
-
schemaVersion: 3,
|
|
268
|
-
status: 'running',
|
|
269
|
-
pause: null,
|
|
270
|
-
recoveryHistory: Object.freeze([...(existing.recoveryHistory ?? []), recovery])
|
|
271
|
-
}, () => now);
|
|
272
|
-
saveRun(resolved.taskDir, resumed);
|
|
273
|
-
return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
|
|
274
|
-
}
|
|
275
|
-
if (existing.status === 'paused' && existing.pause?.recoverable && existing.pendingDelegation === null) {
|
|
276
|
-
if (existing.schemaVersion === 2 && !canMigrateRecoverableV2Pause(existing, resolved.taskDir)) {
|
|
277
|
-
const paused = withUpdatedRun(existing, {
|
|
278
|
-
pause: {
|
|
279
|
-
code: 'ORCHESTRATION_SCHEMA_MIGRATION_REQUIRED',
|
|
280
|
-
message: 'schemaVersion 2 recoverable pause contains execution evidence and cannot migrate automatically',
|
|
281
|
-
recoverable: false
|
|
282
|
-
}
|
|
283
|
-
});
|
|
284
|
-
saveRun(resolved.taskDir, paused);
|
|
285
|
-
return { status: 'paused', changed: true, taskId: resolved.taskId, run: paused, next: null, error: null };
|
|
286
|
-
}
|
|
287
|
-
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
288
|
-
const recovery = existing.schemaVersion === 2 ? {
|
|
289
|
-
code: 'SCHEMA_V3_MIGRATED',
|
|
290
|
-
recoveredAt: now,
|
|
291
|
-
previousSchemaVersion: 2,
|
|
292
|
-
previousStatus: 'paused',
|
|
293
|
-
previousPause: existing.pause,
|
|
294
|
-
guards: {
|
|
295
|
-
stepCount: 0,
|
|
296
|
-
nextStage: null,
|
|
297
|
-
baselineEmpty: true,
|
|
298
|
-
receiptCount: 0,
|
|
299
|
-
pendingDelegation: false,
|
|
300
|
-
commitAuthorizationUnused: true,
|
|
301
|
-
completionEvidenceAbsent: true,
|
|
302
|
-
commitIntentAbsent: true
|
|
303
|
-
},
|
|
304
|
-
resultingStatus: 'running'
|
|
305
|
-
} : null;
|
|
306
|
-
const resumed = withUpdatedRun(existing, {
|
|
307
|
-
schemaVersion: 3,
|
|
308
|
-
status: 'running',
|
|
309
|
-
pause: null,
|
|
310
|
-
...(recovery ? { recoveryHistory: Object.freeze([...(existing.recoveryHistory ?? []), recovery]) } : {})
|
|
311
|
-
}, () => now);
|
|
312
|
-
saveRun(resolved.taskDir, resumed);
|
|
313
|
-
return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
|
|
314
|
-
}
|
|
315
|
-
return { status: existing.status, changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
241
|
+
if (existing.modelPolicySource.client !== options.client) {
|
|
242
|
+
return failed('ORCHESTRATION_CLIENT_MISMATCH', 'provided client does not match the persisted run policy source', resolved.taskId);
|
|
316
243
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
catch (error) {
|
|
325
|
-
return failed('ORCHESTRATION_CONFIG_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
244
|
+
if (options.modelPolicy) {
|
|
245
|
+
const suppliedError = validateModelPolicy(options.modelPolicy);
|
|
246
|
+
if (suppliedError)
|
|
247
|
+
return failed(suppliedError.code, suppliedError.message, resolved.taskId);
|
|
248
|
+
if (!sameModelPolicy(existing.modelPolicy, options.modelPolicy)) {
|
|
249
|
+
return failed('ORCHESTRATION_MODEL_POLICY_MISMATCH', 'provided model policy does not match the persisted run policy', resolved.taskId);
|
|
326
250
|
}
|
|
327
251
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
client: options.client,
|
|
332
|
-
missingFields: ['executor.model', 'executor.reasoningEffort', 'reviewer.model', 'reviewer.reasoningEffort'],
|
|
333
|
-
modelSelectionContext: getAgentClientModelSelection(options.client)
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
if (existing.pendingDelegation !== null) {
|
|
337
|
-
const alreadyPaused = existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_DELEGATION_BUSY';
|
|
338
|
-
if (alreadyPaused)
|
|
339
|
-
return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
340
|
-
const paused = withUpdatedRun(existing, {
|
|
341
|
-
status: 'paused',
|
|
342
|
-
pause: { code: 'ORCHESTRATION_DELEGATION_BUSY', message: 'legacy run has a pending delegation', recoverable: false }
|
|
343
|
-
});
|
|
344
|
-
saveRun(resolved.taskDir, paused);
|
|
345
|
-
return { status: 'paused', changed: true, taskId: resolved.taskId, run: paused, next: null, error: null };
|
|
346
|
-
}
|
|
347
|
-
if (existing.receipts.length > 0) {
|
|
348
|
-
const alreadyPaused = existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_HISTORICAL_EFFORT_UNVERIFIED';
|
|
349
|
-
if (alreadyPaused)
|
|
252
|
+
if (existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED') {
|
|
253
|
+
if (existing.modelPolicySource.client !== 'claude-code'
|
|
254
|
+
|| !canRecoverClaudeCodeUnsupportedPause(existing, resolved.taskDir)) {
|
|
350
255
|
return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
256
|
+
}
|
|
257
|
+
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
258
|
+
const recovery = {
|
|
259
|
+
code: 'CLIENT_CAPABILITY_ENABLED',
|
|
260
|
+
recoveredAt: now,
|
|
261
|
+
previousStatus: 'paused',
|
|
262
|
+
previousPause: existing.pause,
|
|
263
|
+
client: 'claude-code',
|
|
264
|
+
guards: {
|
|
265
|
+
stepCount: 0,
|
|
266
|
+
nextStage: null,
|
|
267
|
+
baselineEmpty: true,
|
|
268
|
+
receiptCount: 0,
|
|
269
|
+
pendingDelegation: false,
|
|
270
|
+
commitAuthorizationUnused: true,
|
|
271
|
+
completionEvidenceAbsent: true
|
|
272
|
+
},
|
|
273
|
+
resultingStatus: 'running'
|
|
274
|
+
};
|
|
275
|
+
const resumed = withUpdatedRun(existing, {
|
|
276
|
+
status: 'running',
|
|
277
|
+
pause: null,
|
|
278
|
+
recoveryHistory: Object.freeze([...existing.recoveryHistory, recovery])
|
|
279
|
+
}, () => now);
|
|
280
|
+
saveRun(resolved.taskDir, resumed);
|
|
281
|
+
return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
|
|
361
282
|
}
|
|
362
|
-
if (existing.
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
return failed('ORCHESTRATION_MODEL_POLICY_MISMATCH', 'provided model policy does not match the persisted run policy', resolved.taskId);
|
|
283
|
+
if (existing.status === 'paused' && existing.pause?.recoverable && existing.pendingDelegation === null) {
|
|
284
|
+
const resumed = withUpdatedRun(existing, { status: 'running', pause: null }, options.now);
|
|
285
|
+
saveRun(resolved.taskDir, resumed);
|
|
286
|
+
return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
|
|
367
287
|
}
|
|
368
|
-
|
|
369
|
-
const source = { kind: sourceKind, client: options.client, resolvedAt: now };
|
|
370
|
-
const clearsModelPause = existing.pause?.code === 'ORCHESTRATION_MODEL_EVIDENCE_MISSING';
|
|
371
|
-
const resultingStatus = clearsModelPause ? 'running' : existing.status;
|
|
372
|
-
const recovery = {
|
|
373
|
-
code: 'MODEL_POLICY_SUPPLEMENTED',
|
|
374
|
-
recoveredAt: now,
|
|
375
|
-
previousSchemaVersion: 1,
|
|
376
|
-
previousStatus: existing.status,
|
|
377
|
-
previousPause: existing.pause,
|
|
378
|
-
policySource: source,
|
|
379
|
-
receiptCount: 0,
|
|
380
|
-
pendingDelegation: false,
|
|
381
|
-
resultingStatus
|
|
382
|
-
};
|
|
383
|
-
const recovered = Object.freeze({
|
|
384
|
-
...existing,
|
|
385
|
-
schemaVersion: 3,
|
|
386
|
-
status: resultingStatus,
|
|
387
|
-
modelPolicy: policy,
|
|
388
|
-
modelPolicySource: source,
|
|
389
|
-
recoveryHistory: Object.freeze([recovery]),
|
|
390
|
-
pause: clearsModelPause ? null : existing.pause,
|
|
391
|
-
updatedAt: now
|
|
392
|
-
});
|
|
393
|
-
saveRun(resolved.taskDir, recovered);
|
|
394
|
-
return { status: resultingStatus, changed: true, taskId: resolved.taskId, run: recovered, next: null, error: null };
|
|
288
|
+
return { status: existing.status, changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
395
289
|
}
|
|
396
290
|
let policy = options.modelPolicy;
|
|
397
291
|
let sourceKind = 'explicit';
|
|
@@ -414,7 +308,6 @@ function beginOrResumeOrchestration(taskRef, options = {}) {
|
|
|
414
308
|
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
415
309
|
const source = { kind: sourceKind, client: options.client, resolvedAt: now };
|
|
416
310
|
const run = {
|
|
417
|
-
schemaVersion: 3,
|
|
418
311
|
taskId: resolved.taskId,
|
|
419
312
|
runId: (options.id ?? randomUUID)(),
|
|
420
313
|
status: 'running',
|
|
@@ -656,401 +549,6 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
656
549
|
}
|
|
657
550
|
return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
|
|
658
551
|
}
|
|
659
|
-
function appendCommitActivity(resolved, step, agent, note) {
|
|
660
|
-
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
661
|
-
const activity = locateActivityLog(taskContent);
|
|
662
|
-
if (!activity)
|
|
663
|
-
return { code: 'ORCHESTRATION_TASK_INVALID', message: 'task activity log is missing or ambiguous' };
|
|
664
|
-
const metadata = captureTaskWriteMetadata();
|
|
665
|
-
const written = writeTask({
|
|
666
|
-
taskRef: resolved.taskId,
|
|
667
|
-
expectedState: 'active',
|
|
668
|
-
mutations: [{
|
|
669
|
-
kind: 'section',
|
|
670
|
-
aliases: ['活动日志', 'Activity Log'],
|
|
671
|
-
heading: activity.heading,
|
|
672
|
-
body: appendActivityEntry(activity, { time: metadata.timestamp, step, agent, note })
|
|
673
|
-
}]
|
|
674
|
-
}, { repoRoot: resolved.repoRoot, metadataProvider: () => metadata });
|
|
675
|
-
return written.status === 'failed' ? written.error : null;
|
|
676
|
-
}
|
|
677
|
-
function startCommitAttempt(taskRef, input, options = {}) {
|
|
678
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
679
|
-
if (!resolved.ok)
|
|
680
|
-
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
681
|
-
try {
|
|
682
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
683
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.start', () => {
|
|
684
|
-
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
685
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
|
|
686
|
-
}
|
|
687
|
-
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
688
|
-
const activity = locateActivityLog(taskContent);
|
|
689
|
-
if (!activity)
|
|
690
|
-
return commitIntentFailed('ORCHESTRATION_TASK_INVALID', 'task activity log is missing or ambiguous', resolved.taskId);
|
|
691
|
-
const open = pairEntries(activity.entries).filter((row) => row.step === 'Commit' && row.started !== '' && row.done === '');
|
|
692
|
-
if (open.length > 0) {
|
|
693
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_ATTEMPT_BUSY', 'an open Commit activity entry already exists', resolved.taskId);
|
|
694
|
-
}
|
|
695
|
-
const currentHead = repositoryHead(gitRoot);
|
|
696
|
-
const attempt = (options.id ?? randomUUID)();
|
|
697
|
-
const error = appendCommitActivity(resolved, 'Commit [started]', input.agent, commitAttemptStartedNote({
|
|
698
|
-
attempt,
|
|
699
|
-
baseline: currentHead,
|
|
700
|
-
agent: input.agent
|
|
701
|
-
}));
|
|
702
|
-
if (error)
|
|
703
|
-
return commitIntentFailed(error.code, error.message, resolved.taskId);
|
|
704
|
-
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
705
|
-
return {
|
|
706
|
-
status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
|
|
707
|
-
finalization: commitFinalizationView(inspection), error: null
|
|
708
|
-
};
|
|
709
|
-
});
|
|
710
|
-
}
|
|
711
|
-
catch (error) {
|
|
712
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
function terminateCommitAttempt(taskRef, input, options = {}) {
|
|
716
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
717
|
-
if (!resolved.ok)
|
|
718
|
-
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
719
|
-
try {
|
|
720
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
721
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.terminate', () => {
|
|
722
|
-
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
723
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'an active commit intent must be aborted or recovered first', resolved.taskId);
|
|
724
|
-
}
|
|
725
|
-
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
726
|
-
if (inspection.disposition !== 'retryable-start'
|
|
727
|
-
|| inspection.attempt?.attempt !== input.attempt
|
|
728
|
-
|| inspection.attempt.agent !== input.agent
|
|
729
|
-
|| inspection.attempt.baseline !== inspection.currentHead) {
|
|
730
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit attempt identity or repository HEAD has drifted', resolved.taskId);
|
|
731
|
-
}
|
|
732
|
-
const error = appendCommitActivity(resolved, 'Commit [aborted]', input.agent, `aborted; attempt=${input.attempt}; code=${input.code}`);
|
|
733
|
-
if (error)
|
|
734
|
-
return commitIntentFailed(error.code, error.message, resolved.taskId);
|
|
735
|
-
return {
|
|
736
|
-
status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
|
|
737
|
-
finalization: commitFinalizationView(inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId)),
|
|
738
|
-
error: null
|
|
739
|
-
};
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
catch (error) {
|
|
743
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
function beginCommitIntent(taskRef, input, options = {}) {
|
|
747
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
748
|
-
if (!resolved.ok)
|
|
749
|
-
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
750
|
-
try {
|
|
751
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
752
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.begin', () => {
|
|
753
|
-
const currentHead = repositoryHead(gitRoot);
|
|
754
|
-
if (currentHead !== input.baselineHead) {
|
|
755
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_BASELINE_MISMATCH', 'baseline HEAD does not match the repository HEAD', resolved.taskId);
|
|
756
|
-
}
|
|
757
|
-
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
758
|
-
try {
|
|
759
|
-
readCommitIntent(resolved.taskDir, resolved.taskId);
|
|
760
|
-
}
|
|
761
|
-
catch (error) {
|
|
762
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
763
|
-
}
|
|
764
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
|
|
765
|
-
}
|
|
766
|
-
const attemptInspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
767
|
-
if (attemptInspection.disposition !== 'retryable-start'
|
|
768
|
-
|| attemptInspection.attempt?.attempt !== input.attempt
|
|
769
|
-
|| attemptInspection.attempt.agent !== input.agent
|
|
770
|
-
|| attemptInspection.attempt.baseline !== input.baselineHead) {
|
|
771
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_ATTEMPT_MISMATCH', 'commit begin requires one matching retryable Commit attempt', resolved.taskId);
|
|
772
|
-
}
|
|
773
|
-
const runFile = orchestrationPath(resolved.taskDir);
|
|
774
|
-
let run = null;
|
|
775
|
-
let sourceRunBytes = null;
|
|
776
|
-
if (fs.existsSync(runFile)) {
|
|
777
|
-
sourceRunBytes = fs.readFileSync(runFile, 'utf8');
|
|
778
|
-
try {
|
|
779
|
-
run = readRun(resolved.taskDir);
|
|
780
|
-
}
|
|
781
|
-
catch (error) {
|
|
782
|
-
return commitIntentFailed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
783
|
-
}
|
|
784
|
-
if (!validCommitRun(run, resolved.taskId)) {
|
|
785
|
-
return commitIntentFailed('ORCHESTRATION_STATE_INVALID', 'orchestration state has an invalid schema', resolved.taskId);
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
789
|
-
const failAndPause = (code, message) => {
|
|
790
|
-
if (run?.status === 'running') {
|
|
791
|
-
saveRun(resolved.taskDir, withUpdatedRun(run, {
|
|
792
|
-
status: 'paused', pause: { code, message, recoverable: true }
|
|
793
|
-
}, () => now));
|
|
794
|
-
}
|
|
795
|
-
return commitIntentFailed(code, message, resolved.taskId);
|
|
796
|
-
};
|
|
797
|
-
let orchestration = null;
|
|
798
|
-
if (!input.orchestrated) {
|
|
799
|
-
if (run?.pendingDelegation) {
|
|
800
|
-
return commitIntentFailed('ORCHESTRATION_STANDALONE_BUSY', 'standalone commit is blocked by a pending orchestration delegation', resolved.taskId);
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
else {
|
|
804
|
-
if (!run || run.status !== 'running') {
|
|
805
|
-
return commitIntentFailed('ORCHESTRATION_RUN_NOT_RUNNING', 'orchestrated commit requires a running orchestration', resolved.taskId);
|
|
806
|
-
}
|
|
807
|
-
const receipt = run.pendingDelegation;
|
|
808
|
-
if (!receipt
|
|
809
|
-
|| receipt.status !== 'activated'
|
|
810
|
-
|| receipt.stage !== 'commit'
|
|
811
|
-
|| receipt.round !== 1
|
|
812
|
-
|| receipt.artifact !== 'commit'
|
|
813
|
-
|| receipt.role !== 'executor') {
|
|
814
|
-
return failAndPause('ORCHESTRATION_PROVENANCE_MISMATCH', 'orchestrated commit requires one matching activated commit delegation');
|
|
815
|
-
}
|
|
816
|
-
if (!run.commitAuthorization.issuedAt || run.commitAuthorization.consumedAt) {
|
|
817
|
-
return failAndPause('ORCHESTRATION_COMMIT_AUTHORIZATION_INVALID', 'orchestrated commit requires an unconsumed one-use authorization');
|
|
818
|
-
}
|
|
819
|
-
const planned = planOrchestrationStageCompletion(taskRef, {
|
|
820
|
-
stage: 'commit', round: 1, artifact: 'commit', role: 'executor', agent: input.agent
|
|
821
|
-
}, { ...options, now: () => now });
|
|
822
|
-
if (planned.result.status === 'failed' || !planned.plan?.updatedRun.pendingDelegation) {
|
|
823
|
-
return failAndPause(planned.result.error?.code ?? 'ORCHESTRATION_PROVENANCE_MISMATCH', planned.result.error?.message ?? 'commit delegation could not be completed');
|
|
824
|
-
}
|
|
825
|
-
const plannedRunBytes = serialize(planned.plan.updatedRun);
|
|
826
|
-
orchestration = {
|
|
827
|
-
runId: run.runId,
|
|
828
|
-
receiptId: receipt.id,
|
|
829
|
-
authorizationIssuedAt: run.commitAuthorization.issuedAt,
|
|
830
|
-
sourceRunDigest: digest(sourceRunBytes),
|
|
831
|
-
plannedRunDigest: digest(plannedRunBytes),
|
|
832
|
-
completionUpdatedAt: planned.plan.updatedRun.updatedAt,
|
|
833
|
-
plannedReceipt: planned.plan.updatedRun.pendingDelegation
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
|
-
const created = createCommitIntent(resolved.taskDir, {
|
|
837
|
-
taskId: resolved.taskId,
|
|
838
|
-
mode: input.orchestrated ? 'orchestrated' : 'standalone',
|
|
839
|
-
phase: 'prepared',
|
|
840
|
-
baselineHead: input.baselineHead,
|
|
841
|
-
committedHead: null,
|
|
842
|
-
pushEvidence: null,
|
|
843
|
-
orchestration,
|
|
844
|
-
createdAt: now,
|
|
845
|
-
updatedAt: now
|
|
846
|
-
}, { token: options.token });
|
|
847
|
-
return {
|
|
848
|
-
status: 'ready', changed: true, taskId: resolved.taskId,
|
|
849
|
-
intent: commitIntentView(created.intent, currentHead), token: created.token, error: null
|
|
850
|
-
};
|
|
851
|
-
});
|
|
852
|
-
}
|
|
853
|
-
catch (error) {
|
|
854
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
function checkpointCommitIntent(taskRef, input, options = {}) {
|
|
858
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
859
|
-
if (!resolved.ok)
|
|
860
|
-
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
861
|
-
try {
|
|
862
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
863
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.checkpoint', () => {
|
|
864
|
-
const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
865
|
-
const currentHead = repositoryHead(gitRoot);
|
|
866
|
-
if (currentHead !== input.head) {
|
|
867
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'checkpoint HEAD does not match the repository HEAD', resolved.taskId);
|
|
868
|
-
}
|
|
869
|
-
if (input.kind === 'committed' && !isAncestor(gitRoot, intent.baselineHead, input.head)) {
|
|
870
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'committed HEAD does not descend from the baseline', resolved.taskId);
|
|
871
|
-
}
|
|
872
|
-
if (input.kind === 'pushed') {
|
|
873
|
-
const expectedHead = intent.committedHead ?? intent.baselineHead;
|
|
874
|
-
if (input.head !== expectedHead || !input.remote || !input.ref) {
|
|
875
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_STATE_INVALID', 'pushed checkpoint requires matching remote, ref and HEAD', resolved.taskId);
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
879
|
-
const updated = updateCommitIntent(resolved.taskDir, resolved.taskId, input.token, input.kind === 'committed'
|
|
880
|
-
? { phase: 'committed', committedHead: input.head, updatedAt: now }
|
|
881
|
-
: {
|
|
882
|
-
phase: 'pushed', committedHead: intent.committedHead ?? intent.baselineHead, updatedAt: now,
|
|
883
|
-
pushEvidence: { remote: input.remote, ref: input.ref, head: input.head }
|
|
884
|
-
});
|
|
885
|
-
return {
|
|
886
|
-
status: 'ready', changed: true, taskId: resolved.taskId,
|
|
887
|
-
intent: commitIntentView(updated, currentHead), error: null
|
|
888
|
-
};
|
|
889
|
-
});
|
|
890
|
-
}
|
|
891
|
-
catch (error) {
|
|
892
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
function completeCommitOrchestration(resolved, intent, agent) {
|
|
896
|
-
if (intent.orchestration === null)
|
|
897
|
-
return null;
|
|
898
|
-
if (intent.orchestration.plannedReceipt.agent !== agent) {
|
|
899
|
-
return { code: 'ORCHESTRATION_PROVENANCE_MISMATCH', message: 'completion agent does not match the planned receipt' };
|
|
900
|
-
}
|
|
901
|
-
const runFile = orchestrationPath(resolved.taskDir);
|
|
902
|
-
if (!fs.existsSync(runFile)) {
|
|
903
|
-
return { code: 'ORCHESTRATION_RUN_MISSING', message: 'orchestration run disappeared' };
|
|
904
|
-
}
|
|
905
|
-
const currentBytes = fs.readFileSync(runFile, 'utf8');
|
|
906
|
-
const currentDigest = digest(currentBytes);
|
|
907
|
-
if (currentDigest === intent.orchestration.sourceRunDigest) {
|
|
908
|
-
const run = readRun(resolved.taskDir);
|
|
909
|
-
if (!run)
|
|
910
|
-
return { code: 'ORCHESTRATION_RUN_MISSING', message: 'orchestration run disappeared' };
|
|
911
|
-
const plannedRun = Object.freeze({
|
|
912
|
-
...run,
|
|
913
|
-
pendingDelegation: intent.orchestration.plannedReceipt,
|
|
914
|
-
updatedAt: intent.orchestration.completionUpdatedAt
|
|
915
|
-
});
|
|
916
|
-
if (digest(serialize(plannedRun)) !== intent.orchestration.plannedRunDigest) {
|
|
917
|
-
return { code: 'ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', message: 'planned orchestration bytes no longer match the intent' };
|
|
918
|
-
}
|
|
919
|
-
atomicWrite(runFile, plannedRun);
|
|
920
|
-
if (digest(fs.readFileSync(runFile)) !== intent.orchestration.plannedRunDigest) {
|
|
921
|
-
return { code: 'ORCHESTRATION_COMMIT_COMPLETE_PARTIAL', message: 'orchestration completion could not be verified' };
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
else if (currentDigest !== intent.orchestration.plannedRunDigest) {
|
|
925
|
-
return { code: 'ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', message: 'orchestration state changed after commit begin' };
|
|
926
|
-
}
|
|
927
|
-
return null;
|
|
928
|
-
}
|
|
929
|
-
function finalizeCommitIntentUnlocked(resolved, inspection, agent, remove) {
|
|
930
|
-
if (inspection.disposition !== 'recoverable' || !inspection.intent) {
|
|
931
|
-
return commitIntentFailed(inspection.code ?? 'COMMIT_FINALIZATION_PENDING', inspection.message, resolved.taskId);
|
|
932
|
-
}
|
|
933
|
-
if (inspection.intent.orchestration !== null
|
|
934
|
-
&& inspection.intent.orchestration.plannedReceipt.agent !== agent) {
|
|
935
|
-
return commitIntentFailed('ORCHESTRATION_PROVENANCE_MISMATCH', 'completion agent does not match the planned receipt', resolved.taskId);
|
|
936
|
-
}
|
|
937
|
-
const metadata = captureTaskWriteMetadata();
|
|
938
|
-
const taskPlan = planCommitTaskFinalization(resolved.taskDir, inspection, agent, metadata.timestamp);
|
|
939
|
-
const written = writeTask({
|
|
940
|
-
taskRef: resolved.taskId,
|
|
941
|
-
expectedState: 'active',
|
|
942
|
-
mutations: taskPlan.mutations
|
|
943
|
-
}, {
|
|
944
|
-
repoRoot: resolved.repoRoot,
|
|
945
|
-
metadataProvider: () => metadata
|
|
946
|
-
});
|
|
947
|
-
if (written.status === 'failed') {
|
|
948
|
-
return commitIntentFailed(written.error.code, written.error.message, resolved.taskId);
|
|
949
|
-
}
|
|
950
|
-
const orchestrationError = completeCommitOrchestration(resolved, inspection.intent, agent);
|
|
951
|
-
if (orchestrationError) {
|
|
952
|
-
return commitIntentFailed(orchestrationError.code, orchestrationError.message, resolved.taskId);
|
|
953
|
-
}
|
|
954
|
-
try {
|
|
955
|
-
remove();
|
|
956
|
-
}
|
|
957
|
-
catch (error) {
|
|
958
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_COMPLETE_PARTIAL', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
959
|
-
}
|
|
960
|
-
return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
|
|
961
|
-
}
|
|
962
|
-
function completeCommitIntent(taskRef, input, options = {}) {
|
|
963
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
964
|
-
if (!resolved.ok)
|
|
965
|
-
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
966
|
-
try {
|
|
967
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
968
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.complete', () => {
|
|
969
|
-
readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
970
|
-
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
971
|
-
return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
|
|
972
|
-
removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
973
|
-
});
|
|
974
|
-
});
|
|
975
|
-
}
|
|
976
|
-
catch (error) {
|
|
977
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
function recoverCommitIntent(taskRef, input, options = {}) {
|
|
981
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
982
|
-
if (!resolved.ok)
|
|
983
|
-
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
984
|
-
try {
|
|
985
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
986
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.recover', () => {
|
|
987
|
-
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
988
|
-
if (inspection.disposition === 'retryable-start') {
|
|
989
|
-
return {
|
|
990
|
-
status: 'ready', changed: false, taskId: resolved.taskId, intent: null,
|
|
991
|
-
finalization: commitFinalizationView(inspection), error: null
|
|
992
|
-
};
|
|
993
|
-
}
|
|
994
|
-
if (inspection.disposition === 'prepared' && inspection.intentDigest) {
|
|
995
|
-
removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest);
|
|
996
|
-
return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
|
|
997
|
-
}
|
|
998
|
-
if (!inspection.intentDigest) {
|
|
999
|
-
return commitIntentFailed(inspection.code ?? 'COMMIT_FINALIZATION_EVIDENCE_MISSING', inspection.message, resolved.taskId);
|
|
1000
|
-
}
|
|
1001
|
-
return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
|
|
1002
|
-
removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest);
|
|
1003
|
-
});
|
|
1004
|
-
});
|
|
1005
|
-
}
|
|
1006
|
-
catch (error) {
|
|
1007
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
function abortCommitIntent(taskRef, input, options = {}) {
|
|
1011
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1012
|
-
if (!resolved.ok)
|
|
1013
|
-
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1014
|
-
try {
|
|
1015
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1016
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.abort', () => {
|
|
1017
|
-
const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1018
|
-
const currentHead = repositoryHead(gitRoot);
|
|
1019
|
-
if (intent.phase !== 'prepared'
|
|
1020
|
-
|| intent.pushEvidence !== null
|
|
1021
|
-
|| input.expectedHead !== intent.baselineHead
|
|
1022
|
-
|| currentHead !== intent.baselineHead) {
|
|
1023
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit intent has side effects or repository drift', resolved.taskId);
|
|
1024
|
-
}
|
|
1025
|
-
removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1026
|
-
return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
catch (error) {
|
|
1030
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
function statusCommitIntent(taskRef, options = {}) {
|
|
1034
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1035
|
-
if (!resolved.ok)
|
|
1036
|
-
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1037
|
-
let gitRoot;
|
|
1038
|
-
try {
|
|
1039
|
-
gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1040
|
-
}
|
|
1041
|
-
catch (error) {
|
|
1042
|
-
return commitIntentFailed('ORCHESTRATION_WORKTREE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
1043
|
-
}
|
|
1044
|
-
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
1045
|
-
return {
|
|
1046
|
-
status: 'ready',
|
|
1047
|
-
changed: false,
|
|
1048
|
-
taskId: resolved.taskId,
|
|
1049
|
-
intent: inspection.intent ? commitIntentView(inspection.intent, inspection.currentHead) : null,
|
|
1050
|
-
finalization: commitFinalizationView(inspection),
|
|
1051
|
-
error: null
|
|
1052
|
-
};
|
|
1053
|
-
}
|
|
1054
552
|
function statusOrchestration(taskRef, options = {}) {
|
|
1055
553
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1056
554
|
if (!resolved.ok)
|
|
@@ -1064,26 +562,12 @@ function prepareOrchestrationDelegationUnlocked(taskRef, input, options = {}) {
|
|
|
1064
562
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1065
563
|
if (!resolved.ok)
|
|
1066
564
|
return failed(resolved.code, resolved.message, resolved.taskId);
|
|
1067
|
-
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
1068
|
-
try {
|
|
1069
|
-
readCommitIntent(resolved.taskDir, resolved.taskId);
|
|
1070
|
-
}
|
|
1071
|
-
catch (error) {
|
|
1072
|
-
return failed('ORCHESTRATION_COMMIT_INTENT_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
1073
|
-
}
|
|
1074
|
-
return failed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent blocks delegation preparation', resolved.taskId);
|
|
1075
|
-
}
|
|
1076
565
|
if (!(options.supportsLifecycleDelegation ?? supportsLifecycleDelegation)(input.client)) {
|
|
1077
566
|
return failed('ORCHESTRATION_CLIENT_UNSUPPORTED', `client '${input.client}' does not support lifecycle orchestration`, resolved.taskId);
|
|
1078
567
|
}
|
|
1079
568
|
const run = readRun(resolved.taskDir);
|
|
1080
569
|
if (!run || run.status !== 'running')
|
|
1081
570
|
return failed('ORCHESTRATION_RUN_NOT_RUNNING', 'a running orchestration is required', resolved.taskId);
|
|
1082
|
-
if (run.schemaVersion !== 3) {
|
|
1083
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'delegation preparation requires orchestration schemaVersion 3', resolved.taskId);
|
|
1084
|
-
}
|
|
1085
|
-
if (!isV2Policy(run.modelPolicy))
|
|
1086
|
-
return failed('ORCHESTRATION_MODEL_EVIDENCE_MISSING', 'running orchestration has no persisted model policy', resolved.taskId);
|
|
1087
571
|
if (run.pendingDelegation)
|
|
1088
572
|
return failed('ORCHESTRATION_DELEGATION_BUSY', 'the run already has a pending delegation', resolved.taskId);
|
|
1089
573
|
const repositoryPending = matchingDelegations(() => true, options);
|
|
@@ -1112,6 +596,10 @@ function prepareOrchestrationDelegationUnlocked(taskRef, input, options = {}) {
|
|
|
1112
596
|
if (input.requestedReasoningEffort !== expectedPolicy.reasoningEffort) {
|
|
1113
597
|
return failed('ORCHESTRATION_REQUESTED_REASONING_EFFORT_MISMATCH', `requested reasoning effort does not match the persisted ${next.role} policy`, resolved.taskId);
|
|
1114
598
|
}
|
|
599
|
+
const capabilityValidationError = options.validateLifecycleCapability?.();
|
|
600
|
+
if (capabilityValidationError) {
|
|
601
|
+
return failed(capabilityValidationError.code, capabilityValidationError.message, resolved.taskId, capabilityValidationError.detail ? { detail: capabilityValidationError.detail } : {});
|
|
602
|
+
}
|
|
1115
603
|
let beforeFingerprint;
|
|
1116
604
|
try {
|
|
1117
605
|
beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
|
|
@@ -1137,6 +625,10 @@ function prepareOrchestrationDelegationUnlocked(taskRef, input, options = {}) {
|
|
|
1137
625
|
lifecycleProvenance: input.lifecycleProvenance ?? null,
|
|
1138
626
|
beforeFingerprint
|
|
1139
627
|
}, { id: options.id, now: options.now, monotonicNow: options.monotonicNow });
|
|
628
|
+
const capabilityError = options.consumeLifecycleCapability?.();
|
|
629
|
+
if (capabilityError) {
|
|
630
|
+
return failed(capabilityError.code, capabilityError.message, resolved.taskId, capabilityError.detail ? { detail: capabilityError.detail } : {});
|
|
631
|
+
}
|
|
1140
632
|
const updated = withUpdatedRun(run, {
|
|
1141
633
|
nextStage: next.stage,
|
|
1142
634
|
pendingDelegation: receipt,
|
|
@@ -1167,8 +659,8 @@ function dispatchOrchestrationDelegationUnlocked(taskRef, options = {}) {
|
|
|
1167
659
|
const run = readRun(resolved.taskDir);
|
|
1168
660
|
if (!run?.pendingDelegation)
|
|
1169
661
|
return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
|
|
1170
|
-
if (run.
|
|
1171
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'spawn dispatch requires a running
|
|
662
|
+
if (run.status !== 'running') {
|
|
663
|
+
return failed('ORCHESTRATION_STATE_INVALID', 'spawn dispatch requires a running orchestration', resolved.taskId);
|
|
1172
664
|
}
|
|
1173
665
|
const result = dispatchDelegation(run.pendingDelegation, {
|
|
1174
666
|
now: options.now,
|
|
@@ -1202,7 +694,7 @@ function matchingDelegations(predicate, options) {
|
|
|
1202
694
|
.filter((entry) => entry.isDirectory())
|
|
1203
695
|
.flatMap((entry) => {
|
|
1204
696
|
const run = readRun(path.join(activeRoot, entry.name));
|
|
1205
|
-
if (run
|
|
697
|
+
if (!run || run.status !== 'running' || !run.pendingDelegation)
|
|
1206
698
|
return [];
|
|
1207
699
|
const receipt = run.pendingDelegation;
|
|
1208
700
|
return predicate(receipt)
|
|
@@ -1240,9 +732,9 @@ function activateMatchingOrchestrationDelegation(client, event, options = {}) {
|
|
|
1240
732
|
return matched;
|
|
1241
733
|
const receipt = replay.run.pendingDelegation;
|
|
1242
734
|
const sameEvidence = receipt.parentId === event.parentId
|
|
1243
|
-
&& receipt.spawnMode === event.spawnMode
|
|
1244
|
-
&& receipt.actualModel === event.actualModel
|
|
1245
|
-
&& receipt.actualReasoningEffort === event.actualReasoningEffort
|
|
735
|
+
&& (receipt.spawnMode ?? null) === (event.spawnMode ?? null)
|
|
736
|
+
&& (receipt.actualModel ?? null) === foldBlankToNull(event.actualModel)
|
|
737
|
+
&& (receipt.actualReasoningEffort ?? null) === foldBlankToNull(event.actualReasoningEffort)
|
|
1246
738
|
&& receipt.modelFallbackReason === (event.modelFallbackReason ?? null)
|
|
1247
739
|
&& receipt.reasoningEffortFallbackReason === (event.reasoningEffortFallbackReason ?? null)
|
|
1248
740
|
&& receipt.hostEvidence?.hookDefinitionHash === event.hostEvidence?.hookDefinitionHash;
|
|
@@ -1287,7 +779,11 @@ function sealMatchingOrchestrationDelegation(client, event, options = {}) {
|
|
|
1287
779
|
childId: event.childId,
|
|
1288
780
|
exitCode: 0,
|
|
1289
781
|
afterFingerprint,
|
|
1290
|
-
changedPaths
|
|
782
|
+
changedPaths,
|
|
783
|
+
actualModel: event.actualModel,
|
|
784
|
+
actualReasoningEffort: event.actualReasoningEffort,
|
|
785
|
+
modelFallbackReason: event.modelFallbackReason,
|
|
786
|
+
reasoningEffortFallbackReason: event.reasoningEffortFallbackReason
|
|
1291
787
|
}, options);
|
|
1292
788
|
}
|
|
1293
789
|
catch (error) {
|
|
@@ -1358,8 +854,6 @@ function activateOrchestrationDelegation(taskRef, event, options = {}) {
|
|
|
1358
854
|
const run = readRun(resolved.taskDir);
|
|
1359
855
|
if (!run?.pendingDelegation)
|
|
1360
856
|
return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
|
|
1361
|
-
if (run.schemaVersion !== 3)
|
|
1362
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'active delegation requires schemaVersion 3', resolved.taskId);
|
|
1363
857
|
if (run.status !== 'running') {
|
|
1364
858
|
return { status: run.status, changed: false, taskId: resolved.taskId, run, next: null, error: null };
|
|
1365
859
|
}
|
|
@@ -1387,9 +881,6 @@ async function awaitOrchestrationDelegationActivation(taskRef, event, options =
|
|
|
1387
881
|
if (!run?.pendingDelegation) {
|
|
1388
882
|
return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
|
|
1389
883
|
}
|
|
1390
|
-
if (run.schemaVersion !== 3) {
|
|
1391
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'activation barrier requires schemaVersion 3', resolved.taskId);
|
|
1392
|
-
}
|
|
1393
884
|
const receipt = run.pendingDelegation;
|
|
1394
885
|
const matches = receipt.stage === event.stage
|
|
1395
886
|
&& receipt.round === event.round
|
|
@@ -1440,16 +931,13 @@ function recoverPreparedOrchestrationDelegation(taskRef, options = {}) {
|
|
|
1440
931
|
const activeLifecycleEvidence = options.hasActiveLifecycleEvidence?.(receipt)
|
|
1441
932
|
?? (receipt.client === 'codex'
|
|
1442
933
|
&& receipt.lifecycleProvenance
|
|
1443
|
-
&& hasActiveCodexLifecycleEvidence(
|
|
934
|
+
&& hasActiveCodexLifecycleEvidence(resolveAgentRuntimeStoreRoot({ repoRoot: resolved.repoRoot, store: 'lifecycle' }), {
|
|
1444
935
|
nativeAgent: `agent-infra-lifecycle-${receipt.role}`,
|
|
1445
936
|
hookDefinitionHash: receipt.lifecycleProvenance.hookDefinitionHash
|
|
1446
937
|
}));
|
|
1447
938
|
if (activeLifecycleEvidence) {
|
|
1448
939
|
return failed('ORCHESTRATION_PREPARED_RECOVERY_ACTIVE_EVIDENCE', 'active Codex lifecycle evidence blocks prepared receipt recovery', resolved.taskId);
|
|
1449
940
|
}
|
|
1450
|
-
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
1451
|
-
return failed('ORCHESTRATION_PREPARED_RECOVERY_UNSAFE', 'active commit intent blocks prepared recovery', resolved.taskId);
|
|
1452
|
-
}
|
|
1453
941
|
const current = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
|
|
1454
942
|
gitRoot: gitRootFor(resolved.repoRoot, options),
|
|
1455
943
|
stateRoot: resolved.repoRoot,
|
|
@@ -1485,8 +973,6 @@ function completeOrchestrationStage(taskRef, event, options = {}) {
|
|
|
1485
973
|
const run = readRun(resolved.taskDir);
|
|
1486
974
|
if (!run)
|
|
1487
975
|
return { status: 'running', changed: false, taskId: resolved.taskId, run: null, next: null, error: null };
|
|
1488
|
-
if (run.schemaVersion !== 3)
|
|
1489
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'stage completion requires schemaVersion 3', resolved.taskId);
|
|
1490
976
|
if (!run.pendingDelegation)
|
|
1491
977
|
return failed('ORCHESTRATION_DELEGATION_MISSING', 'active run has no pending delegation', resolved.taskId);
|
|
1492
978
|
const result = completeDelegationStage(run.pendingDelegation, event);
|
|
@@ -1509,8 +995,6 @@ function inspectOrchestrationStage(taskRef, event, options = {}) {
|
|
|
1509
995
|
}
|
|
1510
996
|
if (!run)
|
|
1511
997
|
return failed('ORCHESTRATION_RUN_MISSING', 'no orchestration run exists', resolved.taskId);
|
|
1512
|
-
if (run.schemaVersion !== 3)
|
|
1513
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'stage inspection requires schemaVersion 3', resolved.taskId);
|
|
1514
998
|
const receipt = run.pendingDelegation;
|
|
1515
999
|
if (!receipt
|
|
1516
1000
|
|| receipt.status !== 'activated'
|
|
@@ -1555,8 +1039,6 @@ function sealOrchestrationDelegation(taskRef, event, options = {}) {
|
|
|
1555
1039
|
const run = readRun(resolved.taskDir);
|
|
1556
1040
|
if (!run?.pendingDelegation)
|
|
1557
1041
|
return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
|
|
1558
|
-
if (run.schemaVersion !== 3)
|
|
1559
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'delegation sealing requires schemaVersion 3', resolved.taskId);
|
|
1560
1042
|
const result = sealDelegation(run.pendingDelegation, event, { now: options.now });
|
|
1561
1043
|
if (!result.ok)
|
|
1562
1044
|
return pauseOrchestration(taskRef, result.code, result.message, true, options);
|
|
@@ -1571,8 +1053,6 @@ function advanceOrchestration(taskRef, options = {}) {
|
|
|
1571
1053
|
const run = readRun(resolved.taskDir);
|
|
1572
1054
|
if (!run?.pendingDelegation)
|
|
1573
1055
|
return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
|
|
1574
|
-
if (run.schemaVersion !== 3)
|
|
1575
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'orchestration advance requires schemaVersion 3', resolved.taskId);
|
|
1576
1056
|
const result = consumeDelegation(run.pendingDelegation, { now: options.now });
|
|
1577
1057
|
if (!result.ok)
|
|
1578
1058
|
return pauseOrchestration(taskRef, result.code, result.message, true, options);
|
|
@@ -1601,5 +1081,5 @@ function pauseOrchestration(taskRef, code, message, recoverable, options = {}) {
|
|
|
1601
1081
|
saveRun(resolved.taskDir, updated);
|
|
1602
1082
|
return { status: 'paused', changed: true, taskId: resolved.taskId, run: updated, next: null, error: null };
|
|
1603
1083
|
}
|
|
1604
|
-
export { activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, awaitOrchestrationDelegationActivation, advanceOrchestration,
|
|
1084
|
+
export { activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, awaitOrchestrationDelegationActivation, advanceOrchestration, beginOrResumeOrchestration, commitOrchestrationStageCompletion, completeOrchestrationStage, dispatchOrchestrationDelegation, inspectOrchestrationStage, hasActivatableOrchestrationDelegation, hasSealableOrchestrationDelegation, OrchestrationStateError, orchestrationPath, pauseMatchingOrchestrationDelegation, pauseOrchestration, planOrchestrationStageCompletion, prepareOrchestrationDelegation, readRun, reconcileMatchingOrchestrationDelegation, recoverPreparedOrchestrationDelegation, routeOrchestration, sealMatchingOrchestrationDelegation, sealMatchingOrchestrationDelegationWithHostEvidence, sealOrchestrationDelegation, statusOrchestration, };
|
|
1605
1085
|
//# sourceMappingURL=orchestration.js.map
|