@fitlab-ai/agent-infra 0.9.4 → 0.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +13 -3
- package/dist/lib/agent-clients/adapter.js +2 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +492 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +175 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +255 -0
- package/dist/lib/agent-clients/adapters/codex.js +3 -3
- package/dist/lib/git/workflow.js +0 -1
- package/dist/lib/git/worktree-identity.js +64 -0
- package/dist/lib/internal/codex-lifecycle.js +300 -0
- package/dist/lib/internal/release-workflow.js +108 -11
- package/dist/lib/internal/sandbox-control.js +43 -6
- package/dist/lib/internal/task-create.js +56 -0
- package/dist/lib/internal/task-orchestration.js +71 -31
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +16 -16
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +318 -89
- package/dist/lib/sandbox/control/client.js +152 -22
- package/dist/lib/sandbox/control/executor.js +136 -0
- package/dist/lib/sandbox/control/lifecycle.js +348 -0
- package/dist/lib/sandbox/control/protocol.js +73 -18
- package/dist/lib/sandbox/control/server.js +235 -80
- package/dist/lib/sandbox/control/state.js +181 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +35 -24
- package/dist/lib/sandbox/recovery.js +262 -45
- package/dist/lib/sandbox/workspace-view.js +80 -11
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/process-control.js +6 -0
- package/dist/lib/task/activity-log.js +32 -5
- package/dist/lib/task/artifact-lifecycle.js +5 -4
- package/dist/lib/task/codex-orchestration.js +161 -0
- package/dist/lib/task/commands/status.js +3 -0
- package/dist/lib/task/commands/validate.js +289 -0
- package/dist/lib/task/commit-finalization.js +50 -12
- package/dist/lib/task/create-service.js +137 -0
- package/dist/lib/task/create.js +333 -0
- package/dist/lib/task/delegation-receipts.js +25 -0
- package/dist/lib/task/events.js +10 -4
- package/dist/lib/task/finalization-artifacts.js +2 -1
- package/dist/lib/task/implementation-inputs.js +1 -7
- package/dist/lib/task/index.js +9 -0
- package/dist/lib/task/orchestration.js +283 -18
- package/dist/lib/task/review-fingerprint.js +4 -0
- package/dist/lib/task/verification-engine.js +38 -6
- package/dist/lib/task/verification.js +1 -0
- package/dist/lib/task/workspace-snapshot.js +78 -3
- package/lib/agent-clients/adapter.ts +4 -4
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +569 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +323 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +298 -0
- package/lib/agent-clients/adapters/codex.ts +3 -3
- package/lib/git/workflow.ts +0 -1
- package/lib/git/worktree-identity.ts +86 -0
- package/lib/internal/codex-lifecycle.ts +308 -0
- package/lib/internal/release-workflow.ts +107 -11
- package/lib/internal/sandbox-control.ts +38 -6
- package/lib/internal/task-create.ts +61 -0
- package/lib/internal/task-orchestration.ts +57 -31
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +18 -21
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +368 -103
- package/lib/sandbox/control/client.ts +173 -25
- package/lib/sandbox/control/executor.ts +163 -0
- package/lib/sandbox/control/lifecycle.ts +342 -0
- package/lib/sandbox/control/protocol.ts +110 -48
- package/lib/sandbox/control/server.ts +252 -85
- package/lib/sandbox/control/state.ts +196 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +31 -13
- package/lib/sandbox/recovery.ts +301 -60
- package/lib/sandbox/workspace-view.ts +89 -12
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/process-control.ts +11 -0
- package/lib/task/activity-log.ts +46 -7
- package/lib/task/artifact-lifecycle.ts +5 -4
- package/lib/task/codex-orchestration.ts +233 -0
- package/lib/task/commands/status.ts +2 -0
- package/lib/task/commands/validate.ts +282 -0
- package/lib/task/commit-finalization.ts +75 -12
- package/lib/task/create-service.ts +171 -0
- package/lib/task/create.ts +372 -0
- package/lib/task/delegation-receipts.ts +52 -3
- package/lib/task/events.ts +9 -4
- package/lib/task/finalization-artifacts.ts +2 -1
- package/lib/task/implementation-inputs.ts +1 -5
- package/lib/task/index.ts +8 -0
- package/lib/task/orchestration.ts +381 -22
- package/lib/task/review-fingerprint.ts +9 -0
- package/lib/task/verification-engine.ts +42 -8
- package/lib/task/verification.ts +2 -1
- package/lib/task/workspace-snapshot.ts +98 -4
- package/package.json +7 -7
- package/templates/.agents/hooks/lifecycle-delegation.js +88 -8
- package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
- package/templates/.agents/rules/task-management.en.md +1 -0
- package/templates/.agents/rules/task-management.zh-CN.md +1 -0
- package/templates/.agents/skills/commit/SKILL.en.md +3 -9
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -10
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +4 -3
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +5 -4
- package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
- package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +44 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +44 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -3
- package/templates/.claude/commands/run-manual-validation.en.md +9 -0
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
- package/templates/.codex/hooks.json +50 -0
- package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
|
@@ -268,14 +268,29 @@ function checkOrchestrationEvidence({ taskDir }: any): any {
|
|
|
268
268
|
return failResult('orchestration-evidence', 'Run model policy source or recovery history is invalid');
|
|
269
269
|
}
|
|
270
270
|
for (const recovery of run.recoveryHistory) {
|
|
271
|
-
|
|
272
|
-
recovery.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
271
|
+
const validModelRecovery = recovery.code === 'MODEL_POLICY_SUPPLEMENTED'
|
|
272
|
+
&& recovery.previousSchemaVersion === 1
|
|
273
|
+
&& recovery.receiptCount === 0
|
|
274
|
+
&& recovery.pendingDelegation === false
|
|
275
|
+
&& exactText(recovery.recoveredAt)
|
|
276
|
+
&& recovery.policySource;
|
|
277
|
+
const guards = recovery.guards;
|
|
278
|
+
const validCapabilityRecovery = recovery.code === 'CLIENT_CAPABILITY_ENABLED'
|
|
279
|
+
&& recovery.previousSchemaVersion === 2
|
|
280
|
+
&& recovery.previousStatus === 'paused'
|
|
281
|
+
&& recovery.previousPause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
|
|
282
|
+
&& recovery.client === 'codex'
|
|
283
|
+
&& recovery.resultingStatus === 'running'
|
|
284
|
+
&& exactText(recovery.recoveredAt)
|
|
285
|
+
&& guards?.stepCount === 0
|
|
286
|
+
&& guards?.nextStage === null
|
|
287
|
+
&& guards?.baselineEmpty === true
|
|
288
|
+
&& guards?.receiptCount === 0
|
|
289
|
+
&& guards?.pendingDelegation === false
|
|
290
|
+
&& guards?.commitAuthorizationUnused === true
|
|
291
|
+
&& guards?.completionEvidenceAbsent === true
|
|
292
|
+
&& guards?.commitIntentAbsent === true;
|
|
293
|
+
if (!validModelRecovery && !validCapabilityRecovery) {
|
|
279
294
|
return failResult('orchestration-evidence', 'Run recovery history contains invalid provenance');
|
|
280
295
|
}
|
|
281
296
|
}
|
|
@@ -333,6 +348,25 @@ function checkOrchestrationEvidence({ taskDir }: any): any {
|
|
|
333
348
|
) {
|
|
334
349
|
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated reasoning-effort fallback reason`);
|
|
335
350
|
}
|
|
351
|
+
if (receipt.client === 'codex') {
|
|
352
|
+
const host = receipt.hostEvidence;
|
|
353
|
+
if (
|
|
354
|
+
host?.kind !== 'codex-lifecycle-v1'
|
|
355
|
+
|| !exactText(host.hookDefinitionHash)
|
|
356
|
+
|| !Number.isSafeInteger(host.startRevision)
|
|
357
|
+
|| host.startRevision < 1
|
|
358
|
+
) {
|
|
359
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid Codex start evidence`);
|
|
360
|
+
}
|
|
361
|
+
if (['sealed', 'consumed'].includes(receipt.status) && (
|
|
362
|
+
!Number.isSafeInteger(host.stopRevision)
|
|
363
|
+
|| host.stopRevision <= host.startRevision
|
|
364
|
+
|| host.consumer !== receipt.id
|
|
365
|
+
|| !exactText(host.consumedAt)
|
|
366
|
+
)) {
|
|
367
|
+
return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid Codex consumed stop evidence`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
336
370
|
}
|
|
337
371
|
}
|
|
338
372
|
if (run.receipts.some((receipt: any) => receipt.status !== 'consumed')) {
|
package/lib/task/verification.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { TaskWorkspaceState } from './resolve-ref.ts';
|
|
|
11
11
|
type VerificationEvent =
|
|
12
12
|
| 'analyze.awaiting-input' | 'analyze.completed'
|
|
13
13
|
| 'review-analysis.completed' | 'plan.completed' | 'review-plan.completed'
|
|
14
|
-
| 'code.completed' | 'review-code.completed' | 'manual-validation.completed'
|
|
14
|
+
| 'code.completed' | 'review-code.completed' | 'manual-validation.completed' | 'validation-run.completed'
|
|
15
15
|
| 'block-task.completed' | 'cancel-task.completed' | 'commit.completed'
|
|
16
16
|
| 'complete-task.preflight' | 'complete-task.completed'
|
|
17
17
|
| 'create-pr.completed' | 'create-task.completed'
|
|
@@ -62,6 +62,7 @@ const VERIFICATION_CATALOG: Readonly<Record<VerificationEvent, VerificationSpec>
|
|
|
62
62
|
'code.completed': gate('code-task', 'active', 'code'),
|
|
63
63
|
'review-code.completed': gate('review-code', 'active', 'review-code'),
|
|
64
64
|
'manual-validation.completed': gate('complete-manual-validation', 'active', 'manual-validation'),
|
|
65
|
+
'validation-run.completed': gate('run-manual-validation', 'active', 'validation-run'),
|
|
65
66
|
'block-task.completed': gate('block-task', 'blocked'),
|
|
66
67
|
'cancel-task.completed': gate('cancel-task', 'completed'),
|
|
67
68
|
'commit.completed': gate('commit', 'active'),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -18,6 +19,25 @@ type RepositorySnapshot = Readonly<{
|
|
|
18
19
|
worktreeTree: string;
|
|
19
20
|
}>;
|
|
20
21
|
|
|
22
|
+
type WorkspaceSnapshotContext = Readonly<{
|
|
23
|
+
gitRoot: string;
|
|
24
|
+
stateRoot: string;
|
|
25
|
+
taskId: string | null;
|
|
26
|
+
}>;
|
|
27
|
+
|
|
28
|
+
type TaskFileFingerprint = Readonly<{
|
|
29
|
+
path: string;
|
|
30
|
+
mode: number;
|
|
31
|
+
kind: 'file' | 'symlink';
|
|
32
|
+
sha256: string;
|
|
33
|
+
}>;
|
|
34
|
+
|
|
35
|
+
type OrchestrationWorkspaceFingerprint = Readonly<{
|
|
36
|
+
version: 2;
|
|
37
|
+
gitTree: string;
|
|
38
|
+
taskFiles: readonly TaskFileFingerprint[];
|
|
39
|
+
}>;
|
|
40
|
+
|
|
21
41
|
function captureWorktreeTree(repoRoot: string, forcedPath: string | null): string {
|
|
22
42
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-infra-orchestration-index-'));
|
|
23
43
|
const indexFile = path.join(tempDir, 'index');
|
|
@@ -33,13 +53,72 @@ function captureWorktreeTree(repoRoot: string, forcedPath: string | null): strin
|
|
|
33
53
|
}
|
|
34
54
|
}
|
|
35
55
|
|
|
36
|
-
function
|
|
56
|
+
function captureLegacyWorkspaceSnapshot(repoRoot: string, taskId: string | null): string {
|
|
37
57
|
const workspaceRelative = taskId
|
|
38
58
|
? `.agents/workspace/active/${taskId}`
|
|
39
59
|
: '.agents/workspace/active';
|
|
40
60
|
return captureWorktreeTree(repoRoot, workspaceRelative);
|
|
41
61
|
}
|
|
42
62
|
|
|
63
|
+
function taskFileFingerprints(stateRoot: string, taskId: string | null): TaskFileFingerprint[] {
|
|
64
|
+
const activeRoot = path.join(stateRoot, '.agents', 'workspace', 'active');
|
|
65
|
+
const roots = taskId ? [path.join(activeRoot, taskId)] : [activeRoot];
|
|
66
|
+
const records: TaskFileFingerprint[] = [];
|
|
67
|
+
const visit = (absolute: string, relative: string): void => {
|
|
68
|
+
const stat = fs.lstatSync(absolute);
|
|
69
|
+
if (stat.isDirectory()) {
|
|
70
|
+
for (const name of fs.readdirSync(absolute).sort()) {
|
|
71
|
+
visit(path.join(absolute, name), path.posix.join(relative, name));
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (!stat.isFile() && !stat.isSymbolicLink()) return;
|
|
76
|
+
const content = stat.isSymbolicLink()
|
|
77
|
+
? Buffer.from(fs.readlinkSync(absolute))
|
|
78
|
+
: fs.readFileSync(absolute);
|
|
79
|
+
records.push({
|
|
80
|
+
path: relative,
|
|
81
|
+
mode: stat.mode & 0o777,
|
|
82
|
+
kind: stat.isSymbolicLink() ? 'symlink' : 'file',
|
|
83
|
+
sha256: createHash('sha256').update(content).digest('hex')
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
for (const root of roots) {
|
|
87
|
+
if (!fs.existsSync(root)) continue;
|
|
88
|
+
const relative = path.posix.join('.agents/workspace/active', path.relative(activeRoot, root).split(path.sep).join('/'));
|
|
89
|
+
visit(root, relative.replace(/\/$/, ''));
|
|
90
|
+
}
|
|
91
|
+
return records.sort((left, right) => left.path.localeCompare(right.path));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function encodeWorkspaceFingerprint(value: OrchestrationWorkspaceFingerprint): string {
|
|
95
|
+
return `ws2:${Buffer.from(JSON.stringify(value)).toString('base64url')}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function decodeWorkspaceFingerprint(value: string): OrchestrationWorkspaceFingerprint | null {
|
|
99
|
+
if (!value.startsWith('ws2:')) return null;
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(Buffer.from(value.slice(4), 'base64url').toString('utf8')) as OrchestrationWorkspaceFingerprint;
|
|
102
|
+
if (parsed.version !== 2 || !/^[a-f0-9]{40,64}$/.test(parsed.gitTree) || !Array.isArray(parsed.taskFiles)) {
|
|
103
|
+
throw new Error('invalid workspace fingerprint fields');
|
|
104
|
+
}
|
|
105
|
+
return parsed;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new Error(`ORCHESTRATION_SNAPSHOT_FAILED: ${error instanceof Error ? error.message : String(error)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function captureWorkspaceSnapshot(context: WorkspaceSnapshotContext): string;
|
|
112
|
+
function captureWorkspaceSnapshot(repoRoot: string, taskId: string | null): string;
|
|
113
|
+
function captureWorkspaceSnapshot(contextOrRoot: WorkspaceSnapshotContext | string, taskId?: string | null): string {
|
|
114
|
+
if (typeof contextOrRoot === 'string') return captureLegacyWorkspaceSnapshot(contextOrRoot, taskId ?? null);
|
|
115
|
+
return encodeWorkspaceFingerprint({
|
|
116
|
+
version: 2,
|
|
117
|
+
gitTree: captureWorktreeTree(contextOrRoot.gitRoot, null),
|
|
118
|
+
taskFiles: taskFileFingerprints(contextOrRoot.stateRoot, contextOrRoot.taskId)
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
43
122
|
function captureRepositorySnapshot(repoRoot: string): RepositorySnapshot {
|
|
44
123
|
return {
|
|
45
124
|
head: git(repoRoot, null, ['rev-parse', 'HEAD']).trim(),
|
|
@@ -49,9 +128,24 @@ function captureRepositorySnapshot(repoRoot: string): RepositorySnapshot {
|
|
|
49
128
|
}
|
|
50
129
|
|
|
51
130
|
function diffWorkspaceSnapshots(repoRoot: string, before: string, after: string): string[] {
|
|
52
|
-
const
|
|
53
|
-
|
|
131
|
+
const beforeV2 = decodeWorkspaceFingerprint(before);
|
|
132
|
+
const afterV2 = decodeWorkspaceFingerprint(after);
|
|
133
|
+
if ((beforeV2 === null) !== (afterV2 === null)) {
|
|
134
|
+
throw new Error('ORCHESTRATION_SNAPSHOT_FAILED: workspace fingerprint versions cannot be mixed');
|
|
135
|
+
}
|
|
136
|
+
if (!beforeV2 || !afterV2) {
|
|
137
|
+
const output = git(repoRoot, null, ['diff', '--name-only', '-z', before, after]);
|
|
138
|
+
return output.split('\0').filter(Boolean);
|
|
139
|
+
}
|
|
140
|
+
const output = git(repoRoot, null, ['diff', '--name-only', '-z', beforeV2.gitTree, afterV2.gitTree]);
|
|
141
|
+
const changed = new Set(output.split('\0').filter(Boolean));
|
|
142
|
+
const beforeFiles = new Map(beforeV2.taskFiles.map((record) => [record.path, JSON.stringify(record)]));
|
|
143
|
+
const afterFiles = new Map(afterV2.taskFiles.map((record) => [record.path, JSON.stringify(record)]));
|
|
144
|
+
for (const filePath of new Set([...beforeFiles.keys(), ...afterFiles.keys()])) {
|
|
145
|
+
if (beforeFiles.get(filePath) !== afterFiles.get(filePath)) changed.add(filePath);
|
|
146
|
+
}
|
|
147
|
+
return [...changed].sort();
|
|
54
148
|
}
|
|
55
149
|
|
|
56
150
|
export { captureRepositorySnapshot, captureWorkspaceSnapshot, diffWorkspaceSnapshots };
|
|
57
|
-
export type { RepositorySnapshot };
|
|
151
|
+
export type { OrchestrationWorkspaceFingerprint, RepositorySnapshot, WorkspaceSnapshotContext };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fitlab-ai/agent-infra",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
4
|
"description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Antigravity CLI, and OpenCode",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -58,13 +58,13 @@
|
|
|
58
58
|
"demo:regen": "sh scripts/demo-regen.sh",
|
|
59
59
|
"prepare": "git config core.hooksPath .git-hooks || true",
|
|
60
60
|
"typecheck": "tsc -p tsconfig.test.json --noEmit && tsc -p tsconfig.jschecks.json --noEmit",
|
|
61
|
-
"test:smoke": "
|
|
62
|
-
"test:core": "
|
|
63
|
-
"test:integration": "
|
|
61
|
+
"test:smoke": "node scripts/run-tests.js \"tests/unit/**/*.test.ts\"",
|
|
62
|
+
"test:core": "node scripts/run-tests.js \"tests/unit/**/*.test.ts\" \"tests/integration/**/*.test.ts\"",
|
|
63
|
+
"test:integration": "node scripts/run-tests.js \"tests/integration/**/*.test.ts\" \"tests/e2e/**/*.test.ts\"",
|
|
64
64
|
"check": "npm run typecheck && npm test",
|
|
65
|
-
"test": "
|
|
66
|
-
"test:coverage": "
|
|
67
|
-
"prepublishOnly": "
|
|
65
|
+
"test": "node scripts/run-tests.js \"tests/**/*.test.ts\"",
|
|
66
|
+
"test:coverage": "node scripts/run-tests.js --experimental-test-coverage \"--test-coverage-exclude=tests/**\" --test-reporter=spec --test-reporter-destination=stdout --test-reporter=lcov --test-reporter-destination=coverage.lcov \"tests/**/*.test.ts\"",
|
|
67
|
+
"prepublishOnly": "node scripts/run-tests.js \"tests/**/*.test.ts\""
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@types/cross-spawn": "^6.0.6",
|
|
@@ -1,15 +1,41 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { existsSync } from 'node:fs';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
3
5
|
import { fileURLToPath } from 'node:url';
|
|
4
6
|
|
|
5
7
|
const MAX_INPUT_BYTES = 64 * 1024;
|
|
8
|
+
const repoPackageJson = fileURLToPath(new URL('../../package.json', import.meta.url));
|
|
9
|
+
const sourceInternalCli = fileURLToPath(new URL('../../bin/internal-cli.ts', import.meta.url));
|
|
6
10
|
const localInternalCli = fileURLToPath(new URL('../../dist/bin/internal-cli.js', import.meta.url));
|
|
11
|
+
const codexHooks = fileURLToPath(new URL('../../.codex/hooks.json', import.meta.url));
|
|
7
12
|
const clientIndex = process.argv.indexOf('--client');
|
|
8
13
|
const client = clientIndex >= 0 ? process.argv[clientIndex + 1] : '';
|
|
9
|
-
if (client !== 'claude-code') {
|
|
14
|
+
if (client !== 'claude-code' && client !== 'codex') {
|
|
10
15
|
process.stderr.write('Lifecycle delegation hook requires a supported --client value\n');
|
|
11
16
|
process.exit(1);
|
|
12
17
|
}
|
|
18
|
+
|
|
19
|
+
function internalCliInvocation(args) {
|
|
20
|
+
let isAgentInfraSourceCheckout = false;
|
|
21
|
+
try {
|
|
22
|
+
isAgentInfraSourceCheckout = JSON.parse(readFileSync(repoPackageJson, 'utf8')).name === '@fitlab-ai/agent-infra';
|
|
23
|
+
} catch {
|
|
24
|
+
// Installed project hooks do not require a repository package.json.
|
|
25
|
+
}
|
|
26
|
+
if (isAgentInfraSourceCheckout && existsSync(sourceInternalCli)) {
|
|
27
|
+
return {
|
|
28
|
+
command: process.execPath,
|
|
29
|
+
commandArgs: ['--experimental-strip-types', sourceInternalCli, ...args],
|
|
30
|
+
shell: false
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (existsSync(localInternalCli)) {
|
|
34
|
+
return { command: process.execPath, commandArgs: [localInternalCli, ...args], shell: false };
|
|
35
|
+
}
|
|
36
|
+
return { command: 'agent-infra-internal', commandArgs: args, shell: process.platform === 'win32' };
|
|
37
|
+
}
|
|
38
|
+
|
|
13
39
|
let input = '';
|
|
14
40
|
process.stdin.setEncoding('utf8');
|
|
15
41
|
process.stdin.on('data', (chunk) => {
|
|
@@ -27,9 +53,65 @@ process.stdin.on('end', () => {
|
|
|
27
53
|
process.stderr.write('Lifecycle delegation hook received invalid JSON\n');
|
|
28
54
|
process.exit(1);
|
|
29
55
|
}
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
56
|
+
const phaseIndex = process.argv.indexOf('--event');
|
|
57
|
+
const explicitPhase = phaseIndex >= 0 ? process.argv[phaseIndex + 1] : '';
|
|
58
|
+
const hook = String(explicitPhase || event.hook_event_name || event.event || '').toLowerCase();
|
|
59
|
+
const toolInput = event.tool_input && typeof event.tool_input === 'object' ? event.tool_input : {};
|
|
60
|
+
const toolName = String(event.tool_name || event.tool || '').toLowerCase();
|
|
61
|
+
let toolResponse = {};
|
|
62
|
+
try {
|
|
63
|
+
toolResponse = typeof event.tool_response === 'string'
|
|
64
|
+
? JSON.parse(event.tool_response)
|
|
65
|
+
: (event.tool_response || {});
|
|
66
|
+
} catch {
|
|
67
|
+
// Core ignores opaque tool responses; only the host timeout bit is forwarded.
|
|
68
|
+
}
|
|
69
|
+
const nativeAgent = event.agent_name || event.agent_type || event.agent?.name
|
|
70
|
+
|| toolInput.agent_type || toolInput.agent || toolInput.name;
|
|
71
|
+
if (client === 'codex') {
|
|
72
|
+
const phases = new Set(['pre-tool', 'subagent-start', 'post-tool', 'subagent-stop']);
|
|
73
|
+
if (!phases.has(hook)) {
|
|
74
|
+
process.stderr.write('Managed Codex lifecycle hook event has an unknown type\n');
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
const managedAgent = /^agent-infra-lifecycle-(executor|reviewer)$/.test(String(nativeAgent || ''));
|
|
78
|
+
const parentReconcile = hook === 'post-tool'
|
|
79
|
+
&& toolName === 'collaborationwait_agent'
|
|
80
|
+
&& toolResponse.timed_out !== true;
|
|
81
|
+
if (!managedAgent && !parentReconcile) process.exit(0);
|
|
82
|
+
const hookDefinitionHash = createHash('sha256').update(readFileSync(codexHooks)).digest('hex');
|
|
83
|
+
const normalized = {
|
|
84
|
+
sessionId: String(event.session_id || ''),
|
|
85
|
+
turnId: String(event.turn_id || ''),
|
|
86
|
+
toolUseId: String(event.tool_use_id || ''),
|
|
87
|
+
childThreadId: String(event.agent_id || event.child_id || event.thread_id || event.tool_response?.agent_id || ''),
|
|
88
|
+
nativeAgent: String(nativeAgent),
|
|
89
|
+
requestedModel: String(toolInput.model || ''),
|
|
90
|
+
requestedReasoningEffort: String(toolInput.reasoning_effort || toolInput.reasoningEffort || ''),
|
|
91
|
+
hookDefinitionHash,
|
|
92
|
+
toolName,
|
|
93
|
+
taskName: String(toolInput.task_name || ''),
|
|
94
|
+
transcriptPath: String(event.transcript_path || '')
|
|
95
|
+
};
|
|
96
|
+
const args = ['codex-lifecycle', 'hook-event', '--event', hook, '--bridge', 'true'];
|
|
97
|
+
try {
|
|
98
|
+
const { command, commandArgs, shell } = internalCliInvocation(args);
|
|
99
|
+
const output = execFileSync(command, commandArgs, {
|
|
100
|
+
encoding: 'utf8',
|
|
101
|
+
input: JSON.stringify(normalized),
|
|
102
|
+
shell,
|
|
103
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
104
|
+
timeout: 15_000
|
|
105
|
+
});
|
|
106
|
+
process.stdout.write(output);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error.stdout) process.stdout.write(String(error.stdout));
|
|
109
|
+
if (error.stderr) process.stderr.write(String(error.stderr));
|
|
110
|
+
process.exit(typeof error.status === 'number' ? error.status : 1);
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (!/^agent-infra-lifecycle-(executor|reviewer)$/.test(String(nativeAgent || ''))) process.exit(0);
|
|
33
115
|
const start = hook.includes('start');
|
|
34
116
|
const stop = hook.includes('stop');
|
|
35
117
|
if (!start && !stop) {
|
|
@@ -58,12 +140,10 @@ process.stdin.on('end', () => {
|
|
|
58
140
|
);
|
|
59
141
|
}
|
|
60
142
|
try {
|
|
61
|
-
const
|
|
62
|
-
const command = useLocal ? process.execPath : 'agent-infra-internal';
|
|
63
|
-
const commandArgs = useLocal ? [localInternalCli, ...args] : args;
|
|
143
|
+
const { command, commandArgs, shell } = internalCliInvocation(args);
|
|
64
144
|
const output = execFileSync(command, commandArgs, {
|
|
65
145
|
encoding: 'utf8',
|
|
66
|
-
shell
|
|
146
|
+
shell,
|
|
67
147
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
68
148
|
});
|
|
69
149
|
process.stdout.write(output);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
## Recovery
|
|
12
12
|
|
|
13
|
-
`orchestration.json` is the detailed state source. A v2 run persists complete policy, source, and append-only recovery history. Only a v1 run with no pending delegation and zero receipts may be upgraded in place after supplying a complete policy; any historical receipt stays paused because effort is unverifiable. Migration is forward-only: old binaries must not advance active v2 runs.
|
|
13
|
+
`orchestration.json` is the detailed state source. A v2 run persists complete policy, source, and append-only recovery history. Only a v1 run with no pending delegation and zero receipts may be upgraded in place after supplying a complete policy; any historical receipt stays paused because effort is unverifiable. A historical Codex `ORCHESTRATION_CLIENT_UNSUPPORTED` v2 pause appends `CLIENT_CAPABILITY_ENABLED` and resumes only when step count, next stage, baseline, receipts, pending delegation, commit authorization, completion evidence, and commit intent all prove that execution never advanced; unknown or non-empty evidence preserves the pause. Migration is forward-only: old binaries must not advance active v2 runs.
|
|
14
14
|
|
|
15
15
|
## Model Policy
|
|
16
16
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
## 恢复语义
|
|
12
12
|
|
|
13
|
-
`orchestration.json` 是详细状态源。v2 run 保存完整策略、来源与 append-only 恢复历史。只有无 pending delegation 且零历史 receipt 的 v1 run 可在补齐完整策略后原地升级;任一历史 receipt 因缺 effort
|
|
13
|
+
`orchestration.json` 是详细状态源。v2 run 保存完整策略、来源与 append-only 恢复历史。只有无 pending delegation 且零历史 receipt 的 v1 run 可在补齐完整策略后原地升级;任一历史 receipt 因缺 effort 证据保持暂停。Codex 的历史 `ORCHESTRATION_CLIENT_UNSUPPORTED` v2 pause 仅在 stepCount、nextStage、baseline、receipts、pending delegation、commit authorization、completion evidence 与 commit intent 全部证明从未推进时追加 `CLIENT_CAPABILITY_ENABLED` 并恢复;任一未知或非空证据保持原暂停。迁移是 forward-only,旧二进制不得推进 v2 active run。
|
|
14
14
|
|
|
15
15
|
## 模型策略
|
|
16
16
|
|
|
@@ -47,6 +47,7 @@ SKILLs currently covered by this exemption:
|
|
|
47
47
|
|
|
48
48
|
- `release`: may confirm publish after presenting the latest release snapshot
|
|
49
49
|
- `create-release-note`: may confirm publish after presenting exact staged notes and their digest
|
|
50
|
+
- `post-release`: may confirm a normal push after preparing the local post commit and presenting the complete `postConfirmation` and digest
|
|
50
51
|
|
|
51
52
|
### Exemption 5: Complete model policy missing at lifecycle start
|
|
52
53
|
|
|
@@ -68,6 +68,7 @@ The six internal commands (`task-event` / `task-lifecycle` / `platform-issue` /
|
|
|
68
68
|
`- {time} — **{base} [started]** by {agent} — started`
|
|
69
69
|
- **done line** (written when the step completes, unchanged from today): the action is the base itself:
|
|
70
70
|
`- {time} — **{base}** by {agent} — {completion summary}`
|
|
71
|
+
- **Commit-specific attempt**: the commit skill must not hand-write started. `commit-start` writes a structured `Commit [started]` with `attempt`, `baseline`, and `agent` under the task lock; `commit-terminate` writes the matching `Commit [aborted]` when an attempt is abandoned before side effects. Aborted closes activity only and is never successful commit evidence.
|
|
71
72
|
- `{base}` is that skill's existing done action text, including `(Round {N})` (e.g. `Plan Task (Round 1)`). started and done must share the same `{base}` to pair.
|
|
72
73
|
|
|
73
74
|
**Pairing and rendering** (`ai task log`): a started entry pairs with the next same-`{base}` done entry onto one row (repeated executions of the same base pair FIFO by ascending time). The STARTED column shows the start time, DONE the completion time; started with no done = in progress (DONE shows `(in progress)`); done with no started (legacy logs) = a standalone completed row. All three shapes are valid and never error.
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
`- {time} — **{基名} [started]** by {agent} — started`
|
|
71
71
|
- **done 行**(步骤完成时写,与现状一致):action 即基名本身:
|
|
72
72
|
`- {time} — **{基名}** by {agent} — {完成说明}`
|
|
73
|
+
- **Commit 专用 attempt**:commit 技能不得手写 started。`commit-start` 在任务锁内写入带 `attempt`、`baseline`、`agent` 的结构化 `Commit [started]`;无副作用放弃时由 `commit-terminate` 写匹配的 `Commit [aborted]`。aborted 只闭合活动日志,不构成提交成功证据。
|
|
73
74
|
- `{基名}` 指该 SKILL 既有 done 条目的 action 文本,含 `(Round {N})`(如 `Plan Task (Round 1)`)。
|
|
74
75
|
started 与 done 共用同一 `{基名}` 才能配对。
|
|
75
76
|
|
|
@@ -31,17 +31,11 @@ Direct invocation still requires explicit user authorization. An orchestrated in
|
|
|
31
31
|
|
|
32
32
|
Without explicit task scope, only `TASK_CONTEXT_NOT_FOUND` may continue through the existing bare-commit path. Detached HEAD, damaged candidates, or multiple matches are ambiguity errors. Any explicit task-scope failure is fatal.
|
|
33
33
|
|
|
34
|
-
## Step Start: Recover
|
|
34
|
+
## Step Start: Recover or Create a Controlled Attempt
|
|
35
35
|
|
|
36
|
-
When `{task-id}` is resolved, call `commit-status` first.
|
|
36
|
+
When `{task-id}` is resolved, call `commit-status` first. Finalize `recoverable`; safely clear `prepared` and query status again; reuse `retryable-start` through `commit-recover --agent {standard-agent-token}`. Fail closed for `invalid` / `conflict` / `orphaned-start`. For `idle`, call `commit-start --agent {standard-agent-token}` so the core atomically writes the structured started entry and returns its attempt/baseline.
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
- {YYYY-MM-DD HH:mm:ss±HH:MM} — **Commit [started]** by {agent} — started
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
`ai task log` pairs it with the done entry written when the commit completes onto one row (in progress → done). Format and pairing rules: see the "Activity Log started / done dual-marker convention" in `.agents/rules/task-management.md`. Only write it when this task has a task.md (a bare commit with no task context may skip it).
|
|
38
|
+
Read `{commit-attempt}` only from structured `commit-start` / `commit-recover` output. Never hand-write, copy, or guess an attempt. A bare commit without task context skips this protocol.
|
|
45
39
|
|
|
46
40
|
## 1. Check Local Modifications (CRITICAL)
|
|
47
41
|
|
|
@@ -31,21 +31,17 @@ description: >
|
|
|
31
31
|
|
|
32
32
|
未显式指定 task scope 时,只有 `TASK_CONTEXT_NOT_FOUND` 可继续既有纯提交路径;detached HEAD、损坏候选或多匹配属于歧义,必须失败。显式 task scope 解析失败时一律失败。
|
|
33
33
|
|
|
34
|
-
##
|
|
34
|
+
## 步骤开始:恢复或创建受控 attempt
|
|
35
35
|
|
|
36
36
|
已解析出 `{task-id}` 时,先调用 `agent-infra-internal task-orchestration {task-id} commit-status`:
|
|
37
37
|
|
|
38
|
-
- `recoverable
|
|
38
|
+
- `recoverable`:调用 `commit-recover --agent {standard-agent-token}` 完成收尾,不得重复 commit 或 push。
|
|
39
|
+
- `prepared`:调用 `commit-recover --agent {standard-agent-token}` 安全清理 intent,再次查询 status 并复用返回的 `retryable-start` attempt。
|
|
40
|
+
- `retryable-start`:调用 `commit-recover --agent {standard-agent-token}` 取得原 attempt,不追加第二条 started。
|
|
39
41
|
- `invalid` / `conflict` / `orphaned-start`:输出结构化 code 与状态证据并停止。
|
|
40
|
-
- `idle
|
|
42
|
+
- `idle`:调用 `commit-start --agent {standard-agent-token}`,由核心在任务锁内原子写入结构化 started 并返回 attempt/baseline。
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
```
|
|
45
|
-
- {YYYY-MM-DD HH:mm:ss±HH:MM} — **Commit [started]** by {agent} — started
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
`ai task log` 会把它与提交完成时写入的 done 条目配对成一行(进行中 → 已完成)。格式与配对规则见 `.agents/rules/task-management.md` 的「Activity Log started / done 双标记约定」。仅当本任务存在 task.md 时写入(无任务上下文的纯提交可跳过)。
|
|
44
|
+
只从 `commit-start` / `commit-recover` 的结构化输出读取 `{commit-attempt}`;不得手写、复制或猜测 attempt。无任务上下文的纯提交跳过本协议。
|
|
49
45
|
|
|
50
46
|
## 1. 检查本地修改(关键)
|
|
51
47
|
|
|
@@ -9,12 +9,12 @@ This protocol applies only when `{task-id}` was resolved. A taskless Git-only co
|
|
|
9
9
|
|
|
10
10
|
## Begin before side effects
|
|
11
11
|
|
|
12
|
-
Call `commit-status` whenever the commit skill starts.
|
|
12
|
+
Call `commit-status` whenever the commit skill starts. Use `commit-recover --agent {standard-agent-token}` for `recoverable` / `prepared` / `retryable-start`; use `commit-start --agent {standard-agent-token}` for `idle`; fail closed for every other state. Read `commit_attempt` only from `finalization.attempt.attempt` in start/recover output.
|
|
13
13
|
|
|
14
14
|
After read-only preparation of status, copyright, message, review snapshot, and push routing, record `baseline_head=$(git rev-parse HEAD)` and run this before any commit, push, success log, or platform success sync:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
commit_intent_result=$(agent-infra-internal task-orchestration {task-id} commit-begin --agent {standard-agent-token} {execution-flag} --baseline-head "$baseline_head")
|
|
17
|
+
commit_intent_result=$(agent-infra-internal task-orchestration {task-id} commit-begin --agent {standard-agent-token} {execution-flag} --baseline-head "$baseline_head" --attempt "$commit_attempt")
|
|
18
18
|
commit_intent_token=$(printf '%s' "$commit_intent_result" | node -e 'let input = ""; process.stdin.on("data", chunk => input += chunk).on("end", () => process.stdout.write(JSON.parse(input).token))')
|
|
19
19
|
```
|
|
20
20
|
|
|
@@ -44,6 +44,7 @@ After the committed/pushed checkpoint and before task synchronization or `task-v
|
|
|
44
44
|
agent-infra-internal task-orchestration {task-id} commit-complete --token "$commit_intent_token" --agent {standard-agent-token}
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
-
|
|
47
|
+
- If an intent exists but no side effect occurred, use `commit-abort --token ... --expected-head ...` while HEAD still equals baseline; when abandoning the attempt, then close it with `commit-terminate --attempt "$commit_attempt" --agent {standard-agent-token} --code <STABLE_CODE>`.
|
|
48
|
+
- If begin failed before intent creation, retain the attempt for retry or terminate it only while HEAD is unchanged.
|
|
48
49
|
- After a commit or push, never abort. Retain the intent and use `commit-status` / `commit-recover` for cross-session recovery.
|
|
49
50
|
- Stop when `commit-complete` fails. Do not repeat commit/push or mark a run complete without its full receipt lifecycle.
|
|
@@ -9,18 +9,18 @@
|
|
|
9
9
|
|
|
10
10
|
## 副作用前 begin
|
|
11
11
|
|
|
12
|
-
每次进入 commit 技能先调用 `commit-status`。`recoverable` / `prepared` 调用以下无 token
|
|
12
|
+
每次进入 commit 技能先调用 `commit-status`。`recoverable` / `prepared` / `retryable-start` 调用以下无 token 恢复入口,`idle` 调用 `commit-start --agent {standard-agent-token}`;其他状态 fail closed:
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
15
|
agent-infra-internal task-orchestration {task-id} commit-recover --agent {standard-agent-token}
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
恢复入口只接受 `--agent`;不得传 token、head 或 `--orchestrated
|
|
18
|
+
恢复入口只接受 `--agent`;不得传 token、head 或 `--orchestrated`。从 start/recover 结构化输出读取 `finalization.attempt.attempt` 为 `commit_attempt`,不得手写 attempt。
|
|
19
19
|
|
|
20
20
|
完成状态、版权、提交信息、review snapshot 和 push 场景的只读准备后,记录 `baseline_head=$(git rev-parse HEAD)`,并在任何 commit、push、任务成功日志或平台成功同步之前调用:
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
commit_intent_result=$(agent-infra-internal task-orchestration {task-id} commit-begin --agent {standard-agent-token} {execution-flag} --baseline-head "$baseline_head")
|
|
23
|
+
commit_intent_result=$(agent-infra-internal task-orchestration {task-id} commit-begin --agent {standard-agent-token} {execution-flag} --baseline-head "$baseline_head" --attempt "$commit_attempt")
|
|
24
24
|
commit_intent_token=$(printf '%s' "$commit_intent_result" | node -e 'let input = ""; process.stdin.on("data", chunk => input += chunk).on("end", () => process.stdout.write(JSON.parse(input).token))')
|
|
25
25
|
```
|
|
26
26
|
|
|
@@ -50,6 +50,7 @@ committed/pushed checkpoint 完成后、任务同步与 `task-verify commit.comp
|
|
|
50
50
|
agent-infra-internal task-orchestration {task-id} commit-complete --token "$commit_intent_token" --agent {standard-agent-token}
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
-
|
|
53
|
+
- begin 已创建 intent、但第一个副作用前失败时,先在 HEAD 仍等于 baseline 时调用 `commit-abort --token ... --expected-head ...`;若本轮明确放弃,再调用 `commit-terminate --attempt "$commit_attempt" --agent {standard-agent-token} --code <STABLE_CODE>` 闭合 started。
|
|
54
|
+
- begin 在 intent 创建前失败时可保留 attempt 供下次复用;明确放弃时仅可在 HEAD 未漂移时调用 `commit-terminate`。
|
|
54
55
|
- commit 或 push 发生后失败时不得 abort;保留 intent,并调用 `commit-status` 输出不含 token/digest 的恢复证据。跨会话重跑通过 `commit-recover` 收尾。
|
|
55
56
|
- `commit-complete` 失败时停止。不得重复执行 commit/push,也不得把缺少完整 receipt 生命周期的 run 标记为完成。
|