@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
|
@@ -9,6 +9,8 @@ import { parseTaskFrontmatter, extractTitle, type Frontmatter } from '../frontma
|
|
|
9
9
|
import { loadShortIdByTaskId } from '../short-id.ts';
|
|
10
10
|
import { getOpenWorkflowWarnings, formatWorkflowWarningSummary, type WorkflowWarning } from '../workflow-warnings.ts';
|
|
11
11
|
import { parseActivityLog, pairEntries } from '../activity-log.ts';
|
|
12
|
+
import { readRun, type OrchestrationRun } from '../orchestration.ts';
|
|
13
|
+
import type { DelegationReceipt } from '../delegation-receipts.ts';
|
|
12
14
|
import { statusCard, type DisplayMessage } from '../../server/display.ts';
|
|
13
15
|
|
|
14
16
|
const USAGE = `Usage: ai task status [<N | TASK-id> | --task <ref> | -t <ref>]
|
|
@@ -219,6 +221,19 @@ type RuntimeInfo = {
|
|
|
219
221
|
log: string;
|
|
220
222
|
};
|
|
221
223
|
|
|
224
|
+
type PendingDelegationInfo = Readonly<Pick<
|
|
225
|
+
DelegationReceipt,
|
|
226
|
+
'id' | 'role' | 'stage' | 'round' | 'artifact' | 'client' | 'status'
|
|
227
|
+
>>;
|
|
228
|
+
|
|
229
|
+
type OrchestrationInfo = Readonly<{
|
|
230
|
+
status: 'absent' | OrchestrationRun['status'];
|
|
231
|
+
runId: string;
|
|
232
|
+
nextStage: string;
|
|
233
|
+
pause: Readonly<{ code: string; message: string; recoverable: boolean }> | null;
|
|
234
|
+
pendingDelegation: PendingDelegationInfo | null;
|
|
235
|
+
}>;
|
|
236
|
+
|
|
222
237
|
type ManagedRunRecord = {
|
|
223
238
|
run_id: string;
|
|
224
239
|
engine: string;
|
|
@@ -323,6 +338,39 @@ function collectRuntime(taskDir: string, workflow: WorkflowInfo, run: Runner): R
|
|
|
323
338
|
};
|
|
324
339
|
}
|
|
325
340
|
|
|
341
|
+
function pendingDelegationInfo(receipt: DelegationReceipt | null): PendingDelegationInfo | null {
|
|
342
|
+
if (!receipt) return null;
|
|
343
|
+
return {
|
|
344
|
+
id: receipt.id,
|
|
345
|
+
role: receipt.role,
|
|
346
|
+
stage: receipt.stage,
|
|
347
|
+
round: receipt.round,
|
|
348
|
+
artifact: receipt.artifact,
|
|
349
|
+
client: receipt.client,
|
|
350
|
+
status: receipt.status
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function collectOrchestration(taskDir: string): OrchestrationInfo {
|
|
355
|
+
const run = readRun(taskDir);
|
|
356
|
+
if (!run) {
|
|
357
|
+
return {
|
|
358
|
+
status: 'absent',
|
|
359
|
+
runId: DASH,
|
|
360
|
+
nextStage: DASH,
|
|
361
|
+
pause: null,
|
|
362
|
+
pendingDelegation: null
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
status: run.status,
|
|
367
|
+
runId: run.runId,
|
|
368
|
+
nextStage: run.nextStage ?? DASH,
|
|
369
|
+
pause: run.pause,
|
|
370
|
+
pendingDelegation: pendingDelegationInfo(run.pendingDelegation)
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
326
374
|
type StatusModel = {
|
|
327
375
|
taskId: string;
|
|
328
376
|
shortId: string;
|
|
@@ -332,6 +380,7 @@ type StatusModel = {
|
|
|
332
380
|
artifacts: { count: number; groups: { stage: string; files: string[] }[] };
|
|
333
381
|
workflow: WorkflowInfo;
|
|
334
382
|
runtime: RuntimeInfo;
|
|
383
|
+
orchestration: OrchestrationInfo;
|
|
335
384
|
git: GitInfo;
|
|
336
385
|
};
|
|
337
386
|
|
|
@@ -380,6 +429,26 @@ function renderStatus(model: StatusModel): string[] {
|
|
|
380
429
|
lines.push(...renderPairs(model.artifacts.groups.map((group) => [group.stage, group.files.join(', ')])));
|
|
381
430
|
}
|
|
382
431
|
|
|
432
|
+
lines.push(
|
|
433
|
+
'',
|
|
434
|
+
'Orchestration',
|
|
435
|
+
...renderPairs([
|
|
436
|
+
['status', model.orchestration.status],
|
|
437
|
+
['run_id', model.orchestration.runId],
|
|
438
|
+
['next_stage', model.orchestration.nextStage],
|
|
439
|
+
['pause_code', model.orchestration.pause?.code ?? DASH],
|
|
440
|
+
['pause_message', model.orchestration.pause?.message ?? DASH],
|
|
441
|
+
['recoverable', model.orchestration.pause ? String(model.orchestration.pause.recoverable) : DASH],
|
|
442
|
+
['pending_id', model.orchestration.pendingDelegation?.id ?? DASH],
|
|
443
|
+
['pending_role', model.orchestration.pendingDelegation?.role ?? DASH],
|
|
444
|
+
['pending_stage', model.orchestration.pendingDelegation?.stage ?? DASH],
|
|
445
|
+
['pending_round', model.orchestration.pendingDelegation ? String(model.orchestration.pendingDelegation.round) : DASH],
|
|
446
|
+
['pending_artifact', model.orchestration.pendingDelegation?.artifact ?? DASH],
|
|
447
|
+
['pending_client', model.orchestration.pendingDelegation?.client ?? DASH],
|
|
448
|
+
['pending_status', model.orchestration.pendingDelegation?.status ?? DASH]
|
|
449
|
+
])
|
|
450
|
+
);
|
|
451
|
+
|
|
383
452
|
lines.push(
|
|
384
453
|
'',
|
|
385
454
|
'Workflow',
|
|
@@ -425,6 +494,9 @@ function renderStatus(model: StatusModel): string[] {
|
|
|
425
494
|
}
|
|
426
495
|
|
|
427
496
|
function modelTone(model: StatusModel): 'info' | 'success' | 'warning' | 'danger' | 'running' {
|
|
497
|
+
if (model.orchestration.status === 'paused') return 'warning';
|
|
498
|
+
if (model.orchestration.status === 'running') return 'running';
|
|
499
|
+
if (model.orchestration.status === 'completed') return 'success';
|
|
428
500
|
if (model.workflow.state === 'in-progress') return model.workflow.stale === 'yes' ? 'warning' : 'running';
|
|
429
501
|
if (model.metadata.some(([key, value]) => key === 'status' && value === 'completed')) return 'success';
|
|
430
502
|
if (model.metadata.some(([key, value]) => key === 'status' && (value === 'blocked' || value === 'cancelled'))) {
|
|
@@ -449,6 +521,7 @@ function buildFromResolved(input: BuildStatusModelInput): StatusModel {
|
|
|
449
521
|
artifacts: { count: artifacts.length, groups: groupArtifacts(artifacts) },
|
|
450
522
|
workflow,
|
|
451
523
|
runtime: collectRuntime(input.taskDir, workflow, run),
|
|
524
|
+
orchestration: collectOrchestration(input.taskDir),
|
|
452
525
|
git: collectGit(fm.branch ?? '', run)
|
|
453
526
|
};
|
|
454
527
|
}
|
|
@@ -483,6 +556,9 @@ function statusModelToDisplay(model: StatusModel): DisplayMessage {
|
|
|
483
556
|
`Task ${model.taskId} (${model.shortId})`,
|
|
484
557
|
modelTone(model),
|
|
485
558
|
[
|
|
559
|
+
['orchestration', model.orchestration.status],
|
|
560
|
+
['run', model.orchestration.runId],
|
|
561
|
+
['next', model.orchestration.nextStage],
|
|
486
562
|
['workflow', model.workflow.state],
|
|
487
563
|
['step', model.workflow.step],
|
|
488
564
|
['runtime', model.runtime.status],
|
|
@@ -530,8 +606,9 @@ export {
|
|
|
530
606
|
collectGit,
|
|
531
607
|
collectWorkflow,
|
|
532
608
|
collectRuntime,
|
|
609
|
+
collectOrchestration,
|
|
533
610
|
renderStatus,
|
|
534
611
|
statusModelToDisplay,
|
|
535
612
|
METADATA_KEYS
|
|
536
613
|
};
|
|
537
|
-
export type { Runner, GitInfo, WorkflowInfo, RuntimeInfo, StatusModel, BuildStatusModelInput };
|
|
614
|
+
export type { Runner, GitInfo, WorkflowInfo, RuntimeInfo, OrchestrationInfo, PendingDelegationInfo, StatusModel, BuildStatusModelInput };
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
|
|
4
|
+
import { commitExplicitPaths, inspectGitWorkflow, pushGitRefs } from '../git/workflow.ts';
|
|
5
|
+
import { parseTaskFrontmatter } from './frontmatter.ts';
|
|
6
|
+
import { resolveTaskContext, resolveTaskRef } from './resolve-ref.ts';
|
|
7
|
+
import { commitPushDecision } from './commit-policy.ts';
|
|
8
|
+
import { mergeOperationWarnings, type OperationWarning } from './operation-outcome.ts';
|
|
9
|
+
import { TaskExecutionLockError, withRepositoryMutationLock, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
10
|
+
import { appendActivityEntry, locateActivityLog } from './activity-log.ts';
|
|
11
|
+
import { captureTaskWriteMetadata, writeTask } from './write.ts';
|
|
12
|
+
import type { TaskMutation } from './write.ts';
|
|
13
|
+
import {
|
|
14
|
+
commitOrchestrationStageCompletion,
|
|
15
|
+
planOrchestrationStageCompletion
|
|
16
|
+
} from './orchestration.ts';
|
|
17
|
+
import type { OrchestrationStageCompletion } from './orchestration.ts';
|
|
18
|
+
|
|
19
|
+
type CommitExecutionMode = 'direct' | 'orchestrated';
|
|
20
|
+
|
|
21
|
+
type CommitOperationInput = Readonly<{
|
|
22
|
+
cwd: string;
|
|
23
|
+
paths: readonly string[];
|
|
24
|
+
message: string;
|
|
25
|
+
expectedHead: string;
|
|
26
|
+
expectedTree: string;
|
|
27
|
+
taskRef?: string;
|
|
28
|
+
agent?: string;
|
|
29
|
+
mode?: CommitExecutionMode;
|
|
30
|
+
push?: Readonly<{
|
|
31
|
+
remote: string;
|
|
32
|
+
refs: readonly string[];
|
|
33
|
+
automatic: boolean;
|
|
34
|
+
}>;
|
|
35
|
+
}>;
|
|
36
|
+
|
|
37
|
+
type CommitOperationResult = Readonly<{
|
|
38
|
+
status: 'applied' | 'no-op' | 'failed' | 'blocked';
|
|
39
|
+
changed: boolean;
|
|
40
|
+
result: 'committed' | 'no_op' | 'committed_with_warnings' | 'failed' | 'blocked';
|
|
41
|
+
taskId: string | null;
|
|
42
|
+
mode: CommitExecutionMode;
|
|
43
|
+
warnings: readonly OperationWarning[];
|
|
44
|
+
snapshot: ReturnType<typeof inspectGitWorkflow>['snapshot'];
|
|
45
|
+
operations: readonly unknown[];
|
|
46
|
+
error: Readonly<{ code: string; message: string }> | null;
|
|
47
|
+
}>;
|
|
48
|
+
|
|
49
|
+
type BoundTask = Readonly<{
|
|
50
|
+
repoRoot: string;
|
|
51
|
+
taskId: string;
|
|
52
|
+
taskDir: string;
|
|
53
|
+
taskMdPath: string;
|
|
54
|
+
state: 'active' | 'blocked' | 'completed' | 'archive';
|
|
55
|
+
branch: string;
|
|
56
|
+
}>;
|
|
57
|
+
|
|
58
|
+
const HEAD_REF = /^refs\/heads\/(.+)$/;
|
|
59
|
+
|
|
60
|
+
function isCommitExecutionMode(value: unknown): value is CommitExecutionMode {
|
|
61
|
+
return value === 'direct' || value === 'orchestrated';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function gitText(repoRoot: string, args: readonly string[]): string {
|
|
65
|
+
return execFileSync('git', [...args], {
|
|
66
|
+
cwd: repoRoot,
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
69
|
+
}).trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function canonicalRepositoryRoot(cwd: string): string {
|
|
73
|
+
return gitText(cwd, ['rev-parse', '--show-toplevel']);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function failure(
|
|
77
|
+
input: CommitOperationInput,
|
|
78
|
+
mode: CommitExecutionMode,
|
|
79
|
+
taskId: string | null,
|
|
80
|
+
code: string,
|
|
81
|
+
message: string,
|
|
82
|
+
status: 'failed' | 'blocked' = 'failed',
|
|
83
|
+
snapshot: ReturnType<typeof inspectGitWorkflow>['snapshot'] = null
|
|
84
|
+
): CommitOperationResult {
|
|
85
|
+
return {
|
|
86
|
+
status,
|
|
87
|
+
changed: false,
|
|
88
|
+
result: status,
|
|
89
|
+
taskId,
|
|
90
|
+
mode,
|
|
91
|
+
warnings: [],
|
|
92
|
+
snapshot: snapshot ?? inspectGitWorkflow(input.cwd).snapshot,
|
|
93
|
+
operations: [],
|
|
94
|
+
error: { code, message }
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function warning(
|
|
99
|
+
code: string,
|
|
100
|
+
message: string,
|
|
101
|
+
retryable: boolean,
|
|
102
|
+
step: string,
|
|
103
|
+
target: string,
|
|
104
|
+
severity: OperationWarning['severity']
|
|
105
|
+
): OperationWarning {
|
|
106
|
+
return { code, message, retryable, step, target, severity };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function resolveBoundTask(input: CommitOperationInput):
|
|
110
|
+
| { kind: 'none' }
|
|
111
|
+
| { kind: 'bound'; task: BoundTask }
|
|
112
|
+
| { kind: 'error'; result: CommitOperationResult } {
|
|
113
|
+
const mode = input.mode ?? 'direct';
|
|
114
|
+
if (input.taskRef !== undefined) {
|
|
115
|
+
const resolved = resolveTaskRef(input.taskRef, { repoRoot: input.cwd });
|
|
116
|
+
if (!resolved.ok) return {
|
|
117
|
+
kind: 'error',
|
|
118
|
+
result: failure(input, mode, resolved.taskId, resolved.code, resolved.message)
|
|
119
|
+
};
|
|
120
|
+
return boundTask(input, resolved.taskId, resolved.taskDir, resolved.taskMdPath, resolved.repoRoot, resolved.state);
|
|
121
|
+
}
|
|
122
|
+
const context = resolveTaskContext(undefined, { repoRoot: input.cwd });
|
|
123
|
+
if (context.ok) return boundTask(input, context.taskId, context.taskDir, context.taskMdPath, context.repoRoot, context.state);
|
|
124
|
+
if (context.code === 'TASK_CONTEXT_NOT_FOUND') return { kind: 'none' };
|
|
125
|
+
return {
|
|
126
|
+
kind: 'error',
|
|
127
|
+
result: failure(input, mode, context.taskId, context.code, context.message)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function boundTask(
|
|
132
|
+
input: CommitOperationInput,
|
|
133
|
+
taskId: string,
|
|
134
|
+
taskDir: string,
|
|
135
|
+
taskMdPath: string,
|
|
136
|
+
repoRoot: string,
|
|
137
|
+
state: BoundTask['state']
|
|
138
|
+
): { kind: 'bound'; task: BoundTask } | { kind: 'error'; result: CommitOperationResult } {
|
|
139
|
+
let branch: string;
|
|
140
|
+
try {
|
|
141
|
+
const frontmatter = parseTaskFrontmatter(fs.readFileSync(taskMdPath, 'utf8'));
|
|
142
|
+
branch = frontmatter.branch ?? '';
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return {
|
|
145
|
+
kind: 'error',
|
|
146
|
+
result: failure(input, input.mode ?? 'direct', taskId, 'TASK_CONTEXT_UNREADABLE', error instanceof Error ? error.message : String(error))
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (!branch) return {
|
|
150
|
+
kind: 'error',
|
|
151
|
+
result: failure(input, input.mode ?? 'direct', taskId, 'TASK_CONTEXT_UNREADABLE', 'task branch is missing')
|
|
152
|
+
};
|
|
153
|
+
return { kind: 'bound', task: { repoRoot, taskId, taskDir, taskMdPath, state, branch } };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function validateCommon(input: CommitOperationInput, branch: string | null): Readonly<{ code: string; message: string }> | null {
|
|
157
|
+
if (!input.message.trim()) return { code: 'GIT_COMMIT_INPUT_INVALID', message: 'Commit message is required' };
|
|
158
|
+
if (input.paths.length === 0 && !input.push) return { code: 'GIT_COMMIT_INPUT_INVALID', message: 'At least one explicit path is required unless this is a push-only retry' };
|
|
159
|
+
if (!/^[a-f0-9]{40,64}$/.test(input.expectedHead)) return { code: 'GIT_HEAD_EXPECTATION_REQUIRED', message: 'A valid expected HEAD is required' };
|
|
160
|
+
if (!/^[a-f0-9]{40,64}$/.test(input.expectedTree)) return { code: 'GIT_TREE_EXPECTATION_REQUIRED', message: 'A valid expected tree is required' };
|
|
161
|
+
if (input.paths.some((candidate) => !candidate || candidate.startsWith('/') || candidate.split(/[\\/]/).includes('..'))) {
|
|
162
|
+
return { code: 'GIT_COMMIT_INPUT_INVALID', message: 'Commit paths must stay inside the repository' };
|
|
163
|
+
}
|
|
164
|
+
if (input.paths.some((candidate) => /(^|\/)(?:\.env|credentials\.json|id_rsa)(?:$|\/)/i.test(candidate))) {
|
|
165
|
+
return { code: 'GIT_COMMIT_INPUT_INVALID', message: 'Sensitive paths cannot be committed' };
|
|
166
|
+
}
|
|
167
|
+
if (branch !== null && branch === '') return { code: 'GIT_BRANCH_INVALID', message: 'A named branch is required' };
|
|
168
|
+
if (input.push) {
|
|
169
|
+
if (!input.push.remote || input.push.refs.length !== 1) return { code: 'GIT_PUSH_INPUT_INVALID', message: 'Push requires one remote and one ref' };
|
|
170
|
+
const ref = input.push.refs[0] ?? '';
|
|
171
|
+
if (!HEAD_REF.test(ref) || HEAD_REF.exec(ref)?.[1] !== branch) {
|
|
172
|
+
return { code: 'GIT_PUSH_INPUT_INVALID', message: 'Push ref must be the current full heads ref' };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function validateOrchestrated(
|
|
179
|
+
input: CommitOperationInput,
|
|
180
|
+
task: BoundTask
|
|
181
|
+
): { plan: OrchestrationStageCompletion } | { error: Readonly<{ code: string; message: string }> } {
|
|
182
|
+
if (!input.agent) return { error: { code: 'ORCHESTRATION_PROVENANCE_MISMATCH', message: 'orchestrated commit requires an agent' } };
|
|
183
|
+
const planned = planOrchestrationStageCompletion(task.taskId, {
|
|
184
|
+
stage: 'commit',
|
|
185
|
+
round: 1,
|
|
186
|
+
artifact: 'commit',
|
|
187
|
+
role: 'executor',
|
|
188
|
+
agent: input.agent
|
|
189
|
+
}, { repoRoot: task.repoRoot });
|
|
190
|
+
const run = planned.result.run;
|
|
191
|
+
if (
|
|
192
|
+
!planned.plan
|
|
193
|
+
|| !run
|
|
194
|
+
|| run.status !== 'running'
|
|
195
|
+
|| !run.commitAuthorization.issuedAt
|
|
196
|
+
|| run.commitAuthorization.consumedAt
|
|
197
|
+
) return {
|
|
198
|
+
error: {
|
|
199
|
+
code: planned.result.error?.code ?? 'ORCHESTRATION_PROVENANCE_MISMATCH',
|
|
200
|
+
message: planned.result.error?.message ?? 'orchestrated commit requires one matching activated commit delegation'
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
return { plan: planned.plan };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function syncTaskCommit(
|
|
207
|
+
input: CommitOperationInput,
|
|
208
|
+
task: BoundTask,
|
|
209
|
+
committedHead: string
|
|
210
|
+
): OperationWarning | null {
|
|
211
|
+
let content: string;
|
|
212
|
+
try {
|
|
213
|
+
content = fs.readFileSync(task.taskMdPath, 'utf8');
|
|
214
|
+
const activity = locateActivityLog(content);
|
|
215
|
+
if (!activity) return warning(
|
|
216
|
+
'TASK_STATUS_SYNC_FAILED',
|
|
217
|
+
'task has no unique Activity Log section',
|
|
218
|
+
true,
|
|
219
|
+
'task',
|
|
220
|
+
task.taskId,
|
|
221
|
+
'ACTION_REQUIRED'
|
|
222
|
+
);
|
|
223
|
+
const metadata = captureTaskWriteMetadata();
|
|
224
|
+
const commitNote = gitText(task.repoRoot, ['show', '-s', '--format=%h %s', committedHead]);
|
|
225
|
+
const alreadyLogged = activity.entries.some((entry) => entry.step === 'Commit' && entry.note === commitNote);
|
|
226
|
+
const mutations: TaskMutation[] = [];
|
|
227
|
+
if (!alreadyLogged) mutations.push({
|
|
228
|
+
kind: 'section',
|
|
229
|
+
aliases: ['活动日志', 'Activity Log'],
|
|
230
|
+
heading: activity.heading,
|
|
231
|
+
body: appendActivityEntry(activity, { time: metadata.timestamp, step: 'Commit', agent: input.agent!, note: commitNote })
|
|
232
|
+
});
|
|
233
|
+
const frontmatter: Record<string, string> = { assigned_to: input.agent! };
|
|
234
|
+
mutations.push({ kind: 'frontmatter', set: frontmatter });
|
|
235
|
+
if (mutations.length === 0) return null;
|
|
236
|
+
const written = writeTask({
|
|
237
|
+
taskRef: task.taskId,
|
|
238
|
+
expectedState: 'active',
|
|
239
|
+
mutations
|
|
240
|
+
}, { repoRoot: task.repoRoot, metadataProvider: () => metadata });
|
|
241
|
+
if (written.status === 'failed') return warning(
|
|
242
|
+
'TASK_STATUS_SYNC_FAILED',
|
|
243
|
+
written.error.message,
|
|
244
|
+
true,
|
|
245
|
+
'task',
|
|
246
|
+
task.taskId,
|
|
247
|
+
'ACTION_REQUIRED'
|
|
248
|
+
);
|
|
249
|
+
return null;
|
|
250
|
+
} catch (error) {
|
|
251
|
+
return warning(
|
|
252
|
+
'TASK_STATUS_SYNC_FAILED',
|
|
253
|
+
error instanceof Error ? error.message : String(error),
|
|
254
|
+
true,
|
|
255
|
+
'task',
|
|
256
|
+
task.taskId,
|
|
257
|
+
'ACTION_REQUIRED'
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function executeUnlocked(input: CommitOperationInput, task: BoundTask | null, mode: CommitExecutionMode): CommitOperationResult {
|
|
263
|
+
const inspected = inspectGitWorkflow(input.cwd);
|
|
264
|
+
if (!inspected.snapshot) return failure(input, mode, task?.taskId ?? null, 'GIT_INSPECT_FAILED', 'Unable to inspect Git repository');
|
|
265
|
+
const branch = inspected.snapshot.branch;
|
|
266
|
+
const commonError = validateCommon(input, branch);
|
|
267
|
+
if (commonError) return failure(input, mode, task?.taskId ?? null, commonError.code, commonError.message, 'blocked', inspected.snapshot);
|
|
268
|
+
if (task && task.state !== 'active') return failure(input, mode, task.taskId, 'TASK_STATE_INVALID', 'task-bound commit requires an active task', 'blocked', inspected.snapshot);
|
|
269
|
+
if (task && task.branch !== branch) return failure(input, mode, task.taskId, 'GIT_BRANCH_MISMATCH', `Task branch '${task.branch}' does not match current branch '${branch}'`, 'blocked', inspected.snapshot);
|
|
270
|
+
if (task && !input.agent) return failure(input, mode, task.taskId, 'COMMIT_AGENT_REQUIRED', 'task-bound commit requires an agent', 'blocked', inspected.snapshot);
|
|
271
|
+
const orchestrationValidation = mode === 'orchestrated'
|
|
272
|
+
? (task
|
|
273
|
+
? validateOrchestrated(input, task)
|
|
274
|
+
: { error: { code: 'ORCHESTRATION_TASK_REQUIRED', message: 'orchestrated commit requires a task context' } })
|
|
275
|
+
: null;
|
|
276
|
+
if (orchestrationValidation && 'error' in orchestrationValidation) {
|
|
277
|
+
const error = orchestrationValidation.error;
|
|
278
|
+
return failure(input, mode, task?.taskId ?? null, error.code, error.message, 'blocked', inspected.snapshot);
|
|
279
|
+
}
|
|
280
|
+
const orchestrationCompletion = orchestrationValidation && 'plan' in orchestrationValidation
|
|
281
|
+
? orchestrationValidation.plan
|
|
282
|
+
: null;
|
|
283
|
+
|
|
284
|
+
const committed = input.paths.length === 0
|
|
285
|
+
? (() => {
|
|
286
|
+
if (inspected.snapshot.worktree.length > 0 || inspected.snapshot.staged.length > 0) return {
|
|
287
|
+
status: 'failed' as const,
|
|
288
|
+
changed: false,
|
|
289
|
+
snapshot: inspected.snapshot,
|
|
290
|
+
operations: [],
|
|
291
|
+
error: { code: 'GIT_COMMIT_INPUT_INVALID', message: 'Push-only retry requires a clean working tree' }
|
|
292
|
+
};
|
|
293
|
+
if (inspected.snapshot.head !== input.expectedHead) return {
|
|
294
|
+
status: 'failed' as const,
|
|
295
|
+
changed: false,
|
|
296
|
+
snapshot: inspected.snapshot,
|
|
297
|
+
operations: [],
|
|
298
|
+
error: { code: 'GIT_HEAD_MISMATCH', message: `Expected HEAD ${input.expectedHead}, received ${inspected.snapshot.head}` }
|
|
299
|
+
};
|
|
300
|
+
const currentTree = gitText(input.cwd, ['rev-parse', 'HEAD^{tree}']);
|
|
301
|
+
if (currentTree !== input.expectedTree) return {
|
|
302
|
+
status: 'failed' as const,
|
|
303
|
+
changed: false,
|
|
304
|
+
snapshot: inspected.snapshot,
|
|
305
|
+
operations: [],
|
|
306
|
+
error: { code: 'GIT_TREE_MISMATCH', message: `Expected current tree ${input.expectedTree}, received ${currentTree}` }
|
|
307
|
+
};
|
|
308
|
+
return {
|
|
309
|
+
status: 'no-op' as const,
|
|
310
|
+
changed: false,
|
|
311
|
+
snapshot: inspected.snapshot,
|
|
312
|
+
operations: [{ name: 'commit', status: 'no-op' as const }],
|
|
313
|
+
error: null
|
|
314
|
+
};
|
|
315
|
+
})()
|
|
316
|
+
: commitExplicitPaths({
|
|
317
|
+
cwd: input.cwd,
|
|
318
|
+
paths: input.paths,
|
|
319
|
+
message: input.message,
|
|
320
|
+
expectedHead: input.expectedHead,
|
|
321
|
+
expectedTree: input.expectedTree
|
|
322
|
+
});
|
|
323
|
+
if (committed.status === 'failed') return {
|
|
324
|
+
...committed,
|
|
325
|
+
result: 'failed',
|
|
326
|
+
taskId: task?.taskId ?? null,
|
|
327
|
+
mode,
|
|
328
|
+
warnings: [],
|
|
329
|
+
snapshot: committed.snapshot
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const warnings: OperationWarning[] = [];
|
|
333
|
+
if (orchestrationCompletion) {
|
|
334
|
+
try {
|
|
335
|
+
commitOrchestrationStageCompletion(orchestrationCompletion);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
warnings.push(warning(
|
|
338
|
+
'ORCHESTRATION_COMPLETION_FAILED',
|
|
339
|
+
error instanceof Error ? error.message : String(error),
|
|
340
|
+
true,
|
|
341
|
+
'orchestration',
|
|
342
|
+
task?.taskId ?? 'orchestration',
|
|
343
|
+
'ACTION_REQUIRED'
|
|
344
|
+
));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
let status: CommitOperationResult['status'] = committed.status;
|
|
348
|
+
let result: CommitOperationResult['result'] = committed.status === 'applied' ? 'committed' : 'no_op';
|
|
349
|
+
if (input.push) {
|
|
350
|
+
const target = `${input.push.remote}:${input.push.refs[0]}`;
|
|
351
|
+
const decision = commitPushDecision({ branch, automatic: input.push.automatic }, target);
|
|
352
|
+
if (!decision.shouldPush) {
|
|
353
|
+
warnings.push(decision.warning!);
|
|
354
|
+
} else {
|
|
355
|
+
const pushed = pushGitRefs({
|
|
356
|
+
cwd: input.cwd,
|
|
357
|
+
remote: input.push.remote,
|
|
358
|
+
refs: input.push.refs,
|
|
359
|
+
expectedSha: committed.snapshot?.head ?? gitText(input.cwd, ['rev-parse', 'HEAD'])
|
|
360
|
+
});
|
|
361
|
+
if (pushed.status !== 'applied') warnings.push(warning(
|
|
362
|
+
'COMMIT_PUSH_FAILED',
|
|
363
|
+
pushed.error?.message ?? 'Git push failed',
|
|
364
|
+
true,
|
|
365
|
+
'push',
|
|
366
|
+
target,
|
|
367
|
+
'ACTION_REQUIRED'
|
|
368
|
+
));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (warnings.length > 0) {
|
|
372
|
+
status = 'applied';
|
|
373
|
+
result = 'committed_with_warnings';
|
|
374
|
+
}
|
|
375
|
+
if (task && (committed.status === 'applied' || committed.status === 'no-op')) {
|
|
376
|
+
const taskWarning = syncTaskCommit(input, task, committed.snapshot?.head ?? gitText(task.repoRoot, ['rev-parse', 'HEAD']));
|
|
377
|
+
if (taskWarning) {
|
|
378
|
+
warnings.push(taskWarning);
|
|
379
|
+
status = 'applied';
|
|
380
|
+
result = 'committed_with_warnings';
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
status,
|
|
385
|
+
changed: committed.changed,
|
|
386
|
+
result,
|
|
387
|
+
taskId: task?.taskId ?? null,
|
|
388
|
+
mode,
|
|
389
|
+
warnings: mergeOperationWarnings(warnings),
|
|
390
|
+
snapshot: committed.snapshot,
|
|
391
|
+
operations: committed.operations,
|
|
392
|
+
error: null
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function executeCommitOperation(input: CommitOperationInput): CommitOperationResult {
|
|
397
|
+
if (input.mode !== undefined && !isCommitExecutionMode(input.mode)) return {
|
|
398
|
+
status: 'blocked',
|
|
399
|
+
changed: false,
|
|
400
|
+
result: 'blocked',
|
|
401
|
+
taskId: null,
|
|
402
|
+
mode: 'direct',
|
|
403
|
+
warnings: [],
|
|
404
|
+
snapshot: null,
|
|
405
|
+
operations: [],
|
|
406
|
+
error: { code: 'COMMIT_MODE_INVALID', message: 'commit mode must be direct or orchestrated' }
|
|
407
|
+
};
|
|
408
|
+
const mode: CommitExecutionMode = input.mode ?? 'direct';
|
|
409
|
+
let normalizedInput: CommitOperationInput;
|
|
410
|
+
try {
|
|
411
|
+
normalizedInput = { ...input, cwd: canonicalRepositoryRoot(input.cwd) };
|
|
412
|
+
} catch (error) {
|
|
413
|
+
return failure(input, mode, null, 'GIT_INSPECT_FAILED', error instanceof Error ? error.message : 'Unable to resolve Git repository root', 'blocked');
|
|
414
|
+
}
|
|
415
|
+
const resolved = resolveBoundTask(normalizedInput);
|
|
416
|
+
if (resolved.kind === 'error') return resolved.result;
|
|
417
|
+
const task = resolved.kind === 'bound' ? resolved.task : null;
|
|
418
|
+
if (mode === 'orchestrated' && !task) return failure(normalizedInput, mode, null, 'ORCHESTRATION_TASK_REQUIRED', 'orchestrated commit requires a task context', 'blocked');
|
|
419
|
+
try {
|
|
420
|
+
return task
|
|
421
|
+
? withRepositoryMutationLock(normalizedInput.cwd, () => withTaskExecutionLock(normalizedInput.cwd, task.taskId, 'commit-operation', () => executeUnlocked(normalizedInput, task, mode)))
|
|
422
|
+
: withRepositoryMutationLock(normalizedInput.cwd, () => executeUnlocked(normalizedInput, null, mode));
|
|
423
|
+
} catch (error) {
|
|
424
|
+
const code = error instanceof TaskExecutionLockError ? error.code : 'COMMIT_OPERATION_FAILED';
|
|
425
|
+
return failure(normalizedInput, mode, task?.taskId ?? null, code, error instanceof Error ? error.message : String(error), 'blocked');
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export { executeCommitOperation };
|
|
430
|
+
export type { CommitExecutionMode, CommitOperationInput, CommitOperationResult };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { OperationWarning } from './operation-outcome.ts';
|
|
2
|
+
|
|
3
|
+
type CommitPushPolicy = Readonly<{
|
|
4
|
+
branch: string;
|
|
5
|
+
automatic: boolean;
|
|
6
|
+
}>;
|
|
7
|
+
|
|
8
|
+
type CommitPushDecision = Readonly<{
|
|
9
|
+
shouldPush: boolean;
|
|
10
|
+
warning: OperationWarning | null;
|
|
11
|
+
}>;
|
|
12
|
+
|
|
13
|
+
function commitPushDecision(policy: CommitPushPolicy, target: string): CommitPushDecision {
|
|
14
|
+
const branch = policy.branch.trim();
|
|
15
|
+
if (policy.automatic && (branch === 'main' || branch === 'master')) return {
|
|
16
|
+
shouldPush: false,
|
|
17
|
+
warning: {
|
|
18
|
+
code: 'COMMIT_AUTOPUSH_PROTECTED_BRANCH',
|
|
19
|
+
message: `Automatic commit push is disabled for protected branch '${branch}'`,
|
|
20
|
+
retryable: false,
|
|
21
|
+
step: 'push',
|
|
22
|
+
target,
|
|
23
|
+
severity: 'IMPORTANT'
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
return { shouldPush: true, warning: null };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { commitPushDecision };
|
|
30
|
+
export type { CommitPushDecision, CommitPushPolicy };
|