helloagents 3.0.12 → 3.0.16-beta.1

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 (73) hide show
  1. package/.claude-plugin/marketplace.json +6 -4
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +182 -35
  5. package/README_CN.md +184 -37
  6. package/bootstrap-lite.md +32 -26
  7. package/bootstrap.md +35 -29
  8. package/cli.mjs +119 -11
  9. package/gemini-extension.json +1 -1
  10. package/install.ps1 +128 -0
  11. package/install.sh +121 -0
  12. package/package.json +23 -4
  13. package/scripts/advisor-state.mjs +36 -63
  14. package/scripts/capability-registry.mjs +4 -4
  15. package/scripts/cli-branch.mjs +84 -0
  16. package/scripts/cli-codex-config.mjs +14 -20
  17. package/scripts/cli-codex.mjs +32 -38
  18. package/scripts/cli-doctor-render.mjs +4 -0
  19. package/scripts/cli-doctor.mjs +40 -30
  20. package/scripts/cli-host-detect.mjs +0 -1
  21. package/scripts/cli-hosts.mjs +16 -8
  22. package/scripts/cli-lifecycle-hosts.mjs +119 -32
  23. package/scripts/cli-lifecycle.mjs +24 -13
  24. package/scripts/cli-messages.mjs +34 -16
  25. package/scripts/cli-runtime-carrier.mjs +15 -0
  26. package/scripts/cli-runtime-root.mjs +72 -0
  27. package/scripts/cli-toml.mjs +0 -79
  28. package/scripts/cli-utils.mjs +30 -4
  29. package/scripts/closeout-state.mjs +35 -62
  30. package/scripts/delivery-gate-messages.mjs +70 -0
  31. package/scripts/delivery-gate.mjs +9 -75
  32. package/scripts/guard-rules.mjs +42 -42
  33. package/scripts/guard.mjs +44 -24
  34. package/scripts/notify-context.mjs +19 -28
  35. package/scripts/notify-events.mjs +3 -1
  36. package/scripts/notify-gates.mjs +2 -0
  37. package/scripts/notify-route.mjs +9 -7
  38. package/scripts/notify-ui.mjs +42 -32
  39. package/scripts/notify.mjs +72 -36
  40. package/scripts/project-storage.mjs +35 -66
  41. package/scripts/ralph-loop.mjs +36 -31
  42. package/scripts/replay-state.mjs +31 -128
  43. package/scripts/review-state.mjs +34 -61
  44. package/scripts/runtime-artifacts.mjs +95 -0
  45. package/scripts/runtime-context.mjs +35 -29
  46. package/scripts/runtime-scope.mjs +313 -0
  47. package/scripts/session-capsule.mjs +202 -0
  48. package/scripts/turn-state-cli.mjs +17 -0
  49. package/scripts/turn-state.mjs +185 -66
  50. package/scripts/turn-stop-gate.mjs +24 -6
  51. package/scripts/verify-state.mjs +34 -85
  52. package/scripts/visual-state.mjs +38 -65
  53. package/scripts/workflow-core.mjs +3 -3
  54. package/scripts/workflow-plan-files.mjs +1 -1
  55. package/scripts/workflow-recommendation.mjs +17 -13
  56. package/scripts/workflow-state.mjs +5 -5
  57. package/skills/commands/build/SKILL.md +1 -1
  58. package/skills/commands/commit/SKILL.md +1 -1
  59. package/skills/commands/help/SKILL.md +5 -3
  60. package/skills/commands/loop/SKILL.md +1 -1
  61. package/skills/commands/plan/SKILL.md +8 -6
  62. package/skills/commands/prd/SKILL.md +5 -3
  63. package/skills/commands/verify/SKILL.md +5 -5
  64. package/skills/hello-debug/SKILL.md +20 -3
  65. package/skills/hello-review/SKILL.md +2 -2
  66. package/skills/hello-subagent/SKILL.md +2 -2
  67. package/skills/hello-test/SKILL.md +6 -2
  68. package/skills/hello-ui/SKILL.md +7 -7
  69. package/skills/hello-verify/SKILL.md +10 -7
  70. package/skills/helloagents/SKILL.md +14 -9
  71. package/templates/context.md +6 -0
  72. package/templates/plans/plan.md +3 -0
  73. package/templates/plans/tasks.md +8 -3
@@ -1,11 +1,16 @@
1
- import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
- import { dirname, join, normalize, resolve } from 'node:path'
3
- import { homedir } from 'node:os'
1
+ import { readFileSync } from 'node:fs'
2
+ import { normalize, resolve } from 'node:path'
4
3
  import { fileURLToPath } from 'node:url'
5
4
 
6
- import { appendReplayEvent } from './replay-state.mjs'
5
+ import {
6
+ appendSessionEvent,
7
+ clearCapsuleSection,
8
+ getSessionCapsulePath,
9
+ getRuntimeScope,
10
+ readCapsuleSection,
11
+ writeCapsuleSection,
12
+ } from './session-capsule.mjs'
7
13
 
8
- const TURN_STATE_PATH = join(homedir(), '.helloagents', 'runtime', 'turn-state.json')
9
14
  const TURN_STATE_TTL_MS = 30 * 60 * 1000
10
15
  const VALID_KINDS = new Set(['complete', 'waiting', 'blocked', 'progress'])
11
16
  const VALID_ROLES = new Set(['main', 'subagent'])
@@ -19,38 +24,31 @@ const VALID_REASON_CATEGORIES = new Set([
19
24
  'external-dependency',
20
25
  'error',
21
26
  ])
27
+ const HELP_TEXT = `Usage:
28
+ helloagents-turn-state write --kind complete --role main
29
+ helloagents-turn-state write --kind waiting --role main --reason-category missing-input --reason "..."
30
+ echo {"kind":"complete","role":"main"} | helloagents-turn-state write
31
+ helloagents-turn-state read [--cwd <path>]
32
+ helloagents-turn-state clear [--cwd <path>]
33
+
34
+ Options:
35
+ --cwd <path>
36
+ --kind <complete|waiting|blocked|progress>
37
+ --role <main|subagent>
38
+ --phase <name>
39
+ --source <name>
40
+ --reason-category <category>
41
+ --reason <text>
42
+ --requires-delivery-gate
43
+ --blocker-target <text>
44
+ --blocker-evidence <text>
45
+ --blocker-required-action <text>
46
+ `
22
47
 
23
48
  function normalizePath(filePath = '') {
24
49
  return filePath ? normalize(resolve(filePath)) : ''
25
50
  }
26
51
 
27
- function ensureRuntimeDir() {
28
- mkdirSync(dirname(TURN_STATE_PATH), { recursive: true })
29
- }
30
-
31
- function readStore() {
32
- try {
33
- return JSON.parse(readFileSync(TURN_STATE_PATH, 'utf-8'))
34
- } catch {
35
- return {}
36
- }
37
- }
38
-
39
- function writeStore(store) {
40
- const keys = Object.keys(store)
41
- if (keys.length === 0) {
42
- rmSync(TURN_STATE_PATH, { force: true })
43
- return
44
- }
45
-
46
- ensureRuntimeDir()
47
- writeFileSync(TURN_STATE_PATH, `${JSON.stringify(store, null, 2)}\n`, 'utf-8')
48
- }
49
-
50
- function getTurnStateKey(cwd = process.cwd()) {
51
- return normalizePath(cwd)
52
- }
53
-
54
52
  function normalizeTurnState(input = {}) {
55
53
  const kind = typeof input.kind === 'string' ? input.kind.trim().toLowerCase() : ''
56
54
  const role = typeof input.role === 'string' ? input.role.trim().toLowerCase() : 'main'
@@ -58,6 +56,7 @@ function normalizeTurnState(input = {}) {
58
56
  ? input.reasonCategory.trim().toLowerCase()
59
57
  : ''
60
58
  const reason = typeof input.reason === 'string' ? input.reason.trim() : ''
59
+ const blocker = normalizeBlocker(input.blocker)
61
60
 
62
61
  return {
63
62
  kind: VALID_KINDS.has(kind) ? kind : '',
@@ -67,59 +66,64 @@ function normalizeTurnState(input = {}) {
67
66
  requiresDeliveryGate: Boolean(input.requiresDeliveryGate),
68
67
  reasonCategory: VALID_REASON_CATEGORIES.has(reasonCategory) ? reasonCategory : '',
69
68
  reason,
69
+ ...(blocker ? { blocker } : {}),
70
70
  }
71
71
  }
72
72
 
73
- function pruneInvalidEntry(store, key) {
74
- delete store[key]
75
- writeStore(store)
76
- }
73
+ function normalizeBlocker(input = {}) {
74
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return null
75
+
76
+ const target = typeof input.target === 'string' ? input.target.trim() : ''
77
+ const evidence = typeof input.evidence === 'string' ? input.evidence.trim() : ''
78
+ const requiredAction = typeof input.requiredAction === 'string'
79
+ ? input.requiredAction.trim()
80
+ : ''
77
81
 
78
- export function clearTurnState(cwd = process.cwd()) {
79
- const key = getTurnStateKey(cwd)
80
- if (!key) return false
81
- const store = readStore()
82
- if (!(key in store)) return false
83
- delete store[key]
84
- writeStore(store)
85
- return true
82
+ if (!target && !evidence && !requiredAction) return null
83
+ return { target, evidence, requiredAction }
86
84
  }
87
85
 
88
- export function readTurnState(cwd = process.cwd(), { now = Date.now() } = {}) {
89
- const key = getTurnStateKey(cwd)
90
- if (!key) return null
86
+ export function clearTurnState(cwd = process.cwd(), options = {}) {
87
+ return clearCapsuleSection(cwd, 'turn', options)
88
+ }
91
89
 
92
- const store = readStore()
93
- const entry = store[key]
90
+ export function readTurnState(cwd = process.cwd(), { now = Date.now(), ...options } = {}) {
91
+ const entry = readCapsuleSection(cwd, 'turn', options)
94
92
  if (!entry?.cwd || !entry?.kind || !entry?.updatedAt) {
95
- if (entry) pruneInvalidEntry(store, key)
96
93
  return null
97
94
  }
98
95
 
99
96
  const updatedAt = Date.parse(entry.updatedAt)
100
97
  if (!Number.isFinite(updatedAt) || (now - updatedAt > TURN_STATE_TTL_MS)) {
101
- pruneInvalidEntry(store, key)
98
+ clearTurnState(cwd, options)
102
99
  return null
103
100
  }
104
101
 
105
102
  const normalized = normalizeTurnState(entry)
106
103
  if (!normalized.kind) {
107
- pruneInvalidEntry(store, key)
104
+ clearTurnState(cwd, options)
108
105
  return null
109
106
  }
110
107
 
111
108
  return {
112
109
  cwd: normalizePath(entry.cwd),
110
+ key: entry.key || '',
111
+ path: getSessionCapsulePath(cwd, options),
113
112
  updatedAt: entry.updatedAt,
114
113
  ...normalized,
115
114
  }
116
115
  }
117
116
 
118
117
  export function writeTurnState(cwd = process.cwd(), input = {}) {
119
- const key = getTurnStateKey(cwd)
118
+ const runtimeOptions = {
119
+ payload: input.payload && typeof input.payload === 'object' ? input.payload : input,
120
+ env: input.env || process.env,
121
+ ppid: input.ppid ?? process.ppid,
122
+ }
123
+ const scope = getRuntimeScope(cwd, runtimeOptions)
120
124
  const normalized = normalizeTurnState(input)
121
- if (!key || !normalized.kind) {
122
- throw new Error('turn-state requires cwd and a valid kind')
125
+ if (!normalized.kind) {
126
+ throw new Error('turn-state write requires a valid kind. Example: helloagents-turn-state write --kind complete --role main')
123
127
  }
124
128
  if (
125
129
  (normalized.kind === 'waiting' || normalized.kind === 'blocked')
@@ -128,16 +132,16 @@ export function writeTurnState(cwd = process.cwd(), input = {}) {
128
132
  throw new Error('turn-state waiting/blocked requires reasonCategory and reason')
129
133
  }
130
134
 
131
- const store = readStore()
132
135
  const payload = {
133
- cwd: key,
136
+ cwd: normalizePath(cwd),
137
+ key: scope.key,
138
+ scope: scope.scope,
134
139
  updatedAt: new Date().toISOString(),
135
140
  ...normalized,
136
141
  }
137
- store[key] = payload
138
- writeStore(store)
142
+ writeCapsuleSection(cwd, 'turn', payload, runtimeOptions)
139
143
 
140
- appendReplayEvent(cwd, {
144
+ appendSessionEvent(cwd, {
141
145
  event: 'turn_state_written',
142
146
  source: normalized.source,
143
147
  details: {
@@ -154,23 +158,130 @@ export function writeTurnState(cwd = process.cwd(), input = {}) {
154
158
  }
155
159
 
156
160
  function readStdinJson() {
161
+ if (process.stdin.isTTY) return {}
157
162
  try {
158
- return JSON.parse(readFileSync(0, 'utf-8'))
163
+ const text = readFileSync(0, 'utf-8').trim()
164
+ return text ? JSON.parse(text) : {}
159
165
  } catch {
160
166
  return {}
161
167
  }
162
168
  }
163
169
 
170
+ function normalizeOptionName(rawName = '') {
171
+ return rawName.replace(/^-+/, '').replace(/-([a-z])/g, (_, char) => char.toUpperCase())
172
+ }
173
+
174
+ function readOptionValue(args, index, name) {
175
+ const raw = args[index]
176
+ const eqIndex = raw.indexOf('=')
177
+ if (eqIndex >= 0) {
178
+ return { value: raw.slice(eqIndex + 1), nextIndex: index }
179
+ }
180
+
181
+ const next = args[index + 1]
182
+ if (next === undefined || next.startsWith('--')) {
183
+ return { value: true, nextIndex: index }
184
+ }
185
+ return { value: next, nextIndex: index + 1 }
186
+ }
187
+
188
+ function assignCliOption(input, name, value) {
189
+ const key = normalizeOptionName(name)
190
+ const aliases = {
191
+ reasonCategory: 'reasonCategory',
192
+ requiresDeliveryGate: 'requiresDeliveryGate',
193
+ blockerTarget: 'blocker.target',
194
+ blockerEvidence: 'blocker.evidence',
195
+ blockerRequiredAction: 'blocker.requiredAction',
196
+ }
197
+ const target = aliases[key] || key
198
+ const allowed = new Set([
199
+ 'cwd',
200
+ 'kind',
201
+ 'role',
202
+ 'phase',
203
+ 'source',
204
+ 'reasonCategory',
205
+ 'reason',
206
+ 'requiresDeliveryGate',
207
+ 'blocker.target',
208
+ 'blocker.evidence',
209
+ 'blocker.requiredAction',
210
+ ])
211
+ if (!allowed.has(target)) {
212
+ throw new Error(`unknown turn-state option: --${name}`)
213
+ }
214
+
215
+ if (target.startsWith('blocker.')) {
216
+ input.blocker = input.blocker || {}
217
+ input.blocker[target.slice('blocker.'.length)] = String(value)
218
+ return
219
+ }
220
+
221
+ input[target] = target === 'requiresDeliveryGate'
222
+ ? value === true || String(value).toLowerCase() === 'true'
223
+ : String(value)
224
+ }
225
+
226
+ function parseCliArgs(args = []) {
227
+ const input = {}
228
+ let wantsHelp = false
229
+
230
+ for (let index = 0; index < args.length; index += 1) {
231
+ const raw = args[index]
232
+ if (raw === '--help' || raw === '-h') {
233
+ wantsHelp = true
234
+ continue
235
+ }
236
+ if (!raw.startsWith('--')) {
237
+ throw new Error(`unexpected turn-state argument: ${raw}`)
238
+ }
239
+
240
+ const optionName = raw.slice(2).split('=')[0]
241
+ const { value, nextIndex } = readOptionValue(args, index, optionName)
242
+ assignCliOption(input, optionName, value)
243
+ index = nextIndex
244
+ }
245
+
246
+ return { input, wantsHelp }
247
+ }
248
+
249
+ function mergeInputs(stdinInput, cliInput) {
250
+ return {
251
+ ...stdinInput,
252
+ ...cliInput,
253
+ blocker: {
254
+ ...(stdinInput.blocker || {}),
255
+ ...(cliInput.blocker || {}),
256
+ },
257
+ }
258
+ }
259
+
260
+ function printHelp() {
261
+ process.stdout.write(HELP_TEXT)
262
+ }
263
+
164
264
  function main() {
165
265
  const command = process.argv[2] || ''
166
- const input = readStdinJson()
266
+ if (!command || command === '--help' || command === '-h' || command === 'help') {
267
+ printHelp()
268
+ return
269
+ }
270
+
271
+ const { input: cliInput, wantsHelp } = parseCliArgs(process.argv.slice(3))
272
+ if (wantsHelp) {
273
+ printHelp()
274
+ return
275
+ }
276
+
277
+ const input = mergeInputs(readStdinJson(), cliInput)
167
278
  const cwd = input.cwd || process.cwd()
168
279
 
169
280
  if (command === 'write') {
170
281
  const payload = writeTurnState(cwd, input)
171
282
  process.stdout.write(JSON.stringify({
172
283
  suppressOutput: true,
173
- path: TURN_STATE_PATH,
284
+ path: getSessionCapsulePath(cwd, input),
174
285
  payload,
175
286
  }))
176
287
  return
@@ -179,7 +290,7 @@ function main() {
179
290
  if (command === 'clear') {
180
291
  process.stdout.write(JSON.stringify({
181
292
  suppressOutput: true,
182
- cleared: clearTurnState(cwd),
293
+ cleared: clearTurnState(cwd, input),
183
294
  }))
184
295
  return
185
296
  }
@@ -187,11 +298,19 @@ function main() {
187
298
  if (command === 'read') {
188
299
  process.stdout.write(JSON.stringify({
189
300
  suppressOutput: true,
190
- state: readTurnState(cwd),
301
+ state: readTurnState(cwd, input),
191
302
  }))
303
+ return
192
304
  }
305
+
306
+ throw new Error(`unknown turn-state command: ${command}`)
193
307
  }
194
308
 
195
309
  if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
196
- main()
310
+ try {
311
+ main()
312
+ } catch (error) {
313
+ process.stderr.write(`${error.message}\n\n${HELP_TEXT}`)
314
+ process.exit(1)
315
+ }
197
316
  }
@@ -30,7 +30,7 @@ function buildWorkflowHint(cwd) {
30
30
  if (!recommendation) return ''
31
31
  return [
32
32
  `当前工作流:${recommendation.summary}`,
33
- `建议路径:${recommendation.nextPath}`,
33
+ `应执行路径:${recommendation.nextPath}`,
34
34
  recommendation.guidance,
35
35
  ].filter(Boolean).join('\n')
36
36
  }
@@ -43,16 +43,27 @@ function buildBlockReason(routeContext, detail, cwd) {
43
43
  detail,
44
44
  workflowHint,
45
45
  '若无真实阻塞,请继续沿当前路径执行。',
46
- `若确需停下,先调用 \`scripts/turn-state.mjs write\` 写结构化状态:\`kind=waiting\` \`kind=blocked\`,并同时填写 \`reasonCategory\` \`reason\`。`,
46
+ `若确需停下,先调用 \`helloagents-turn-state write --kind waiting --role main --reason-category <category> --reason "..."\` 写结构化状态;阻塞则把 \`waiting\` 改为 \`blocked\`。`,
47
47
  `允许的 \`reasonCategory\`:${ALLOWED_STOP_REASON_CATEGORIES.join(' | ')}。`,
48
48
  ].filter(Boolean).join('\n')
49
49
  }
50
50
 
51
- function getMainTurnState(cwd) {
52
- const turnState = readTurnState(cwd)
51
+ function getMainTurnState(cwd, payload = {}) {
52
+ const turnState = readTurnState(cwd, { payload })
53
53
  return turnState?.role === 'main' ? turnState : null
54
54
  }
55
55
 
56
+ function hasStructuredBlocker(turnState) {
57
+ const blocker = turnState?.blocker
58
+ return Boolean(
59
+ blocker
60
+ && typeof blocker === 'object'
61
+ && blocker.target
62
+ && blocker.evidence
63
+ && blocker.requiredAction,
64
+ )
65
+ }
66
+
56
67
  function validateTurnState(routeContext, turnState, cwd) {
57
68
  if (!turnState) {
58
69
  return buildBlockReason(routeContext, '缺少主代理 turn-state。', cwd)
@@ -62,6 +73,13 @@ function validateTurnState(routeContext, turnState, cwd) {
62
73
  }
63
74
  if (turnState.kind === 'waiting' || turnState.kind === 'blocked') {
64
75
  if (turnState.reasonCategory && turnState.reason) {
76
+ if (!hasStructuredBlocker(turnState)) {
77
+ return buildBlockReason(
78
+ routeContext,
79
+ '当前 waiting/blocked 缺少结构化 `blocker.target`、`blocker.evidence` 或 `blocker.requiredAction`,不能证明存在可核实的真实阻塞。',
80
+ cwd,
81
+ )
82
+ }
65
83
  return ''
66
84
  }
67
85
  return buildBlockReason(
@@ -76,14 +94,14 @@ function validateTurnState(routeContext, turnState, cwd) {
76
94
  function main() {
77
95
  const payload = readStdinJson()
78
96
  const cwd = payload.cwd || process.cwd()
79
- const routeContext = getApplicableRouteContext({ cwd })
97
+ const routeContext = getApplicableRouteContext({ cwd, payload })
80
98
 
81
99
  if (!routeContext || !ENFORCED_COMMANDS.has(routeContext.skillName)) {
82
100
  process.stdout.write(JSON.stringify({ decision: 'continue' }))
83
101
  return
84
102
  }
85
103
 
86
- const reason = validateTurnState(routeContext, getMainTurnState(cwd), cwd)
104
+ const reason = validateTurnState(routeContext, getMainTurnState(cwd, payload), cwd)
87
105
  process.stdout.write(JSON.stringify(reason ? { decision: 'block', reason } : { decision: 'continue' }))
88
106
  }
89
107
 
@@ -1,27 +1,33 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
- import { execSync } from 'node:child_process'
1
+ import { existsSync, readFileSync } from 'node:fs'
3
2
  import { join } from 'node:path'
4
3
  import { appendReplayEvent } from './replay-state.mjs'
5
- import { getProjectVerifyYamlPath } from './project-storage.mjs'
6
-
7
- export const VERIFY_EVIDENCE_FILE_NAME = '.ralph-verify.json'
8
- const VERIFY_EVIDENCE_MAX_AGE_MS = 30 * 60 * 1000
4
+ import {
5
+ getProjectVerifyYamlPath,
6
+ } from './project-storage.mjs'
7
+ import {
8
+ captureWorkspaceFingerprint,
9
+ clearRuntimeEvidence,
10
+ getRuntimeEvidencePath,
11
+ getRuntimeEvidenceRelativePath,
12
+ readRuntimeEvidence,
13
+ validateEvidenceFingerprint,
14
+ validateEvidenceTimestamp,
15
+ writeRuntimeEvidence,
16
+ } from './runtime-artifacts.mjs'
17
+
18
+ export const VERIFY_EVIDENCE_FILE_NAME = 'verify.json'
9
19
  const SHELL_OPERATORS = /[;&|`$(){}\n\r]/
10
20
 
11
- export function getVerifyEvidencePath(cwd) {
12
- return join(cwd, '.helloagents', VERIFY_EVIDENCE_FILE_NAME)
21
+ export function getVerifyEvidencePath(cwd, options = {}) {
22
+ return getRuntimeEvidencePath(cwd, VERIFY_EVIDENCE_FILE_NAME, options)
13
23
  }
14
24
 
15
- export function readVerifyEvidence(cwd) {
16
- try {
17
- return JSON.parse(readFileSync(getVerifyEvidencePath(cwd), 'utf-8'))
18
- } catch {
19
- return null
20
- }
25
+ export function readVerifyEvidence(cwd, options = {}) {
26
+ return readRuntimeEvidence(cwd, VERIFY_EVIDENCE_FILE_NAME, options)
21
27
  }
22
28
 
23
- export function clearVerifyEvidence(cwd) {
24
- rmSync(getVerifyEvidencePath(cwd), { force: true })
29
+ export function clearVerifyEvidence(cwd, options = {}) {
30
+ clearRuntimeEvidence(cwd, VERIFY_EVIDENCE_FILE_NAME, options)
25
31
  }
26
32
 
27
33
  function loadVerifyYaml(cwd) {
@@ -89,34 +95,7 @@ export function hasUnsafeVerifyCommand(commands = []) {
89
95
  return commands.some((cmd) => SHELL_OPERATORS.test(cmd))
90
96
  }
91
97
 
92
- function readGitDiffStat(cwd, args) {
93
- try {
94
- return execSync(`git diff --stat ${args}`.trim(), {
95
- cwd,
96
- encoding: 'utf-8',
97
- timeout: 10_000,
98
- stdio: ['pipe', 'pipe', 'pipe'],
99
- }).trim()
100
- } catch {
101
- return null
102
- }
103
- }
104
-
105
- export function captureWorkspaceFingerprint(cwd) {
106
- const unstaged = readGitDiffStat(cwd, 'HEAD')
107
- const staged = readGitDiffStat(cwd, '--cached')
108
- const available = unstaged !== null || staged !== null
109
-
110
- return {
111
- available,
112
- unstaged: unstaged || '',
113
- staged: staged || '',
114
- combined: `${unstaged || ''}\n---\n${staged || ''}`.trim(),
115
- }
116
- }
117
-
118
- export function writeVerifyEvidence(cwd, { commands = [], fastOnly = false, source = 'ralph-loop' } = {}) {
119
- mkdirSync(join(cwd, '.helloagents'), { recursive: true })
98
+ export function writeVerifyEvidence(cwd, { commands = [], fastOnly = false, source = 'ralph-loop' } = {}, options = {}) {
120
99
  const payload = {
121
100
  updatedAt: new Date().toISOString(),
122
101
  commands,
@@ -124,15 +103,16 @@ export function writeVerifyEvidence(cwd, { commands = [], fastOnly = false, sour
124
103
  source,
125
104
  fingerprint: captureWorkspaceFingerprint(cwd),
126
105
  }
127
- writeFileSync(getVerifyEvidencePath(cwd), `${JSON.stringify(payload, null, 2)}\n`, 'utf-8')
106
+ writeRuntimeEvidence(cwd, VERIFY_EVIDENCE_FILE_NAME, payload, options)
128
107
  appendReplayEvent(cwd, {
129
108
  event: 'verify_evidence_written',
130
109
  source,
110
+ payload: options.payload || {},
131
111
  details: {
132
112
  commands,
133
113
  fastOnly,
134
114
  },
135
- artifacts: ['.helloagents/.ralph-verify.json'],
115
+ artifacts: [getRuntimeEvidenceRelativePath(cwd, VERIFY_EVIDENCE_FILE_NAME, options)],
136
116
  })
137
117
  }
138
118
 
@@ -142,7 +122,7 @@ function validateVerifyEvidencePresence(commands, evidence) {
142
122
  required: true,
143
123
  status: 'missing',
144
124
  commands,
145
- details: ['missing successful verification evidence for the current workflow'],
125
+ details: ['缺少当前工作流的成功验证证据'],
146
126
  }
147
127
  }
148
128
 
@@ -153,51 +133,20 @@ function validateVerifyEvidenceFreshness(cwd, commands, evidence, now) {
153
133
  status: 'fast-only',
154
134
  commands,
155
135
  evidence,
156
- details: ['latest verification evidence only covers subagent fast checks'],
136
+ details: ['最新验证证据只覆盖子代理快速检查'],
157
137
  }
158
138
  }
159
139
 
160
- const updatedAt = Date.parse(evidence.updatedAt || '')
161
- if (!Number.isFinite(updatedAt)) {
162
- return {
163
- required: true,
164
- status: 'invalid',
165
- commands,
166
- evidence,
167
- details: ['verification evidence timestamp is invalid'],
168
- }
169
- }
170
- if (now - updatedAt > VERIFY_EVIDENCE_MAX_AGE_MS) {
171
- return {
172
- required: true,
173
- status: 'stale-time',
174
- commands,
175
- evidence,
176
- details: ['verification evidence is older than 30 minutes'],
177
- }
178
- }
179
- return null
140
+ const timestampError = validateEvidenceTimestamp(evidence, now, '验证证据')
141
+ return timestampError ? { ...timestampError, commands } : null
180
142
  }
181
143
 
182
144
  function validateVerifyFingerprint(cwd, commands, evidence) {
183
- const currentFingerprint = captureWorkspaceFingerprint(cwd)
184
- if (
185
- currentFingerprint.available
186
- && evidence.fingerprint?.available
187
- && currentFingerprint.combined !== evidence.fingerprint.combined
188
- ) {
189
- return {
190
- required: true,
191
- status: 'stale-diff',
192
- commands,
193
- evidence,
194
- details: ['workspace diff changed after the last successful verification evidence'],
195
- }
196
- }
197
- return null
145
+ const fingerprintError = validateEvidenceFingerprint(cwd, evidence, '成功验证证据')
146
+ return fingerprintError ? { ...fingerprintError, commands } : null
198
147
  }
199
148
 
200
- export function getVerifyEvidenceStatus(cwd, now = Date.now()) {
149
+ export function getVerifyEvidenceStatus(cwd, { now = Date.now(), ...options } = {}) {
201
150
  const commands = detectCommands(cwd)
202
151
  if (!commands.length) {
203
152
  return {
@@ -207,7 +156,7 @@ export function getVerifyEvidenceStatus(cwd, now = Date.now()) {
207
156
  }
208
157
  }
209
158
 
210
- const evidence = readVerifyEvidence(cwd)
159
+ const evidence = readVerifyEvidence(cwd, options)
211
160
  const missingError = validateVerifyEvidencePresence(commands, evidence)
212
161
  if (missingError) return missingError
213
162