@fitlab-ai/agent-infra 0.9.3 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/README.md +3 -1
  2. package/README.zh-CN.md +3 -1
  3. package/bin/cli.ts +12 -0
  4. package/dist/bin/cli.js +14 -0
  5. package/dist/bin/internal-cli.js +144 -123
  6. package/dist/lib/agent-clients/adapter.js +94 -4
  7. package/dist/lib/{sandbox/credentials.js → agent-clients/adapters/claude-code-credentials.js} +4 -14
  8. package/dist/lib/agent-clients/adapters/claude-code-refresh.js +85 -0
  9. package/dist/lib/agent-clients/adapters/claude-code-sandbox.js +247 -0
  10. package/dist/lib/agent-clients/adapters/claude-code.js +17 -3
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +382 -0
  12. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +163 -0
  13. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -0
  14. package/dist/lib/agent-clients/adapters/codex.js +2 -2
  15. package/dist/lib/agent-clients/adapters/opencode-sandbox.js +108 -0
  16. package/dist/lib/agent-clients/adapters/opencode.js +15 -3
  17. package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  18. package/dist/lib/agent-clients/registry.js +9 -1
  19. package/dist/lib/git/worktree-identity.js +64 -0
  20. package/dist/lib/internal/codex-lifecycle.js +202 -0
  21. package/dist/lib/internal/sandbox-control.js +24 -0
  22. package/dist/lib/internal/task-activity.js +45 -13
  23. package/dist/lib/internal/task-create.js +48 -0
  24. package/dist/lib/internal/task-ledger.js +12 -3
  25. package/dist/lib/internal/task-lifecycle.js +19 -1
  26. package/dist/lib/internal/task-orchestration.js +170 -27
  27. package/dist/lib/platform/github-client.js +19 -2
  28. package/dist/lib/platform/pr-checks.js +1 -1
  29. package/dist/lib/platform/pull-requests.js +47 -1
  30. package/dist/lib/platform/releases.js +13 -5
  31. package/dist/lib/process-data/index.js +239 -0
  32. package/dist/lib/process-data/normalize.js +198 -0
  33. package/dist/lib/process-data/privacy.js +33 -0
  34. package/dist/lib/process-data/reconcile.js +95 -0
  35. package/dist/lib/process-data/repair.js +58 -0
  36. package/dist/lib/process-data/sources.js +345 -0
  37. package/dist/lib/process-data/store.js +282 -0
  38. package/dist/lib/process-data/types.js +2 -0
  39. package/dist/lib/run/index.js +4 -3
  40. package/dist/lib/run/tui.js +7 -8
  41. package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
  42. package/dist/lib/sandbox/capture.js +7 -2
  43. package/dist/lib/sandbox/commands/create.js +110 -228
  44. package/dist/lib/sandbox/commands/enter.js +15 -47
  45. package/dist/lib/sandbox/commands/list-running.js +16 -6
  46. package/dist/lib/sandbox/commands/ls.js +10 -6
  47. package/dist/lib/sandbox/commands/rebuild.js +4 -3
  48. package/dist/lib/sandbox/commands/refresh.js +4 -84
  49. package/dist/lib/sandbox/commands/rm.js +24 -3
  50. package/dist/lib/sandbox/commands/show.js +12 -3
  51. package/dist/lib/sandbox/commands/start.js +7 -4
  52. package/dist/lib/sandbox/config.js +2 -0
  53. package/dist/lib/sandbox/constants.js +6 -0
  54. package/dist/lib/sandbox/control/client.js +58 -0
  55. package/dist/lib/sandbox/control/protocol.js +54 -0
  56. package/dist/lib/sandbox/control/server.js +144 -0
  57. package/dist/lib/sandbox/dockerfile.js +36 -13
  58. package/dist/lib/sandbox/mounts.js +17 -4
  59. package/dist/lib/sandbox/recovery.js +128 -5
  60. package/dist/lib/sandbox/redaction.js +12 -0
  61. package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
  62. package/dist/lib/sandbox/workspace-identity.js +95 -0
  63. package/dist/lib/sandbox/workspace-view.js +109 -0
  64. package/dist/lib/task/activity-intent.js +257 -51
  65. package/dist/lib/task/activity-log.js +32 -5
  66. package/dist/lib/task/commit-finalization.js +278 -0
  67. package/dist/lib/task/commit-intent.js +165 -0
  68. package/dist/lib/task/create-service.js +137 -0
  69. package/dist/lib/task/create.js +333 -0
  70. package/dist/lib/task/decision-intents.js +31 -17
  71. package/dist/lib/task/delegation-receipts.js +2 -2
  72. package/dist/lib/task/implementation-inputs.js +54 -7
  73. package/dist/lib/task/ledger-intents.js +35 -1
  74. package/dist/lib/task/orchestration.js +505 -34
  75. package/dist/lib/task/resolve-ref.js +41 -1
  76. package/dist/lib/task/review-fingerprint.js +4 -0
  77. package/dist/lib/task/workspace-snapshot.js +78 -3
  78. package/lib/agent-clients/adapter.ts +130 -6
  79. package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
  80. package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
  81. package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
  82. package/lib/agent-clients/adapters/claude-code.ts +21 -3
  83. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +442 -0
  84. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +307 -0
  85. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +250 -0
  86. package/lib/agent-clients/adapters/codex.ts +2 -2
  87. package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
  88. package/lib/agent-clients/adapters/opencode.ts +18 -3
  89. package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  90. package/lib/agent-clients/registry.ts +9 -1
  91. package/lib/git/worktree-identity.ts +86 -0
  92. package/lib/internal/codex-lifecycle.ts +204 -0
  93. package/lib/internal/sandbox-control.ts +24 -0
  94. package/lib/internal/task-activity.ts +50 -14
  95. package/lib/internal/task-create.ts +49 -0
  96. package/lib/internal/task-ledger.ts +12 -3
  97. package/lib/internal/task-lifecycle.ts +22 -1
  98. package/lib/internal/task-orchestration.ts +117 -21
  99. package/lib/platform/github-client.ts +20 -2
  100. package/lib/platform/pr-checks.ts +1 -1
  101. package/lib/platform/pull-requests.ts +49 -1
  102. package/lib/platform/releases.ts +16 -4
  103. package/lib/process-data/index.ts +224 -0
  104. package/lib/process-data/normalize.ts +202 -0
  105. package/lib/process-data/privacy.ts +41 -0
  106. package/lib/process-data/reconcile.ts +110 -0
  107. package/lib/process-data/repair.ts +64 -0
  108. package/lib/process-data/sources.ts +354 -0
  109. package/lib/process-data/store.ts +315 -0
  110. package/lib/process-data/types.ts +135 -0
  111. package/lib/run/index.ts +5 -3
  112. package/lib/run/tui.ts +11 -8
  113. package/lib/sandbox/agent-client-reconciler.ts +44 -2
  114. package/lib/sandbox/capture.ts +9 -2
  115. package/lib/sandbox/commands/create.ts +127 -260
  116. package/lib/sandbox/commands/enter.ts +13 -59
  117. package/lib/sandbox/commands/list-running.ts +23 -6
  118. package/lib/sandbox/commands/ls.ts +22 -6
  119. package/lib/sandbox/commands/rebuild.ts +4 -3
  120. package/lib/sandbox/commands/refresh.ts +4 -126
  121. package/lib/sandbox/commands/rm.ts +24 -3
  122. package/lib/sandbox/commands/show.ts +29 -3
  123. package/lib/sandbox/commands/start.ts +6 -4
  124. package/lib/sandbox/config.ts +4 -0
  125. package/lib/sandbox/constants.ts +8 -0
  126. package/lib/sandbox/control/client.ts +79 -0
  127. package/lib/sandbox/control/protocol.ts +110 -0
  128. package/lib/sandbox/control/server.ts +155 -0
  129. package/lib/sandbox/dockerfile.ts +66 -13
  130. package/lib/sandbox/mounts.ts +24 -4
  131. package/lib/sandbox/recovery.ts +163 -6
  132. package/lib/sandbox/redaction.ts +14 -0
  133. package/lib/sandbox/runtimes/base.dockerfile +24 -74
  134. package/lib/sandbox/tool-types.ts +16 -0
  135. package/lib/sandbox/workspace-identity.ts +123 -0
  136. package/lib/sandbox/workspace-view.ts +150 -0
  137. package/lib/task/activity-intent.ts +350 -61
  138. package/lib/task/activity-log.ts +46 -7
  139. package/lib/task/commit-finalization.ts +376 -0
  140. package/lib/task/commit-intent.ts +230 -0
  141. package/lib/task/create-service.ts +171 -0
  142. package/lib/task/create.ts +372 -0
  143. package/lib/task/decision-intents.ts +32 -12
  144. package/lib/task/delegation-receipts.ts +2 -2
  145. package/lib/task/implementation-inputs.ts +62 -8
  146. package/lib/task/ledger-intents.ts +43 -3
  147. package/lib/task/orchestration.ts +673 -41
  148. package/lib/task/resolve-ref.ts +46 -1
  149. package/lib/task/review-fingerprint.ts +9 -0
  150. package/lib/task/workspace-snapshot.ts +98 -4
  151. package/package.json +7 -7
  152. package/templates/.agents/hooks/lifecycle-delegation.js +47 -3
  153. package/templates/.agents/rules/README.en.md +1 -1
  154. package/templates/.agents/rules/README.zh-CN.md +1 -1
  155. package/templates/.agents/rules/human-decision-context.en.md +11 -2
  156. package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
  157. package/templates/.agents/rules/pr-checks-commands.en.md +1 -1
  158. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  159. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
  160. package/templates/.agents/rules/pr-checks-commands.zh-CN.md +1 -1
  161. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  162. package/templates/.agents/rules/task-management.en.md +3 -0
  163. package/templates/.agents/rules/task-management.zh-CN.md +3 -0
  164. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  165. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  166. package/templates/.agents/skills/commit/SKILL.en.md +21 -17
  167. package/templates/.agents/skills/commit/SKILL.zh-CN.md +28 -16
  168. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +50 -0
  169. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +56 -0
  170. package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
  171. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
  172. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  173. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  174. package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
  175. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
  176. package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
  177. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
  178. package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
  179. package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
  180. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  181. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  182. package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
  183. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
  184. package/templates/.agents/skills/review-pr/SKILL.en.md +36 -8
  185. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +36 -8
  186. package/templates/.agents/skills/review-pr/reference/report-template.en.md +1 -1
  187. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +1 -1
  188. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +67 -50
  189. package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
  190. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
  191. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +2 -2
  192. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +2 -2
  193. package/templates/.agents/templates/task.en.md +1 -1
  194. package/templates/.agents/templates/task.zh-CN.md +1 -1
  195. package/templates/.claude/commands/watch-pr.en.md +1 -1
  196. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  197. package/templates/.claude/rules/tool-preferences.en.md +20 -0
  198. package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
  199. package/templates/.codex/hooks.json +50 -0
  200. package/templates/.opencode/commands/watch-pr.en.md +1 -1
  201. package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
  202. package/templates/.gemini/commands/_project_/review-pr.en.toml +0 -10
  203. package/templates/.gemini/commands/_project_/review-pr.zh-CN.toml +0 -10
@@ -1,101 +1,368 @@
1
1
  import fs from 'node:fs';
2
- import path from 'node:path';
3
2
 
3
+ import { normalizeAgentToken } from '../agent-clients/tokens.ts';
4
4
  import { resolveTaskRef } from './resolve-ref.ts';
5
5
  import { locateActivityLog, appendActivityEntry } from './activity-log.ts';
6
- import { buildArtifactLinkSection, parseArtifactName } from './artifact-lifecycle.ts';
6
+ import {
7
+ assertWritableInventory,
8
+ buildArtifactLinkSection,
9
+ inspectTaskArtifacts,
10
+ parseArtifactName
11
+ } from './artifact-lifecycle.ts';
12
+ import type { ArtifactIdentity } from './artifact-lifecycle.ts';
13
+ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
14
+ import type { TaskExecutionLockOptions } from './task-execution-lock.ts';
7
15
  import { captureTaskWriteMetadata, writeTask } from './write.ts';
8
16
  import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
9
17
 
10
- export type ActivityAppendIntent = {
11
- kind: 'append';
18
+ type PrReviewVerdict = 'approved' | 'changes-requested' | 'commented';
19
+ type PrReviewOutcome = 'aborted' | 'superseded';
20
+ type PrReviewArtifactStatus = 'pending' | 'applied' | 'no-op' | 'blocked' | 'failed' | 'aborted' | 'superseded';
21
+
22
+ export type PrReviewInspectIntent = {
23
+ kind: 'pr-review-inspect';
24
+ taskRef: string;
25
+ };
26
+
27
+ export type PrReviewStartIntent = {
28
+ kind: 'pr-review-start';
12
29
  taskRef: string;
13
- step: string;
14
30
  agent: string;
15
- note: string;
16
- artifact?: string;
31
+ artifact: string;
32
+ head: string;
17
33
  dryRun?: boolean;
18
34
  };
19
35
 
20
- export type ActivityIntentResult = {
36
+ export type PrReviewCompleteIntent = {
37
+ kind: 'pr-review-complete';
38
+ taskRef: string;
39
+ agent: string;
40
+ artifact: string;
41
+ head: string;
42
+ verdict: PrReviewVerdict;
43
+ blockers: number;
44
+ major: number;
45
+ minor: number;
46
+ dryRun?: boolean;
47
+ };
48
+
49
+ export type PrReviewTerminateIntent = {
50
+ kind: 'pr-review-terminate';
51
+ taskRef: string;
52
+ agent: string;
53
+ artifact: string;
54
+ head: string;
55
+ outcome: PrReviewOutcome;
56
+ reason: string;
57
+ dryRun?: boolean;
58
+ };
59
+
60
+ export type PrReviewActivityIntent = PrReviewStartIntent | PrReviewCompleteIntent | PrReviewTerminateIntent;
61
+
62
+ type PrReviewIdentity = {
63
+ round: number;
64
+ artifact: string;
65
+ head: string;
66
+ agent: string | null;
67
+ artifactStatus: PrReviewArtifactStatus;
68
+ note: string | null;
69
+ };
70
+
71
+ export type PrReviewActivitySnapshot = {
72
+ nextRound: number;
73
+ prepared: PrReviewIdentity | null;
74
+ open: PrReviewIdentity | null;
75
+ latestTerminal: PrReviewIdentity | null;
76
+ latestSuccessful: PrReviewIdentity | null;
77
+ };
78
+
79
+ export type PrReviewActivityResult = {
21
80
  status: 'planned' | 'applied' | 'no-op' | 'failed';
22
81
  changed: boolean;
23
- intent: 'append';
82
+ intent: PrReviewActivityIntent['kind'];
24
83
  taskId: string | null;
25
84
  artifact: string | null;
26
85
  operations: readonly TaskOperationSummary[];
27
86
  error: { code: string; message: string } | null;
28
87
  };
29
88
 
30
- function failed(intent: ActivityAppendIntent, code: string, message: string, taskId: string | null = null): ActivityIntentResult {
89
+ export type PrReviewInspectionResult = {
90
+ status: 'ready' | 'failed';
91
+ changed: false;
92
+ intent: 'pr-review-inspect';
93
+ taskId: string | null;
94
+ snapshot: PrReviewActivitySnapshot | null;
95
+ error: { code: string; message: string } | null;
96
+ };
97
+
98
+ type ActivityIntentOptions = TaskWriteOptions & { lockOptions?: TaskExecutionLockOptions };
99
+ type ArtifactRecord = { identity: ArtifactIdentity; head: string; status: PrReviewArtifactStatus };
100
+ type RoundState = ArtifactRecord & {
101
+ startedAgent: string | null;
102
+ terminalAgent: string | null;
103
+ terminalNote: string | null;
104
+ };
105
+
106
+ const HEAD_RE = /^[0-9a-f]{40}$/;
107
+ const REVIEW_ACTION_RE = /^Review PR \(Round ([1-9]\d*)\)( \[started\])?$/;
108
+ const VERDICTS: ReadonlySet<string> = new Set(['approved', 'changes-requested', 'commented']);
109
+ const OUTCOMES: ReadonlySet<string> = new Set(['aborted', 'superseded']);
110
+ const SUCCESS_STATUSES: ReadonlySet<PrReviewArtifactStatus> = new Set(['applied', 'no-op']);
111
+ const ARTIFACT_STATUSES: ReadonlySet<string> = new Set([
112
+ 'pending', 'applied', 'no-op', 'blocked', 'failed', 'aborted', 'superseded'
113
+ ]);
114
+
115
+ function failed(
116
+ intent: PrReviewActivityIntent,
117
+ code: string,
118
+ message: string,
119
+ taskId: string | null = null
120
+ ): PrReviewActivityResult {
31
121
  return {
32
- status: 'failed', changed: false, intent: 'append', taskId,
33
- artifact: intent.artifact ?? null, operations: [], error: { code, message }
122
+ status: 'failed', changed: false, intent: intent.kind, taskId,
123
+ artifact: intent.artifact, operations: [], error: { code, message }
34
124
  };
35
125
  }
36
126
 
37
- function oneLine(value: string): string {
38
- return value.replace(/\s*\r?\n\s*/g, ' ').trim();
127
+ function inspectionFailed(code: string, message: string, taskId: string | null = null): PrReviewInspectionResult {
128
+ return {
129
+ status: 'failed', changed: false, intent: 'pr-review-inspect', taskId,
130
+ snapshot: null, error: { code, message }
131
+ };
39
132
  }
40
133
 
41
- /**
42
- * Append an Activity Log entry (and, optionally, a `## 审查反馈` artifact link)
43
- * to an active task via the atomic `writeTask` path. `current_step` is never
44
- * touched (PL-2): review-pr does not join the standard stage chain.
45
- */
46
- export function applyActivityAppendIntent(intent: ActivityAppendIntent, options: TaskWriteOptions = {}): ActivityIntentResult {
47
- if (intent.kind !== 'append') return failed(intent, 'ACTIVITY_INTENT_INVALID', 'intent kind must be append');
48
- const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
49
- if (!resolved.ok) return failed(intent, resolved.code, resolved.message, resolved.taskId);
50
- if (resolved.state !== 'active') {
51
- return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
134
+ function parseArtifactRecord(identity: ArtifactIdentity): ArtifactRecord | { code: string; message: string } {
135
+ let content: string;
136
+ try {
137
+ content = fs.readFileSync(identity.path, 'utf8');
138
+ } catch (error) {
139
+ return { code: 'ACTIVITY_ARTIFACT_READ_FAILED', message: error instanceof Error ? error.message : String(error) };
52
140
  }
53
- const step = oneLine(intent.step);
54
- const agent = oneLine(intent.agent);
55
- const note = oneLine(intent.note);
56
- if (!step || !agent || !note || /[\r\n]/.test(intent.step) || /[\r\n]/.test(intent.agent) || /[\r\n]/.test(intent.note)) {
57
- return failed(intent, 'ACTIVITY_INTENT_INVALID', 'step, agent and note must be non-empty single-line values', resolved.taskId);
141
+ const heads = [...content.matchAll(/^- \*\*(?:被审 head SHA|Reviewed Head SHA)\*\*[::]\s*([0-9a-f]{40})\s*$/gm)];
142
+ if (heads.length !== 1) {
143
+ return { code: 'ACTIVITY_ARTIFACT_INVALID', message: `${identity.name} must contain exactly one reviewed head SHA` };
58
144
  }
145
+ const statuses = [...content.matchAll(/^- \*\*(?:正式 Review 状态|Formal Review Status)\*\*[::][ \t]*([a-z-]+)[^\r\n]*$/gm)];
146
+ const status = statuses[0]?.[1];
147
+ if (statuses.length !== 1 || !status || !ARTIFACT_STATUSES.has(status)) {
148
+ return { code: 'ACTIVITY_ARTIFACT_INVALID', message: `${identity.name} must contain exactly one supported formal review status` };
149
+ }
150
+ return { identity, head: heads[0]![1]!, status: status as PrReviewArtifactStatus };
151
+ }
59
152
 
60
- let artifact: ReturnType<typeof parseArtifactName> | null = null;
61
- if (intent.artifact !== undefined) {
62
- artifact = parseArtifactName(intent.artifact);
63
- if (!artifact || artifact.family !== 'pr-review') {
64
- return failed(intent, 'ACTIVITY_ARTIFACT_INVALID', `artifact '${intent.artifact}' is not a canonical pr-review artifact`, resolved.taskId);
65
- }
153
+ function toIdentity(state: RoundState, terminal = false): PrReviewIdentity {
154
+ return {
155
+ round: state.identity.round,
156
+ artifact: state.identity.name,
157
+ head: state.head,
158
+ agent: terminal ? state.terminalAgent : state.startedAgent,
159
+ artifactStatus: state.status,
160
+ note: terminal ? state.terminalNote : null
161
+ };
162
+ }
163
+
164
+ function readSnapshot(taskRef: string, repoRoot?: string):
165
+ | { ok: true; taskId: string; taskDir: string; taskMdPath: string; repoRoot: string; content: string; states: RoundState[]; snapshot: PrReviewActivitySnapshot }
166
+ | { ok: false; code: string; message: string; taskId: string | null } {
167
+ const resolved = resolveTaskRef(taskRef, { repoRoot });
168
+ if (!resolved.ok) return { ok: false, code: resolved.code, message: resolved.message, taskId: resolved.taskId };
169
+ if (resolved.state !== 'active') {
170
+ return { ok: false, code: 'TASK_STATE_MISMATCH', message: `task ${resolved.taskId} is ${resolved.state}, expected active`, taskId: resolved.taskId };
171
+ }
172
+ const inventory = inspectTaskArtifacts(taskRef, 'pr-review', { repoRoot: resolved.repoRoot });
173
+ if (inventory.status === 'failed') {
174
+ return { ok: false, code: inventory.error?.code ?? 'ACTIVITY_ARTIFACT_INVALID', message: inventory.error?.message ?? 'artifact inspection failed', taskId: resolved.taskId };
66
175
  }
176
+ const topology = assertWritableInventory(inventory);
177
+ if (topology) return { ok: false, code: topology.code, message: topology.message, taskId: resolved.taskId };
67
178
 
68
179
  let content: string;
69
180
  try {
70
181
  content = fs.readFileSync(resolved.taskMdPath, 'utf8');
71
182
  } catch (error) {
72
- return failed(intent, 'TASK_READ_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
183
+ return { ok: false, code: 'TASK_READ_FAILED', message: error instanceof Error ? error.message : String(error), taskId: resolved.taskId };
73
184
  }
74
- const section = locateActivityLog(content);
75
- if (!section) {
76
- return failed(intent, 'ACTIVITY_SECTION_MISSING', 'task has no unique Activity Log section', resolved.taskId);
185
+ const activity = locateActivityLog(content);
186
+ if (!activity) return { ok: false, code: 'ACTIVITY_SECTION_MISSING', message: 'task has no unique Activity Log section', taskId: resolved.taskId };
187
+
188
+ const knownRounds = new Set(inventory.artifacts.map((artifact) => artifact.round));
189
+ for (const entry of activity.entries) {
190
+ const match = REVIEW_ACTION_RE.exec(entry.step);
191
+ if (match && !knownRounds.has(Number(match[1]))) {
192
+ return { ok: false, code: 'ACTIVITY_STATE_CONFLICT', message: `Activity Log round ${match[1]} has no canonical pr-review artifact`, taskId: resolved.taskId };
193
+ }
194
+ }
195
+
196
+ const states: RoundState[] = [];
197
+ for (const identity of inventory.artifacts) {
198
+ const parsed = parseArtifactRecord(identity);
199
+ if ('code' in parsed) return { ok: false, ...parsed, taskId: resolved.taskId };
200
+ const base = `Review PR (Round ${identity.round})`;
201
+ const started = activity.entries.filter((entry) => entry.step === `${base} [started]`);
202
+ const terminal = activity.entries.filter((entry) => entry.step === base);
203
+ if (started.length > 1 || terminal.length > 1) {
204
+ return { ok: false, code: 'ACTIVITY_STATE_CONFLICT', message: `${base} has duplicate started or terminal rows`, taskId: resolved.taskId };
205
+ }
206
+ if (terminal.length === 1 && started.length === 0 && !terminal[0]!.note.startsWith('receipt ')) {
207
+ return { ok: false, code: 'ACTIVITY_STATE_CONFLICT', message: `${base} has a terminal row without a matching started row`, taskId: resolved.taskId };
208
+ }
209
+ if (terminal.length === 1) {
210
+ const note = terminal[0]!.note;
211
+ const terminalMatchesStatus =
212
+ (note.startsWith('Verdict: ') && SUCCESS_STATUSES.has(parsed.status))
213
+ || (note.startsWith('Outcome: Aborted, ') && parsed.status === 'aborted')
214
+ || (note.startsWith('Outcome: Superseded, ') && parsed.status === 'superseded')
215
+ || (note.startsWith('receipt ') && SUCCESS_STATUSES.has(parsed.status));
216
+ if (!terminalMatchesStatus) {
217
+ return { ok: false, code: 'ACTIVITY_STATE_CONFLICT', message: `${base} terminal note conflicts with artifact status ${parsed.status}`, taskId: resolved.taskId };
218
+ }
219
+ }
220
+ states.push({
221
+ ...parsed,
222
+ startedAgent: started[0]?.agent ?? null,
223
+ terminalAgent: terminal[0]?.agent ?? null,
224
+ terminalNote: terminal[0]?.note ?? null
225
+ });
226
+ }
227
+
228
+ const openStates = states.filter((state) => state.startedAgent !== null && state.terminalNote === null);
229
+ const preparedStates = states.filter((state) => state.status === 'pending' && state.startedAgent === null && state.terminalNote === null);
230
+ if (openStates.length > 1 || preparedStates.length > 1) {
231
+ return { ok: false, code: 'ACTIVITY_STATE_CONFLICT', message: 'multiple prepared or open PR review rounds exist', taskId: resolved.taskId };
232
+ }
233
+ const terminalStates = states.filter((state) => state.terminalNote !== null);
234
+ const successfulStates = states.filter((state) => SUCCESS_STATUSES.has(state.status));
235
+ const latestTerminal = terminalStates.at(-1) ?? null;
236
+ const latestSuccessful = successfulStates.at(-1) ?? null;
237
+ return {
238
+ ok: true,
239
+ taskId: resolved.taskId,
240
+ taskDir: resolved.taskDir,
241
+ taskMdPath: resolved.taskMdPath,
242
+ repoRoot: resolved.repoRoot,
243
+ content,
244
+ states,
245
+ snapshot: {
246
+ nextRound: inventory.next?.round ?? 1,
247
+ prepared: preparedStates[0] ? toIdentity(preparedStates[0]) : null,
248
+ open: openStates[0] ? toIdentity(openStates[0]) : null,
249
+ latestTerminal: latestTerminal ? toIdentity(latestTerminal, true) : null,
250
+ latestSuccessful: latestSuccessful ? toIdentity(latestSuccessful, true) : null
251
+ }
252
+ };
253
+ }
254
+
255
+ export function inspectPrReviewActivity(intent: PrReviewInspectIntent, options: Pick<ActivityIntentOptions, 'repoRoot'> = {}): PrReviewInspectionResult {
256
+ const state = readSnapshot(intent.taskRef, options.repoRoot);
257
+ if (!state.ok) return inspectionFailed(state.code, state.message, state.taskId);
258
+ return { status: 'ready', changed: false, intent: intent.kind, taskId: state.taskId, snapshot: state.snapshot, error: null };
259
+ }
260
+
261
+ function validateIntent(intent: PrReviewActivityIntent): { agent: string; artifact: { round: number; name: string } } | { code: string; message: string } {
262
+ const agent = normalizeAgentToken(intent.agent);
263
+ if (!agent) return { code: 'ACTIVITY_INTENT_INVALID', message: 'agent must be a supported collaborator token' };
264
+ const artifact = parseArtifactName(intent.artifact);
265
+ if (!artifact || artifact.family !== 'pr-review') {
266
+ return { code: 'ACTIVITY_ARTIFACT_INVALID', message: `artifact '${intent.artifact}' is not a canonical pr-review artifact` };
267
+ }
268
+ if (!HEAD_RE.test(intent.head)) return { code: 'ACTIVITY_INTENT_INVALID', message: 'head must be a lowercase 40-character SHA' };
269
+ if (intent.kind === 'pr-review-complete') {
270
+ if (!VERDICTS.has(intent.verdict)) return { code: 'ACTIVITY_INTENT_INVALID', message: 'verdict is invalid' };
271
+ if (![intent.blockers, intent.major, intent.minor].every((value) => Number.isSafeInteger(value) && value >= 0)) {
272
+ return { code: 'ACTIVITY_INTENT_INVALID', message: 'finding counts must be non-negative safe integers' };
273
+ }
274
+ }
275
+ if (intent.kind === 'pr-review-terminate') {
276
+ if (!OUTCOMES.has(intent.outcome)) return { code: 'ACTIVITY_INTENT_INVALID', message: 'outcome is invalid' };
277
+ if (!intent.reason.trim() || /[\r\n]/.test(intent.reason) || intent.reason !== intent.reason.trim()) {
278
+ return { code: 'ACTIVITY_INTENT_INVALID', message: 'reason must be a non-empty trimmed single-line value' };
279
+ }
280
+ }
281
+ return { agent, artifact };
282
+ }
283
+
284
+ function verdictLabel(verdict: PrReviewVerdict): string {
285
+ if (verdict === 'approved') return 'Approved';
286
+ if (verdict === 'changes-requested') return 'Changes Requested';
287
+ return 'Commented';
288
+ }
289
+
290
+ function outcomeLabel(outcome: PrReviewOutcome): string {
291
+ return outcome === 'aborted' ? 'Aborted' : 'Superseded';
292
+ }
293
+
294
+ function terminalNote(intent: PrReviewCompleteIntent | PrReviewTerminateIntent): string {
295
+ if (intent.kind === 'pr-review-complete') {
296
+ return `Verdict: ${verdictLabel(intent.verdict)}, blockers: ${intent.blockers}, major: ${intent.major}, minor: ${intent.minor} → ${intent.artifact}`;
297
+ }
298
+ return `Outcome: ${outcomeLabel(intent.outcome)}, reason: ${intent.reason} → ${intent.artifact}`;
299
+ }
300
+
301
+ function applyLocked(intent: PrReviewActivityIntent, normalizedAgent: string, options: ActivityIntentOptions): PrReviewActivityResult {
302
+ const state = readSnapshot(intent.taskRef, options.repoRoot);
303
+ if (!state.ok) return failed(intent, state.code, state.message, state.taskId);
304
+ const parsed = parseArtifactName(intent.artifact)!;
305
+ const roundState = state.states.find((item) => item.identity.name === parsed.name);
306
+ if (!roundState) return failed(intent, 'ACTIVITY_ARTIFACT_INVALID', `artifact '${intent.artifact}' is not landed`, state.taskId);
307
+ if (roundState.head !== intent.head) {
308
+ return failed(intent, 'ACTIVITY_IDENTITY_MISMATCH', `head does not match ${intent.artifact}`, state.taskId);
309
+ }
310
+ const base = `Review PR (Round ${parsed.round})`;
311
+ let step: string;
312
+ let note: string;
313
+ let linkArtifact = false;
314
+
315
+ if (intent.kind === 'pr-review-start') {
316
+ if (roundState.terminalNote !== null) {
317
+ return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${base} is already terminal`, state.taskId);
318
+ }
319
+ if (roundState.startedAgent !== null) {
320
+ return { status: 'no-op', changed: false, intent: intent.kind, taskId: state.taskId, artifact: intent.artifact, operations: [], error: null };
321
+ }
322
+ if (roundState.status !== 'pending') {
323
+ return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${intent.artifact} must be pending before start`, state.taskId);
324
+ }
325
+ if (state.snapshot.open && state.snapshot.open.artifact !== intent.artifact) {
326
+ return failed(intent, 'ACTIVITY_STATE_CONFLICT', `another PR review round is open: ${state.snapshot.open.artifact}`, state.taskId);
327
+ }
328
+ step = `${base} [started]`;
329
+ note = 'started';
330
+ } else {
331
+ note = terminalNote(intent);
332
+ if (roundState.terminalNote !== null) {
333
+ if (roundState.terminalNote === note) {
334
+ return { status: 'no-op', changed: false, intent: intent.kind, taskId: state.taskId, artifact: intent.artifact, operations: [], error: null };
335
+ }
336
+ return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${base} has a different terminal payload`, state.taskId);
337
+ }
338
+ if (roundState.startedAgent === null) {
339
+ return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${base} has no matching started row`, state.taskId);
340
+ }
341
+ if (intent.kind === 'pr-review-complete' && !SUCCESS_STATUSES.has(roundState.status)) {
342
+ return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${intent.artifact} must be applied or no-op before completion`, state.taskId);
343
+ }
344
+ if (intent.kind === 'pr-review-terminate' && roundState.status !== intent.outcome) {
345
+ return failed(intent, 'ACTIVITY_STATE_CONFLICT', `${intent.artifact} status must match outcome ${intent.outcome}`, state.taskId);
346
+ }
347
+ step = base;
348
+ linkArtifact = true;
77
349
  }
78
350
 
79
351
  let metadata;
80
352
  try {
81
353
  metadata = (options.metadataProvider ?? captureTaskWriteMetadata)();
82
354
  } catch (error) {
83
- return failed(intent, 'METADATA_CAPTURE_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
84
- }
85
-
86
- const mutations: TaskMutation[] = [];
87
- const activityBody = appendActivityEntry(section, { time: metadata.timestamp, step, agent, note });
88
- mutations.push({ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: section.heading, body: activityBody });
89
- if (artifact) {
90
- const link = buildArtifactLinkSection(content, {
91
- ...artifact,
92
- path: path.join(resolved.taskDir, artifact.name),
93
- size: 0,
94
- mtimeMs: 0
95
- });
355
+ return failed(intent, 'METADATA_CAPTURE_FAILED', error instanceof Error ? error.message : String(error), state.taskId);
356
+ }
357
+ const section = locateActivityLog(state.content)!;
358
+ const mutations: TaskMutation[] = [{
359
+ kind: 'section', aliases: ['活动日志', 'Activity Log'], heading: section.heading,
360
+ body: appendActivityEntry(section, { time: metadata.timestamp, step, agent: normalizedAgent, note })
361
+ }];
362
+ if (linkArtifact) {
363
+ const link = buildArtifactLinkSection(state.content, roundState.identity);
96
364
  mutations.push({ kind: 'section', aliases: link.aliases, heading: link.heading, body: link.body });
97
365
  }
98
-
99
366
  const writeResult = writeTask({
100
367
  taskRef: intent.taskRef,
101
368
  expectedState: 'active',
@@ -104,10 +371,10 @@ export function applyActivityAppendIntent(intent: ActivityAppendIntent, options:
104
371
  }, {
105
372
  ...options,
106
373
  taskLocation: {
107
- repoRoot: resolved.repoRoot,
108
- taskId: resolved.taskId,
109
- taskMdPath: resolved.taskMdPath,
110
- state: resolved.state
374
+ repoRoot: state.repoRoot,
375
+ taskId: state.taskId,
376
+ taskMdPath: state.taskMdPath,
377
+ state: 'active'
111
378
  },
112
379
  metadataProvider: () => metadata
113
380
  });
@@ -117,12 +384,34 @@ export function applyActivityAppendIntent(intent: ActivityAppendIntent, options:
117
384
  return {
118
385
  status: writeResult.status,
119
386
  changed: writeResult.changed,
120
- intent: 'append',
387
+ intent: intent.kind,
121
388
  taskId: writeResult.taskId,
122
- artifact: intent.artifact ?? null,
389
+ artifact: intent.artifact,
123
390
  operations: writeResult.operations,
124
391
  error: null
125
392
  };
126
393
  }
127
394
 
128
- export type { TaskWriteOptions } from './write.ts';
395
+ export function applyPrReviewActivityIntent(intent: PrReviewActivityIntent, options: ActivityIntentOptions = {}): PrReviewActivityResult {
396
+ const validated = validateIntent(intent);
397
+ if ('code' in validated) return failed(intent, validated.code, validated.message);
398
+ const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
399
+ if (!resolved.ok) return failed(intent, resolved.code, resolved.message, resolved.taskId);
400
+ if (resolved.state !== 'active') {
401
+ return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
402
+ }
403
+ try {
404
+ return withTaskExecutionLock(
405
+ resolved.repoRoot,
406
+ resolved.taskId,
407
+ `task-activity:${intent.kind}`,
408
+ () => applyLocked(intent, validated.agent, { ...options, repoRoot: resolved.repoRoot }),
409
+ options.lockOptions
410
+ );
411
+ } catch (error) {
412
+ if (error instanceof TaskExecutionLockError) return failed(intent, error.code, error.message, resolved.taskId);
413
+ return failed(intent, 'ACTIVITY_LOCK_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
414
+ }
415
+ }
416
+
417
+ export type { ActivityIntentOptions };
@@ -2,10 +2,14 @@ const HEADING_RE = /^##\s+(活动日志|Activity Log)\s*$/;
2
2
  const NEXT_H2_RE = /^##\s/;
3
3
  const ENTRY_RE = /^- (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}) — \*\*(.+?)\*\* by (.+?) — (.*)$/;
4
4
  const STARTED_SUFFIX_RE = /\s*\[started\]\s*$/;
5
+ const ABORTED_SUFFIX_RE = /\s*\[aborted\]\s*$/;
6
+ const ATTEMPT_RE = /(?:^|;\s*)attempt=([A-Za-z0-9-]{8,64})(?:;|$)/;
7
+ const COMMIT_STARTED_RE = /^started; attempt=([A-Za-z0-9-]{8,64}); baseline=([a-f0-9]{40,64}); agent=([a-z-]+)$/;
5
8
 
6
9
  type LogEntry = { time: string; step: string; agent: string; note: string };
7
- type StepRow = { step: string; agent: string; started: string; done: string; note: string };
10
+ type StepRow = { step: string; agent: string; started: string; done: string; note: string; attempt?: string };
8
11
  type ActivityLogSection = { heading: string; body: string; entries: LogEntry[] };
12
+ type CommitAttempt = Readonly<{ attempt: string; baseline: string; agent: string }>;
9
13
 
10
14
  function parseActivityLog(content: string): { sectionFound: boolean; entries: LogEntry[] } {
11
15
  const section = locateActivityLog(content);
@@ -37,26 +41,61 @@ function appendActivityEntry(section: ActivityLogSection, entry: LogEntry): stri
37
41
  return section.body ? `${section.body}\n${line}` : line;
38
42
  }
39
43
 
44
+ function activityAttempt(note: string): string | null {
45
+ return ATTEMPT_RE.exec(note)?.[1] ?? null;
46
+ }
47
+
48
+ function parseCommitAttemptStarted(note: string): CommitAttempt | null {
49
+ const match = COMMIT_STARTED_RE.exec(note);
50
+ return match ? { attempt: match[1]!, baseline: match[2]!, agent: match[3]! } : null;
51
+ }
52
+
53
+ function commitAttemptStartedNote(attempt: CommitAttempt): string {
54
+ return `started; attempt=${attempt.attempt}; baseline=${attempt.baseline}; agent=${attempt.agent}`;
55
+ }
56
+
40
57
  function pairEntries(entries: LogEntry[]): StepRow[] {
41
58
  const rows: StepRow[] = [];
42
59
  const open = new Map<string, StepRow[]>();
43
60
  for (const entry of entries) {
44
61
  const started = STARTED_SUFFIX_RE.test(entry.step);
45
- const base = entry.step.replace(STARTED_SUFFIX_RE, '');
62
+ const aborted = ABORTED_SUFFIX_RE.test(entry.step);
63
+ const base = entry.step.replace(STARTED_SUFFIX_RE, '').replace(ABORTED_SUFFIX_RE, '');
46
64
  if (started) {
47
- const row = { step: base, agent: entry.agent, started: entry.time, done: '', note: entry.note };
65
+ const attempt = activityAttempt(entry.note);
66
+ const row: StepRow = {
67
+ step: base, agent: entry.agent, started: entry.time, done: '', note: entry.note,
68
+ ...(attempt ? { attempt } : {})
69
+ };
48
70
  rows.push(row);
49
71
  const queue = open.get(base) ?? [];
50
72
  queue.push(row);
51
73
  open.set(base, queue);
52
74
  } else {
53
- const pending = open.get(base)?.shift();
75
+ const queue = open.get(base) ?? [];
76
+ const terminalAttempt = aborted ? activityAttempt(entry.note) : null;
77
+ const pendingIndex = terminalAttempt === null
78
+ ? 0
79
+ : queue.findIndex((row) => row.attempt === terminalAttempt);
80
+ const pending = pendingIndex >= 0 ? queue.splice(pendingIndex, 1)[0] : undefined;
54
81
  if (pending) Object.assign(pending, { done: entry.time, agent: entry.agent, note: entry.note });
55
- else rows.push({ step: base, agent: entry.agent, started: '', done: entry.time, note: entry.note });
82
+ else rows.push({
83
+ step: aborted ? entry.step : base,
84
+ agent: entry.agent, started: '', done: entry.time, note: entry.note,
85
+ ...(terminalAttempt ? { attempt: terminalAttempt } : {})
86
+ });
56
87
  }
57
88
  }
58
89
  return rows;
59
90
  }
60
91
 
61
- export { parseActivityLog, locateActivityLog, appendActivityEntry, pairEntries };
62
- export type { LogEntry, StepRow, ActivityLogSection };
92
+ export {
93
+ activityAttempt,
94
+ appendActivityEntry,
95
+ commitAttemptStartedNote,
96
+ locateActivityLog,
97
+ pairEntries,
98
+ parseActivityLog,
99
+ parseCommitAttemptStarted
100
+ };
101
+ export type { ActivityLogSection, CommitAttempt, LogEntry, StepRow };