@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.
Files changed (130) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +13 -3
  4. package/dist/lib/agent-clients/adapter.js +2 -2
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +492 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +175 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +255 -0
  8. package/dist/lib/agent-clients/adapters/codex.js +3 -3
  9. package/dist/lib/git/workflow.js +0 -1
  10. package/dist/lib/git/worktree-identity.js +64 -0
  11. package/dist/lib/internal/codex-lifecycle.js +300 -0
  12. package/dist/lib/internal/release-workflow.js +108 -11
  13. package/dist/lib/internal/sandbox-control.js +43 -6
  14. package/dist/lib/internal/task-create.js +56 -0
  15. package/dist/lib/internal/task-orchestration.js +71 -31
  16. package/dist/lib/platform/issue-comments.js +2 -1
  17. package/dist/lib/release/workflow.js +29 -21
  18. package/dist/lib/sandbox/commands/create.js +16 -16
  19. package/dist/lib/sandbox/commands/prune.js +40 -16
  20. package/dist/lib/sandbox/commands/rm.js +318 -89
  21. package/dist/lib/sandbox/control/client.js +152 -22
  22. package/dist/lib/sandbox/control/executor.js +136 -0
  23. package/dist/lib/sandbox/control/lifecycle.js +348 -0
  24. package/dist/lib/sandbox/control/protocol.js +73 -18
  25. package/dist/lib/sandbox/control/server.js +235 -80
  26. package/dist/lib/sandbox/control/state.js +181 -0
  27. package/dist/lib/sandbox/index.js +2 -2
  28. package/dist/lib/sandbox/managed-fs.js +14 -2
  29. package/dist/lib/sandbox/mounts.js +35 -24
  30. package/dist/lib/sandbox/recovery.js +262 -45
  31. package/dist/lib/sandbox/workspace-view.js +80 -11
  32. package/dist/lib/sandbox/worktree-safety.js +196 -0
  33. package/dist/lib/server/process-control.js +6 -0
  34. package/dist/lib/task/activity-log.js +32 -5
  35. package/dist/lib/task/artifact-lifecycle.js +5 -4
  36. package/dist/lib/task/codex-orchestration.js +161 -0
  37. package/dist/lib/task/commands/status.js +3 -0
  38. package/dist/lib/task/commands/validate.js +289 -0
  39. package/dist/lib/task/commit-finalization.js +50 -12
  40. package/dist/lib/task/create-service.js +137 -0
  41. package/dist/lib/task/create.js +333 -0
  42. package/dist/lib/task/delegation-receipts.js +25 -0
  43. package/dist/lib/task/events.js +10 -4
  44. package/dist/lib/task/finalization-artifacts.js +2 -1
  45. package/dist/lib/task/implementation-inputs.js +1 -7
  46. package/dist/lib/task/index.js +9 -0
  47. package/dist/lib/task/orchestration.js +283 -18
  48. package/dist/lib/task/review-fingerprint.js +4 -0
  49. package/dist/lib/task/verification-engine.js +38 -6
  50. package/dist/lib/task/verification.js +1 -0
  51. package/dist/lib/task/workspace-snapshot.js +78 -3
  52. package/lib/agent-clients/adapter.ts +4 -4
  53. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +569 -0
  54. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +323 -0
  55. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +298 -0
  56. package/lib/agent-clients/adapters/codex.ts +3 -3
  57. package/lib/git/workflow.ts +0 -1
  58. package/lib/git/worktree-identity.ts +86 -0
  59. package/lib/internal/codex-lifecycle.ts +308 -0
  60. package/lib/internal/release-workflow.ts +107 -11
  61. package/lib/internal/sandbox-control.ts +38 -6
  62. package/lib/internal/task-create.ts +61 -0
  63. package/lib/internal/task-orchestration.ts +57 -31
  64. package/lib/platform/issue-comments.ts +2 -1
  65. package/lib/release/workflow.ts +69 -20
  66. package/lib/sandbox/commands/create.ts +18 -21
  67. package/lib/sandbox/commands/prune.ts +53 -15
  68. package/lib/sandbox/commands/rm.ts +368 -103
  69. package/lib/sandbox/control/client.ts +173 -25
  70. package/lib/sandbox/control/executor.ts +163 -0
  71. package/lib/sandbox/control/lifecycle.ts +342 -0
  72. package/lib/sandbox/control/protocol.ts +110 -48
  73. package/lib/sandbox/control/server.ts +252 -85
  74. package/lib/sandbox/control/state.ts +196 -0
  75. package/lib/sandbox/index.ts +2 -2
  76. package/lib/sandbox/managed-fs.ts +17 -2
  77. package/lib/sandbox/mounts.ts +31 -13
  78. package/lib/sandbox/recovery.ts +301 -60
  79. package/lib/sandbox/workspace-view.ts +89 -12
  80. package/lib/sandbox/worktree-safety.ts +234 -0
  81. package/lib/server/process-control.ts +11 -0
  82. package/lib/task/activity-log.ts +46 -7
  83. package/lib/task/artifact-lifecycle.ts +5 -4
  84. package/lib/task/codex-orchestration.ts +233 -0
  85. package/lib/task/commands/status.ts +2 -0
  86. package/lib/task/commands/validate.ts +282 -0
  87. package/lib/task/commit-finalization.ts +75 -12
  88. package/lib/task/create-service.ts +171 -0
  89. package/lib/task/create.ts +372 -0
  90. package/lib/task/delegation-receipts.ts +52 -3
  91. package/lib/task/events.ts +9 -4
  92. package/lib/task/finalization-artifacts.ts +2 -1
  93. package/lib/task/implementation-inputs.ts +1 -5
  94. package/lib/task/index.ts +8 -0
  95. package/lib/task/orchestration.ts +381 -22
  96. package/lib/task/review-fingerprint.ts +9 -0
  97. package/lib/task/verification-engine.ts +42 -8
  98. package/lib/task/verification.ts +2 -1
  99. package/lib/task/workspace-snapshot.ts +98 -4
  100. package/package.json +7 -7
  101. package/templates/.agents/hooks/lifecycle-delegation.js +88 -8
  102. package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
  103. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
  104. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
  105. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
  106. package/templates/.agents/rules/task-management.en.md +1 -0
  107. package/templates/.agents/rules/task-management.zh-CN.md +1 -0
  108. package/templates/.agents/skills/commit/SKILL.en.md +3 -9
  109. package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -10
  110. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +4 -3
  111. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +5 -4
  112. package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
  113. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
  114. package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
  115. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
  116. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
  117. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
  118. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -0
  119. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -0
  120. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +44 -0
  121. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +44 -0
  122. package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
  123. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
  124. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -3
  125. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -3
  126. package/templates/.claude/commands/run-manual-validation.en.md +9 -0
  127. package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
  128. package/templates/.codex/hooks.json +50 -0
  129. package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
  130. 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
- if (
272
- recovery.code !== 'MODEL_POLICY_SUPPLEMENTED'
273
- || recovery.previousSchemaVersion !== 1
274
- || recovery.receiptCount !== 0
275
- || recovery.pendingDelegation !== false
276
- || !exactText(recovery.recoveredAt)
277
- || !recovery.policySource
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')) {
@@ -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 captureWorkspaceSnapshot(repoRoot: string, taskId: string | null): string {
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 output = git(repoRoot, null, ['diff', '--name-only', '-z', before, after]);
53
- return output.split('\0').filter(Boolean);
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.4",
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": "npm run build && node --experimental-strip-types --no-warnings --test \"tests/unit/**/*.test.ts\"",
62
- "test:core": "npm run build && node --experimental-strip-types --no-warnings --test \"tests/unit/**/*.test.ts\" \"tests/integration/**/*.test.ts\"",
63
- "test:integration": "npm run build && node --experimental-strip-types --no-warnings --test \"tests/integration/**/*.test.ts\" \"tests/e2e/**/*.test.ts\"",
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": "npm run build && node --experimental-strip-types --no-warnings --test \"tests/**/*.test.ts\"",
66
- "test:coverage": "npm run build && node --experimental-strip-types --no-warnings --test --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": "npm run build && node --experimental-strip-types --no-warnings --test \"tests/**/*.test.ts\""
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 hook = String(event.hook_event_name || event.event || '').toLowerCase();
31
- const nativeAgent = event.agent_name || event.agent_type || event.agent?.name;
32
- if (!String(nativeAgent || '').startsWith('agent-infra-lifecycle-')) process.exit(0);
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 useLocal = existsSync(localInternalCli);
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: !useLocal && process.platform === 'win32',
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 证据保持暂停。其他阻塞、未封存 receipt 或基线漂移继续保持暂停。迁移是 forward-only,旧二进制不得推进 v2 active run。
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
 
@@ -47,6 +47,7 @@
47
47
 
48
48
  - `release`:展示最新 release snapshot 后确认是否 publish
49
49
  - `create-release-note`:展示 stage 后的精确 notes 与摘要后确认是否 publish
50
+ - `post-release`:完成本地 post commit 并展示完整 `postConfirmation` 与摘要后确认是否普通 push
50
51
 
51
52
  ### 例外 5:生命周期启动缺少完整模型策略
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 First, Then Write the started Marker
34
+ ## Step Start: Recover or Create a Controlled Attempt
35
35
 
36
- When `{task-id}` is resolved, call `commit-status` first. For `recoverable` / `prepared`, call `commit-recover --agent {standard-agent-token}` without adding a second started entry or repeating Git side effects. Fail closed for `invalid` / `conflict` / `orphaned-start`; only `idle` enters the normal flow.
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
- Before checking local modifications, append a started marker to task.md `## Activity Log` (same base action as this step's done entry plus a ` [started]` suffix, note `started`):
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
- ## 步骤开始:先恢复,再写 started 标记
34
+ ## 步骤开始:恢复或创建受控 attempt
35
35
 
36
36
  已解析出 `{task-id}` 时,先调用 `agent-infra-internal task-orchestration {task-id} commit-status`:
37
37
 
38
- - `recoverable` / `prepared`:调用 `commit-recover --agent {standard-agent-token}`;不得追加第二条 started、重复 commit 或重复 push。recoverable 完成后直接进入同步与完成校验;prepared 清理后复用既有 open started 并继续正常提交。
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
- 开始检查本地修改之前,向 task.md `## 活动日志` 追加一条 started 标记(与本步骤 done 条目同基名 + ` [started]` 后缀,note 用 `started`):
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. For `recoverable` / `prepared`, call `commit-recover --agent {standard-agent-token}`; fail closed for every other non-`idle` state. The recovery intent accepts no token, head, or `--orchestrated` flag.
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
- - Before the first side effect, abort only while HEAD still equals the baseline by using `commit-abort --token ... --expected-head ...`.
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 恢复入口,其他非 `idle` 状态 fail closed:
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`。recoverable 会幂等补齐 task/run 并最后删除 intent;prepared 仅在 HEAD=baseline 且恰有一个 open Commit started 时安全清理,随后复用该 started 继续正常流程。
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
- - 第一个副作用前失败时,仅可在 HEAD 仍等于 baseline 时调用 `commit-abort --token ... --expected-head ...`。
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 标记为完成。