@fitlab-ai/agent-infra 0.9.3 → 0.9.5
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 +3 -1
- package/README.zh-CN.md +3 -1
- package/bin/cli.ts +12 -0
- package/dist/bin/cli.js +14 -0
- package/dist/bin/internal-cli.js +144 -123
- package/dist/lib/agent-clients/adapter.js +94 -4
- package/dist/lib/{sandbox/credentials.js → agent-clients/adapters/claude-code-credentials.js} +4 -14
- package/dist/lib/agent-clients/adapters/claude-code-refresh.js +85 -0
- package/dist/lib/agent-clients/adapters/claude-code-sandbox.js +247 -0
- package/dist/lib/agent-clients/adapters/claude-code.js +17 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +382 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +163 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -0
- package/dist/lib/agent-clients/adapters/codex.js +2 -2
- package/dist/lib/agent-clients/adapters/opencode-sandbox.js +108 -0
- package/dist/lib/agent-clients/adapters/opencode.js +15 -3
- package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
- package/dist/lib/agent-clients/registry.js +9 -1
- package/dist/lib/git/worktree-identity.js +64 -0
- package/dist/lib/internal/codex-lifecycle.js +202 -0
- package/dist/lib/internal/sandbox-control.js +24 -0
- package/dist/lib/internal/task-activity.js +45 -13
- package/dist/lib/internal/task-create.js +48 -0
- package/dist/lib/internal/task-ledger.js +12 -3
- package/dist/lib/internal/task-lifecycle.js +19 -1
- package/dist/lib/internal/task-orchestration.js +170 -27
- package/dist/lib/platform/github-client.js +19 -2
- package/dist/lib/platform/pr-checks.js +1 -1
- package/dist/lib/platform/pull-requests.js +47 -1
- package/dist/lib/platform/releases.js +13 -5
- package/dist/lib/process-data/index.js +239 -0
- package/dist/lib/process-data/normalize.js +198 -0
- package/dist/lib/process-data/privacy.js +33 -0
- package/dist/lib/process-data/reconcile.js +95 -0
- package/dist/lib/process-data/repair.js +58 -0
- package/dist/lib/process-data/sources.js +345 -0
- package/dist/lib/process-data/store.js +282 -0
- package/dist/lib/process-data/types.js +2 -0
- package/dist/lib/run/index.js +4 -3
- package/dist/lib/run/tui.js +7 -8
- package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
- package/dist/lib/sandbox/capture.js +7 -2
- package/dist/lib/sandbox/commands/create.js +110 -228
- package/dist/lib/sandbox/commands/enter.js +15 -47
- package/dist/lib/sandbox/commands/list-running.js +16 -6
- package/dist/lib/sandbox/commands/ls.js +10 -6
- package/dist/lib/sandbox/commands/rebuild.js +4 -3
- package/dist/lib/sandbox/commands/refresh.js +4 -84
- package/dist/lib/sandbox/commands/rm.js +24 -3
- package/dist/lib/sandbox/commands/show.js +12 -3
- package/dist/lib/sandbox/commands/start.js +7 -4
- package/dist/lib/sandbox/config.js +2 -0
- package/dist/lib/sandbox/constants.js +6 -0
- package/dist/lib/sandbox/control/client.js +58 -0
- package/dist/lib/sandbox/control/protocol.js +54 -0
- package/dist/lib/sandbox/control/server.js +144 -0
- package/dist/lib/sandbox/dockerfile.js +36 -13
- package/dist/lib/sandbox/mounts.js +17 -4
- package/dist/lib/sandbox/recovery.js +128 -5
- package/dist/lib/sandbox/redaction.js +12 -0
- package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
- package/dist/lib/sandbox/workspace-identity.js +95 -0
- package/dist/lib/sandbox/workspace-view.js +109 -0
- package/dist/lib/task/activity-intent.js +257 -51
- package/dist/lib/task/activity-log.js +32 -5
- package/dist/lib/task/commit-finalization.js +278 -0
- package/dist/lib/task/commit-intent.js +165 -0
- package/dist/lib/task/create-service.js +137 -0
- package/dist/lib/task/create.js +333 -0
- package/dist/lib/task/decision-intents.js +31 -17
- package/dist/lib/task/delegation-receipts.js +2 -2
- package/dist/lib/task/implementation-inputs.js +54 -7
- package/dist/lib/task/ledger-intents.js +35 -1
- package/dist/lib/task/orchestration.js +505 -34
- package/dist/lib/task/resolve-ref.js +41 -1
- package/dist/lib/task/review-fingerprint.js +4 -0
- package/dist/lib/task/workspace-snapshot.js +78 -3
- package/lib/agent-clients/adapter.ts +130 -6
- package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
- package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
- package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
- package/lib/agent-clients/adapters/claude-code.ts +21 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +442 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +307 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +250 -0
- package/lib/agent-clients/adapters/codex.ts +2 -2
- package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
- package/lib/agent-clients/adapters/opencode.ts +18 -3
- package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
- package/lib/agent-clients/registry.ts +9 -1
- package/lib/git/worktree-identity.ts +86 -0
- package/lib/internal/codex-lifecycle.ts +204 -0
- package/lib/internal/sandbox-control.ts +24 -0
- package/lib/internal/task-activity.ts +50 -14
- package/lib/internal/task-create.ts +49 -0
- package/lib/internal/task-ledger.ts +12 -3
- package/lib/internal/task-lifecycle.ts +22 -1
- package/lib/internal/task-orchestration.ts +117 -21
- package/lib/platform/github-client.ts +20 -2
- package/lib/platform/pr-checks.ts +1 -1
- package/lib/platform/pull-requests.ts +49 -1
- package/lib/platform/releases.ts +16 -4
- package/lib/process-data/index.ts +224 -0
- package/lib/process-data/normalize.ts +202 -0
- package/lib/process-data/privacy.ts +41 -0
- package/lib/process-data/reconcile.ts +110 -0
- package/lib/process-data/repair.ts +64 -0
- package/lib/process-data/sources.ts +354 -0
- package/lib/process-data/store.ts +315 -0
- package/lib/process-data/types.ts +135 -0
- package/lib/run/index.ts +5 -3
- package/lib/run/tui.ts +11 -8
- package/lib/sandbox/agent-client-reconciler.ts +44 -2
- package/lib/sandbox/capture.ts +9 -2
- package/lib/sandbox/commands/create.ts +127 -260
- package/lib/sandbox/commands/enter.ts +13 -59
- package/lib/sandbox/commands/list-running.ts +23 -6
- package/lib/sandbox/commands/ls.ts +22 -6
- package/lib/sandbox/commands/rebuild.ts +4 -3
- package/lib/sandbox/commands/refresh.ts +4 -126
- package/lib/sandbox/commands/rm.ts +24 -3
- package/lib/sandbox/commands/show.ts +29 -3
- package/lib/sandbox/commands/start.ts +6 -4
- package/lib/sandbox/config.ts +4 -0
- package/lib/sandbox/constants.ts +8 -0
- package/lib/sandbox/control/client.ts +79 -0
- package/lib/sandbox/control/protocol.ts +110 -0
- package/lib/sandbox/control/server.ts +155 -0
- package/lib/sandbox/dockerfile.ts +66 -13
- package/lib/sandbox/mounts.ts +24 -4
- package/lib/sandbox/recovery.ts +163 -6
- package/lib/sandbox/redaction.ts +14 -0
- package/lib/sandbox/runtimes/base.dockerfile +24 -74
- package/lib/sandbox/tool-types.ts +16 -0
- package/lib/sandbox/workspace-identity.ts +123 -0
- package/lib/sandbox/workspace-view.ts +150 -0
- package/lib/task/activity-intent.ts +350 -61
- package/lib/task/activity-log.ts +46 -7
- package/lib/task/commit-finalization.ts +376 -0
- package/lib/task/commit-intent.ts +230 -0
- package/lib/task/create-service.ts +171 -0
- package/lib/task/create.ts +372 -0
- package/lib/task/decision-intents.ts +32 -12
- package/lib/task/delegation-receipts.ts +2 -2
- package/lib/task/implementation-inputs.ts +62 -8
- package/lib/task/ledger-intents.ts +43 -3
- package/lib/task/orchestration.ts +673 -41
- package/lib/task/resolve-ref.ts +46 -1
- package/lib/task/review-fingerprint.ts +9 -0
- package/lib/task/workspace-snapshot.ts +98 -4
- package/package.json +7 -7
- package/templates/.agents/hooks/lifecycle-delegation.js +47 -3
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/human-decision-context.en.md +11 -2
- package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
- package/templates/.agents/rules/pr-checks-commands.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
- package/templates/.agents/rules/pr-checks-commands.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
- package/templates/.agents/rules/task-management.en.md +3 -0
- package/templates/.agents/rules/task-management.zh-CN.md +3 -0
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/SKILL.en.md +21 -17
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +28 -16
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +50 -0
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +56 -0
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
- package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
- package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
- package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
- package/templates/.agents/skills/review-pr/SKILL.en.md +36 -8
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +36 -8
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +67 -50
- package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +2 -2
- package/templates/.agents/templates/task.en.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +1 -1
- package/templates/.claude/commands/watch-pr.en.md +1 -1
- package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
- package/templates/.claude/rules/tool-preferences.en.md +20 -0
- package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
- package/templates/.codex/hooks.json +50 -0
- package/templates/.opencode/commands/watch-pr.en.md +1 -1
- package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
- package/templates/.gemini/commands/_project_/review-pr.en.toml +0 -10
- package/templates/.gemini/commands/_project_/review-pr.zh-CN.toml +0 -10
|
@@ -1,6 +1,7 @@
|
|
|
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';
|
|
4
5
|
|
|
5
6
|
import { parseTypedTaskFrontmatter } from './frontmatter.ts';
|
|
6
7
|
import { parseReviewSummary } from './review-artifacts.ts';
|
|
@@ -35,7 +36,33 @@ import {
|
|
|
35
36
|
diffWorkspaceSnapshots
|
|
36
37
|
} from './workspace-snapshot.ts';
|
|
37
38
|
import type { RepositorySnapshot } from './workspace-snapshot.ts';
|
|
39
|
+
import type { WorkspaceSnapshotContext } from './workspace-snapshot.ts';
|
|
40
|
+
import { assertGitRepositoryBinding } from '../git/worktree-identity.ts';
|
|
38
41
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
42
|
+
import {
|
|
43
|
+
CommitIntentError,
|
|
44
|
+
commitIntentPath,
|
|
45
|
+
createCommitIntent,
|
|
46
|
+
digest,
|
|
47
|
+
readCommitIntent,
|
|
48
|
+
removeCommitIntent,
|
|
49
|
+
removeCommitIntentByDigest,
|
|
50
|
+
serialize,
|
|
51
|
+
updateCommitIntent
|
|
52
|
+
} from './commit-intent.ts';
|
|
53
|
+
import type { CommitIntent, PushEvidence } from './commit-intent.ts';
|
|
54
|
+
import {
|
|
55
|
+
inspectCommitFinalization,
|
|
56
|
+
planCommitTaskFinalization
|
|
57
|
+
} from './commit-finalization.ts';
|
|
58
|
+
import type { CommitFinalizationInspection } from './commit-finalization.ts';
|
|
59
|
+
import { captureTaskWriteMetadata, writeTask } from './write.ts';
|
|
60
|
+
import {
|
|
61
|
+
appendActivityEntry,
|
|
62
|
+
commitAttemptStartedNote,
|
|
63
|
+
locateActivityLog,
|
|
64
|
+
pairEntries
|
|
65
|
+
} from './activity-log.ts';
|
|
39
66
|
|
|
40
67
|
type OrchestrationStatus = 'running' | 'paused' | 'completed';
|
|
41
68
|
type LegacyOrchestrationModelPolicy = Readonly<{
|
|
@@ -128,12 +155,13 @@ type OrchestrationCompletionPlanResult = Readonly<{
|
|
|
128
155
|
}>;
|
|
129
156
|
type OrchestrationOptions = {
|
|
130
157
|
repoRoot?: string;
|
|
158
|
+
gitWorktreeRoot?: string;
|
|
131
159
|
id?: () => string;
|
|
132
160
|
now?: () => string;
|
|
133
161
|
maxSteps?: number;
|
|
134
162
|
client?: AgentClientId;
|
|
135
163
|
modelPolicy?: OrchestrationModelPolicy;
|
|
136
|
-
captureWorkspace?: (
|
|
164
|
+
captureWorkspace?: (context: WorkspaceSnapshotContext) => string;
|
|
137
165
|
captureRepository?: (repoRoot: string) => RepositorySnapshot;
|
|
138
166
|
inspectPullRequest?: (taskId: string, options: { cwd: string }) => Readonly<{
|
|
139
167
|
status: string;
|
|
@@ -144,8 +172,37 @@ type OrchestrationOptions = {
|
|
|
144
172
|
}>;
|
|
145
173
|
diffWorkspace?: (repoRoot: string, before: string, after: string) => string[];
|
|
146
174
|
supportsLifecycleDelegation?: (client: AgentClientId) => boolean;
|
|
175
|
+
token?: () => string;
|
|
147
176
|
};
|
|
148
177
|
|
|
178
|
+
type CommitIntentResult = Readonly<{
|
|
179
|
+
status: 'ready' | 'failed';
|
|
180
|
+
changed: boolean;
|
|
181
|
+
taskId: string | null;
|
|
182
|
+
intent: Readonly<{
|
|
183
|
+
mode: CommitIntent['mode'];
|
|
184
|
+
phase: CommitIntent['phase'];
|
|
185
|
+
baselineHead: string;
|
|
186
|
+
currentHead: string;
|
|
187
|
+
committedHead: string | null;
|
|
188
|
+
pushEvidence: PushEvidence | null;
|
|
189
|
+
runId: string | null;
|
|
190
|
+
receiptId: string | null;
|
|
191
|
+
}> | null;
|
|
192
|
+
finalization?: Readonly<{
|
|
193
|
+
disposition: CommitFinalizationInspection['disposition'];
|
|
194
|
+
code: CommitFinalizationInspection['code'];
|
|
195
|
+
message: string;
|
|
196
|
+
currentHead: string;
|
|
197
|
+
committedHead: string | null;
|
|
198
|
+
needsAnchor: boolean;
|
|
199
|
+
needsLog: boolean;
|
|
200
|
+
attempt: Readonly<{ attempt: string; baseline: string; agent: string }> | null;
|
|
201
|
+
}>;
|
|
202
|
+
token?: string;
|
|
203
|
+
error: Readonly<{ code: string; message: string }> | null;
|
|
204
|
+
}>;
|
|
205
|
+
|
|
149
206
|
function supportsLifecycleDelegation(client: AgentClientId): boolean {
|
|
150
207
|
return getAgentClientCapability(client, 'subagents').level !== 'unsupported'
|
|
151
208
|
&& getAgentClientCapability(client, 'orchestration').level !== 'unsupported'
|
|
@@ -187,8 +244,8 @@ function saveRun(taskDir: string, run: OrchestrationRun): void {
|
|
|
187
244
|
atomicWrite(orchestrationPath(taskDir), run);
|
|
188
245
|
}
|
|
189
246
|
|
|
190
|
-
function withUpdatedRun(run: OrchestrationRun, updates: Partial<OrchestrationRun
|
|
191
|
-
return Object.freeze({ ...run, ...updates, updatedAt: new Date().toISOString() });
|
|
247
|
+
function withUpdatedRun(run: OrchestrationRun, updates: Partial<OrchestrationRun>, now?: () => string): OrchestrationRun {
|
|
248
|
+
return Object.freeze({ ...run, ...updates, updatedAt: (now ?? (() => new Date().toISOString()))() });
|
|
192
249
|
}
|
|
193
250
|
|
|
194
251
|
function failed(
|
|
@@ -200,6 +257,79 @@ function failed(
|
|
|
200
257
|
return { status: 'failed', changed: false, taskId, run: null, next: null, error: { code, message, ...details } };
|
|
201
258
|
}
|
|
202
259
|
|
|
260
|
+
function commitIntentFailed(code: string, message: string, taskId: string | null = null): CommitIntentResult {
|
|
261
|
+
return { status: 'failed', changed: false, taskId, intent: null, error: { code, message } };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function repositoryHead(repoRoot: string): string {
|
|
265
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
266
|
+
cwd: repoRoot,
|
|
267
|
+
encoding: 'utf8',
|
|
268
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
269
|
+
}).trim();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function gitRootFor(stateRoot: string, options: OrchestrationOptions): string {
|
|
273
|
+
if (!options.gitWorktreeRoot) return stateRoot;
|
|
274
|
+
return assertGitRepositoryBinding(stateRoot, options.gitWorktreeRoot).worktreeRoot;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function isAncestor(repoRoot: string, ancestor: string, descendant: string): boolean {
|
|
278
|
+
return spawnSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
|
|
279
|
+
cwd: repoRoot,
|
|
280
|
+
stdio: 'ignore'
|
|
281
|
+
}).status === 0;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function commitIntentView(intent: CommitIntent, currentHead: string): NonNullable<CommitIntentResult['intent']> {
|
|
285
|
+
return {
|
|
286
|
+
mode: intent.mode,
|
|
287
|
+
phase: intent.phase,
|
|
288
|
+
baselineHead: intent.baselineHead,
|
|
289
|
+
currentHead,
|
|
290
|
+
committedHead: intent.committedHead,
|
|
291
|
+
pushEvidence: intent.pushEvidence,
|
|
292
|
+
runId: intent.orchestration?.runId ?? null,
|
|
293
|
+
receiptId: intent.orchestration?.receiptId ?? null
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function commitFinalizationView(inspection: CommitFinalizationInspection): NonNullable<CommitIntentResult['finalization']> {
|
|
298
|
+
return {
|
|
299
|
+
disposition: inspection.disposition,
|
|
300
|
+
code: inspection.code,
|
|
301
|
+
message: inspection.message,
|
|
302
|
+
currentHead: inspection.currentHead,
|
|
303
|
+
committedHead: inspection.committedHead,
|
|
304
|
+
needsAnchor: inspection.needsAnchor,
|
|
305
|
+
needsLog: inspection.needsLog,
|
|
306
|
+
attempt: inspection.attempt
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function validCommitRun(run: OrchestrationRun | null, taskId: string): run is OrchestrationRun {
|
|
311
|
+
return run !== null
|
|
312
|
+
&& [1, 2].includes(run.schemaVersion)
|
|
313
|
+
&& run.taskId === taskId
|
|
314
|
+
&& ['running', 'paused', 'completed'].includes(run.status)
|
|
315
|
+
&& Array.isArray(run.receipts)
|
|
316
|
+
&& Object.prototype.hasOwnProperty.call(run, 'pendingDelegation')
|
|
317
|
+
&& typeof run.commitAuthorization === 'object'
|
|
318
|
+
&& run.commitAuthorization !== null
|
|
319
|
+
&& Object.prototype.hasOwnProperty.call(run.commitAuthorization, 'issuedAt')
|
|
320
|
+
&& Object.prototype.hasOwnProperty.call(run.commitAuthorization, 'consumedAt');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function mapCommitIntentError(error: unknown, taskId: string | null): CommitIntentResult {
|
|
324
|
+
if (error instanceof CommitIntentError) return commitIntentFailed(error.code, error.message, taskId);
|
|
325
|
+
if (error instanceof TaskExecutionLockError) return commitIntentFailed(error.code, error.message, taskId);
|
|
326
|
+
return commitIntentFailed(
|
|
327
|
+
'ORCHESTRATION_COMMIT_INTENT_INVALID',
|
|
328
|
+
error instanceof Error ? error.message : String(error),
|
|
329
|
+
taskId
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
203
333
|
function validModel(value: unknown): value is string {
|
|
204
334
|
return typeof value === 'string' && value.length > 0 && value.trim() === value;
|
|
205
335
|
}
|
|
@@ -493,6 +623,12 @@ function routeFromFacts(taskDir: string): Omit<OrchestrationNext, 'requestedMode
|
|
|
493
623
|
function routeOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
|
|
494
624
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
495
625
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
626
|
+
let gitRoot: string;
|
|
627
|
+
try {
|
|
628
|
+
gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
629
|
+
} catch (error) {
|
|
630
|
+
return failed('ORCHESTRATION_WORKTREE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
631
|
+
}
|
|
496
632
|
let content: string;
|
|
497
633
|
try {
|
|
498
634
|
content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
@@ -563,7 +699,7 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
563
699
|
const capture = options.captureRepository ?? captureRepositorySnapshot;
|
|
564
700
|
let before: RepositorySnapshot;
|
|
565
701
|
try {
|
|
566
|
-
before = capture(
|
|
702
|
+
before = capture(gitRoot);
|
|
567
703
|
} catch (error) {
|
|
568
704
|
return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
569
705
|
}
|
|
@@ -579,7 +715,7 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
579
715
|
options.inspectPullRequest ?? inspectPlatformPullRequest;
|
|
580
716
|
let inspection: ReturnType<NonNullable<OrchestrationOptions['inspectPullRequest']>>;
|
|
581
717
|
try {
|
|
582
|
-
inspection = inspect(resolved.taskId, { cwd:
|
|
718
|
+
inspection = inspect(resolved.taskId, { cwd: gitRoot });
|
|
583
719
|
} catch (error) {
|
|
584
720
|
return failed('ORCHESTRATION_PR_INSPECTION_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
585
721
|
}
|
|
@@ -600,7 +736,7 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
600
736
|
|
|
601
737
|
let after: RepositorySnapshot;
|
|
602
738
|
try {
|
|
603
|
-
after = capture(
|
|
739
|
+
after = capture(gitRoot);
|
|
604
740
|
} catch (error) {
|
|
605
741
|
return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
606
742
|
}
|
|
@@ -634,6 +770,488 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
634
770
|
return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
|
|
635
771
|
}
|
|
636
772
|
|
|
773
|
+
function appendCommitActivity(
|
|
774
|
+
resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
|
|
775
|
+
step: string,
|
|
776
|
+
agent: string,
|
|
777
|
+
note: string
|
|
778
|
+
): Readonly<{ code: string; message: string }> | null {
|
|
779
|
+
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
780
|
+
const activity = locateActivityLog(taskContent);
|
|
781
|
+
if (!activity) return { code: 'ORCHESTRATION_TASK_INVALID', message: 'task activity log is missing or ambiguous' };
|
|
782
|
+
const metadata = captureTaskWriteMetadata();
|
|
783
|
+
const written = writeTask({
|
|
784
|
+
taskRef: resolved.taskId,
|
|
785
|
+
expectedState: 'active',
|
|
786
|
+
mutations: [{
|
|
787
|
+
kind: 'section',
|
|
788
|
+
aliases: ['活动日志', 'Activity Log'],
|
|
789
|
+
heading: activity.heading,
|
|
790
|
+
body: appendActivityEntry(activity, { time: metadata.timestamp, step, agent, note })
|
|
791
|
+
}]
|
|
792
|
+
}, { repoRoot: resolved.repoRoot, metadataProvider: () => metadata });
|
|
793
|
+
return written.status === 'failed' ? written.error : null;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function startCommitAttempt(
|
|
797
|
+
taskRef: string,
|
|
798
|
+
input: Readonly<{ agent: string }>,
|
|
799
|
+
options: OrchestrationOptions = {}
|
|
800
|
+
): CommitIntentResult {
|
|
801
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
802
|
+
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
803
|
+
try {
|
|
804
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
805
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.start', () => {
|
|
806
|
+
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
807
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
|
|
808
|
+
}
|
|
809
|
+
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
810
|
+
const activity = locateActivityLog(taskContent);
|
|
811
|
+
if (!activity) return commitIntentFailed('ORCHESTRATION_TASK_INVALID', 'task activity log is missing or ambiguous', resolved.taskId);
|
|
812
|
+
const open = pairEntries(activity.entries).filter((row) => row.step === 'Commit' && row.started !== '' && row.done === '');
|
|
813
|
+
if (open.length > 0) {
|
|
814
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_ATTEMPT_BUSY', 'an open Commit activity entry already exists', resolved.taskId);
|
|
815
|
+
}
|
|
816
|
+
const currentHead = repositoryHead(gitRoot);
|
|
817
|
+
const attempt = (options.id ?? randomUUID)();
|
|
818
|
+
const error = appendCommitActivity(resolved, 'Commit [started]', input.agent, commitAttemptStartedNote({
|
|
819
|
+
attempt,
|
|
820
|
+
baseline: currentHead,
|
|
821
|
+
agent: input.agent
|
|
822
|
+
}));
|
|
823
|
+
if (error) return commitIntentFailed(error.code, error.message, resolved.taskId);
|
|
824
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
825
|
+
return {
|
|
826
|
+
status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
|
|
827
|
+
finalization: commitFinalizationView(inspection), error: null
|
|
828
|
+
};
|
|
829
|
+
});
|
|
830
|
+
} catch (error) {
|
|
831
|
+
return mapCommitIntentError(error, resolved.taskId);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function terminateCommitAttempt(
|
|
836
|
+
taskRef: string,
|
|
837
|
+
input: Readonly<{ attempt: string; agent: string; code: string }>,
|
|
838
|
+
options: OrchestrationOptions = {}
|
|
839
|
+
): CommitIntentResult {
|
|
840
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
841
|
+
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
842
|
+
try {
|
|
843
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
844
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.terminate', () => {
|
|
845
|
+
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
846
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'an active commit intent must be aborted or recovered first', resolved.taskId);
|
|
847
|
+
}
|
|
848
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
849
|
+
if (
|
|
850
|
+
inspection.disposition !== 'retryable-start'
|
|
851
|
+
|| inspection.attempt?.attempt !== input.attempt
|
|
852
|
+
|| inspection.attempt.agent !== input.agent
|
|
853
|
+
|| inspection.attempt.baseline !== inspection.currentHead
|
|
854
|
+
) {
|
|
855
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit attempt identity or repository HEAD has drifted', resolved.taskId);
|
|
856
|
+
}
|
|
857
|
+
const error = appendCommitActivity(
|
|
858
|
+
resolved,
|
|
859
|
+
'Commit [aborted]',
|
|
860
|
+
input.agent,
|
|
861
|
+
`aborted; attempt=${input.attempt}; code=${input.code}`
|
|
862
|
+
);
|
|
863
|
+
if (error) return commitIntentFailed(error.code, error.message, resolved.taskId);
|
|
864
|
+
return {
|
|
865
|
+
status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
|
|
866
|
+
finalization: commitFinalizationView(inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId)),
|
|
867
|
+
error: null
|
|
868
|
+
};
|
|
869
|
+
});
|
|
870
|
+
} catch (error) {
|
|
871
|
+
return mapCommitIntentError(error, resolved.taskId);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function beginCommitIntent(
|
|
876
|
+
taskRef: string,
|
|
877
|
+
input: Readonly<{ agent: string; orchestrated: boolean; baselineHead: string; attempt: string }>,
|
|
878
|
+
options: OrchestrationOptions = {}
|
|
879
|
+
): CommitIntentResult {
|
|
880
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
881
|
+
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
882
|
+
try {
|
|
883
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
884
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.begin', () => {
|
|
885
|
+
const currentHead = repositoryHead(gitRoot);
|
|
886
|
+
if (currentHead !== input.baselineHead) {
|
|
887
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_BASELINE_MISMATCH', 'baseline HEAD does not match the repository HEAD', resolved.taskId);
|
|
888
|
+
}
|
|
889
|
+
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
890
|
+
try {
|
|
891
|
+
readCommitIntent(resolved.taskDir, resolved.taskId);
|
|
892
|
+
} catch (error) {
|
|
893
|
+
return mapCommitIntentError(error, resolved.taskId);
|
|
894
|
+
}
|
|
895
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
|
|
896
|
+
}
|
|
897
|
+
const attemptInspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
898
|
+
if (
|
|
899
|
+
attemptInspection.disposition !== 'retryable-start'
|
|
900
|
+
|| attemptInspection.attempt?.attempt !== input.attempt
|
|
901
|
+
|| attemptInspection.attempt.agent !== input.agent
|
|
902
|
+
|| attemptInspection.attempt.baseline !== input.baselineHead
|
|
903
|
+
) {
|
|
904
|
+
return commitIntentFailed(
|
|
905
|
+
'ORCHESTRATION_COMMIT_ATTEMPT_MISMATCH',
|
|
906
|
+
'commit begin requires one matching retryable Commit attempt',
|
|
907
|
+
resolved.taskId
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
const runFile = orchestrationPath(resolved.taskDir);
|
|
911
|
+
let run: OrchestrationRun | null = null;
|
|
912
|
+
let sourceRunBytes: string | null = null;
|
|
913
|
+
if (fs.existsSync(runFile)) {
|
|
914
|
+
sourceRunBytes = fs.readFileSync(runFile, 'utf8');
|
|
915
|
+
try {
|
|
916
|
+
run = readRun(resolved.taskDir);
|
|
917
|
+
} catch (error) {
|
|
918
|
+
return commitIntentFailed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
919
|
+
}
|
|
920
|
+
if (!validCommitRun(run, resolved.taskId)) {
|
|
921
|
+
return commitIntentFailed('ORCHESTRATION_STATE_INVALID', 'orchestration state has an invalid schema', resolved.taskId);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
926
|
+
const failAndPause = (code: string, message: string): CommitIntentResult => {
|
|
927
|
+
if (run?.status === 'running') {
|
|
928
|
+
saveRun(resolved.taskDir, withUpdatedRun(run, {
|
|
929
|
+
status: 'paused', pause: { code, message, recoverable: true }
|
|
930
|
+
}, () => now));
|
|
931
|
+
}
|
|
932
|
+
return commitIntentFailed(code, message, resolved.taskId);
|
|
933
|
+
};
|
|
934
|
+
let orchestration: CommitIntent['orchestration'] = null;
|
|
935
|
+
if (!input.orchestrated) {
|
|
936
|
+
if (run?.pendingDelegation) {
|
|
937
|
+
return commitIntentFailed(
|
|
938
|
+
'ORCHESTRATION_STANDALONE_BUSY',
|
|
939
|
+
'standalone commit is blocked by a pending orchestration delegation',
|
|
940
|
+
resolved.taskId
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
} else {
|
|
944
|
+
if (!run || run.status !== 'running') {
|
|
945
|
+
return commitIntentFailed('ORCHESTRATION_RUN_NOT_RUNNING', 'orchestrated commit requires a running orchestration', resolved.taskId);
|
|
946
|
+
}
|
|
947
|
+
const receipt = run.pendingDelegation;
|
|
948
|
+
if (
|
|
949
|
+
!receipt
|
|
950
|
+
|| receipt.status !== 'activated'
|
|
951
|
+
|| receipt.stage !== 'commit'
|
|
952
|
+
|| receipt.round !== 1
|
|
953
|
+
|| receipt.artifact !== 'commit'
|
|
954
|
+
|| receipt.role !== 'executor'
|
|
955
|
+
) {
|
|
956
|
+
return failAndPause(
|
|
957
|
+
'ORCHESTRATION_PROVENANCE_MISMATCH',
|
|
958
|
+
'orchestrated commit requires one matching activated commit delegation'
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
if (!run.commitAuthorization.issuedAt || run.commitAuthorization.consumedAt) {
|
|
962
|
+
return failAndPause(
|
|
963
|
+
'ORCHESTRATION_COMMIT_AUTHORIZATION_INVALID',
|
|
964
|
+
'orchestrated commit requires an unconsumed one-use authorization'
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
const planned = planOrchestrationStageCompletion(taskRef, {
|
|
968
|
+
stage: 'commit', round: 1, artifact: 'commit', role: 'executor', agent: input.agent
|
|
969
|
+
}, { ...options, now: () => now });
|
|
970
|
+
if (planned.result.status === 'failed' || !planned.plan?.updatedRun.pendingDelegation) {
|
|
971
|
+
return failAndPause(
|
|
972
|
+
planned.result.error?.code ?? 'ORCHESTRATION_PROVENANCE_MISMATCH',
|
|
973
|
+
planned.result.error?.message ?? 'commit delegation could not be completed'
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
const plannedRunBytes = serialize(planned.plan.updatedRun);
|
|
977
|
+
orchestration = {
|
|
978
|
+
runId: run.runId,
|
|
979
|
+
receiptId: receipt.id,
|
|
980
|
+
authorizationIssuedAt: run.commitAuthorization.issuedAt,
|
|
981
|
+
sourceRunDigest: digest(sourceRunBytes!),
|
|
982
|
+
plannedRunDigest: digest(plannedRunBytes),
|
|
983
|
+
completionUpdatedAt: planned.plan.updatedRun.updatedAt,
|
|
984
|
+
plannedReceipt: planned.plan.updatedRun.pendingDelegation
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const created = createCommitIntent(resolved.taskDir, {
|
|
989
|
+
taskId: resolved.taskId,
|
|
990
|
+
mode: input.orchestrated ? 'orchestrated' : 'standalone',
|
|
991
|
+
phase: 'prepared',
|
|
992
|
+
baselineHead: input.baselineHead,
|
|
993
|
+
committedHead: null,
|
|
994
|
+
pushEvidence: null,
|
|
995
|
+
orchestration,
|
|
996
|
+
createdAt: now,
|
|
997
|
+
updatedAt: now
|
|
998
|
+
}, { token: options.token });
|
|
999
|
+
return {
|
|
1000
|
+
status: 'ready', changed: true, taskId: resolved.taskId,
|
|
1001
|
+
intent: commitIntentView(created.intent, currentHead), token: created.token, error: null
|
|
1002
|
+
};
|
|
1003
|
+
});
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
return mapCommitIntentError(error, resolved.taskId);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function checkpointCommitIntent(
|
|
1010
|
+
taskRef: string,
|
|
1011
|
+
input: Readonly<{
|
|
1012
|
+
token: string;
|
|
1013
|
+
kind: 'committed' | 'pushed';
|
|
1014
|
+
head: string;
|
|
1015
|
+
remote?: string;
|
|
1016
|
+
ref?: string;
|
|
1017
|
+
}>,
|
|
1018
|
+
options: OrchestrationOptions = {}
|
|
1019
|
+
): CommitIntentResult {
|
|
1020
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1021
|
+
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1022
|
+
try {
|
|
1023
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1024
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.checkpoint', () => {
|
|
1025
|
+
const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1026
|
+
const currentHead = repositoryHead(gitRoot);
|
|
1027
|
+
if (currentHead !== input.head) {
|
|
1028
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'checkpoint HEAD does not match the repository HEAD', resolved.taskId);
|
|
1029
|
+
}
|
|
1030
|
+
if (input.kind === 'committed' && !isAncestor(gitRoot, intent.baselineHead, input.head)) {
|
|
1031
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'committed HEAD does not descend from the baseline', resolved.taskId);
|
|
1032
|
+
}
|
|
1033
|
+
if (input.kind === 'pushed') {
|
|
1034
|
+
const expectedHead = intent.committedHead ?? intent.baselineHead;
|
|
1035
|
+
if (input.head !== expectedHead || !input.remote || !input.ref) {
|
|
1036
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_STATE_INVALID', 'pushed checkpoint requires matching remote, ref and HEAD', resolved.taskId);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
1040
|
+
const updated = updateCommitIntent(resolved.taskDir, resolved.taskId, input.token, input.kind === 'committed'
|
|
1041
|
+
? { phase: 'committed', committedHead: input.head, updatedAt: now }
|
|
1042
|
+
: {
|
|
1043
|
+
phase: 'pushed', committedHead: intent.committedHead ?? intent.baselineHead, updatedAt: now,
|
|
1044
|
+
pushEvidence: { remote: input.remote!, ref: input.ref!, head: input.head }
|
|
1045
|
+
});
|
|
1046
|
+
return {
|
|
1047
|
+
status: 'ready', changed: true, taskId: resolved.taskId,
|
|
1048
|
+
intent: commitIntentView(updated, currentHead), error: null
|
|
1049
|
+
};
|
|
1050
|
+
});
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
return mapCommitIntentError(error, resolved.taskId);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function completeCommitOrchestration(
|
|
1057
|
+
resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
|
|
1058
|
+
intent: CommitIntent,
|
|
1059
|
+
agent: string
|
|
1060
|
+
): Readonly<{ code: string; message: string }> | null {
|
|
1061
|
+
if (intent.orchestration === null) return null;
|
|
1062
|
+
if (intent.orchestration.plannedReceipt.agent !== agent) {
|
|
1063
|
+
return { code: 'ORCHESTRATION_PROVENANCE_MISMATCH', message: 'completion agent does not match the planned receipt' };
|
|
1064
|
+
}
|
|
1065
|
+
const runFile = orchestrationPath(resolved.taskDir);
|
|
1066
|
+
if (!fs.existsSync(runFile)) {
|
|
1067
|
+
return { code: 'ORCHESTRATION_RUN_MISSING', message: 'orchestration run disappeared' };
|
|
1068
|
+
}
|
|
1069
|
+
const currentBytes = fs.readFileSync(runFile, 'utf8');
|
|
1070
|
+
const currentDigest = digest(currentBytes);
|
|
1071
|
+
if (currentDigest === intent.orchestration.sourceRunDigest) {
|
|
1072
|
+
const run = readRun(resolved.taskDir);
|
|
1073
|
+
if (!run) return { code: 'ORCHESTRATION_RUN_MISSING', message: 'orchestration run disappeared' };
|
|
1074
|
+
const plannedRun: OrchestrationRun = Object.freeze({
|
|
1075
|
+
...run,
|
|
1076
|
+
pendingDelegation: intent.orchestration.plannedReceipt,
|
|
1077
|
+
updatedAt: intent.orchestration.completionUpdatedAt
|
|
1078
|
+
});
|
|
1079
|
+
if (digest(serialize(plannedRun)) !== intent.orchestration.plannedRunDigest) {
|
|
1080
|
+
return { code: 'ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', message: 'planned orchestration bytes no longer match the intent' };
|
|
1081
|
+
}
|
|
1082
|
+
atomicWrite(runFile, plannedRun);
|
|
1083
|
+
if (digest(fs.readFileSync(runFile)) !== intent.orchestration.plannedRunDigest) {
|
|
1084
|
+
return { code: 'ORCHESTRATION_COMMIT_COMPLETE_PARTIAL', message: 'orchestration completion could not be verified' };
|
|
1085
|
+
}
|
|
1086
|
+
} else if (currentDigest !== intent.orchestration.plannedRunDigest) {
|
|
1087
|
+
return { code: 'ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', message: 'orchestration state changed after commit begin' };
|
|
1088
|
+
}
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function finalizeCommitIntentUnlocked(
|
|
1093
|
+
resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
|
|
1094
|
+
inspection: CommitFinalizationInspection,
|
|
1095
|
+
agent: string,
|
|
1096
|
+
remove: () => void
|
|
1097
|
+
): CommitIntentResult {
|
|
1098
|
+
if (inspection.disposition !== 'recoverable' || !inspection.intent) {
|
|
1099
|
+
return commitIntentFailed(
|
|
1100
|
+
inspection.code ?? 'COMMIT_FINALIZATION_PENDING',
|
|
1101
|
+
inspection.message,
|
|
1102
|
+
resolved.taskId
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
if (
|
|
1106
|
+
inspection.intent.orchestration !== null
|
|
1107
|
+
&& inspection.intent.orchestration.plannedReceipt.agent !== agent
|
|
1108
|
+
) {
|
|
1109
|
+
return commitIntentFailed(
|
|
1110
|
+
'ORCHESTRATION_PROVENANCE_MISMATCH',
|
|
1111
|
+
'completion agent does not match the planned receipt',
|
|
1112
|
+
resolved.taskId
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
const metadata = captureTaskWriteMetadata();
|
|
1116
|
+
const taskPlan = planCommitTaskFinalization(
|
|
1117
|
+
resolved.taskDir,
|
|
1118
|
+
inspection,
|
|
1119
|
+
agent,
|
|
1120
|
+
metadata.timestamp
|
|
1121
|
+
);
|
|
1122
|
+
const written = writeTask({
|
|
1123
|
+
taskRef: resolved.taskId,
|
|
1124
|
+
expectedState: 'active',
|
|
1125
|
+
mutations: taskPlan.mutations
|
|
1126
|
+
}, {
|
|
1127
|
+
repoRoot: resolved.repoRoot,
|
|
1128
|
+
metadataProvider: () => metadata
|
|
1129
|
+
});
|
|
1130
|
+
if (written.status === 'failed') {
|
|
1131
|
+
return commitIntentFailed(written.error.code, written.error.message, resolved.taskId);
|
|
1132
|
+
}
|
|
1133
|
+
const orchestrationError = completeCommitOrchestration(resolved, inspection.intent, agent);
|
|
1134
|
+
if (orchestrationError) {
|
|
1135
|
+
return commitIntentFailed(orchestrationError.code, orchestrationError.message, resolved.taskId);
|
|
1136
|
+
}
|
|
1137
|
+
try {
|
|
1138
|
+
remove();
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
return commitIntentFailed(
|
|
1141
|
+
'ORCHESTRATION_COMMIT_COMPLETE_PARTIAL',
|
|
1142
|
+
error instanceof Error ? error.message : String(error),
|
|
1143
|
+
resolved.taskId
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function completeCommitIntent(
|
|
1150
|
+
taskRef: string,
|
|
1151
|
+
input: Readonly<{ token: string; agent: string }>,
|
|
1152
|
+
options: OrchestrationOptions = {}
|
|
1153
|
+
): CommitIntentResult {
|
|
1154
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1155
|
+
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1156
|
+
try {
|
|
1157
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1158
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.complete', () => {
|
|
1159
|
+
readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1160
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
1161
|
+
return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
|
|
1162
|
+
removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1163
|
+
});
|
|
1164
|
+
});
|
|
1165
|
+
} catch (error) {
|
|
1166
|
+
return mapCommitIntentError(error, resolved.taskId);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function recoverCommitIntent(
|
|
1171
|
+
taskRef: string,
|
|
1172
|
+
input: Readonly<{ agent: string }>,
|
|
1173
|
+
options: OrchestrationOptions = {}
|
|
1174
|
+
): CommitIntentResult {
|
|
1175
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1176
|
+
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1177
|
+
try {
|
|
1178
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1179
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.recover', () => {
|
|
1180
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
1181
|
+
if (inspection.disposition === 'retryable-start') {
|
|
1182
|
+
return {
|
|
1183
|
+
status: 'ready', changed: false, taskId: resolved.taskId, intent: null,
|
|
1184
|
+
finalization: commitFinalizationView(inspection), error: null
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
if (inspection.disposition === 'prepared' && inspection.intentDigest) {
|
|
1188
|
+
removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest);
|
|
1189
|
+
return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
|
|
1190
|
+
}
|
|
1191
|
+
if (!inspection.intentDigest) {
|
|
1192
|
+
return commitIntentFailed(
|
|
1193
|
+
inspection.code ?? 'COMMIT_FINALIZATION_EVIDENCE_MISSING',
|
|
1194
|
+
inspection.message,
|
|
1195
|
+
resolved.taskId
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
|
|
1199
|
+
removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest!);
|
|
1200
|
+
});
|
|
1201
|
+
});
|
|
1202
|
+
} catch (error) {
|
|
1203
|
+
return mapCommitIntentError(error, resolved.taskId);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function abortCommitIntent(
|
|
1208
|
+
taskRef: string,
|
|
1209
|
+
input: Readonly<{ token: string; expectedHead: string }>,
|
|
1210
|
+
options: OrchestrationOptions = {}
|
|
1211
|
+
): CommitIntentResult {
|
|
1212
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1213
|
+
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1214
|
+
try {
|
|
1215
|
+
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1216
|
+
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.abort', () => {
|
|
1217
|
+
const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1218
|
+
const currentHead = repositoryHead(gitRoot);
|
|
1219
|
+
if (
|
|
1220
|
+
intent.phase !== 'prepared'
|
|
1221
|
+
|| intent.pushEvidence !== null
|
|
1222
|
+
|| input.expectedHead !== intent.baselineHead
|
|
1223
|
+
|| currentHead !== intent.baselineHead
|
|
1224
|
+
) {
|
|
1225
|
+
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit intent has side effects or repository drift', resolved.taskId);
|
|
1226
|
+
}
|
|
1227
|
+
removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1228
|
+
return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
|
|
1229
|
+
});
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
return mapCommitIntentError(error, resolved.taskId);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function statusCommitIntent(taskRef: string, options: OrchestrationOptions = {}): CommitIntentResult {
|
|
1236
|
+
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1237
|
+
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1238
|
+
let gitRoot: string;
|
|
1239
|
+
try {
|
|
1240
|
+
gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
return commitIntentFailed('ORCHESTRATION_WORKTREE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
1243
|
+
}
|
|
1244
|
+
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
1245
|
+
return {
|
|
1246
|
+
status: 'ready',
|
|
1247
|
+
changed: false,
|
|
1248
|
+
taskId: resolved.taskId,
|
|
1249
|
+
intent: inspection.intent ? commitIntentView(inspection.intent, inspection.currentHead) : null,
|
|
1250
|
+
finalization: commitFinalizationView(inspection),
|
|
1251
|
+
error: null
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
|
|
637
1255
|
function statusOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
|
|
638
1256
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
639
1257
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
@@ -649,6 +1267,18 @@ function prepareOrchestrationDelegationUnlocked(
|
|
|
649
1267
|
): OrchestrationResult {
|
|
650
1268
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
651
1269
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
1270
|
+
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
1271
|
+
try {
|
|
1272
|
+
readCommitIntent(resolved.taskDir, resolved.taskId);
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
return failed(
|
|
1275
|
+
'ORCHESTRATION_COMMIT_INTENT_INVALID',
|
|
1276
|
+
error instanceof Error ? error.message : String(error),
|
|
1277
|
+
resolved.taskId
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
return failed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent blocks delegation preparation', resolved.taskId);
|
|
1281
|
+
}
|
|
652
1282
|
if (!(options.supportsLifecycleDelegation ?? supportsLifecycleDelegation)(input.client)) {
|
|
653
1283
|
return failed('ORCHESTRATION_CLIENT_UNSUPPORTED', `client '${input.client}' does not support lifecycle orchestration`, resolved.taskId);
|
|
654
1284
|
}
|
|
@@ -656,8 +1286,7 @@ function prepareOrchestrationDelegationUnlocked(
|
|
|
656
1286
|
if (!run || run.status !== 'running') return failed('ORCHESTRATION_RUN_NOT_RUNNING', 'a running orchestration is required', resolved.taskId);
|
|
657
1287
|
if (!isV2Policy(run.modelPolicy)) return failed('ORCHESTRATION_MODEL_EVIDENCE_MISSING', 'running orchestration has no persisted model policy', resolved.taskId);
|
|
658
1288
|
if (run.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_BUSY', 'the run already has a pending delegation', resolved.taskId);
|
|
659
|
-
const repositoryPending = (
|
|
660
|
-
.flatMap((client) => matchingDelegations(client, () => true, options));
|
|
1289
|
+
const repositoryPending = matchingDelegations(() => true, options);
|
|
661
1290
|
if (repositoryPending.length > 0) {
|
|
662
1291
|
return failed('ORCHESTRATION_DELEGATION_BUSY', 'the repository already has a pending lifecycle delegation', resolved.taskId);
|
|
663
1292
|
}
|
|
@@ -680,7 +1309,11 @@ function prepareOrchestrationDelegationUnlocked(
|
|
|
680
1309
|
}
|
|
681
1310
|
let beforeFingerprint: string;
|
|
682
1311
|
try {
|
|
683
|
-
beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)(
|
|
1312
|
+
beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
|
|
1313
|
+
gitRoot: gitRootFor(resolved.repoRoot, options),
|
|
1314
|
+
stateRoot: resolved.repoRoot,
|
|
1315
|
+
taskId: resolved.taskId
|
|
1316
|
+
});
|
|
684
1317
|
} catch (error) {
|
|
685
1318
|
return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
686
1319
|
}
|
|
@@ -718,9 +1351,14 @@ function prepareOrchestrationDelegation(
|
|
|
718
1351
|
try {
|
|
719
1352
|
return withTaskExecutionLock(
|
|
720
1353
|
resolved.repoRoot,
|
|
721
|
-
|
|
722
|
-
'task-orchestration.prepare',
|
|
723
|
-
() =>
|
|
1354
|
+
'__repository__',
|
|
1355
|
+
'task-orchestration.prepare.repository',
|
|
1356
|
+
() => withTaskExecutionLock(
|
|
1357
|
+
resolved.repoRoot,
|
|
1358
|
+
resolved.taskId,
|
|
1359
|
+
'task-orchestration.prepare.task',
|
|
1360
|
+
() => prepareOrchestrationDelegationUnlocked(taskRef, input, options)
|
|
1361
|
+
)
|
|
724
1362
|
);
|
|
725
1363
|
} catch (error) {
|
|
726
1364
|
if (error instanceof TaskExecutionLockError) return failed(error.code, error.message, resolved.taskId);
|
|
@@ -729,7 +1367,6 @@ function prepareOrchestrationDelegation(
|
|
|
729
1367
|
}
|
|
730
1368
|
|
|
731
1369
|
function matchingDelegations(
|
|
732
|
-
client: AgentClientId,
|
|
733
1370
|
predicate: (receipt: DelegationReceipt) => boolean,
|
|
734
1371
|
options: OrchestrationOptions
|
|
735
1372
|
): Array<{ taskId: string; run: OrchestrationRun }> {
|
|
@@ -742,7 +1379,7 @@ function matchingDelegations(
|
|
|
742
1379
|
const run = readRun(path.join(activeRoot, entry.name));
|
|
743
1380
|
if (!run?.pendingDelegation) return [];
|
|
744
1381
|
const receipt = run.pendingDelegation;
|
|
745
|
-
return
|
|
1382
|
+
return predicate(receipt)
|
|
746
1383
|
? [{ taskId: entry.name, run }]
|
|
747
1384
|
: [];
|
|
748
1385
|
});
|
|
@@ -753,7 +1390,10 @@ function uniqueMatchingDelegation(
|
|
|
753
1390
|
predicate: (receipt: DelegationReceipt) => boolean,
|
|
754
1391
|
options: OrchestrationOptions
|
|
755
1392
|
): { taskId: string; run: OrchestrationRun } | OrchestrationResult {
|
|
756
|
-
const matches = matchingDelegations(
|
|
1393
|
+
const matches = matchingDelegations(
|
|
1394
|
+
(receipt) => receipt.client === client && predicate(receipt),
|
|
1395
|
+
options
|
|
1396
|
+
);
|
|
757
1397
|
if (matches.length === 0) return failed('ORCHESTRATION_DELEGATION_MISSING', 'no matching lifecycle delegation exists');
|
|
758
1398
|
if (matches.length > 1) return failed('ORCHESTRATION_DELEGATION_AMBIGUOUS', 'multiple lifecycle delegations match the native hook event');
|
|
759
1399
|
return matches[0]!;
|
|
@@ -797,9 +1437,14 @@ function sealMatchingOrchestrationDelegation(
|
|
|
797
1437
|
}
|
|
798
1438
|
const repoRoot = options.repoRoot ?? process.cwd();
|
|
799
1439
|
try {
|
|
1440
|
+
const gitRoot = gitRootFor(repoRoot, options);
|
|
800
1441
|
const snapshotTaskId = receipt.workspaceSnapshotScope === 'task' ? receipt.taskId : null;
|
|
801
|
-
const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)(
|
|
802
|
-
|
|
1442
|
+
const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
|
|
1443
|
+
gitRoot,
|
|
1444
|
+
stateRoot: repoRoot,
|
|
1445
|
+
taskId: snapshotTaskId
|
|
1446
|
+
});
|
|
1447
|
+
const changedPaths = (options.diffWorkspace ?? diffWorkspaceSnapshots)(gitRoot, receipt.beforeFingerprint, afterFingerprint);
|
|
803
1448
|
return sealOrchestrationDelegation(matched.taskId, {
|
|
804
1449
|
childId: event.childId,
|
|
805
1450
|
exitCode: 0,
|
|
@@ -901,7 +1546,7 @@ function planOrchestrationStageCompletion(
|
|
|
901
1546
|
}
|
|
902
1547
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
903
1548
|
if (!resolved.ok) return { result: failed(resolved.code, resolved.message, resolved.taskId), plan: null };
|
|
904
|
-
const updatedRun = withUpdatedRun(inspected.run, { pendingDelegation: completed.receipt });
|
|
1549
|
+
const updatedRun = withUpdatedRun(inspected.run, { pendingDelegation: completed.receipt }, options.now);
|
|
905
1550
|
return {
|
|
906
1551
|
result: { status: 'running', changed: true, taskId: resolved.taskId, run: updatedRun, next: null, error: null },
|
|
907
1552
|
plan: {
|
|
@@ -916,27 +1561,6 @@ function commitOrchestrationStageCompletion(plan: OrchestrationStageCompletion):
|
|
|
916
1561
|
saveRun(plan.taskDir, plan.updatedRun);
|
|
917
1562
|
}
|
|
918
1563
|
|
|
919
|
-
function completeCommitOrchestrationStage(
|
|
920
|
-
taskRef: string,
|
|
921
|
-
agent: string,
|
|
922
|
-
options: OrchestrationOptions = {}
|
|
923
|
-
): OrchestrationResult {
|
|
924
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
925
|
-
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
926
|
-
const run = readRun(resolved.taskDir);
|
|
927
|
-
if (!run) return { status: 'running', changed: false, taskId: resolved.taskId, run: null, next: null, error: null };
|
|
928
|
-
const receipt = run.pendingDelegation;
|
|
929
|
-
if (!receipt || receipt.stage !== 'commit') {
|
|
930
|
-
return failed('ORCHESTRATION_DELEGATION_MISSING', 'active run has no pending commit delegation', resolved.taskId);
|
|
931
|
-
}
|
|
932
|
-
return completeOrchestrationStage(taskRef, {
|
|
933
|
-
stage: receipt.stage,
|
|
934
|
-
round: receipt.round,
|
|
935
|
-
artifact: receipt.artifact,
|
|
936
|
-
agent
|
|
937
|
-
}, options);
|
|
938
|
-
}
|
|
939
|
-
|
|
940
1564
|
function sealOrchestrationDelegation(
|
|
941
1565
|
taskRef: string,
|
|
942
1566
|
event: Parameters<typeof sealDelegation>[1],
|
|
@@ -995,9 +1619,12 @@ export {
|
|
|
995
1619
|
activateMatchingOrchestrationDelegation,
|
|
996
1620
|
activateOrchestrationDelegation,
|
|
997
1621
|
advanceOrchestration,
|
|
1622
|
+
abortCommitIntent,
|
|
1623
|
+
beginCommitIntent,
|
|
998
1624
|
beginOrResumeOrchestration,
|
|
1625
|
+
checkpointCommitIntent,
|
|
999
1626
|
commitOrchestrationStageCompletion,
|
|
1000
|
-
|
|
1627
|
+
completeCommitIntent,
|
|
1001
1628
|
completeOrchestrationStage,
|
|
1002
1629
|
inspectOrchestrationStage,
|
|
1003
1630
|
orchestrationPath,
|
|
@@ -1005,12 +1632,17 @@ export {
|
|
|
1005
1632
|
planOrchestrationStageCompletion,
|
|
1006
1633
|
prepareOrchestrationDelegation,
|
|
1007
1634
|
readRun,
|
|
1635
|
+
recoverCommitIntent,
|
|
1008
1636
|
routeOrchestration,
|
|
1009
1637
|
sealMatchingOrchestrationDelegation,
|
|
1010
1638
|
sealOrchestrationDelegation,
|
|
1011
|
-
|
|
1639
|
+
startCommitAttempt,
|
|
1640
|
+
statusCommitIntent,
|
|
1641
|
+
statusOrchestration,
|
|
1642
|
+
terminateCommitAttempt
|
|
1012
1643
|
};
|
|
1013
1644
|
export type {
|
|
1645
|
+
CommitIntentResult,
|
|
1014
1646
|
OrchestrationCompletionPlanResult,
|
|
1015
1647
|
OrchestrationModelPolicy,
|
|
1016
1648
|
OrchestrationNext,
|