@fitlab-ai/agent-infra 0.9.5 → 0.9.7

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 (172) hide show
  1. package/dist/bin/internal-cli.js +8 -3
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
  8. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  9. package/dist/lib/defaults.json +10 -2
  10. package/dist/lib/git/workflow.js +30 -4
  11. package/dist/lib/internal/codex-lifecycle.js +169 -3
  12. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  13. package/dist/lib/internal/release-workflow.js +108 -11
  14. package/dist/lib/internal/sandbox-control.js +43 -6
  15. package/dist/lib/internal/task-create.js +12 -4
  16. package/dist/lib/internal/task-orchestration.js +40 -7
  17. package/dist/lib/platform/context.js +6 -4
  18. package/dist/lib/platform/github-client.js +55 -2
  19. package/dist/lib/platform/issue-comments.js +2 -1
  20. package/dist/lib/process-data/checkpoint.js +319 -0
  21. package/dist/lib/process-data/index.js +128 -60
  22. package/dist/lib/process-data/materialize.js +84 -0
  23. package/dist/lib/process-data/normalize.js +34 -1
  24. package/dist/lib/process-data/reconcile.js +55 -1
  25. package/dist/lib/process-data/sources.js +422 -1
  26. package/dist/lib/process-data/store.js +341 -11
  27. package/dist/lib/release/workflow.js +29 -21
  28. package/dist/lib/sandbox/commands/create.js +111 -60
  29. package/dist/lib/sandbox/commands/prune.js +40 -16
  30. package/dist/lib/sandbox/commands/rm.js +372 -94
  31. package/dist/lib/sandbox/commands/start.js +4 -3
  32. package/dist/lib/sandbox/control/client.js +138 -22
  33. package/dist/lib/sandbox/control/container-identity.js +64 -0
  34. package/dist/lib/sandbox/control/executor.js +142 -0
  35. package/dist/lib/sandbox/control/lifecycle.js +557 -0
  36. package/dist/lib/sandbox/control/protocol.js +69 -26
  37. package/dist/lib/sandbox/control/server.js +395 -99
  38. package/dist/lib/sandbox/control/state.js +191 -0
  39. package/dist/lib/sandbox/control/timing.js +8 -0
  40. package/dist/lib/sandbox/index.js +2 -2
  41. package/dist/lib/sandbox/managed-fs.js +14 -2
  42. package/dist/lib/sandbox/mounts.js +22 -19
  43. package/dist/lib/sandbox/recovery.js +284 -50
  44. package/dist/lib/sandbox/workspace-view.js +90 -10
  45. package/dist/lib/sandbox/worktree-safety.js +196 -0
  46. package/dist/lib/server/daemon.js +1 -1
  47. package/dist/lib/server/process-control.js +17 -1
  48. package/dist/lib/server/process-state.js +57 -20
  49. package/dist/lib/task/artifact-lifecycle.js +87 -15
  50. package/dist/lib/task/artifact-receipts.js +128 -0
  51. package/dist/lib/task/codex-orchestration.js +236 -0
  52. package/dist/lib/task/commands/status.js +3 -0
  53. package/dist/lib/task/commands/validate.js +289 -0
  54. package/dist/lib/task/commit-finalization.js +36 -9
  55. package/dist/lib/task/create.js +10 -2
  56. package/dist/lib/task/delegation-receipts.js +109 -2
  57. package/dist/lib/task/events.js +149 -6
  58. package/dist/lib/task/finalization-artifacts.js +2 -1
  59. package/dist/lib/task/implementation-inputs.js +1 -7
  60. package/dist/lib/task/index.js +9 -0
  61. package/dist/lib/task/ledger.js +13 -1
  62. package/dist/lib/task/orchestration.js +365 -15
  63. package/dist/lib/task/sections.js +5 -1
  64. package/dist/lib/task/task-execution-lock.js +5 -4
  65. package/dist/lib/task/verification-engine.js +78 -14
  66. package/dist/lib/task/verification.js +1 -0
  67. package/dist/lifecycle-build-manifest.json +6 -0
  68. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
  69. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  70. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
  71. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
  72. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  73. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
  74. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
  75. package/lib/agent-clients/adapters/codex.ts +1 -1
  76. package/lib/defaults.json +10 -2
  77. package/lib/git/workflow.ts +25 -3
  78. package/lib/internal/codex-lifecycle.ts +175 -4
  79. package/lib/internal/codex-sandbox-controller.ts +109 -0
  80. package/lib/internal/release-workflow.ts +107 -11
  81. package/lib/internal/sandbox-control.ts +38 -6
  82. package/lib/internal/task-create.ts +16 -4
  83. package/lib/internal/task-orchestration.ts +34 -7
  84. package/lib/platform/context.ts +8 -5
  85. package/lib/platform/github-client.ts +61 -3
  86. package/lib/platform/issue-comments.ts +2 -1
  87. package/lib/process-data/checkpoint.ts +335 -0
  88. package/lib/process-data/index.ts +112 -60
  89. package/lib/process-data/materialize.ts +83 -0
  90. package/lib/process-data/normalize.ts +29 -1
  91. package/lib/process-data/reconcile.ts +64 -2
  92. package/lib/process-data/sources.ts +449 -2
  93. package/lib/process-data/store.ts +304 -11
  94. package/lib/process-data/types.ts +124 -2
  95. package/lib/release/workflow.ts +69 -20
  96. package/lib/sandbox/commands/create.ts +70 -25
  97. package/lib/sandbox/commands/prune.ts +53 -15
  98. package/lib/sandbox/commands/rm.ts +423 -108
  99. package/lib/sandbox/commands/start.ts +4 -3
  100. package/lib/sandbox/control/client.ts +152 -28
  101. package/lib/sandbox/control/container-identity.ts +93 -0
  102. package/lib/sandbox/control/executor.ts +167 -0
  103. package/lib/sandbox/control/lifecycle.ts +573 -0
  104. package/lib/sandbox/control/protocol.ts +121 -68
  105. package/lib/sandbox/control/server.ts +398 -109
  106. package/lib/sandbox/control/state.ts +213 -0
  107. package/lib/sandbox/control/timing.ts +12 -0
  108. package/lib/sandbox/index.ts +2 -2
  109. package/lib/sandbox/managed-fs.ts +17 -2
  110. package/lib/sandbox/mounts.ts +16 -8
  111. package/lib/sandbox/recovery.ts +335 -65
  112. package/lib/sandbox/workspace-view.ts +102 -11
  113. package/lib/sandbox/worktree-safety.ts +234 -0
  114. package/lib/server/daemon.ts +1 -1
  115. package/lib/server/process-control.ts +24 -3
  116. package/lib/server/process-state.ts +82 -27
  117. package/lib/task/artifact-lifecycle.ts +78 -14
  118. package/lib/task/artifact-receipts.ts +161 -0
  119. package/lib/task/codex-orchestration.ts +319 -0
  120. package/lib/task/commands/status.ts +2 -0
  121. package/lib/task/commands/validate.ts +282 -0
  122. package/lib/task/commit-finalization.ts +52 -9
  123. package/lib/task/create.ts +11 -2
  124. package/lib/task/delegation-receipts.ts +208 -7
  125. package/lib/task/events.ts +131 -5
  126. package/lib/task/finalization-artifacts.ts +2 -1
  127. package/lib/task/implementation-inputs.ts +1 -5
  128. package/lib/task/index.ts +8 -0
  129. package/lib/task/ledger.ts +16 -1
  130. package/lib/task/orchestration.ts +552 -18
  131. package/lib/task/sections.ts +6 -2
  132. package/lib/task/task-execution-lock.ts +11 -9
  133. package/lib/task/verification-engine.ts +93 -24
  134. package/lib/task/verification.ts +2 -1
  135. package/package.json +1 -1
  136. package/templates/.agents/README.en.md +7 -0
  137. package/templates/.agents/README.zh-CN.md +5 -0
  138. package/templates/.agents/hooks/lifecycle-delegation.js +72 -12
  139. package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
  140. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
  141. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
  142. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
  143. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  144. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  145. package/templates/.agents/rules/review-handshake.en.md +3 -1
  146. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  147. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  148. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  149. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  150. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  151. package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
  152. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
  153. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
  154. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
  155. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +25 -0
  156. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
  157. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  158. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  159. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +69 -0
  160. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -0
  161. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  162. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  163. package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -3
  164. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
  165. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  166. package/templates/.claude/commands/run-manual-validation.en.md +9 -0
  167. package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
  168. package/templates/.codex/hooks.json +8 -8
  169. package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
  170. package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
  171. package/templates/.agents/workspace/README.en.md +0 -26
  172. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -0,0 +1,196 @@
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { runProbe } from './shell.js';
5
+ function probeText(probe, cwd, args) {
6
+ const result = probe('git', ['-C', cwd, ...args], { encoding: 'utf8' });
7
+ if (result.error)
8
+ throw result.error;
9
+ if (result.status !== 0) {
10
+ const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : result.stderr.toString('utf8').trim();
11
+ throw new Error(stderr || `git ${args[0] ?? 'command'} failed with exit code ${result.status ?? 'unknown'}`);
12
+ }
13
+ return typeof result.stdout === 'string' ? result.stdout : result.stdout.toString('utf8');
14
+ }
15
+ function parseStatus(raw) {
16
+ const records = raw.split('\0');
17
+ const changes = [];
18
+ for (let index = 0; index < records.length; index += 1) {
19
+ const record = records[index] ?? '';
20
+ if (!record || record.startsWith('# '))
21
+ continue;
22
+ if (record.startsWith('? ')) {
23
+ changes.push({ indexStatus: '?', worktreeStatus: '?', path: record.slice(2) });
24
+ continue;
25
+ }
26
+ if (record.startsWith('1 ')) {
27
+ const match = /^1 (..) \S+ \S+ \S+ \S+ \S+ \S+ (.*)$/s.exec(record);
28
+ if (!match)
29
+ throw new Error('Unable to parse git porcelain v2 ordinary record');
30
+ changes.push({ indexStatus: match[1][0], worktreeStatus: match[1][1], path: match[2] });
31
+ continue;
32
+ }
33
+ if (record.startsWith('2 ')) {
34
+ const match = /^2 (..) \S+ \S+ \S+ \S+ \S+ \S+ \S+ (.*)$/s.exec(record);
35
+ const originalPath = records[++index];
36
+ if (!match || originalPath === undefined || originalPath === '') {
37
+ throw new Error('Unable to parse git porcelain v2 rename record');
38
+ }
39
+ changes.push({
40
+ indexStatus: match[1][0],
41
+ worktreeStatus: match[1][1],
42
+ path: match[2],
43
+ originalPath
44
+ });
45
+ continue;
46
+ }
47
+ if (record.startsWith('u ')) {
48
+ const match = /^u (..) \S+ \S+ \S+ \S+ \S+ \S+ \S+ \S+ (.*)$/s.exec(record);
49
+ if (!match)
50
+ throw new Error('Unable to parse git porcelain v2 unmerged record');
51
+ changes.push({ indexStatus: match[1][0], worktreeStatus: match[1][1], path: match[2] });
52
+ continue;
53
+ }
54
+ throw new Error(`Unsupported git porcelain v2 record '${record[0] ?? ''}'`);
55
+ }
56
+ return changes;
57
+ }
58
+ function updatePart(hash, label, value) {
59
+ const data = Buffer.isBuffer(value) ? value : Buffer.from(value, 'utf8');
60
+ hash.update(`${label.length}:${label}:${data.length}:`, 'utf8');
61
+ hash.update(data);
62
+ }
63
+ function resolveSnapshotPath(worktree, relativePath) {
64
+ const resolved = path.resolve(worktree, relativePath);
65
+ const relative = path.relative(worktree, resolved);
66
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
67
+ throw new Error(`Git reported a path outside the worktree: ${JSON.stringify(relativePath)}`);
68
+ }
69
+ return resolved;
70
+ }
71
+ function hashPath(hash, worktree, relativePath, probe) {
72
+ updatePart(hash, 'path', relativePath);
73
+ const target = resolveSnapshotPath(worktree, relativePath);
74
+ let stat;
75
+ try {
76
+ stat = fs.lstatSync(target);
77
+ }
78
+ catch (error) {
79
+ if (error.code === 'ENOENT') {
80
+ updatePart(hash, 'kind', 'absent');
81
+ return;
82
+ }
83
+ throw error;
84
+ }
85
+ updatePart(hash, 'mode', String(stat.mode & 0o7777));
86
+ if (stat.isFile()) {
87
+ updatePart(hash, 'kind', 'file');
88
+ updatePart(hash, 'content', fs.readFileSync(target));
89
+ return;
90
+ }
91
+ if (stat.isSymbolicLink()) {
92
+ updatePart(hash, 'kind', 'symlink');
93
+ updatePart(hash, 'target', fs.readlinkSync(target));
94
+ return;
95
+ }
96
+ if (stat.isDirectory()) {
97
+ updatePart(hash, 'kind', 'git-directory');
98
+ const nestedHead = probeText(probe, target, ['rev-parse', '--verify', 'HEAD']).trim();
99
+ const nestedStatus = probeText(probe, target, [
100
+ 'status', '--porcelain=v2', '-z', '--branch', '--untracked-files=all', '--ignore-submodules=none'
101
+ ]);
102
+ const nestedIndex = probeText(probe, target, ['ls-files', '--stage', '-z']);
103
+ updatePart(hash, 'nested-head', nestedHead);
104
+ updatePart(hash, 'nested-status', nestedStatus);
105
+ updatePart(hash, 'nested-index', nestedIndex);
106
+ for (const change of parseStatus(nestedStatus)) {
107
+ hashPath(hash, target, change.path, probe);
108
+ if (change.originalPath)
109
+ hashPath(hash, target, change.originalPath, probe);
110
+ }
111
+ return;
112
+ }
113
+ throw new Error(`Unsupported file type in dirty snapshot: ${JSON.stringify(relativePath)}`);
114
+ }
115
+ function captureOnce(worktree, probe) {
116
+ const resolvedWorktree = path.resolve(worktree);
117
+ const branch = probeText(probe, resolvedWorktree, ['rev-parse', '--abbrev-ref', 'HEAD']).trim();
118
+ const head = probeText(probe, resolvedWorktree, ['rev-parse', '--verify', 'HEAD']).trim();
119
+ const status = probeText(probe, resolvedWorktree, [
120
+ 'status', '--porcelain=v2', '-z', '--branch', '--untracked-files=all', '--ignore-submodules=none'
121
+ ]);
122
+ const index = probeText(probe, resolvedWorktree, ['ls-files', '--stage', '-z']);
123
+ if (!branch)
124
+ throw new Error('Unable to determine worktree branch');
125
+ if (!head)
126
+ throw new Error('Unable to determine worktree HEAD');
127
+ const changes = parseStatus(status);
128
+ const hash = createHash('sha256');
129
+ updatePart(hash, 'worktree', resolvedWorktree);
130
+ updatePart(hash, 'branch', branch);
131
+ updatePart(hash, 'head', head);
132
+ updatePart(hash, 'status', status);
133
+ updatePart(hash, 'index', index);
134
+ for (const change of changes) {
135
+ hashPath(hash, resolvedWorktree, change.path, probe);
136
+ if (change.originalPath)
137
+ hashPath(hash, resolvedWorktree, change.originalPath, probe);
138
+ }
139
+ return { worktree: resolvedWorktree, branch, head, changes, identity: hash.digest('hex') };
140
+ }
141
+ function inspectWorktree(worktree, { probe = runProbe } = {}) {
142
+ const resolvedWorktree = path.resolve(worktree);
143
+ try {
144
+ const first = captureOnce(resolvedWorktree, probe);
145
+ const second = captureOnce(resolvedWorktree, probe);
146
+ if (first.identity !== second.identity) {
147
+ return { status: 'failed', worktree: resolvedWorktree, message: 'Snapshot changed while inspecting worktree' };
148
+ }
149
+ return { status: second.changes.length === 0 ? 'clean' : 'dirty', snapshot: second };
150
+ }
151
+ catch (error) {
152
+ return {
153
+ status: 'failed',
154
+ worktree: resolvedWorktree,
155
+ message: error instanceof Error ? error.message : String(error)
156
+ };
157
+ }
158
+ }
159
+ function inspectWorktrees(worktrees) {
160
+ return [...new Set(worktrees.map((worktree) => path.resolve(worktree)))].sort()
161
+ .map((worktree) => inspectWorktree(worktree));
162
+ }
163
+ function createCleanPermit(snapshot) {
164
+ if (snapshot.changes.length > 0)
165
+ throw new Error('Cannot create a clean permit for a dirty worktree');
166
+ return { mode: 'clean', snapshot };
167
+ }
168
+ function createDiscardPermit(snapshot) {
169
+ if (snapshot.changes.length === 0)
170
+ throw new Error('Cannot create a discard permit for a clean worktree');
171
+ return { mode: 'discard', snapshot };
172
+ }
173
+ function verifyWorktreePermit(permit, options = {}) {
174
+ const current = inspectWorktree(permit.snapshot.worktree, options);
175
+ if (current.status === 'failed')
176
+ throw new Error(`Unable to verify worktree permit: ${current.message}`);
177
+ if (current.snapshot.identity !== permit.snapshot.identity || (permit.mode === 'clean' && current.status !== 'clean')) {
178
+ throw new Error(`Worktree changed after authorization: ${permit.snapshot.worktree}`);
179
+ }
180
+ return current.snapshot;
181
+ }
182
+ function formatWorktreeSnapshot(snapshot) {
183
+ const lines = snapshot.changes.map((change) => {
184
+ const rename = change.originalPath ? ` <- ${JSON.stringify(change.originalPath)}` : '';
185
+ return ` ${change.indexStatus}${change.worktreeStatus} ${JSON.stringify(change.path)}${rename}`;
186
+ });
187
+ return [
188
+ `Worktree: ${JSON.stringify(snapshot.worktree)}`,
189
+ `Branch: ${snapshot.branch}`,
190
+ `HEAD: ${snapshot.head}`,
191
+ ...lines,
192
+ `Snapshot identity: ${snapshot.identity}`
193
+ ].join('\n');
194
+ }
195
+ export { createCleanPermit, createDiscardPermit, formatWorktreeSnapshot, inspectWorktree, inspectWorktrees, verifyWorktreePermit };
196
+ //# sourceMappingURL=worktree-safety.js.map
@@ -110,7 +110,7 @@ export async function runDaemon() {
110
110
  logger.close();
111
111
  if (ownStartTime !== null) {
112
112
  removePidRecordIfMatches(config.pidFile, {
113
- version: 1,
113
+ version: 2,
114
114
  pid: process.pid,
115
115
  startTime: ownStartTime
116
116
  });
@@ -4,6 +4,12 @@ import { loadServerConfig } from './config.js';
4
4
  import { runDaemon } from './daemon.js';
5
5
  import { getProcessStartTime, isProcessAlive, processIdentityMatches, readProcessState, removePidFileIfMatches, writePidRecord } from './process-state.js';
6
6
  export { isProcessAlive } from './process-state.js';
7
+ export function buildProcessTreeStopCommand(pid, platform) {
8
+ if (platform === 'win32') {
9
+ return { kind: 'exec', command: 'taskkill', args: ['/PID', String(pid), '/T', '/F'] };
10
+ }
11
+ return { kind: 'group-signal', pid: -pid, signal: 'SIGTERM' };
12
+ }
7
13
  export function buildStopCommand(pid, platform) {
8
14
  if (platform === 'win32') {
9
15
  return { kind: 'exec', command: 'taskkill', args: ['/PID', String(pid), '/T', '/F'] };
@@ -20,7 +26,7 @@ async function waitForProcessIdentity(pid, timeoutMs = 2000) {
20
26
  while (Date.now() < deadline) {
21
27
  const startTime = getProcessStartTime(pid);
22
28
  if (startTime !== null)
23
- return { version: 1, pid, startTime };
29
+ return { version: 2, pid, startTime };
24
30
  await delay(25);
25
31
  }
26
32
  return null;
@@ -52,6 +58,9 @@ export async function start({ foreground = false } = {}) {
52
58
  process.stdout.write(`server already running (pid ${existing.record.pid})\n`);
53
59
  return;
54
60
  }
61
+ if (existing.kind === 'legacy-json' || existing.kind === 'legacy-pid-only' || existing.kind === 'legacy-unknown') {
62
+ throw new Error('server: legacy pid record requires the matching previous version to stop it before starting');
63
+ }
55
64
  if (existing.kind !== 'missing') {
56
65
  removePidFileIfMatches(pidPath, existing.snapshot);
57
66
  }
@@ -98,6 +107,9 @@ export async function stop() {
98
107
  return;
99
108
  }
100
109
  if (state.kind !== 'running') {
110
+ if (state.kind === 'legacy-json' || state.kind === 'legacy-pid-only' || state.kind === 'legacy-unknown') {
111
+ throw new Error('server: legacy pid record requires the matching previous version to stop it');
112
+ }
101
113
  removePidFileIfMatches(config.pidFile, state.snapshot);
102
114
  process.stdout.write('server is not running (removed stale pid file)\n');
103
115
  return;
@@ -129,6 +141,10 @@ export function status() {
129
141
  const config = loadServerConfig();
130
142
  const state = readProcessState(config.pidFile);
131
143
  if (state.kind !== 'running') {
144
+ if (state.kind === 'legacy-json' || state.kind === 'legacy-pid-only' || state.kind === 'legacy-unknown') {
145
+ process.stdout.write('server: legacy pid record requires the matching previous version to stop it\n');
146
+ return;
147
+ }
132
148
  if (state.kind !== 'missing')
133
149
  removePidFileIfMatches(config.pidFile, state.snapshot);
134
150
  process.stdout.write('server: stopped\n');
@@ -11,7 +11,10 @@ export function parseLinuxProcessStat(raw) {
11
11
  if (typeof state !== 'string' || state.length !== 1 || !/^\d+$/.test(startTime ?? '')) {
12
12
  return null;
13
13
  }
14
- return { state, startTime: startTime };
14
+ const numericStartTime = Number(startTime);
15
+ if (!Number.isSafeInteger(numericStartTime))
16
+ return null;
17
+ return { state, startTime: numericStartTime };
15
18
  }
16
19
  export function buildProcessStartTimeQuery(pid, platform) {
17
20
  if (platform === 'darwin') {
@@ -47,6 +50,17 @@ export function isProcessAlive(pid, platform = process.platform) {
47
50
  return error.code === 'EPERM';
48
51
  }
49
52
  }
53
+ function parseDarwinStartTime(output) {
54
+ const match = output.trim().match(/^(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat) ([A-Z][a-z]{2}) (\d{1,2}) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/);
55
+ if (!match)
56
+ return null;
57
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
58
+ const month = months.indexOf(match[1]);
59
+ if (month < 0)
60
+ return null;
61
+ const timestamp = Date.UTC(Number(match[6]), month, Number(match[2]), Number(match[3]), Number(match[4]), Number(match[5]));
62
+ return Number.isNaN(timestamp) ? null : timestamp;
63
+ }
50
64
  export function getProcessStartTime(pid, platform = process.platform) {
51
65
  if (!Number.isInteger(pid) || pid <= 0)
52
66
  return null;
@@ -65,14 +79,19 @@ export function getProcessStartTime(pid, platform = process.platform) {
65
79
  if (query === null)
66
80
  return null;
67
81
  try {
68
- const output = execFileSync(query.command, query.args, { encoding: 'utf8' }).trim();
82
+ const output = execFileSync(query.command, query.args, {
83
+ encoding: 'utf8',
84
+ env: platform === 'darwin' ? { ...process.env, LC_ALL: 'C', LANG: 'C', TZ: 'UTC' } : process.env
85
+ }).trim();
69
86
  if (output.length === 0)
70
87
  return null;
71
88
  if (platform === 'win32') {
72
89
  const timestamp = new Date(output);
73
- return Number.isNaN(timestamp.getTime()) ? null : timestamp.toISOString();
90
+ return Number.isNaN(timestamp.getTime()) ? null : timestamp.getTime();
74
91
  }
75
- return output;
92
+ if (platform === 'darwin')
93
+ return parseDarwinStartTime(output);
94
+ return null;
76
95
  }
77
96
  catch {
78
97
  return null;
@@ -82,26 +101,42 @@ function parsePidRecord(raw) {
82
101
  const trimmed = raw.trim();
83
102
  if (/^[1-9]\d*$/.test(trimmed)) {
84
103
  const pid = Number(trimmed);
85
- return Number.isSafeInteger(pid) ? { kind: 'legacy', pid } : { kind: 'invalid' };
104
+ return Number.isSafeInteger(pid)
105
+ ? { kind: 'legacy-pid-only', legacy: { pid, raw, format: 'pid-only' } }
106
+ : { kind: 'legacy-unknown', legacy: { raw, format: 'malformed' }, reason: 'pid is not a safe integer' };
86
107
  }
87
108
  try {
88
109
  const value = JSON.parse(trimmed);
110
+ const pidValue = value?.pid;
111
+ const startTimeValue = value?.startTime;
89
112
  if (value !== null &&
90
- value.version === 1 &&
91
- Number.isSafeInteger(value.pid) &&
92
- (value.pid ?? 0) > 0 &&
93
- typeof value.startTime === 'string' &&
94
- value.startTime.length > 0) {
113
+ value.version === 2 &&
114
+ typeof pidValue === 'number' &&
115
+ Number.isSafeInteger(pidValue) &&
116
+ pidValue > 0 &&
117
+ typeof startTimeValue === 'number' &&
118
+ Number.isSafeInteger(startTimeValue) &&
119
+ startTimeValue >= 0) {
120
+ const startTime = startTimeValue;
95
121
  return {
96
122
  kind: 'record',
97
- record: { version: 1, pid: value.pid, startTime: value.startTime }
123
+ record: { version: 2, pid: pidValue, startTime }
98
124
  };
99
125
  }
126
+ if (value !== null &&
127
+ value.version === 1 &&
128
+ typeof pidValue === 'number' &&
129
+ Number.isSafeInteger(pidValue) &&
130
+ pidValue > 0 &&
131
+ typeof value.startTime === 'string' &&
132
+ value.startTime.length > 0) {
133
+ return { kind: 'legacy-json', legacy: { pid: pidValue, raw, format: 'json-v1' } };
134
+ }
135
+ return { kind: 'legacy-unknown', legacy: { raw, format: 'malformed' }, reason: 'unsupported PID record schema' };
100
136
  }
101
137
  catch {
102
- // Invalid JSON is an invalid PID record.
138
+ return { kind: 'legacy-unknown', legacy: { raw, format: 'malformed' }, reason: 'invalid PID record JSON' };
103
139
  }
104
- return { kind: 'invalid' };
105
140
  }
106
141
  export function readProcessState(pidFile) {
107
142
  let snapshot;
@@ -111,13 +146,15 @@ export function readProcessState(pidFile) {
111
146
  catch (error) {
112
147
  if (error.code === 'ENOENT')
113
148
  return { kind: 'missing', snapshot: null };
114
- return { kind: 'invalid', snapshot: '' };
149
+ return { kind: 'legacy-unknown', snapshot: '', legacy: { raw: '', format: 'malformed' }, reason: 'PID record cannot be read' };
115
150
  }
116
151
  const parsed = parsePidRecord(snapshot);
117
- if (parsed.kind === 'invalid')
118
- return { kind: 'invalid', snapshot };
119
- if (parsed.kind === 'legacy')
120
- return { kind: 'legacy', snapshot, pid: parsed.pid };
152
+ if (parsed.kind === 'legacy-json')
153
+ return { kind: parsed.kind, snapshot, legacy: parsed.legacy };
154
+ if (parsed.kind === 'legacy-pid-only')
155
+ return { kind: parsed.kind, snapshot, legacy: parsed.legacy };
156
+ if (parsed.kind === 'legacy-unknown')
157
+ return { kind: parsed.kind, snapshot, legacy: parsed.legacy, reason: parsed.reason };
121
158
  const currentStartTime = getProcessStartTime(parsed.record.pid);
122
159
  if (currentStartTime === null || currentStartTime !== parsed.record.startTime) {
123
160
  return { kind: 'stale', snapshot, record: parsed.record };
@@ -162,7 +199,7 @@ export function removePidRecordIfMatches(pidFile, expected) {
162
199
  return false;
163
200
  return removePidFileIfMatches(pidFile, state.snapshot);
164
201
  }
165
- export function processIdentityMatches(record) {
166
- return getProcessStartTime(record.pid) === record.startTime;
202
+ export function processIdentityMatches(identity) {
203
+ return getProcessStartTime(identity.pid) === identity.startTime;
167
204
  }
168
205
  //# sourceMappingURL=process-state.js.map
@@ -5,6 +5,7 @@ import { locateActivityLog } from './activity-log.js';
5
5
  import { parseImplementationInputs, selectPendingImplementationInput } from './implementation-inputs.js';
6
6
  import { parseVerdict } from './review-artifacts.js';
7
7
  import { extractSection, findSectionHeading } from './sections.js';
8
+ import { receiptForOutput, sha256File } from './artifact-receipts.js';
8
9
  const artifactFamilyCatalog = [
9
10
  { family: 'analysis', sectionAliases: ['分析', 'Analysis'], heading: '分析', labels: ['需求分析报告', 'Requirements Analysis'] },
10
11
  { family: 'review-analysis', sectionAliases: ['审查反馈', 'Review Feedback'], heading: '审查反馈', labels: ['需求分析审查', 'Analysis Review'] },
@@ -13,6 +14,7 @@ const artifactFamilyCatalog = [
13
14
  { family: 'code', sectionAliases: ['实现备注', 'Implementation Notes'], heading: '实现备注', labels: ['实现报告', 'Implementation Report'] },
14
15
  { family: 'review-code', sectionAliases: ['审查反馈', 'Review Feedback'], heading: '审查反馈', labels: ['代码审查', 'Code Review'] },
15
16
  { family: 'manual-validation', sectionAliases: ['实现备注', 'Implementation Notes'], heading: '实现备注', labels: ['人工验证', 'Manual Validation'] },
17
+ { family: 'validation-run', sectionAliases: ['实现备注', 'Implementation Notes'], heading: '实现备注', labels: ['验证运行证据', 'Validation Run Evidence'] },
16
18
  { family: 'pr-review', sectionAliases: ['审查反馈', 'Review Feedback'], heading: '审查反馈', labels: ['PR 审查报告', 'PR Review Report'] }
17
19
  ];
18
20
  const FAMILY_SET = new Set(artifactFamilyCatalog.map((item) => item.family));
@@ -150,11 +152,7 @@ function resolveReviewedInput(taskDir, review, expectedFamily, diagnostics) {
150
152
  diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, String(error)));
151
153
  return null;
152
154
  }
153
- const lines = content.split(/\r?\n/);
154
- const header = lines.findIndex((line) => /\*\*(?:审查输入|Review Input)\*\*[::]/.test(line));
155
- const referenceBlock = header >= 0 ? lines.slice(header, header + 12).join('\n') : '';
156
- const candidates = [...referenceBlock.matchAll(/`([^`]+\.md)`/g)].map((match) => match[1]);
157
- const parsed = candidates.map((name) => parseArtifactName(name)).find((item) => item?.family === expectedFamily);
155
+ const parsed = parseReviewedInputReference(content, expectedFamily);
158
156
  if (!parsed) {
159
157
  diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, `${review.name} does not reference a canonical ${expectedFamily} artifact`));
160
158
  return null;
@@ -164,9 +162,17 @@ function resolveReviewedInput(taskDir, review, expectedFamily, diagnostics) {
164
162
  const stat = fs.lstatSync(abs);
165
163
  if (!stat.isFile() || stat.isSymbolicLink())
166
164
  throw new Error('referenced input is not a regular file');
167
- if (stat.mtimeMs > review.mtimeMs) {
168
- diagnostics.push(diagnostic('BROKEN_REFERENCE', review.family, review.name, `${parsed.name} is newer than ${review.name}`));
169
- return null;
165
+ const taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
166
+ const receipt = receiptForOutput(taskContent, review.name);
167
+ if (!receipt)
168
+ throw new Error(`receipt for ${review.name} is missing`);
169
+ const expectedEvent = reviewEventName(review.family);
170
+ if (receipt.event !== expectedEvent || receipt.input !== parsed.name) {
171
+ throw new Error(`receipt for ${review.name} does not match ${parsed.name}`);
172
+ }
173
+ const actualSha256 = sha256File(abs);
174
+ if (actualSha256 !== receipt.inputSha256) {
175
+ throw new Error(`${parsed.name} content digest does not match receipt`);
170
176
  }
171
177
  return { ...parsed, path: abs, size: stat.size, mtimeMs: stat.mtimeMs };
172
178
  }
@@ -175,6 +181,64 @@ function resolveReviewedInput(taskDir, review, expectedFamily, diagnostics) {
175
181
  return null;
176
182
  }
177
183
  }
184
+ function parseReviewedInputReference(content, expectedFamily) {
185
+ const lines = content.split(/\r?\n/);
186
+ const header = lines.findIndex((line) => /\*\*(?:审查输入|Review Input)\*\*[::]/.test(line));
187
+ const referenceBlock = header >= 0 ? lines.slice(header, header + 12).join('\n') : '';
188
+ const candidates = [...referenceBlock.matchAll(/`([^`]+\.md)`/g)].map((match) => match[1]);
189
+ return candidates
190
+ .map((name) => parseArtifactName(name))
191
+ .find((item) => item?.family === expectedFamily) ?? null;
192
+ }
193
+ function parseCodePlanInputReference(content) {
194
+ const lines = content.split(/\r?\n/);
195
+ const sectionStart = lines.findIndex((line) => /^##\s+(?:实现输入|Implementation Input)\s*$/.test(line.trim()));
196
+ if (sectionStart < 0)
197
+ return null;
198
+ const bodyLines = lines.slice(sectionStart + 1);
199
+ const nextSection = bodyLines.findIndex((line) => /^##\s+/.test(line.trim()));
200
+ const body = (nextSection < 0 ? bodyLines : bodyLines.slice(0, nextSection)).join('\n');
201
+ const match = body.match(/\*\*(?:方案输入|Plan Input)\*\*[::]\s*`([^`]+\.md)`/);
202
+ if (!match)
203
+ return null;
204
+ const parsed = parseArtifactName(match[1]);
205
+ return parsed?.family === 'plan' ? { ...parsed, family: 'plan' } : null;
206
+ }
207
+ function reviewEventName(family) {
208
+ if (family === 'review-analysis')
209
+ return 'review-analysis.completed';
210
+ if (family === 'review-plan')
211
+ return 'review-plan.completed';
212
+ if (family === 'review-code')
213
+ return 'review-code.completed';
214
+ throw new Error(`unsupported review family '${family}'`);
215
+ }
216
+ function resolveCodeInputReceipt(taskDir, code) {
217
+ let taskContent;
218
+ try {
219
+ taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
220
+ }
221
+ catch {
222
+ return null;
223
+ }
224
+ let receipt;
225
+ try {
226
+ receipt = receiptForOutput(taskContent, code.name);
227
+ }
228
+ catch {
229
+ return null;
230
+ }
231
+ if (!receipt || receipt.event !== 'code.completed')
232
+ return null;
233
+ const plan = inspectArtifactDirectory(taskDir, 'plan');
234
+ if (plan.status !== 'ready')
235
+ return null;
236
+ const input = plan.artifacts.find((artifact) => artifact.name === receipt.input);
237
+ return input ? { input, inputSha256: receipt.inputSha256 } : null;
238
+ }
239
+ function resolveCodePlanInput(taskDir, code) {
240
+ return resolveCodeInputReceipt(taskDir, code);
241
+ }
178
242
  function assertWritableInventory(inventory) {
179
243
  if (inventory.status === 'failed')
180
244
  return inventory.error;
@@ -191,7 +255,8 @@ const OPTIONAL_CONTEXT = {
191
255
  analysis: { family: 'review-analysis', requireReference: true },
192
256
  plan: { family: 'review-plan', requireReference: true },
193
257
  'review-code': { family: 'review-plan' },
194
- 'manual-validation': { family: 'review-code' }
258
+ 'manual-validation': { family: 'review-code' },
259
+ 'validation-run': { family: 'review-code' }
195
260
  };
196
261
  function resolveArtifactContext(taskRef, family, options = {}) {
197
262
  const inventory = inspectTaskArtifacts(taskRef, family, options);
@@ -246,10 +311,18 @@ function resolveCodeContext(inventory, options) {
246
311
  }
247
312
  return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, null, null, 'No prior code artifact. Starting initial implementation (round 1 -> code.md).');
248
313
  }
314
+ const codePlanInput = resolveCodePlanInput(inventory.taskDir, latestCode);
315
+ if (!codePlanInput) {
316
+ return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `code completion receipt for ${latestCode.name} is missing or does not match the current plan`, latestCode.name);
317
+ }
318
+ if (reviewPlan.latest && !reviewPlan.reviewedInput) {
319
+ return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `${reviewPlan.latest.name} has no valid reviewed input`, reviewPlan.latest.name);
320
+ }
249
321
  if (reviewPlan.latest && reviewPlan.reviewedInput?.name === plan.latest.name) {
250
322
  const verdict = parseVerdict(reviewPlan.latest.path);
251
- if (verdict.ok && verdict.verdict === 'Approved' && reviewPlan.latest.mtimeMs > latestCode.mtimeMs) {
252
- return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, verdict.verdict, reviewPlan.latest.name, `Latest ${reviewPlan.latest.name} is approved and newer than the latest code artifact. Entering replan-driven init.`);
323
+ const reviewedPlanSha256 = sha256File(reviewPlan.reviewedInput.path);
324
+ if (verdict.ok && verdict.verdict === 'Approved' && (codePlanInput.input.name !== plan.latest.name || codePlanInput.inputSha256 !== reviewedPlanSha256)) {
325
+ return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, verdict.verdict, reviewPlan.latest.name, `Latest ${reviewPlan.latest.name} approves plan content not captured by the latest code input receipt. Entering replan-driven init.`);
253
326
  }
254
327
  }
255
328
  if (reviewMax < codeMax) {
@@ -306,12 +379,11 @@ function resolveDecisionImplementationInput(inventory, review) {
306
379
  const activity = locateActivityLog(content);
307
380
  if (!activity)
308
381
  return { error: 'task has no unique Activity Log section' };
309
- const reviewEntries = activity.entries.filter((entry) => /^Review Code \(Round \d+\)$/.test(entry.step) && entry.note.includes(`→ ${review.name}`));
310
- const completed = reviewEntries.at(-1);
382
+ const completed = activity.entries.some((entry) => /^Review Code \(Round \d+\)$/.test(entry.step) && entry.note.includes(`→ ${review.name}`));
311
383
  if (!completed)
312
384
  return { error: `cannot find completed Activity Log identity for ${review.name}` };
313
385
  try {
314
- return { input: selectPendingImplementationInput(rows, completed.time) };
386
+ return { input: selectPendingImplementationInput(rows) };
315
387
  }
316
388
  catch (error) {
317
389
  return { error: error instanceof Error ? error.message : String(error) };
@@ -376,5 +448,5 @@ function buildArtifactLinkSection(content, artifact) {
376
448
  body = lines.join('\n').replace(/^\n+|\n+$/g, '');
377
449
  return { aliases, heading, body };
378
450
  }
379
- export { artifactFamilyCatalog, artifactName, parseArtifactName, familySpec, inspectTaskArtifacts, inspectArtifactDirectory, resolveArtifactContext, assertWritableInventory, validateCompletedArtifact, buildArtifactLinkSection };
451
+ export { artifactFamilyCatalog, artifactName, parseArtifactName, familySpec, inspectTaskArtifacts, inspectArtifactDirectory, resolveArtifactContext, parseReviewedInputReference, parseCodePlanInputReference, resolveCodePlanInput, reviewEventName, assertWritableInventory, validateCompletedArtifact, buildArtifactLinkSection };
380
452
  //# sourceMappingURL=artifact-lifecycle.js.map