@fitlab-ai/agent-infra 0.9.8 → 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 +6 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +58 -21
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +173 -10
- 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 +56 -67
- package/dist/lib/git/workflow.js +10 -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 +2 -1
- package/dist/lib/internal/sandbox-control.js +79 -2
- package/dist/lib/internal/task-finalization.js +54 -0
- package/dist/lib/internal/task-orchestration.js +156 -291
- package/dist/lib/platform/pr-summary.js +48 -10
- package/dist/lib/platform/pull-requests.js +205 -41
- package/dist/lib/sandbox/commands/rm.js +316 -55
- package/dist/lib/sandbox/control/client.js +190 -6
- package/dist/lib/sandbox/control/controller-registration.js +273 -0
- package/dist/lib/sandbox/control/executor.js +137 -5
- package/dist/lib/sandbox/control/lifecycle.js +3 -1
- package/dist/lib/sandbox/control/protocol.js +67 -6
- package/dist/lib/sandbox/index.js +5 -3
- package/dist/lib/sandbox/managed-fs.js +5 -0
- package/dist/lib/sandbox/task-resolver.js +50 -6
- package/dist/lib/sandbox/workspace-identity.js +27 -0
- package/dist/lib/sandbox/worktree-safety.js +215 -2
- 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/delegation-receipts.js +243 -2
- package/dist/lib/task/finalization.js +567 -0
- package/dist/lib/task/operation-outcome.js +33 -0
- package/dist/lib/task/orchestration.js +187 -761
- 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 +113 -60
- package/dist/lib/task/verification.js +2 -1
- package/dist/lib/task/workflow-warning-intents.js +50 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +61 -19
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +276 -12
- 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 +83 -91
- package/lib/git/workflow.ts +11 -2
- 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 +2 -1
- package/lib/internal/sandbox-control.ts +92 -2
- package/lib/internal/task-finalization.ts +68 -0
- package/lib/internal/task-orchestration.ts +20 -103
- package/lib/platform/pr-summary.ts +55 -11
- package/lib/platform/pull-requests.ts +211 -41
- package/lib/sandbox/commands/rm.ts +413 -61
- package/lib/sandbox/control/client.ts +255 -6
- package/lib/sandbox/control/controller-registration.ts +347 -0
- package/lib/sandbox/control/executor.ts +163 -5
- package/lib/sandbox/control/lifecycle.ts +2 -1
- package/lib/sandbox/control/protocol.ts +80 -7
- package/lib/sandbox/index.ts +5 -3
- package/lib/sandbox/managed-fs.ts +5 -0
- package/lib/sandbox/task-resolver.ts +58 -7
- package/lib/sandbox/workspace-identity.ts +50 -2
- package/lib/sandbox/workspace-view.ts +3 -3
- package/lib/sandbox/worktree-safety.ts +236 -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/delegation-receipts.ts +258 -1
- package/lib/task/finalization.ts +719 -0
- package/lib/task/operation-outcome.ts +48 -0
- package/lib/task/orchestration.ts +217 -982
- package/lib/task/short-id.ts +65 -1
- package/lib/task/task-execution-lock.ts +10 -1
- package/lib/task/verification-engine.ts +118 -63
- package/lib/task/verification.ts +3 -2
- package/lib/task/workflow-warning-intents.ts +57 -2
- 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 +1 -21
- 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/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/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,13 +1,12 @@
|
|
|
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, foldBlankToNull, 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';
|
|
12
11
|
import { resolveAgentRuntimeStoreRoot } from '../runtime/agent-runtime.js';
|
|
13
12
|
import { inspectPlatformPullRequest } from '../platform/pull-requests.js';
|
|
@@ -17,10 +16,6 @@ import { captureRepositorySnapshot, captureWorkspaceSnapshot, diffWorkspaceSnaps
|
|
|
17
16
|
import { assertGitRepositoryBinding } from '../git/worktree-identity.js';
|
|
18
17
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
|
|
19
18
|
import { hasActiveCodexLifecycleEvidence } from '../agent-clients/adapters/codex-lifecycle/store.js';
|
|
20
|
-
import { CommitIntentError, commitIntentPath, createCommitIntent, digest, readCommitIntent, removeCommitIntent, removeCommitIntentByDigest, serialize, updateCommitIntent } from './commit-intent.js';
|
|
21
|
-
import { inspectCommitFinalization, planCommitTaskFinalization } from './commit-finalization.js';
|
|
22
|
-
import { captureTaskWriteMetadata, writeTask } from './write.js';
|
|
23
|
-
import { appendActivityEntry, commitAttemptStartedNote, locateActivityLog, pairEntries } from './activity-log.js';
|
|
24
19
|
function supportsLifecycleDelegation(client) {
|
|
25
20
|
return getAgentClientCapability(client, 'subagents').level !== 'unsupported'
|
|
26
21
|
&& getAgentClientCapability(client, 'orchestration').level !== 'unsupported'
|
|
@@ -30,26 +25,138 @@ function supportsLifecycleDelegation(client) {
|
|
|
30
25
|
function orchestrationPath(taskDir) {
|
|
31
26
|
return path.join(taskDir, 'orchestration.json');
|
|
32
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
|
+
}
|
|
33
147
|
function readRun(taskDir) {
|
|
34
148
|
const file = orchestrationPath(taskDir);
|
|
35
149
|
if (!fs.existsSync(file))
|
|
36
150
|
return null;
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
return
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
},
|
|
47
|
-
reviewer: {
|
|
48
|
-
model: run.modelPolicy.reviewer.model,
|
|
49
|
-
reasoningEffort: run.modelPolicy.reviewer.reasoningEffort
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
};
|
|
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
|
+
}
|
|
53
160
|
}
|
|
54
161
|
function atomicWrite(file, value) {
|
|
55
162
|
const temp = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
@@ -65,70 +172,11 @@ function withUpdatedRun(run, updates, now) {
|
|
|
65
172
|
function failed(code, message, taskId = null, details = {}) {
|
|
66
173
|
return { status: 'failed', changed: false, taskId, run: null, next: null, error: { code, message, ...details } };
|
|
67
174
|
}
|
|
68
|
-
function commitIntentFailed(code, message, taskId = null) {
|
|
69
|
-
return { status: 'failed', changed: false, taskId, intent: null, error: { code, message } };
|
|
70
|
-
}
|
|
71
|
-
function repositoryHead(repoRoot) {
|
|
72
|
-
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
73
|
-
cwd: repoRoot,
|
|
74
|
-
encoding: 'utf8',
|
|
75
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
76
|
-
}).trim();
|
|
77
|
-
}
|
|
78
175
|
function gitRootFor(stateRoot, options) {
|
|
79
176
|
if (!options.gitWorktreeRoot)
|
|
80
177
|
return stateRoot;
|
|
81
178
|
return assertGitRepositoryBinding(stateRoot, options.gitWorktreeRoot).worktreeRoot;
|
|
82
179
|
}
|
|
83
|
-
function isAncestor(repoRoot, ancestor, descendant) {
|
|
84
|
-
return spawnSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
|
|
85
|
-
cwd: repoRoot,
|
|
86
|
-
stdio: 'ignore'
|
|
87
|
-
}).status === 0;
|
|
88
|
-
}
|
|
89
|
-
function commitIntentView(intent, currentHead) {
|
|
90
|
-
return {
|
|
91
|
-
mode: intent.mode,
|
|
92
|
-
phase: intent.phase,
|
|
93
|
-
baselineHead: intent.baselineHead,
|
|
94
|
-
currentHead,
|
|
95
|
-
committedHead: intent.committedHead,
|
|
96
|
-
pushEvidence: intent.pushEvidence,
|
|
97
|
-
runId: intent.orchestration?.runId ?? null,
|
|
98
|
-
receiptId: intent.orchestration?.receiptId ?? null
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
function commitFinalizationView(inspection) {
|
|
102
|
-
return {
|
|
103
|
-
disposition: inspection.disposition,
|
|
104
|
-
code: inspection.code,
|
|
105
|
-
message: inspection.message,
|
|
106
|
-
currentHead: inspection.currentHead,
|
|
107
|
-
committedHead: inspection.committedHead,
|
|
108
|
-
needsAnchor: inspection.needsAnchor,
|
|
109
|
-
needsLog: inspection.needsLog,
|
|
110
|
-
attempt: inspection.attempt
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
function validCommitRun(run, taskId) {
|
|
114
|
-
return run !== null
|
|
115
|
-
&& [1, 2, 3].includes(run.schemaVersion)
|
|
116
|
-
&& run.taskId === taskId
|
|
117
|
-
&& ['running', 'paused', 'completed'].includes(run.status)
|
|
118
|
-
&& Array.isArray(run.receipts)
|
|
119
|
-
&& Object.prototype.hasOwnProperty.call(run, 'pendingDelegation')
|
|
120
|
-
&& typeof run.commitAuthorization === 'object'
|
|
121
|
-
&& run.commitAuthorization !== null
|
|
122
|
-
&& Object.prototype.hasOwnProperty.call(run.commitAuthorization, 'issuedAt')
|
|
123
|
-
&& Object.prototype.hasOwnProperty.call(run.commitAuthorization, 'consumedAt');
|
|
124
|
-
}
|
|
125
|
-
function mapCommitIntentError(error, taskId) {
|
|
126
|
-
if (error instanceof CommitIntentError)
|
|
127
|
-
return commitIntentFailed(error.code, error.message, taskId);
|
|
128
|
-
if (error instanceof TaskExecutionLockError)
|
|
129
|
-
return commitIntentFailed(error.code, error.message, taskId);
|
|
130
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_INVALID', error instanceof Error ? error.message : String(error), taskId);
|
|
131
|
-
}
|
|
132
180
|
function validModel(value) {
|
|
133
181
|
return typeof value === 'string' && value.length > 0 && value.trim() === value;
|
|
134
182
|
}
|
|
@@ -148,11 +196,8 @@ function sameModelPolicy(left, right) {
|
|
|
148
196
|
&& left.reviewer.model === right.reviewer.model
|
|
149
197
|
&& left.reviewer.reasoningEffort === right.reviewer.reasoningEffort;
|
|
150
198
|
}
|
|
151
|
-
function isV2Policy(policy) {
|
|
152
|
-
return typeof policy?.executor === 'object' && typeof policy?.reviewer === 'object';
|
|
153
|
-
}
|
|
154
199
|
function rolePolicy(run, role) {
|
|
155
|
-
return
|
|
200
|
+
return run.modelPolicy[role];
|
|
156
201
|
}
|
|
157
202
|
function resolveProjectPolicy(repoRoot, client) {
|
|
158
203
|
const configPath = path.join(repoRoot, '.agents', '.airc.json');
|
|
@@ -161,25 +206,8 @@ function resolveProjectPolicy(repoRoot, client) {
|
|
|
161
206
|
const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
162
207
|
return normalizeAgentClients(raw).state[client].orchestration;
|
|
163
208
|
}
|
|
164
|
-
function canRecoverCodexUnsupportedPause(run, taskDir) {
|
|
165
|
-
return run.schemaVersion === 2
|
|
166
|
-
&& run.status === 'paused'
|
|
167
|
-
&& run.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
|
|
168
|
-
&& run.modelPolicySource?.client === 'codex'
|
|
169
|
-
&& run.stepCount === 0
|
|
170
|
-
&& run.nextStage === null
|
|
171
|
-
&& run.baseline === ''
|
|
172
|
-
&& run.pendingDelegation === null
|
|
173
|
-
&& run.receipts.length === 0
|
|
174
|
-
&& run.commitAuthorization?.issuedAt === null
|
|
175
|
-
&& run.commitAuthorization?.consumedAt === null
|
|
176
|
-
&& run.completionEvidence == null
|
|
177
|
-
&& !fs.existsSync(commitIntentPath(taskDir))
|
|
178
|
-
&& (run.recoveryHistory ?? []).every((entry) => entry.code === 'MODEL_POLICY_SUPPLEMENTED');
|
|
179
|
-
}
|
|
180
209
|
function canRecoverClaudeCodeUnsupportedPause(run, taskDir) {
|
|
181
|
-
return run.
|
|
182
|
-
&& run.status === 'paused'
|
|
210
|
+
return run.status === 'paused'
|
|
183
211
|
&& run.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
|
|
184
212
|
&& run.modelPolicySource?.client === 'claude-code'
|
|
185
213
|
&& run.stepCount === 0
|
|
@@ -190,22 +218,7 @@ function canRecoverClaudeCodeUnsupportedPause(run, taskDir) {
|
|
|
190
218
|
&& run.commitAuthorization?.issuedAt === null
|
|
191
219
|
&& run.commitAuthorization?.consumedAt === null
|
|
192
220
|
&& run.completionEvidence == null
|
|
193
|
-
&&
|
|
194
|
-
&& (run.recoveryHistory ?? []).length === 0;
|
|
195
|
-
}
|
|
196
|
-
function canMigrateRecoverableV2Pause(run, taskDir) {
|
|
197
|
-
return run.schemaVersion === 2
|
|
198
|
-
&& run.status === 'paused'
|
|
199
|
-
&& run.pause?.recoverable === true
|
|
200
|
-
&& run.stepCount === 0
|
|
201
|
-
&& run.nextStage === null
|
|
202
|
-
&& run.baseline === ''
|
|
203
|
-
&& run.pendingDelegation === null
|
|
204
|
-
&& run.receipts.length === 0
|
|
205
|
-
&& run.commitAuthorization?.issuedAt === null
|
|
206
|
-
&& run.commitAuthorization?.consumedAt === null
|
|
207
|
-
&& run.completionEvidence == null
|
|
208
|
-
&& !fs.existsSync(commitIntentPath(taskDir));
|
|
221
|
+
&& run.recoveryHistory.length === 0;
|
|
209
222
|
}
|
|
210
223
|
function beginOrResumeOrchestration(taskRef, options = {}) {
|
|
211
224
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
@@ -222,226 +235,57 @@ function beginOrResumeOrchestration(taskRef, options = {}) {
|
|
|
222
235
|
return failed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
223
236
|
}
|
|
224
237
|
if (existing) {
|
|
225
|
-
if (
|
|
226
|
-
|| !Array.isArray(existing.receipts)
|
|
227
|
-
|| !Object.prototype.hasOwnProperty.call(existing, 'pendingDelegation')) {
|
|
228
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'orchestration state has an invalid schema', resolved.taskId);
|
|
229
|
-
}
|
|
230
|
-
if (existing.status === 'completed' && existing.schemaVersion === 1) {
|
|
238
|
+
if (existing.status === 'completed') {
|
|
231
239
|
return { status: 'completed', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
232
240
|
}
|
|
233
|
-
if (existing.
|
|
234
|
-
|
|
235
|
-
if (policyError
|
|
236
|
-
|| !existing.modelPolicySource
|
|
237
|
-
|| !isAgentClientId(existing.modelPolicySource.client)
|
|
238
|
-
|| !Array.isArray(existing.recoveryHistory)) {
|
|
239
|
-
return failed('ORCHESTRATION_STATE_INVALID', `schemaVersion ${existing.schemaVersion} orchestration state is incomplete`, resolved.taskId);
|
|
240
|
-
}
|
|
241
|
-
if (existing.status === 'completed') {
|
|
242
|
-
return { status: 'completed', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
243
|
-
}
|
|
244
|
-
if (existing.schemaVersion === 2 && existing.status === 'running') {
|
|
245
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'active schemaVersion 2 runs cannot be advanced by lifecycle protocol v3', resolved.taskId);
|
|
246
|
-
}
|
|
247
|
-
if (existing.modelPolicySource.client !== options.client) {
|
|
248
|
-
return failed('ORCHESTRATION_CLIENT_MISMATCH', 'provided client does not match the persisted run policy source', resolved.taskId);
|
|
249
|
-
}
|
|
250
|
-
if (options.modelPolicy) {
|
|
251
|
-
const suppliedError = validateModelPolicy(options.modelPolicy);
|
|
252
|
-
if (suppliedError)
|
|
253
|
-
return failed(suppliedError.code, suppliedError.message, resolved.taskId);
|
|
254
|
-
if (!sameModelPolicy(existing.modelPolicy, options.modelPolicy)) {
|
|
255
|
-
return failed('ORCHESTRATION_MODEL_POLICY_MISMATCH', 'provided model policy does not match the persisted run policy', resolved.taskId);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
if (existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED') {
|
|
259
|
-
if (existing.modelPolicySource.client === 'claude-code') {
|
|
260
|
-
if (!canRecoverClaudeCodeUnsupportedPause(existing, resolved.taskDir)) {
|
|
261
|
-
return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
262
|
-
}
|
|
263
|
-
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
264
|
-
const previousPause = existing.pause;
|
|
265
|
-
const recovery = {
|
|
266
|
-
code: 'CLIENT_CAPABILITY_ENABLED_NO_MIGRATION',
|
|
267
|
-
recoveredAt: now,
|
|
268
|
-
previousSchemaVersion: 3,
|
|
269
|
-
previousStatus: 'paused',
|
|
270
|
-
previousPause,
|
|
271
|
-
client: 'claude-code',
|
|
272
|
-
guards: {
|
|
273
|
-
stepCount: 0,
|
|
274
|
-
nextStage: null,
|
|
275
|
-
baselineEmpty: true,
|
|
276
|
-
receiptCount: 0,
|
|
277
|
-
pendingDelegation: false,
|
|
278
|
-
commitAuthorizationUnused: true,
|
|
279
|
-
completionEvidenceAbsent: true,
|
|
280
|
-
commitIntentAbsent: true
|
|
281
|
-
},
|
|
282
|
-
resultingStatus: 'running'
|
|
283
|
-
};
|
|
284
|
-
const resumed = withUpdatedRun(existing, {
|
|
285
|
-
status: 'running',
|
|
286
|
-
pause: null,
|
|
287
|
-
recoveryHistory: Object.freeze([...(existing.recoveryHistory ?? []), recovery])
|
|
288
|
-
}, () => now);
|
|
289
|
-
saveRun(resolved.taskDir, resumed);
|
|
290
|
-
return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
|
|
291
|
-
}
|
|
292
|
-
if (!canRecoverCodexUnsupportedPause(existing, resolved.taskDir)) {
|
|
293
|
-
return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
294
|
-
}
|
|
295
|
-
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
296
|
-
const previousPause = existing.pause;
|
|
297
|
-
const recovery = {
|
|
298
|
-
code: 'CLIENT_CAPABILITY_ENABLED',
|
|
299
|
-
recoveredAt: now,
|
|
300
|
-
previousSchemaVersion: 2,
|
|
301
|
-
previousStatus: 'paused',
|
|
302
|
-
previousPause,
|
|
303
|
-
client: 'codex',
|
|
304
|
-
guards: {
|
|
305
|
-
stepCount: 0,
|
|
306
|
-
nextStage: null,
|
|
307
|
-
baselineEmpty: true,
|
|
308
|
-
receiptCount: 0,
|
|
309
|
-
pendingDelegation: false,
|
|
310
|
-
commitAuthorizationUnused: true,
|
|
311
|
-
completionEvidenceAbsent: true,
|
|
312
|
-
commitIntentAbsent: true
|
|
313
|
-
},
|
|
314
|
-
resultingStatus: 'running'
|
|
315
|
-
};
|
|
316
|
-
const resumed = withUpdatedRun(existing, {
|
|
317
|
-
schemaVersion: 3,
|
|
318
|
-
status: 'running',
|
|
319
|
-
pause: null,
|
|
320
|
-
recoveryHistory: Object.freeze([...(existing.recoveryHistory ?? []), recovery])
|
|
321
|
-
}, () => now);
|
|
322
|
-
saveRun(resolved.taskDir, resumed);
|
|
323
|
-
return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
|
|
324
|
-
}
|
|
325
|
-
if (existing.status === 'paused' && existing.pause?.recoverable && existing.pendingDelegation === null) {
|
|
326
|
-
if (existing.schemaVersion === 2 && !canMigrateRecoverableV2Pause(existing, resolved.taskDir)) {
|
|
327
|
-
const paused = withUpdatedRun(existing, {
|
|
328
|
-
pause: {
|
|
329
|
-
code: 'ORCHESTRATION_SCHEMA_MIGRATION_REQUIRED',
|
|
330
|
-
message: 'schemaVersion 2 recoverable pause contains execution evidence and cannot migrate automatically',
|
|
331
|
-
recoverable: false
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
saveRun(resolved.taskDir, paused);
|
|
335
|
-
return { status: 'paused', changed: true, taskId: resolved.taskId, run: paused, next: null, error: null };
|
|
336
|
-
}
|
|
337
|
-
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
338
|
-
const recovery = existing.schemaVersion === 2 ? {
|
|
339
|
-
code: 'SCHEMA_V3_MIGRATED',
|
|
340
|
-
recoveredAt: now,
|
|
341
|
-
previousSchemaVersion: 2,
|
|
342
|
-
previousStatus: 'paused',
|
|
343
|
-
previousPause: existing.pause,
|
|
344
|
-
guards: {
|
|
345
|
-
stepCount: 0,
|
|
346
|
-
nextStage: null,
|
|
347
|
-
baselineEmpty: true,
|
|
348
|
-
receiptCount: 0,
|
|
349
|
-
pendingDelegation: false,
|
|
350
|
-
commitAuthorizationUnused: true,
|
|
351
|
-
completionEvidenceAbsent: true,
|
|
352
|
-
commitIntentAbsent: true
|
|
353
|
-
},
|
|
354
|
-
resultingStatus: 'running'
|
|
355
|
-
} : null;
|
|
356
|
-
const resumed = withUpdatedRun(existing, {
|
|
357
|
-
schemaVersion: 3,
|
|
358
|
-
status: 'running',
|
|
359
|
-
pause: null,
|
|
360
|
-
...(recovery ? { recoveryHistory: Object.freeze([...(existing.recoveryHistory ?? []), recovery]) } : {})
|
|
361
|
-
}, () => now);
|
|
362
|
-
saveRun(resolved.taskDir, resumed);
|
|
363
|
-
return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
|
|
364
|
-
}
|
|
365
|
-
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);
|
|
366
243
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
catch (error) {
|
|
375
|
-
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);
|
|
376
250
|
}
|
|
377
251
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
client: options.client,
|
|
382
|
-
missingFields: ['executor.model', 'executor.reasoningEffort', 'reviewer.model', 'reviewer.reasoningEffort'],
|
|
383
|
-
modelSelectionContext: getAgentClientModelSelection(options.client)
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
if (existing.pendingDelegation !== null) {
|
|
387
|
-
const alreadyPaused = existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_DELEGATION_BUSY';
|
|
388
|
-
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)) {
|
|
389
255
|
return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
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 };
|
|
411
282
|
}
|
|
412
|
-
if (existing.
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
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 };
|
|
417
287
|
}
|
|
418
|
-
|
|
419
|
-
const source = { kind: sourceKind, client: options.client, resolvedAt: now };
|
|
420
|
-
const clearsModelPause = existing.pause?.code === 'ORCHESTRATION_MODEL_EVIDENCE_MISSING';
|
|
421
|
-
const resultingStatus = clearsModelPause ? 'running' : existing.status;
|
|
422
|
-
const recovery = {
|
|
423
|
-
code: 'MODEL_POLICY_SUPPLEMENTED',
|
|
424
|
-
recoveredAt: now,
|
|
425
|
-
previousSchemaVersion: 1,
|
|
426
|
-
previousStatus: existing.status,
|
|
427
|
-
previousPause: existing.pause,
|
|
428
|
-
policySource: source,
|
|
429
|
-
receiptCount: 0,
|
|
430
|
-
pendingDelegation: false,
|
|
431
|
-
resultingStatus
|
|
432
|
-
};
|
|
433
|
-
const recovered = Object.freeze({
|
|
434
|
-
...existing,
|
|
435
|
-
schemaVersion: 3,
|
|
436
|
-
status: resultingStatus,
|
|
437
|
-
modelPolicy: policy,
|
|
438
|
-
modelPolicySource: source,
|
|
439
|
-
recoveryHistory: Object.freeze([recovery]),
|
|
440
|
-
pause: clearsModelPause ? null : existing.pause,
|
|
441
|
-
updatedAt: now
|
|
442
|
-
});
|
|
443
|
-
saveRun(resolved.taskDir, recovered);
|
|
444
|
-
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 };
|
|
445
289
|
}
|
|
446
290
|
let policy = options.modelPolicy;
|
|
447
291
|
let sourceKind = 'explicit';
|
|
@@ -464,7 +308,6 @@ function beginOrResumeOrchestration(taskRef, options = {}) {
|
|
|
464
308
|
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
465
309
|
const source = { kind: sourceKind, client: options.client, resolvedAt: now };
|
|
466
310
|
const run = {
|
|
467
|
-
schemaVersion: 3,
|
|
468
311
|
taskId: resolved.taskId,
|
|
469
312
|
runId: (options.id ?? randomUUID)(),
|
|
470
313
|
status: 'running',
|
|
@@ -706,401 +549,6 @@ function routeOrchestration(taskRef, options = {}) {
|
|
|
706
549
|
}
|
|
707
550
|
return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
|
|
708
551
|
}
|
|
709
|
-
function appendCommitActivity(resolved, step, agent, note) {
|
|
710
|
-
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
711
|
-
const activity = locateActivityLog(taskContent);
|
|
712
|
-
if (!activity)
|
|
713
|
-
return { code: 'ORCHESTRATION_TASK_INVALID', message: 'task activity log is missing or ambiguous' };
|
|
714
|
-
const metadata = captureTaskWriteMetadata();
|
|
715
|
-
const written = writeTask({
|
|
716
|
-
taskRef: resolved.taskId,
|
|
717
|
-
expectedState: 'active',
|
|
718
|
-
mutations: [{
|
|
719
|
-
kind: 'section',
|
|
720
|
-
aliases: ['活动日志', 'Activity Log'],
|
|
721
|
-
heading: activity.heading,
|
|
722
|
-
body: appendActivityEntry(activity, { time: metadata.timestamp, step, agent, note })
|
|
723
|
-
}]
|
|
724
|
-
}, { repoRoot: resolved.repoRoot, metadataProvider: () => metadata });
|
|
725
|
-
return written.status === 'failed' ? written.error : null;
|
|
726
|
-
}
|
|
727
|
-
function startCommitAttempt(taskRef, input, options = {}) {
|
|
728
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
729
|
-
if (!resolved.ok)
|
|
730
|
-
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
731
|
-
try {
|
|
732
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
733
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.start', () => {
|
|
734
|
-
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
735
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
|
|
736
|
-
}
|
|
737
|
-
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
738
|
-
const activity = locateActivityLog(taskContent);
|
|
739
|
-
if (!activity)
|
|
740
|
-
return commitIntentFailed('ORCHESTRATION_TASK_INVALID', 'task activity log is missing or ambiguous', resolved.taskId);
|
|
741
|
-
const open = pairEntries(activity.entries).filter((row) => row.step === 'Commit' && row.started !== '' && row.done === '');
|
|
742
|
-
if (open.length > 0) {
|
|
743
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_ATTEMPT_BUSY', 'an open Commit activity entry already exists', resolved.taskId);
|
|
744
|
-
}
|
|
745
|
-
const currentHead = repositoryHead(gitRoot);
|
|
746
|
-
const attempt = (options.id ?? randomUUID)();
|
|
747
|
-
const error = appendCommitActivity(resolved, 'Commit [started]', input.agent, commitAttemptStartedNote({
|
|
748
|
-
attempt,
|
|
749
|
-
baseline: currentHead,
|
|
750
|
-
agent: input.agent
|
|
751
|
-
}));
|
|
752
|
-
if (error)
|
|
753
|
-
return commitIntentFailed(error.code, error.message, resolved.taskId);
|
|
754
|
-
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
755
|
-
return {
|
|
756
|
-
status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
|
|
757
|
-
finalization: commitFinalizationView(inspection), error: null
|
|
758
|
-
};
|
|
759
|
-
});
|
|
760
|
-
}
|
|
761
|
-
catch (error) {
|
|
762
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
function terminateCommitAttempt(taskRef, input, options = {}) {
|
|
766
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
767
|
-
if (!resolved.ok)
|
|
768
|
-
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
769
|
-
try {
|
|
770
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
771
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.terminate', () => {
|
|
772
|
-
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
773
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'an active commit intent must be aborted or recovered first', resolved.taskId);
|
|
774
|
-
}
|
|
775
|
-
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
776
|
-
if (inspection.disposition !== 'retryable-start'
|
|
777
|
-
|| inspection.attempt?.attempt !== input.attempt
|
|
778
|
-
|| inspection.attempt.agent !== input.agent
|
|
779
|
-
|| inspection.attempt.baseline !== inspection.currentHead) {
|
|
780
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit attempt identity or repository HEAD has drifted', resolved.taskId);
|
|
781
|
-
}
|
|
782
|
-
const error = appendCommitActivity(resolved, 'Commit [aborted]', input.agent, `aborted; attempt=${input.attempt}; code=${input.code}`);
|
|
783
|
-
if (error)
|
|
784
|
-
return commitIntentFailed(error.code, error.message, resolved.taskId);
|
|
785
|
-
return {
|
|
786
|
-
status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
|
|
787
|
-
finalization: commitFinalizationView(inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId)),
|
|
788
|
-
error: null
|
|
789
|
-
};
|
|
790
|
-
});
|
|
791
|
-
}
|
|
792
|
-
catch (error) {
|
|
793
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
function beginCommitIntent(taskRef, input, options = {}) {
|
|
797
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
798
|
-
if (!resolved.ok)
|
|
799
|
-
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
800
|
-
try {
|
|
801
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
802
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.begin', () => {
|
|
803
|
-
const currentHead = repositoryHead(gitRoot);
|
|
804
|
-
if (currentHead !== input.baselineHead) {
|
|
805
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_BASELINE_MISMATCH', 'baseline HEAD does not match the repository HEAD', resolved.taskId);
|
|
806
|
-
}
|
|
807
|
-
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
808
|
-
try {
|
|
809
|
-
readCommitIntent(resolved.taskDir, resolved.taskId);
|
|
810
|
-
}
|
|
811
|
-
catch (error) {
|
|
812
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
813
|
-
}
|
|
814
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
|
|
815
|
-
}
|
|
816
|
-
const attemptInspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
817
|
-
if (attemptInspection.disposition !== 'retryable-start'
|
|
818
|
-
|| attemptInspection.attempt?.attempt !== input.attempt
|
|
819
|
-
|| attemptInspection.attempt.agent !== input.agent
|
|
820
|
-
|| attemptInspection.attempt.baseline !== input.baselineHead) {
|
|
821
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_ATTEMPT_MISMATCH', 'commit begin requires one matching retryable Commit attempt', resolved.taskId);
|
|
822
|
-
}
|
|
823
|
-
const runFile = orchestrationPath(resolved.taskDir);
|
|
824
|
-
let run = null;
|
|
825
|
-
let sourceRunBytes = null;
|
|
826
|
-
if (fs.existsSync(runFile)) {
|
|
827
|
-
sourceRunBytes = fs.readFileSync(runFile, 'utf8');
|
|
828
|
-
try {
|
|
829
|
-
run = readRun(resolved.taskDir);
|
|
830
|
-
}
|
|
831
|
-
catch (error) {
|
|
832
|
-
return commitIntentFailed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
833
|
-
}
|
|
834
|
-
if (!validCommitRun(run, resolved.taskId)) {
|
|
835
|
-
return commitIntentFailed('ORCHESTRATION_STATE_INVALID', 'orchestration state has an invalid schema', resolved.taskId);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
839
|
-
const failAndPause = (code, message) => {
|
|
840
|
-
if (run?.status === 'running') {
|
|
841
|
-
saveRun(resolved.taskDir, withUpdatedRun(run, {
|
|
842
|
-
status: 'paused', pause: { code, message, recoverable: true }
|
|
843
|
-
}, () => now));
|
|
844
|
-
}
|
|
845
|
-
return commitIntentFailed(code, message, resolved.taskId);
|
|
846
|
-
};
|
|
847
|
-
let orchestration = null;
|
|
848
|
-
if (!input.orchestrated) {
|
|
849
|
-
if (run?.pendingDelegation) {
|
|
850
|
-
return commitIntentFailed('ORCHESTRATION_STANDALONE_BUSY', 'standalone commit is blocked by a pending orchestration delegation', resolved.taskId);
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
else {
|
|
854
|
-
if (!run || run.status !== 'running') {
|
|
855
|
-
return commitIntentFailed('ORCHESTRATION_RUN_NOT_RUNNING', 'orchestrated commit requires a running orchestration', resolved.taskId);
|
|
856
|
-
}
|
|
857
|
-
const receipt = run.pendingDelegation;
|
|
858
|
-
if (!receipt
|
|
859
|
-
|| receipt.status !== 'activated'
|
|
860
|
-
|| receipt.stage !== 'commit'
|
|
861
|
-
|| receipt.round !== 1
|
|
862
|
-
|| receipt.artifact !== 'commit'
|
|
863
|
-
|| receipt.role !== 'executor') {
|
|
864
|
-
return failAndPause('ORCHESTRATION_PROVENANCE_MISMATCH', 'orchestrated commit requires one matching activated commit delegation');
|
|
865
|
-
}
|
|
866
|
-
if (!run.commitAuthorization.issuedAt || run.commitAuthorization.consumedAt) {
|
|
867
|
-
return failAndPause('ORCHESTRATION_COMMIT_AUTHORIZATION_INVALID', 'orchestrated commit requires an unconsumed one-use authorization');
|
|
868
|
-
}
|
|
869
|
-
const planned = planOrchestrationStageCompletion(taskRef, {
|
|
870
|
-
stage: 'commit', round: 1, artifact: 'commit', role: 'executor', agent: input.agent
|
|
871
|
-
}, { ...options, now: () => now });
|
|
872
|
-
if (planned.result.status === 'failed' || !planned.plan?.updatedRun.pendingDelegation) {
|
|
873
|
-
return failAndPause(planned.result.error?.code ?? 'ORCHESTRATION_PROVENANCE_MISMATCH', planned.result.error?.message ?? 'commit delegation could not be completed');
|
|
874
|
-
}
|
|
875
|
-
const plannedRunBytes = serialize(planned.plan.updatedRun);
|
|
876
|
-
orchestration = {
|
|
877
|
-
runId: run.runId,
|
|
878
|
-
receiptId: receipt.id,
|
|
879
|
-
authorizationIssuedAt: run.commitAuthorization.issuedAt,
|
|
880
|
-
sourceRunDigest: digest(sourceRunBytes),
|
|
881
|
-
plannedRunDigest: digest(plannedRunBytes),
|
|
882
|
-
completionUpdatedAt: planned.plan.updatedRun.updatedAt,
|
|
883
|
-
plannedReceipt: planned.plan.updatedRun.pendingDelegation
|
|
884
|
-
};
|
|
885
|
-
}
|
|
886
|
-
const created = createCommitIntent(resolved.taskDir, {
|
|
887
|
-
taskId: resolved.taskId,
|
|
888
|
-
mode: input.orchestrated ? 'orchestrated' : 'standalone',
|
|
889
|
-
phase: 'prepared',
|
|
890
|
-
baselineHead: input.baselineHead,
|
|
891
|
-
committedHead: null,
|
|
892
|
-
pushEvidence: null,
|
|
893
|
-
orchestration,
|
|
894
|
-
createdAt: now,
|
|
895
|
-
updatedAt: now
|
|
896
|
-
}, { token: options.token });
|
|
897
|
-
return {
|
|
898
|
-
status: 'ready', changed: true, taskId: resolved.taskId,
|
|
899
|
-
intent: commitIntentView(created.intent, currentHead), token: created.token, error: null
|
|
900
|
-
};
|
|
901
|
-
});
|
|
902
|
-
}
|
|
903
|
-
catch (error) {
|
|
904
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
function checkpointCommitIntent(taskRef, input, options = {}) {
|
|
908
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
909
|
-
if (!resolved.ok)
|
|
910
|
-
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
911
|
-
try {
|
|
912
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
913
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.checkpoint', () => {
|
|
914
|
-
const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
915
|
-
const currentHead = repositoryHead(gitRoot);
|
|
916
|
-
if (currentHead !== input.head) {
|
|
917
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'checkpoint HEAD does not match the repository HEAD', resolved.taskId);
|
|
918
|
-
}
|
|
919
|
-
if (input.kind === 'committed' && !isAncestor(gitRoot, intent.baselineHead, input.head)) {
|
|
920
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'committed HEAD does not descend from the baseline', resolved.taskId);
|
|
921
|
-
}
|
|
922
|
-
if (input.kind === 'pushed') {
|
|
923
|
-
const expectedHead = intent.committedHead ?? intent.baselineHead;
|
|
924
|
-
if (input.head !== expectedHead || !input.remote || !input.ref) {
|
|
925
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_STATE_INVALID', 'pushed checkpoint requires matching remote, ref and HEAD', resolved.taskId);
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
929
|
-
const updated = updateCommitIntent(resolved.taskDir, resolved.taskId, input.token, input.kind === 'committed'
|
|
930
|
-
? { phase: 'committed', committedHead: input.head, updatedAt: now }
|
|
931
|
-
: {
|
|
932
|
-
phase: 'pushed', committedHead: intent.committedHead ?? intent.baselineHead, updatedAt: now,
|
|
933
|
-
pushEvidence: { remote: input.remote, ref: input.ref, head: input.head }
|
|
934
|
-
});
|
|
935
|
-
return {
|
|
936
|
-
status: 'ready', changed: true, taskId: resolved.taskId,
|
|
937
|
-
intent: commitIntentView(updated, currentHead), error: null
|
|
938
|
-
};
|
|
939
|
-
});
|
|
940
|
-
}
|
|
941
|
-
catch (error) {
|
|
942
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
function completeCommitOrchestration(resolved, intent, agent) {
|
|
946
|
-
if (intent.orchestration === null)
|
|
947
|
-
return null;
|
|
948
|
-
if (intent.orchestration.plannedReceipt.agent !== agent) {
|
|
949
|
-
return { code: 'ORCHESTRATION_PROVENANCE_MISMATCH', message: 'completion agent does not match the planned receipt' };
|
|
950
|
-
}
|
|
951
|
-
const runFile = orchestrationPath(resolved.taskDir);
|
|
952
|
-
if (!fs.existsSync(runFile)) {
|
|
953
|
-
return { code: 'ORCHESTRATION_RUN_MISSING', message: 'orchestration run disappeared' };
|
|
954
|
-
}
|
|
955
|
-
const currentBytes = fs.readFileSync(runFile, 'utf8');
|
|
956
|
-
const currentDigest = digest(currentBytes);
|
|
957
|
-
if (currentDigest === intent.orchestration.sourceRunDigest) {
|
|
958
|
-
const run = readRun(resolved.taskDir);
|
|
959
|
-
if (!run)
|
|
960
|
-
return { code: 'ORCHESTRATION_RUN_MISSING', message: 'orchestration run disappeared' };
|
|
961
|
-
const plannedRun = Object.freeze({
|
|
962
|
-
...run,
|
|
963
|
-
pendingDelegation: intent.orchestration.plannedReceipt,
|
|
964
|
-
updatedAt: intent.orchestration.completionUpdatedAt
|
|
965
|
-
});
|
|
966
|
-
if (digest(serialize(plannedRun)) !== intent.orchestration.plannedRunDigest) {
|
|
967
|
-
return { code: 'ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', message: 'planned orchestration bytes no longer match the intent' };
|
|
968
|
-
}
|
|
969
|
-
atomicWrite(runFile, plannedRun);
|
|
970
|
-
if (digest(fs.readFileSync(runFile)) !== intent.orchestration.plannedRunDigest) {
|
|
971
|
-
return { code: 'ORCHESTRATION_COMMIT_COMPLETE_PARTIAL', message: 'orchestration completion could not be verified' };
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
else if (currentDigest !== intent.orchestration.plannedRunDigest) {
|
|
975
|
-
return { code: 'ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', message: 'orchestration state changed after commit begin' };
|
|
976
|
-
}
|
|
977
|
-
return null;
|
|
978
|
-
}
|
|
979
|
-
function finalizeCommitIntentUnlocked(resolved, inspection, agent, remove) {
|
|
980
|
-
if (inspection.disposition !== 'recoverable' || !inspection.intent) {
|
|
981
|
-
return commitIntentFailed(inspection.code ?? 'COMMIT_FINALIZATION_PENDING', inspection.message, resolved.taskId);
|
|
982
|
-
}
|
|
983
|
-
if (inspection.intent.orchestration !== null
|
|
984
|
-
&& inspection.intent.orchestration.plannedReceipt.agent !== agent) {
|
|
985
|
-
return commitIntentFailed('ORCHESTRATION_PROVENANCE_MISMATCH', 'completion agent does not match the planned receipt', resolved.taskId);
|
|
986
|
-
}
|
|
987
|
-
const metadata = captureTaskWriteMetadata();
|
|
988
|
-
const taskPlan = planCommitTaskFinalization(resolved.taskDir, inspection, agent, metadata.timestamp);
|
|
989
|
-
const written = writeTask({
|
|
990
|
-
taskRef: resolved.taskId,
|
|
991
|
-
expectedState: 'active',
|
|
992
|
-
mutations: taskPlan.mutations
|
|
993
|
-
}, {
|
|
994
|
-
repoRoot: resolved.repoRoot,
|
|
995
|
-
metadataProvider: () => metadata
|
|
996
|
-
});
|
|
997
|
-
if (written.status === 'failed') {
|
|
998
|
-
return commitIntentFailed(written.error.code, written.error.message, resolved.taskId);
|
|
999
|
-
}
|
|
1000
|
-
const orchestrationError = completeCommitOrchestration(resolved, inspection.intent, agent);
|
|
1001
|
-
if (orchestrationError) {
|
|
1002
|
-
return commitIntentFailed(orchestrationError.code, orchestrationError.message, resolved.taskId);
|
|
1003
|
-
}
|
|
1004
|
-
try {
|
|
1005
|
-
remove();
|
|
1006
|
-
}
|
|
1007
|
-
catch (error) {
|
|
1008
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_COMPLETE_PARTIAL', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
1009
|
-
}
|
|
1010
|
-
return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
|
|
1011
|
-
}
|
|
1012
|
-
function completeCommitIntent(taskRef, input, options = {}) {
|
|
1013
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1014
|
-
if (!resolved.ok)
|
|
1015
|
-
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1016
|
-
try {
|
|
1017
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1018
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.complete', () => {
|
|
1019
|
-
readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1020
|
-
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
1021
|
-
return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
|
|
1022
|
-
removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1023
|
-
});
|
|
1024
|
-
});
|
|
1025
|
-
}
|
|
1026
|
-
catch (error) {
|
|
1027
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
function recoverCommitIntent(taskRef, input, options = {}) {
|
|
1031
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1032
|
-
if (!resolved.ok)
|
|
1033
|
-
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1034
|
-
try {
|
|
1035
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1036
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.recover', () => {
|
|
1037
|
-
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
1038
|
-
if (inspection.disposition === 'retryable-start') {
|
|
1039
|
-
return {
|
|
1040
|
-
status: 'ready', changed: false, taskId: resolved.taskId, intent: null,
|
|
1041
|
-
finalization: commitFinalizationView(inspection), error: null
|
|
1042
|
-
};
|
|
1043
|
-
}
|
|
1044
|
-
if (inspection.disposition === 'prepared' && inspection.intentDigest) {
|
|
1045
|
-
removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest);
|
|
1046
|
-
return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
|
|
1047
|
-
}
|
|
1048
|
-
if (!inspection.intentDigest) {
|
|
1049
|
-
return commitIntentFailed(inspection.code ?? 'COMMIT_FINALIZATION_EVIDENCE_MISSING', inspection.message, resolved.taskId);
|
|
1050
|
-
}
|
|
1051
|
-
return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
|
|
1052
|
-
removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest);
|
|
1053
|
-
});
|
|
1054
|
-
});
|
|
1055
|
-
}
|
|
1056
|
-
catch (error) {
|
|
1057
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
function abortCommitIntent(taskRef, input, options = {}) {
|
|
1061
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1062
|
-
if (!resolved.ok)
|
|
1063
|
-
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1064
|
-
try {
|
|
1065
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1066
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.abort', () => {
|
|
1067
|
-
const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1068
|
-
const currentHead = repositoryHead(gitRoot);
|
|
1069
|
-
if (intent.phase !== 'prepared'
|
|
1070
|
-
|| intent.pushEvidence !== null
|
|
1071
|
-
|| input.expectedHead !== intent.baselineHead
|
|
1072
|
-
|| currentHead !== intent.baselineHead) {
|
|
1073
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit intent has side effects or repository drift', resolved.taskId);
|
|
1074
|
-
}
|
|
1075
|
-
removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1076
|
-
return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
|
|
1077
|
-
});
|
|
1078
|
-
}
|
|
1079
|
-
catch (error) {
|
|
1080
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
function statusCommitIntent(taskRef, options = {}) {
|
|
1084
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1085
|
-
if (!resolved.ok)
|
|
1086
|
-
return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1087
|
-
let gitRoot;
|
|
1088
|
-
try {
|
|
1089
|
-
gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1090
|
-
}
|
|
1091
|
-
catch (error) {
|
|
1092
|
-
return commitIntentFailed('ORCHESTRATION_WORKTREE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
1093
|
-
}
|
|
1094
|
-
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
1095
|
-
return {
|
|
1096
|
-
status: 'ready',
|
|
1097
|
-
changed: false,
|
|
1098
|
-
taskId: resolved.taskId,
|
|
1099
|
-
intent: inspection.intent ? commitIntentView(inspection.intent, inspection.currentHead) : null,
|
|
1100
|
-
finalization: commitFinalizationView(inspection),
|
|
1101
|
-
error: null
|
|
1102
|
-
};
|
|
1103
|
-
}
|
|
1104
552
|
function statusOrchestration(taskRef, options = {}) {
|
|
1105
553
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1106
554
|
if (!resolved.ok)
|
|
@@ -1114,26 +562,12 @@ function prepareOrchestrationDelegationUnlocked(taskRef, input, options = {}) {
|
|
|
1114
562
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1115
563
|
if (!resolved.ok)
|
|
1116
564
|
return failed(resolved.code, resolved.message, resolved.taskId);
|
|
1117
|
-
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
1118
|
-
try {
|
|
1119
|
-
readCommitIntent(resolved.taskDir, resolved.taskId);
|
|
1120
|
-
}
|
|
1121
|
-
catch (error) {
|
|
1122
|
-
return failed('ORCHESTRATION_COMMIT_INTENT_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
1123
|
-
}
|
|
1124
|
-
return failed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent blocks delegation preparation', resolved.taskId);
|
|
1125
|
-
}
|
|
1126
565
|
if (!(options.supportsLifecycleDelegation ?? supportsLifecycleDelegation)(input.client)) {
|
|
1127
566
|
return failed('ORCHESTRATION_CLIENT_UNSUPPORTED', `client '${input.client}' does not support lifecycle orchestration`, resolved.taskId);
|
|
1128
567
|
}
|
|
1129
568
|
const run = readRun(resolved.taskDir);
|
|
1130
569
|
if (!run || run.status !== 'running')
|
|
1131
570
|
return failed('ORCHESTRATION_RUN_NOT_RUNNING', 'a running orchestration is required', resolved.taskId);
|
|
1132
|
-
if (run.schemaVersion !== 3) {
|
|
1133
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'delegation preparation requires orchestration schemaVersion 3', resolved.taskId);
|
|
1134
|
-
}
|
|
1135
|
-
if (!isV2Policy(run.modelPolicy))
|
|
1136
|
-
return failed('ORCHESTRATION_MODEL_EVIDENCE_MISSING', 'running orchestration has no persisted model policy', resolved.taskId);
|
|
1137
571
|
if (run.pendingDelegation)
|
|
1138
572
|
return failed('ORCHESTRATION_DELEGATION_BUSY', 'the run already has a pending delegation', resolved.taskId);
|
|
1139
573
|
const repositoryPending = matchingDelegations(() => true, options);
|
|
@@ -1162,6 +596,10 @@ function prepareOrchestrationDelegationUnlocked(taskRef, input, options = {}) {
|
|
|
1162
596
|
if (input.requestedReasoningEffort !== expectedPolicy.reasoningEffort) {
|
|
1163
597
|
return failed('ORCHESTRATION_REQUESTED_REASONING_EFFORT_MISMATCH', `requested reasoning effort does not match the persisted ${next.role} policy`, resolved.taskId);
|
|
1164
598
|
}
|
|
599
|
+
const capabilityValidationError = options.validateLifecycleCapability?.();
|
|
600
|
+
if (capabilityValidationError) {
|
|
601
|
+
return failed(capabilityValidationError.code, capabilityValidationError.message, resolved.taskId, capabilityValidationError.detail ? { detail: capabilityValidationError.detail } : {});
|
|
602
|
+
}
|
|
1165
603
|
let beforeFingerprint;
|
|
1166
604
|
try {
|
|
1167
605
|
beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
|
|
@@ -1187,6 +625,10 @@ function prepareOrchestrationDelegationUnlocked(taskRef, input, options = {}) {
|
|
|
1187
625
|
lifecycleProvenance: input.lifecycleProvenance ?? null,
|
|
1188
626
|
beforeFingerprint
|
|
1189
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
|
+
}
|
|
1190
632
|
const updated = withUpdatedRun(run, {
|
|
1191
633
|
nextStage: next.stage,
|
|
1192
634
|
pendingDelegation: receipt,
|
|
@@ -1217,8 +659,8 @@ function dispatchOrchestrationDelegationUnlocked(taskRef, options = {}) {
|
|
|
1217
659
|
const run = readRun(resolved.taskDir);
|
|
1218
660
|
if (!run?.pendingDelegation)
|
|
1219
661
|
return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
|
|
1220
|
-
if (run.
|
|
1221
|
-
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);
|
|
1222
664
|
}
|
|
1223
665
|
const result = dispatchDelegation(run.pendingDelegation, {
|
|
1224
666
|
now: options.now,
|
|
@@ -1252,7 +694,7 @@ function matchingDelegations(predicate, options) {
|
|
|
1252
694
|
.filter((entry) => entry.isDirectory())
|
|
1253
695
|
.flatMap((entry) => {
|
|
1254
696
|
const run = readRun(path.join(activeRoot, entry.name));
|
|
1255
|
-
if (run
|
|
697
|
+
if (!run || run.status !== 'running' || !run.pendingDelegation)
|
|
1256
698
|
return [];
|
|
1257
699
|
const receipt = run.pendingDelegation;
|
|
1258
700
|
return predicate(receipt)
|
|
@@ -1412,8 +854,6 @@ function activateOrchestrationDelegation(taskRef, event, options = {}) {
|
|
|
1412
854
|
const run = readRun(resolved.taskDir);
|
|
1413
855
|
if (!run?.pendingDelegation)
|
|
1414
856
|
return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
|
|
1415
|
-
if (run.schemaVersion !== 3)
|
|
1416
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'active delegation requires schemaVersion 3', resolved.taskId);
|
|
1417
857
|
if (run.status !== 'running') {
|
|
1418
858
|
return { status: run.status, changed: false, taskId: resolved.taskId, run, next: null, error: null };
|
|
1419
859
|
}
|
|
@@ -1441,9 +881,6 @@ async function awaitOrchestrationDelegationActivation(taskRef, event, options =
|
|
|
1441
881
|
if (!run?.pendingDelegation) {
|
|
1442
882
|
return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
|
|
1443
883
|
}
|
|
1444
|
-
if (run.schemaVersion !== 3) {
|
|
1445
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'activation barrier requires schemaVersion 3', resolved.taskId);
|
|
1446
|
-
}
|
|
1447
884
|
const receipt = run.pendingDelegation;
|
|
1448
885
|
const matches = receipt.stage === event.stage
|
|
1449
886
|
&& receipt.round === event.round
|
|
@@ -1501,9 +938,6 @@ function recoverPreparedOrchestrationDelegation(taskRef, options = {}) {
|
|
|
1501
938
|
if (activeLifecycleEvidence) {
|
|
1502
939
|
return failed('ORCHESTRATION_PREPARED_RECOVERY_ACTIVE_EVIDENCE', 'active Codex lifecycle evidence blocks prepared receipt recovery', resolved.taskId);
|
|
1503
940
|
}
|
|
1504
|
-
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
1505
|
-
return failed('ORCHESTRATION_PREPARED_RECOVERY_UNSAFE', 'active commit intent blocks prepared recovery', resolved.taskId);
|
|
1506
|
-
}
|
|
1507
941
|
const current = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
|
|
1508
942
|
gitRoot: gitRootFor(resolved.repoRoot, options),
|
|
1509
943
|
stateRoot: resolved.repoRoot,
|
|
@@ -1539,8 +973,6 @@ function completeOrchestrationStage(taskRef, event, options = {}) {
|
|
|
1539
973
|
const run = readRun(resolved.taskDir);
|
|
1540
974
|
if (!run)
|
|
1541
975
|
return { status: 'running', changed: false, taskId: resolved.taskId, run: null, next: null, error: null };
|
|
1542
|
-
if (run.schemaVersion !== 3)
|
|
1543
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'stage completion requires schemaVersion 3', resolved.taskId);
|
|
1544
976
|
if (!run.pendingDelegation)
|
|
1545
977
|
return failed('ORCHESTRATION_DELEGATION_MISSING', 'active run has no pending delegation', resolved.taskId);
|
|
1546
978
|
const result = completeDelegationStage(run.pendingDelegation, event);
|
|
@@ -1563,8 +995,6 @@ function inspectOrchestrationStage(taskRef, event, options = {}) {
|
|
|
1563
995
|
}
|
|
1564
996
|
if (!run)
|
|
1565
997
|
return failed('ORCHESTRATION_RUN_MISSING', 'no orchestration run exists', resolved.taskId);
|
|
1566
|
-
if (run.schemaVersion !== 3)
|
|
1567
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'stage inspection requires schemaVersion 3', resolved.taskId);
|
|
1568
998
|
const receipt = run.pendingDelegation;
|
|
1569
999
|
if (!receipt
|
|
1570
1000
|
|| receipt.status !== 'activated'
|
|
@@ -1609,8 +1039,6 @@ function sealOrchestrationDelegation(taskRef, event, options = {}) {
|
|
|
1609
1039
|
const run = readRun(resolved.taskDir);
|
|
1610
1040
|
if (!run?.pendingDelegation)
|
|
1611
1041
|
return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
|
|
1612
|
-
if (run.schemaVersion !== 3)
|
|
1613
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'delegation sealing requires schemaVersion 3', resolved.taskId);
|
|
1614
1042
|
const result = sealDelegation(run.pendingDelegation, event, { now: options.now });
|
|
1615
1043
|
if (!result.ok)
|
|
1616
1044
|
return pauseOrchestration(taskRef, result.code, result.message, true, options);
|
|
@@ -1625,8 +1053,6 @@ function advanceOrchestration(taskRef, options = {}) {
|
|
|
1625
1053
|
const run = readRun(resolved.taskDir);
|
|
1626
1054
|
if (!run?.pendingDelegation)
|
|
1627
1055
|
return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
|
|
1628
|
-
if (run.schemaVersion !== 3)
|
|
1629
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'orchestration advance requires schemaVersion 3', resolved.taskId);
|
|
1630
1056
|
const result = consumeDelegation(run.pendingDelegation, { now: options.now });
|
|
1631
1057
|
if (!result.ok)
|
|
1632
1058
|
return pauseOrchestration(taskRef, result.code, result.message, true, options);
|
|
@@ -1655,5 +1081,5 @@ function pauseOrchestration(taskRef, code, message, recoverable, options = {}) {
|
|
|
1655
1081
|
saveRun(resolved.taskDir, updated);
|
|
1656
1082
|
return { status: 'paused', changed: true, taskId: resolved.taskId, run: updated, next: null, error: null };
|
|
1657
1083
|
}
|
|
1658
|
-
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, };
|
|
1659
1085
|
//# sourceMappingURL=orchestration.js.map
|