helloagents 3.0.12 → 3.0.15-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 (72) 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 +169 -30
  5. package/README_CN.md +169 -30
  6. package/bootstrap-lite.md +27 -20
  7. package/bootstrap.md +30 -23
  8. package/cli.mjs +119 -11
  9. package/gemini-extension.json +1 -1
  10. package/install.ps1 +125 -0
  11. package/install.sh +118 -0
  12. package/package.json +23 -4
  13. package/scripts/advisor-state.mjs +36 -63
  14. package/scripts/capability-registry.mjs +3 -3
  15. package/scripts/cli-branch.mjs +84 -0
  16. package/scripts/cli-codex-config.mjs +11 -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 +92 -27
  23. package/scripts/cli-lifecycle.mjs +9 -7
  24. package/scripts/cli-messages.mjs +34 -16
  25. package/scripts/cli-runtime-carrier.mjs +36 -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-gates.mjs +2 -0
  36. package/scripts/notify-route.mjs +9 -7
  37. package/scripts/notify-ui.mjs +46 -33
  38. package/scripts/notify.mjs +60 -32
  39. package/scripts/project-storage.mjs +35 -66
  40. package/scripts/ralph-loop.mjs +36 -31
  41. package/scripts/replay-state.mjs +31 -128
  42. package/scripts/review-state.mjs +34 -61
  43. package/scripts/runtime-artifacts.mjs +95 -0
  44. package/scripts/runtime-context.mjs +35 -29
  45. package/scripts/runtime-scope.mjs +313 -0
  46. package/scripts/session-capsule.mjs +202 -0
  47. package/scripts/turn-state-cli.mjs +17 -0
  48. package/scripts/turn-state.mjs +185 -66
  49. package/scripts/turn-stop-gate.mjs +24 -6
  50. package/scripts/verify-state.mjs +34 -85
  51. package/scripts/visual-state.mjs +38 -65
  52. package/scripts/workflow-core.mjs +2 -2
  53. package/scripts/workflow-plan-files.mjs +1 -1
  54. package/scripts/workflow-recommendation.mjs +17 -13
  55. package/scripts/workflow-state.mjs +5 -5
  56. package/skills/commands/build/SKILL.md +1 -1
  57. package/skills/commands/commit/SKILL.md +1 -1
  58. package/skills/commands/help/SKILL.md +3 -3
  59. package/skills/commands/loop/SKILL.md +1 -1
  60. package/skills/commands/plan/SKILL.md +8 -6
  61. package/skills/commands/prd/SKILL.md +5 -3
  62. package/skills/commands/verify/SKILL.md +5 -5
  63. package/skills/hello-debug/SKILL.md +20 -3
  64. package/skills/hello-review/SKILL.md +2 -2
  65. package/skills/hello-subagent/SKILL.md +2 -2
  66. package/skills/hello-test/SKILL.md +6 -2
  67. package/skills/hello-ui/SKILL.md +4 -4
  68. package/skills/hello-verify/SKILL.md +10 -7
  69. package/skills/helloagents/SKILL.md +12 -7
  70. package/templates/context.md +6 -0
  71. package/templates/plans/plan.md +3 -0
  72. package/templates/plans/tasks.md +8 -3
@@ -0,0 +1,36 @@
1
+ import { join } from 'node:path'
2
+
3
+ import { DEFAULTS } from './cli-config.mjs'
4
+ import { safeJson } from './cli-utils.mjs'
5
+
6
+ const CARRIER_SETTING_KEYS = [
7
+ 'output_language',
8
+ 'output_format',
9
+ 'notify_level',
10
+ 'ralph_loop_enabled',
11
+ 'guard_enabled',
12
+ 'kb_create_mode',
13
+ 'project_store_mode',
14
+ 'commit_attribution',
15
+ ]
16
+
17
+ function pickCarrierSettings(settings) {
18
+ const merged = { ...DEFAULTS, ...(settings || {}) }
19
+ return Object.fromEntries(CARRIER_SETTING_KEYS.map((key) => [key, merged[key]]))
20
+ }
21
+
22
+ export function readCarrierSettings(home) {
23
+ return pickCarrierSettings(safeJson(join(home, '.helloagents', 'helloagents.json')) || {})
24
+ }
25
+
26
+ export function buildRuntimeCarrier(bootstrapContent, settings = {}) {
27
+ const normalized = String(bootstrapContent || '').trim()
28
+ if (!normalized) return ''
29
+
30
+ const carrierSettings = pickCarrierSettings(settings)
31
+ const snapshot = Object.keys(carrierSettings).length
32
+ ? `\n\n## 当前用户设置\n\`\`\`json\n${JSON.stringify(carrierSettings, null, 2)}\n\`\`\``
33
+ : ''
34
+
35
+ return `${normalized}${snapshot}\n`
36
+ }
@@ -0,0 +1,72 @@
1
+ import { mkdtempSync, realpathSync, renameSync } from 'node:fs'
2
+ import { dirname, join, resolve } from 'node:path'
3
+
4
+ import { copyEntries, ensureDir, removeIfExists } from './cli-utils.mjs'
5
+
6
+ export const RUNTIME_ROOT_ENTRIES = [
7
+ '.claude-plugin',
8
+ '.codex-plugin',
9
+ 'assets',
10
+ 'bootstrap-lite.md',
11
+ 'bootstrap.md',
12
+ 'cli.mjs',
13
+ 'gemini-extension.json',
14
+ 'hooks',
15
+ 'install.ps1',
16
+ 'install.sh',
17
+ 'LICENSE.md',
18
+ 'package.json',
19
+ 'README.md',
20
+ 'README_CN.md',
21
+ 'scripts',
22
+ 'skills',
23
+ 'templates',
24
+ ]
25
+
26
+ /** Return the stable per-user runtime copy used by host integrations. */
27
+ export function getStableRuntimeRoot(home) {
28
+ return join(home, '.helloagents', 'helloagents')
29
+ }
30
+
31
+ function normalizePath(path) {
32
+ const resolved = resolve(path)
33
+ try {
34
+ return realpathSync(resolved)
35
+ } catch {
36
+ return resolved
37
+ }
38
+ }
39
+
40
+ function samePath(left, right) {
41
+ const a = normalizePath(left)
42
+ const b = normalizePath(right)
43
+ return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b
44
+ }
45
+
46
+ /** Sync package runtime files into the stable root without copying repo-only files. */
47
+ export function syncRuntimeRoot(sourceRoot, runtimeRoot) {
48
+ const source = resolve(sourceRoot)
49
+ const target = resolve(runtimeRoot)
50
+ if (samePath(source, target)) {
51
+ return { synced: false, root: target }
52
+ }
53
+
54
+ const parent = dirname(target)
55
+ ensureDir(parent)
56
+ const staging = mkdtempSync(join(parent, '.helloagents-runtime-'))
57
+
58
+ try {
59
+ copyEntries(source, staging, RUNTIME_ROOT_ENTRIES)
60
+ removeIfExists(target)
61
+ renameSync(staging, target)
62
+ return { synced: true, root: target }
63
+ } catch (error) {
64
+ removeIfExists(staging)
65
+ throw error
66
+ }
67
+ }
68
+
69
+ /** Remove the stable runtime copy while leaving user settings under ~/.helloagents intact. */
70
+ export function removeRuntimeRoot(runtimeRoot) {
71
+ removeIfExists(runtimeRoot)
72
+ }
@@ -164,85 +164,6 @@ export function ensureTopLevelTomlLine(text, key, line) {
164
164
  return upsertTopLevelTomlKey(text, key, value);
165
165
  }
166
166
 
167
- export function readTomlKeyInSection(text, headerLine, key) {
168
- const lines = String(text || '').replace(/\r\n/g, '\n').split('\n');
169
- const headerIndex = lines.findIndex((line) => line.trim() === headerLine);
170
- if (headerIndex < 0) return '';
171
-
172
- const keyRe = new RegExp(`^\\s*${key}\\s*=.*$`);
173
- for (let index = headerIndex + 1; index < lines.length; index += 1) {
174
- const line = lines[index];
175
- if (isTomlTableHeader(line)) break;
176
- if (keyRe.test(line)) return line.trim();
177
- }
178
- return '';
179
- }
180
-
181
- export function removeTomlKeyInSection(text, headerLine, key) {
182
- const lines = String(text || '').replace(/\r\n/g, '\n').split('\n');
183
- const headerIndex = lines.findIndex((line) => line.trim() === headerLine);
184
- if (headerIndex < 0) return normalizeToml(text);
185
-
186
- const keyRe = new RegExp(`^\\s*${key}\\s*=`);
187
- const nextLines = [];
188
- let removed = false;
189
- for (let index = 0; index < lines.length; index += 1) {
190
- const line = lines[index];
191
- if (index > headerIndex && isTomlTableHeader(line)) {
192
- nextLines.push(...lines.slice(index));
193
- break;
194
- }
195
- if (index > headerIndex && keyRe.test(line)) {
196
- removed = true;
197
- continue;
198
- }
199
- nextLines.push(line);
200
- }
201
-
202
- if (!removed) return normalizeToml(text);
203
- return normalizeToml(nextLines.join('\n'));
204
- }
205
-
206
- export function upsertTomlKeyInSection(text, headerLine, key, value) {
207
- const lines = String(text || '').replace(/\r\n/g, '\n').split('\n');
208
- const headerIndex = lines.findIndex((line) => line.trim() === headerLine);
209
-
210
- if (headerIndex < 0) {
211
- const base = normalizeToml(text).trimEnd();
212
- return base
213
- ? `${base}\n\n${headerLine}\n${key} = ${value}\n`
214
- : `${headerLine}\n${key} = ${value}\n`;
215
- }
216
-
217
- let endIndex = headerIndex + 1;
218
- while (endIndex < lines.length && !isTomlTableHeader(lines[endIndex])) {
219
- endIndex += 1;
220
- }
221
-
222
- const keyRe = new RegExp(`^\\s*${key}\\s*=`);
223
- let updated = false;
224
- for (let index = headerIndex + 1; index < endIndex; index += 1) {
225
- if (keyRe.test(lines[index])) {
226
- lines[index] = `${key} = ${value}`;
227
- updated = true;
228
- break;
229
- }
230
- }
231
-
232
- if (!updated) {
233
- lines.splice(endIndex, 0, `${key} = ${value}`);
234
- }
235
-
236
- return normalizeToml(lines.join('\n'));
237
- }
238
-
239
- export function ensureTomlKeyInSection(text, headerLine, key, line) {
240
- const normalized = String(line || '').trim();
241
- if (!normalized) return normalizeToml(text);
242
- const value = normalized.slice(normalized.indexOf('=') + 1).trim();
243
- return upsertTomlKeyInSection(text, headerLine, key, value);
244
- }
245
-
246
167
  export function stripTomlSection(text, headerLine) {
247
168
  const lines = String(text || '').replace(/\r\n/g, '\n').split('\n');
248
169
  const kept = [];
@@ -130,10 +130,36 @@ export function cleanSettingsHooks(settingsPath, cleanPermissions = false) {
130
130
  }
131
131
  }
132
132
 
133
- /** Read hooks source file and replace path variable with absolute PKG_ROOT. */
134
- export function loadHooksWithAbsPath(pkgRoot, hooksFile, pathVar) {
133
+ function rewriteHookCommandToCli(command = '', pathVar = '') {
134
+ const replacements = new Map([
135
+ [`node "${pathVar}/scripts/notify.mjs"`, 'helloagents-js notify'],
136
+ [`node "${pathVar}/scripts/guard.mjs"`, 'helloagents-js guard'],
137
+ [`node "${pathVar}/scripts/ralph-loop.mjs"`, 'helloagents-js ralph-loop'],
138
+ ]);
139
+
140
+ let next = command;
141
+ for (const [from, to] of replacements) {
142
+ next = next.replaceAll(from, to);
143
+ }
144
+ return next;
145
+ }
146
+
147
+ function rewriteHookCommands(value, pathVar) {
148
+ if (Array.isArray(value)) return value.map((item) => rewriteHookCommands(item, pathVar));
149
+ if (value && typeof value === 'object') {
150
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
151
+ key,
152
+ key === 'command' && typeof entry === 'string'
153
+ ? rewriteHookCommandToCli(entry, pathVar)
154
+ : rewriteHookCommands(entry, pathVar),
155
+ ]));
156
+ }
157
+ return value;
158
+ }
159
+
160
+ /** Read hooks source file and rewrite standby hooks to the stable CLI entrypoint. */
161
+ export function loadHooksWithCliEntry(pkgRoot, hooksFile, pathVar) {
135
162
  const src = safeRead(join(pkgRoot, 'hooks', hooksFile));
136
163
  if (!src) return null;
137
- const absRoot = pkgRoot.replace(/\\/g, '/');
138
- return JSON.parse(src.replace(new RegExp(pathVar.replace(/[{}$]/g, '\\$&'), 'g'), absRoot));
164
+ return rewriteHookCommands(JSON.parse(src), pathVar);
139
165
  }
@@ -1,11 +1,18 @@
1
- import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
1
+ import { readFileSync } from 'node:fs'
2
2
  import { fileURLToPath } from 'node:url'
3
- import { join } from 'node:path'
4
- import { captureWorkspaceFingerprint } from './verify-state.mjs'
5
3
  import { appendReplayEvent } from './replay-state.mjs'
6
-
7
- export const CLOSEOUT_EVIDENCE_FILE_NAME = '.ralph-closeout.json'
8
- const CLOSEOUT_EVIDENCE_MAX_AGE_MS = 30 * 60 * 1000
4
+ import {
5
+ captureWorkspaceFingerprint,
6
+ clearRuntimeEvidence,
7
+ getRuntimeEvidencePath,
8
+ getRuntimeEvidenceRelativePath,
9
+ readRuntimeEvidence,
10
+ validateEvidenceFingerprint,
11
+ validateEvidenceTimestamp,
12
+ writeRuntimeEvidence,
13
+ } from './runtime-artifacts.mjs'
14
+
15
+ export const CLOSEOUT_EVIDENCE_FILE_NAME = 'closeout.json'
9
16
  const ALLOWED_STATUSES = new Set(['PASS', 'BLOCKED'])
10
17
 
11
18
  function normalizeEntry(entry = {}) {
@@ -15,20 +22,16 @@ function normalizeEntry(entry = {}) {
15
22
  }
16
23
  }
17
24
 
18
- export function getCloseoutEvidencePath(cwd) {
19
- return join(cwd, '.helloagents', CLOSEOUT_EVIDENCE_FILE_NAME)
25
+ export function getCloseoutEvidencePath(cwd, options = {}) {
26
+ return getRuntimeEvidencePath(cwd, CLOSEOUT_EVIDENCE_FILE_NAME, options)
20
27
  }
21
28
 
22
- export function readCloseoutEvidence(cwd) {
23
- try {
24
- return JSON.parse(readFileSync(getCloseoutEvidencePath(cwd), 'utf-8'))
25
- } catch {
26
- return null
27
- }
29
+ export function readCloseoutEvidence(cwd, options = {}) {
30
+ return readRuntimeEvidence(cwd, CLOSEOUT_EVIDENCE_FILE_NAME, options)
28
31
  }
29
32
 
30
- export function clearCloseoutEvidence(cwd) {
31
- rmSync(getCloseoutEvidencePath(cwd), { force: true })
33
+ export function clearCloseoutEvidence(cwd, options = {}) {
34
+ clearRuntimeEvidence(cwd, CLOSEOUT_EVIDENCE_FILE_NAME, options)
32
35
  }
33
36
 
34
37
  export function normalizeCloseoutEvidence(input = {}) {
@@ -40,8 +43,7 @@ export function normalizeCloseoutEvidence(input = {}) {
40
43
  }
41
44
  }
42
45
 
43
- export function writeCloseoutEvidence(cwd, input = {}) {
44
- mkdirSync(join(cwd, '.helloagents'), { recursive: true })
46
+ export function writeCloseoutEvidence(cwd, input = {}, options = {}) {
45
47
  const normalized = normalizeCloseoutEvidence(input)
46
48
  const payload = {
47
49
  updatedAt: new Date().toISOString(),
@@ -51,51 +53,35 @@ export function writeCloseoutEvidence(cwd, input = {}) {
51
53
  deliveryChecklist: normalized.deliveryChecklist,
52
54
  fingerprint: captureWorkspaceFingerprint(cwd),
53
55
  }
54
- writeFileSync(getCloseoutEvidencePath(cwd), `${JSON.stringify(payload, null, 2)}\n`, 'utf-8')
56
+ writeRuntimeEvidence(cwd, CLOSEOUT_EVIDENCE_FILE_NAME, payload, options)
55
57
  appendReplayEvent(cwd, {
56
58
  event: 'closeout_evidence_written',
57
59
  source: normalized.source || 'manual',
58
60
  skillName: normalized.originCommand,
61
+ payload: options.payload || {},
59
62
  details: {
60
63
  requirementsCoverage: normalized.requirementsCoverage,
61
64
  deliveryChecklist: normalized.deliveryChecklist,
62
65
  },
63
- artifacts: ['.helloagents/.ralph-closeout.json'],
66
+ artifacts: [getRuntimeEvidenceRelativePath(cwd, CLOSEOUT_EVIDENCE_FILE_NAME, options)],
64
67
  })
65
68
  return payload
66
69
  }
67
70
 
68
- function readRequiredCloseoutEvidence(cwd) {
69
- const evidence = readCloseoutEvidence(cwd)
71
+ function readRequiredCloseoutEvidence(cwd, options = {}) {
72
+ const evidence = readCloseoutEvidence(cwd, options)
70
73
  if (evidence) return { evidence }
71
74
  return {
72
75
  error: {
73
76
  required: true,
74
77
  status: 'missing',
75
- details: ['missing closeout evidence for requirements coverage and delivery checklist'],
78
+ details: ['缺少需求覆盖和交付清单的收尾证据'],
76
79
  },
77
80
  }
78
81
  }
79
82
 
80
83
  function validateCloseoutTimestamp(evidence, now) {
81
- const updatedAt = Date.parse(evidence.updatedAt || '')
82
- if (!Number.isFinite(updatedAt)) {
83
- return {
84
- required: true,
85
- status: 'invalid',
86
- evidence,
87
- details: ['closeout evidence timestamp is invalid'],
88
- }
89
- }
90
- if (now - updatedAt > CLOSEOUT_EVIDENCE_MAX_AGE_MS) {
91
- return {
92
- required: true,
93
- status: 'stale-time',
94
- evidence,
95
- details: ['closeout evidence is older than 30 minutes'],
96
- }
97
- }
98
- return null
84
+ return validateEvidenceTimestamp(evidence, now, '收尾证据')
99
85
  }
100
86
 
101
87
  function validateCloseoutEntries(evidence) {
@@ -112,7 +98,7 @@ function validateCloseoutEntries(evidence) {
112
98
  required: true,
113
99
  status: 'invalid',
114
100
  evidence,
115
- details: ['closeout evidence must record requirements coverage and delivery checklist with explicit PASS/BLOCKED status plus summary'],
101
+ details: ['收尾证据必须记录需求覆盖和交付清单,并包含明确的 PASS/BLOCKED 状态和 summary'],
116
102
  }
117
103
  }
118
104
  if (requirementsCoverage.status !== 'PASS') {
@@ -120,7 +106,7 @@ function validateCloseoutEntries(evidence) {
120
106
  required: true,
121
107
  status: 'blocked',
122
108
  evidence,
123
- details: ['requirements coverage is not marked as PASS in the latest closeout evidence'],
109
+ details: ['最新收尾证据中的需求覆盖未标记为 PASS'],
124
110
  }
125
111
  }
126
112
  if (deliveryChecklist.status !== 'PASS') {
@@ -128,7 +114,7 @@ function validateCloseoutEntries(evidence) {
128
114
  required: true,
129
115
  status: 'blocked',
130
116
  evidence,
131
- details: ['delivery checklist is not marked as PASS in the latest closeout evidence'],
117
+ details: ['最新收尾证据中的交付清单未标记为 PASS'],
132
118
  }
133
119
  }
134
120
  return {
@@ -138,23 +124,10 @@ function validateCloseoutEntries(evidence) {
138
124
  }
139
125
 
140
126
  function validateCloseoutFingerprint(cwd, evidence) {
141
- const currentFingerprint = captureWorkspaceFingerprint(cwd)
142
- if (
143
- currentFingerprint.available
144
- && evidence.fingerprint?.available
145
- && currentFingerprint.combined !== evidence.fingerprint.combined
146
- ) {
147
- return {
148
- required: true,
149
- status: 'stale-diff',
150
- evidence,
151
- details: ['workspace diff changed after the last successful closeout evidence'],
152
- }
153
- }
154
- return null
127
+ return validateEvidenceFingerprint(cwd, evidence, '成功收尾证据')
155
128
  }
156
129
 
157
- export function getCloseoutEvidenceStatus(cwd, { required = false, now = Date.now() } = {}) {
130
+ export function getCloseoutEvidenceStatus(cwd, { required = false, now = Date.now(), ...options } = {}) {
158
131
  if (!required) {
159
132
  return {
160
133
  required: false,
@@ -162,7 +135,7 @@ export function getCloseoutEvidenceStatus(cwd, { required = false, now = Date.no
162
135
  }
163
136
  }
164
137
 
165
- const requiredEvidence = readRequiredCloseoutEvidence(cwd)
138
+ const requiredEvidence = readRequiredCloseoutEvidence(cwd, options)
166
139
  if (requiredEvidence.error) return requiredEvidence.error
167
140
 
168
141
  const { evidence } = requiredEvidence
@@ -200,10 +173,10 @@ function main() {
200
173
 
201
174
  const input = readStdinJson()
202
175
  const cwd = input.cwd || process.cwd()
203
- const payload = writeCloseoutEvidence(cwd, input)
176
+ const payload = writeCloseoutEvidence(cwd, input, { payload: input })
204
177
  process.stdout.write(JSON.stringify({
205
178
  suppressOutput: true,
206
- path: getCloseoutEvidencePath(cwd),
179
+ path: getCloseoutEvidencePath(cwd, { payload: input }),
207
180
  payload,
208
181
  }))
209
182
  }
@@ -0,0 +1,70 @@
1
+ export function buildUnderSpecifiedDetails(entry) {
2
+ return entry.taskSummary.underSpecifiedItems
3
+ .slice(0, 3)
4
+ .map((item) => {
5
+ const missing = []
6
+ if (item.files.length === 0) missing.push('缺少涉及文件')
7
+ if (!item.acceptance) missing.push('缺少完成标准')
8
+ if (!item.validation) missing.push('缺少验证方式')
9
+ return `${item.text}(${missing.join('、')})`
10
+ })
11
+ }
12
+
13
+ function issueHeading(issue) {
14
+ switch (issue.type) {
15
+ case 'missing-files':
16
+ return '方案包缺少必需文件'
17
+ case 'template-placeholders':
18
+ return '方案包仍包含模板占位内容'
19
+ case 'missing-task-checklist':
20
+ return '方案包没有可执行任务'
21
+ case 'unfinished-tasks':
22
+ return '方案包仍有未完成任务'
23
+ case 'under-specified-tasks':
24
+ return '任务缺少可交付元数据'
25
+ case 'missing-contract':
26
+ return '方案包缺少可信的结构化契约'
27
+ case 'missing-verify-evidence':
28
+ return '当前工作流缺少最新验证证据'
29
+ case 'missing-review-evidence':
30
+ return '当前工作流缺少最新审查证据'
31
+ case 'missing-advisor-evidence':
32
+ return '当前工作流缺少最新 advisor 证据'
33
+ case 'missing-visual-evidence':
34
+ return '当前工作流缺少最新视觉验收证据'
35
+ case 'missing-closeout-evidence':
36
+ return '当前工作流缺少最新收尾证据'
37
+ default:
38
+ return '方案包尚未达到交付条件'
39
+ }
40
+ }
41
+
42
+ export function buildDeliveryBlockReason(issues, recommendation, gateHint) {
43
+ const lines = ['[Delivery Gate] 当前工作流尚未闭合,暂不能交付:']
44
+
45
+ for (const issue of issues) {
46
+ lines.push(`- ${issue.planName}: ${issueHeading(issue)}`)
47
+ for (const detail of issue.details) {
48
+ lines.push(` - ${detail}`)
49
+ }
50
+ if (issue.extraCount) {
51
+ lines.push(` - 另有 ${issue.extraCount} 项`)
52
+ }
53
+ }
54
+
55
+ lines.push('')
56
+ if (recommendation?.nextPath) {
57
+ lines.push(`建议路径:${recommendation.nextPath}`)
58
+ }
59
+ if (issues.some((issue) => issue.type === 'missing-closeout-evidence')) {
60
+ lines.push('下一步收尾:先写入当前会话 `artifacts/closeout.json`,记录 `requirementsCoverage` 和 `deliveryChecklist`,再报告完成。')
61
+ }
62
+ if (issues.some((issue) => issue.type === 'missing-visual-evidence')) {
63
+ lines.push('下一步视觉验收:先写入当前会话 `artifacts/visual.json`,记录 `tooling`、`screensChecked`、`statesChecked`、`status` 和 `summary`,再报告完成。')
64
+ }
65
+ if (gateHint) {
66
+ lines.push(gateHint)
67
+ }
68
+ lines.push('暂不要报告完成。先完成剩余任务、明确关闭任务,或修复方案包,使其成为可信的交付记录。')
69
+ return lines.join('\n')
70
+ }
@@ -12,24 +12,13 @@ import { getVisualEvidenceStatus } from './visual-state.mjs'
12
12
  import { buildDeliveryGateHint, getDeliveryAction, getWorkflowRecommendation, getWorkflowSnapshot } from './workflow-state.mjs'
13
13
  import { getReviewEvidenceStatus } from './review-state.mjs'
14
14
  import { getVerifyEvidenceStatus } from './verify-state.mjs'
15
+ import { buildDeliveryBlockReason, buildUnderSpecifiedDetails } from './delivery-gate-messages.mjs'
15
16
 
16
17
  function selectGatePlans(snapshot) {
17
18
  if (snapshot.activePlans.length > 0) return snapshot.activePlans
18
19
  return snapshot.plans
19
20
  }
20
21
 
21
- function buildUnderSpecifiedDetails(entry) {
22
- return entry.taskSummary.underSpecifiedItems
23
- .slice(0, 3)
24
- .map((item) => {
25
- const missing = []
26
- if (item.files.length === 0) missing.push('missing files')
27
- if (!item.acceptance) missing.push('missing acceptance')
28
- if (!item.validation) missing.push('missing validation')
29
- return `${item.text} (${missing.join(', ')})`
30
- })
31
- }
32
-
33
22
  function collectTaskMetadataIssues(entry, issues) {
34
23
  if (entry.taskSummary.underSpecifiedCount === 0) return
35
24
  issues.push({
@@ -48,7 +37,7 @@ function collectPlanIssues(planEntries) {
48
37
  issues.push({
49
38
  type: 'missing-files',
50
39
  planName: entry.planName,
51
- details: entry.missingFiles.map((file) => `missing ${file}`),
40
+ details: entry.missingFiles.map((file) => `缺少 ${file}`),
52
41
  })
53
42
  }
54
43
 
@@ -64,7 +53,7 @@ function collectPlanIssues(planEntries) {
64
53
  issues.push({
65
54
  type: 'missing-task-checklist',
66
55
  planName: entry.planName,
67
- details: ['tasks.md does not contain any checklist items'],
56
+ details: ['tasks.md 没有可执行检查项'],
68
57
  })
69
58
  continue
70
59
  }
@@ -140,65 +129,6 @@ function collectGateIssues(planEntries, verificationStatus, reviewStatus, adviso
140
129
  return issues
141
130
  }
142
131
 
143
- function issueHeading(issue) {
144
- switch (issue.type) {
145
- case 'missing-files':
146
- return 'active plan package is missing required artifacts'
147
- case 'template-placeholders':
148
- return 'active plan package still contains template placeholders'
149
- case 'missing-task-checklist':
150
- return 'active plan package has no executable tasks'
151
- case 'unfinished-tasks':
152
- return 'active plan package still has unfinished tasks'
153
- case 'under-specified-tasks':
154
- return 'active plan package has under-specified task metadata'
155
- case 'missing-contract':
156
- return 'active plan package is missing a trustworthy structured contract'
157
- case 'missing-verify-evidence':
158
- return 'current workflow is missing fresh verification evidence'
159
- case 'missing-review-evidence':
160
- return 'current workflow is missing fresh review evidence'
161
- case 'missing-advisor-evidence':
162
- return 'current workflow is missing fresh advisor evidence'
163
- case 'missing-visual-evidence':
164
- return 'current workflow is missing fresh visual validation evidence'
165
- case 'missing-closeout-evidence':
166
- return 'current workflow is missing fresh closeout evidence'
167
- default:
168
- return 'active plan package is not ready for delivery'
169
- }
170
- }
171
-
172
- function buildBlockReason(issues, recommendation, gateHint) {
173
- const lines = ['[Delivery Gate] Delivery is blocked because the current workflow state is not closed yet:']
174
-
175
- for (const issue of issues) {
176
- lines.push(`- ${issue.planName}: ${issueHeading(issue)}`)
177
- for (const detail of issue.details) {
178
- lines.push(` - ${detail}`)
179
- }
180
- if (issue.extraCount) {
181
- lines.push(` - ...and ${issue.extraCount} more`)
182
- }
183
- }
184
-
185
- lines.push('')
186
- if (recommendation?.nextPath) {
187
- lines.push(`Recommended path: ${recommendation.nextPath}`)
188
- }
189
- if (issues.some((issue) => issue.type === 'missing-closeout-evidence')) {
190
- lines.push('Next closeout step: write `.helloagents/.ralph-closeout.json` with `requirementsCoverage` and `deliveryChecklist` before reporting completion.')
191
- }
192
- if (issues.some((issue) => issue.type === 'missing-visual-evidence')) {
193
- lines.push('Next visual step: write `.helloagents/.ralph-visual.json` with `tooling`, `screensChecked`, `statesChecked`, `status`, and `summary` before reporting completion.')
194
- }
195
- if (gateHint) {
196
- lines.push(gateHint)
197
- }
198
- lines.push('Do not report completion yet. First finish or explicitly close the remaining tasks, or repair the active plan package so it becomes a trustworthy delivery record.')
199
- return lines.join('\n')
200
- }
201
-
202
132
  function main() {
203
133
  let data = {}
204
134
  try {
@@ -208,11 +138,12 @@ function main() {
208
138
  const workflowOptions = { payload: data }
209
139
  const snapshot = getWorkflowSnapshot(cwd, workflowOptions)
210
140
  const recommendation = getWorkflowRecommendation(cwd, workflowOptions)
211
- const verificationStatus = getVerifyEvidenceStatus(cwd)
141
+ const verificationStatus = getVerifyEvidenceStatus(cwd, workflowOptions)
212
142
  const deliveryAction = getDeliveryAction(cwd, workflowOptions)
213
143
  const gatePlans = selectGatePlans(snapshot)
214
144
  const reviewStatus = getReviewEvidenceStatus(cwd, {
215
145
  required: deliveryAction?.phase === 'verify' && deliveryAction?.mode === 'review-first',
146
+ ...workflowOptions,
216
147
  })
217
148
  if (gatePlans.length === 0) {
218
149
  process.stdout.write(JSON.stringify({ suppressOutput: true }))
@@ -223,12 +154,14 @@ function main() {
223
154
  const advisorStatus = getAdvisorEvidenceStatus(cwd, {
224
155
  required: advisorRequirements.some((entry) => entry.required),
225
156
  focus: advisorRequirements.flatMap((entry) => entry.focus || []),
157
+ ...workflowOptions,
226
158
  })
227
159
  const visualRequirements = gatePlans.map((entry) => getVisualValidationRequirement(entry.contract))
228
160
  const visualStatus = getVisualEvidenceStatus(cwd, {
229
161
  required: visualRequirements.some((entry) => entry.required),
230
162
  screens: visualRequirements.flatMap((entry) => entry.screens || []),
231
163
  states: visualRequirements.flatMap((entry) => entry.states || []),
164
+ ...workflowOptions,
232
165
  })
233
166
  const closeoutRequired = (
234
167
  gatePlans.every((entry) => entry.missingFiles.length === 0 && entry.templateIssues.length === 0 && entry.taskSummary.total > 0 && entry.taskSummary.open === 0 && entry.taskSummary.underSpecifiedCount === 0)
@@ -239,6 +172,7 @@ function main() {
239
172
  )
240
173
  const closeoutStatus = getCloseoutEvidenceStatus(cwd, {
241
174
  required: closeoutRequired,
175
+ ...workflowOptions,
242
176
  })
243
177
 
244
178
  const issues = collectGateIssues(gatePlans, verificationStatus, reviewStatus, advisorStatus, visualStatus, closeoutStatus)
@@ -249,7 +183,7 @@ function main() {
249
183
 
250
184
  process.stdout.write(JSON.stringify({
251
185
  decision: 'block',
252
- reason: buildBlockReason(issues, recommendation, buildDeliveryGateHint(cwd, workflowOptions)),
186
+ reason: buildDeliveryBlockReason(issues, recommendation, buildDeliveryGateHint(cwd, workflowOptions)),
253
187
  suppressOutput: true,
254
188
  }))
255
189
  }