helloagents 3.0.3-beta.1 → 3.0.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 (72) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/README.md +134 -41
  5. package/README_CN.md +135 -42
  6. package/bootstrap-lite.md +104 -46
  7. package/bootstrap.md +143 -113
  8. package/cli.mjs +80 -427
  9. package/gemini-extension.json +1 -1
  10. package/hooks/hooks-claude.json +10 -0
  11. package/hooks/hooks.json +10 -0
  12. package/package.json +2 -12
  13. package/scripts/advisor-state.mjs +222 -0
  14. package/scripts/capability-registry.mjs +59 -0
  15. package/scripts/cli-codex-backup.mjs +59 -0
  16. package/scripts/cli-codex-config.mjs +100 -0
  17. package/scripts/cli-codex.mjs +34 -156
  18. package/scripts/cli-config.mjs +1 -0
  19. package/scripts/cli-doctor-render.mjs +28 -0
  20. package/scripts/cli-doctor.mjs +367 -0
  21. package/scripts/cli-host-detect.mjs +94 -0
  22. package/scripts/cli-lifecycle-hosts.mjs +123 -0
  23. package/scripts/cli-lifecycle.mjs +213 -0
  24. package/scripts/cli-messages.mjs +76 -52
  25. package/scripts/closeout-state.mjs +213 -0
  26. package/scripts/delivery-gate.mjs +256 -0
  27. package/scripts/guard-rules.mjs +122 -0
  28. package/scripts/guard.mjs +190 -168
  29. package/scripts/notify-context.mjs +77 -17
  30. package/scripts/notify-events.mjs +5 -1
  31. package/scripts/notify-route.mjs +111 -0
  32. package/scripts/notify-shared.mjs +0 -2
  33. package/scripts/notify-source.mjs +113 -0
  34. package/scripts/notify-ui.mjs +40 -6
  35. package/scripts/notify.mjs +120 -59
  36. package/scripts/plan-contract.mjs +210 -0
  37. package/scripts/project-storage.mjs +235 -0
  38. package/scripts/ralph-loop.mjs +9 -58
  39. package/scripts/replay-state.mjs +210 -0
  40. package/scripts/review-state.mjs +220 -0
  41. package/scripts/runtime-context.mjs +74 -0
  42. package/scripts/verify-state.mjs +226 -0
  43. package/scripts/visual-state.mjs +244 -0
  44. package/scripts/workflow-core.mjs +165 -0
  45. package/scripts/workflow-plan-files.mjs +249 -0
  46. package/scripts/workflow-recommendation.mjs +335 -0
  47. package/scripts/workflow-state.mjs +113 -0
  48. package/skills/commands/auto/SKILL.md +37 -71
  49. package/skills/commands/build/SKILL.md +67 -0
  50. package/skills/commands/clean/SKILL.md +10 -8
  51. package/skills/commands/commit/SKILL.md +8 -4
  52. package/skills/commands/help/SKILL.md +17 -10
  53. package/skills/commands/idea/SKILL.md +55 -0
  54. package/skills/commands/init/SKILL.md +6 -3
  55. package/skills/commands/loop/SKILL.md +6 -5
  56. package/skills/commands/plan/SKILL.md +116 -0
  57. package/skills/commands/prd/SKILL.md +20 -15
  58. package/skills/commands/verify/SKILL.md +32 -9
  59. package/skills/commands/wiki/SKILL.md +7 -5
  60. package/skills/hello-review/SKILL.md +9 -0
  61. package/skills/hello-subagent/SKILL.md +4 -3
  62. package/skills/hello-ui/SKILL.md +36 -8
  63. package/skills/hello-verify/SKILL.md +10 -2
  64. package/skills/helloagents/SKILL.md +24 -13
  65. package/templates/DESIGN.md +25 -4
  66. package/templates/STATE.md +3 -0
  67. package/templates/plans/contract.json +48 -0
  68. package/templates/plans/plan.md +23 -0
  69. package/templates/plans/tasks.md +3 -3
  70. package/skills/commands/design/SKILL.md +0 -108
  71. package/skills/commands/review/SKILL.md +0 -16
  72. package/templates/plans/design.md +0 -14
@@ -0,0 +1,226 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { execSync } from 'node:child_process'
3
+ import { join } from 'node:path'
4
+ 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
9
+ const SHELL_OPERATORS = /[;&|`$(){}\n\r]/
10
+
11
+ export function getVerifyEvidencePath(cwd) {
12
+ return join(cwd, '.helloagents', VERIFY_EVIDENCE_FILE_NAME)
13
+ }
14
+
15
+ export function readVerifyEvidence(cwd) {
16
+ try {
17
+ return JSON.parse(readFileSync(getVerifyEvidencePath(cwd), 'utf-8'))
18
+ } catch {
19
+ return null
20
+ }
21
+ }
22
+
23
+ export function clearVerifyEvidence(cwd) {
24
+ rmSync(getVerifyEvidencePath(cwd), { force: true })
25
+ }
26
+
27
+ function loadVerifyYaml(cwd) {
28
+ const f = getProjectVerifyYamlPath(cwd)
29
+ if (!existsSync(f)) return null
30
+ try {
31
+ const content = readFileSync(f, 'utf-8')
32
+ const cmds = []
33
+ let inCmds = false
34
+ for (const line of content.split('\n')) {
35
+ const s = line.trim()
36
+ if (s.startsWith('commands:')) { inCmds = true; continue }
37
+ if (inCmds) {
38
+ if (s.startsWith('- ') && !s.startsWith('# ')) {
39
+ const cmd = s.slice(2).trim().replace(/^["']|["']$/g, '')
40
+ if (cmd && !cmd.startsWith('#')) cmds.push(cmd)
41
+ } else if (s && !s.startsWith('#')) {
42
+ break
43
+ }
44
+ }
45
+ }
46
+ return cmds.length ? cmds : null
47
+ } catch {
48
+ return null
49
+ }
50
+ }
51
+
52
+ function detectFromPackageJson(cwd) {
53
+ const f = join(cwd, 'package.json')
54
+ if (!existsSync(f)) return []
55
+ try {
56
+ const scripts = JSON.parse(readFileSync(f, 'utf-8')).scripts || {}
57
+ return ['lint', 'typecheck', 'type-check', 'test', 'build']
58
+ .filter((k) => k in scripts)
59
+ .map((k) => `npm run ${k}`)
60
+ } catch {
61
+ return []
62
+ }
63
+ }
64
+
65
+ function detectFromPyproject(cwd) {
66
+ const f = join(cwd, 'pyproject.toml')
67
+ if (!existsSync(f)) return []
68
+ try {
69
+ const content = readFileSync(f, 'utf-8')
70
+ const cmds = []
71
+ if (content.includes('[tool.ruff')) cmds.push('ruff check .')
72
+ if (content.includes('[tool.mypy')) cmds.push('mypy .')
73
+ if (content.includes('[tool.pytest')) cmds.push('pytest --tb=short -q')
74
+ return cmds
75
+ } catch {
76
+ return []
77
+ }
78
+ }
79
+
80
+ export function detectCommands(cwd) {
81
+ const yaml = loadVerifyYaml(cwd)
82
+ if (yaml?.length) return yaml
83
+ const pkg = detectFromPackageJson(cwd)
84
+ if (pkg.length) return pkg
85
+ return detectFromPyproject(cwd)
86
+ }
87
+
88
+ export function hasUnsafeVerifyCommand(commands = []) {
89
+ return commands.some((cmd) => SHELL_OPERATORS.test(cmd))
90
+ }
91
+
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 })
120
+ const payload = {
121
+ updatedAt: new Date().toISOString(),
122
+ commands,
123
+ fastOnly,
124
+ source,
125
+ fingerprint: captureWorkspaceFingerprint(cwd),
126
+ }
127
+ writeFileSync(getVerifyEvidencePath(cwd), `${JSON.stringify(payload, null, 2)}\n`, 'utf-8')
128
+ appendReplayEvent(cwd, {
129
+ event: 'verify_evidence_written',
130
+ source,
131
+ details: {
132
+ commands,
133
+ fastOnly,
134
+ },
135
+ artifacts: ['.helloagents/.ralph-verify.json'],
136
+ })
137
+ }
138
+
139
+ function validateVerifyEvidencePresence(commands, evidence) {
140
+ if (evidence) return null
141
+ return {
142
+ required: true,
143
+ status: 'missing',
144
+ commands,
145
+ details: ['missing successful verification evidence for the current workflow'],
146
+ }
147
+ }
148
+
149
+ function validateVerifyEvidenceFreshness(cwd, commands, evidence, now) {
150
+ if (evidence.fastOnly) {
151
+ return {
152
+ required: true,
153
+ status: 'fast-only',
154
+ commands,
155
+ evidence,
156
+ details: ['latest verification evidence only covers subagent fast checks'],
157
+ }
158
+ }
159
+
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
180
+ }
181
+
182
+ 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
198
+ }
199
+
200
+ export function getVerifyEvidenceStatus(cwd, now = Date.now()) {
201
+ const commands = detectCommands(cwd)
202
+ if (!commands.length) {
203
+ return {
204
+ required: false,
205
+ status: 'not-applicable',
206
+ commands,
207
+ }
208
+ }
209
+
210
+ const evidence = readVerifyEvidence(cwd)
211
+ const missingError = validateVerifyEvidencePresence(commands, evidence)
212
+ if (missingError) return missingError
213
+
214
+ const freshnessError = validateVerifyEvidenceFreshness(cwd, commands, evidence, now)
215
+ if (freshnessError) return freshnessError
216
+
217
+ const fingerprintError = validateVerifyFingerprint(cwd, commands, evidence)
218
+ if (fingerprintError) return fingerprintError
219
+
220
+ return {
221
+ required: true,
222
+ status: 'valid',
223
+ commands,
224
+ evidence,
225
+ }
226
+ }
@@ -0,0 +1,244 @@
1
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ import { appendReplayEvent } from './replay-state.mjs'
6
+ import { captureWorkspaceFingerprint } from './verify-state.mjs'
7
+
8
+ export const VISUAL_EVIDENCE_FILE_NAME = '.ralph-visual.json'
9
+ const VISUAL_EVIDENCE_MAX_AGE_MS = 30 * 60 * 1000
10
+ const VALID_VISUAL_STATUSES = new Set(['PASS', 'BLOCKED'])
11
+
12
+ function normalizeStringArray(values) {
13
+ if (!Array.isArray(values)) return []
14
+ return [...new Set(values.map((value) => (typeof value === 'string' ? value.trim() : '')).filter(Boolean))]
15
+ }
16
+
17
+ function normalizeVisualStatus(value) {
18
+ const normalized = typeof value === 'string' ? value.trim().toUpperCase() : ''
19
+ return VALID_VISUAL_STATUSES.has(normalized) ? normalized : ''
20
+ }
21
+
22
+ function findMissingCoverage(requested = [], completed = []) {
23
+ const completedSet = new Set(normalizeStringArray(completed))
24
+ return normalizeStringArray(requested).filter((entry) => !completedSet.has(entry))
25
+ }
26
+
27
+ export function getVisualEvidencePath(cwd) {
28
+ return join(cwd, '.helloagents', VISUAL_EVIDENCE_FILE_NAME)
29
+ }
30
+
31
+ export function readVisualEvidence(cwd) {
32
+ try {
33
+ return JSON.parse(readFileSync(getVisualEvidencePath(cwd), 'utf-8'))
34
+ } catch {
35
+ return null
36
+ }
37
+ }
38
+
39
+ export function clearVisualEvidence(cwd) {
40
+ rmSync(getVisualEvidencePath(cwd), { force: true })
41
+ }
42
+
43
+ export function normalizeVisualEvidence(input = {}) {
44
+ return {
45
+ source: typeof input.source === 'string' && input.source.trim() ? input.source.trim() : 'manual',
46
+ originCommand: typeof input.originCommand === 'string' ? input.originCommand.trim() : '',
47
+ reason: typeof input.reason === 'string' ? input.reason.trim() : '',
48
+ tooling: normalizeStringArray(input.tooling),
49
+ screensChecked: normalizeStringArray(input.screensChecked),
50
+ statesChecked: normalizeStringArray(input.statesChecked),
51
+ status: normalizeVisualStatus(input.status),
52
+ summary: typeof input.summary === 'string' ? input.summary.trim() : '',
53
+ findings: normalizeStringArray(input.findings),
54
+ recommendations: normalizeStringArray(input.recommendations),
55
+ }
56
+ }
57
+
58
+ export function writeVisualEvidence(cwd, input = {}) {
59
+ mkdirSync(join(cwd, '.helloagents'), { recursive: true })
60
+ const normalized = normalizeVisualEvidence(input)
61
+ const payload = {
62
+ updatedAt: new Date().toISOString(),
63
+ ...normalized,
64
+ fingerprint: captureWorkspaceFingerprint(cwd),
65
+ }
66
+ writeFileSync(getVisualEvidencePath(cwd), `${JSON.stringify(payload, null, 2)}\n`, 'utf-8')
67
+ appendReplayEvent(cwd, {
68
+ event: 'visual_evidence_written',
69
+ source: normalized.source,
70
+ skillName: normalized.originCommand,
71
+ details: {
72
+ reason: normalized.reason,
73
+ tooling: normalized.tooling,
74
+ screensChecked: normalized.screensChecked,
75
+ statesChecked: normalized.statesChecked,
76
+ status: normalized.status,
77
+ },
78
+ artifacts: ['.helloagents/.ralph-visual.json'],
79
+ })
80
+ return payload
81
+ }
82
+
83
+ function readRequiredVisualEvidence(cwd, required) {
84
+ if (!required) {
85
+ return {
86
+ required: false,
87
+ status: 'not-applicable',
88
+ }
89
+ }
90
+
91
+ const evidence = readVisualEvidence(cwd)
92
+ if (evidence) return { evidence }
93
+ return {
94
+ error: {
95
+ required: true,
96
+ status: 'missing',
97
+ details: ['missing visual validation evidence required by the active UI contract'],
98
+ },
99
+ }
100
+ }
101
+
102
+ function validateVisualTimestamp(evidence, now) {
103
+ const updatedAt = Date.parse(evidence.updatedAt || '')
104
+ if (!Number.isFinite(updatedAt)) {
105
+ return {
106
+ required: true,
107
+ status: 'invalid',
108
+ evidence,
109
+ details: ['visual validation evidence timestamp is invalid'],
110
+ }
111
+ }
112
+ if (now - updatedAt > VISUAL_EVIDENCE_MAX_AGE_MS) {
113
+ return {
114
+ required: true,
115
+ status: 'stale-time',
116
+ evidence,
117
+ details: ['visual validation evidence is older than 30 minutes'],
118
+ }
119
+ }
120
+ return null
121
+ }
122
+
123
+ function validateVisualFingerprint(cwd, evidence) {
124
+ const currentFingerprint = captureWorkspaceFingerprint(cwd)
125
+ if (
126
+ currentFingerprint.available
127
+ && evidence.fingerprint?.available
128
+ && currentFingerprint.combined !== evidence.fingerprint.combined
129
+ ) {
130
+ return {
131
+ required: true,
132
+ status: 'stale-diff',
133
+ evidence,
134
+ details: ['workspace diff changed after the last visual validation evidence'],
135
+ }
136
+ }
137
+ return null
138
+ }
139
+
140
+ function validateVisualContent(evidence, { screens = [], states = [] } = {}) {
141
+ const normalized = normalizeVisualEvidence(evidence)
142
+ if (!normalized.status || !normalized.summary || !normalized.reason) {
143
+ return {
144
+ required: true,
145
+ status: 'invalid',
146
+ evidence,
147
+ details: ['visual validation evidence must record explicit status, reason, and summary'],
148
+ }
149
+ }
150
+ if (normalized.tooling.length === 0) {
151
+ return {
152
+ required: true,
153
+ status: 'invalid',
154
+ evidence,
155
+ details: ['visual validation evidence must record the tooling used for the check'],
156
+ }
157
+ }
158
+ if (normalized.screensChecked.length === 0 && normalized.statesChecked.length === 0) {
159
+ return {
160
+ required: true,
161
+ status: 'invalid',
162
+ evidence,
163
+ details: ['visual validation evidence must record at least one checked screen or state'],
164
+ }
165
+ }
166
+
167
+ const missingScreens = findMissingCoverage(screens, normalized.screensChecked)
168
+ if (missingScreens.length > 0) {
169
+ return {
170
+ required: true,
171
+ status: 'invalid',
172
+ evidence,
173
+ details: [`visual validation evidence does not cover requested screens: ${missingScreens.join(', ')}`],
174
+ }
175
+ }
176
+
177
+ const missingStates = findMissingCoverage(states, normalized.statesChecked)
178
+ if (missingStates.length > 0) {
179
+ return {
180
+ required: true,
181
+ status: 'invalid',
182
+ evidence,
183
+ details: [`visual validation evidence does not cover requested states: ${missingStates.join(', ')}`],
184
+ }
185
+ }
186
+
187
+ if (normalized.status !== 'PASS') {
188
+ return {
189
+ required: true,
190
+ status: 'blocked',
191
+ evidence,
192
+ details: ['latest visual validation evidence still records blocking findings'],
193
+ }
194
+ }
195
+ return null
196
+ }
197
+
198
+ export function getVisualEvidenceStatus(cwd, { required = false, screens = [], states = [], now = Date.now() } = {}) {
199
+ const requiredEvidence = readRequiredVisualEvidence(cwd, required)
200
+ if ('status' in requiredEvidence) return requiredEvidence
201
+ if (requiredEvidence.error) return requiredEvidence.error
202
+
203
+ const { evidence } = requiredEvidence
204
+ const timestampError = validateVisualTimestamp(evidence, now)
205
+ if (timestampError) return timestampError
206
+
207
+ const fingerprintError = validateVisualFingerprint(cwd, evidence)
208
+ if (fingerprintError) return fingerprintError
209
+
210
+ const contentError = validateVisualContent(evidence, { screens, states })
211
+ if (contentError) return contentError
212
+
213
+ return {
214
+ required: true,
215
+ status: 'valid',
216
+ evidence: normalizeVisualEvidence(evidence),
217
+ }
218
+ }
219
+
220
+ function readStdinJson() {
221
+ try {
222
+ return JSON.parse(readFileSync(0, 'utf-8'))
223
+ } catch {
224
+ return {}
225
+ }
226
+ }
227
+
228
+ function main() {
229
+ const command = process.argv[2] || ''
230
+ if (command !== 'write') return
231
+
232
+ const input = readStdinJson()
233
+ const cwd = input.cwd || process.cwd()
234
+ const payload = writeVisualEvidence(cwd, input)
235
+ process.stdout.write(JSON.stringify({
236
+ suppressOutput: true,
237
+ path: getVisualEvidencePath(cwd),
238
+ payload,
239
+ }))
240
+ }
241
+
242
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
243
+ main()
244
+ }
@@ -0,0 +1,165 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ import {
5
+ getWorkflowSnapshot,
6
+ listPlanPackages,
7
+ normalizeTaskFile,
8
+ readStateSnapshot,
9
+ } from './workflow-plan-files.mjs'
10
+ import { getAdvisorRequirement, getVisualValidationRequirement } from './plan-contract.mjs'
11
+ import { describeProjectStoreFile, getProjectDesignContractPath } from './project-storage.mjs'
12
+
13
+ export function getTargetPlans(snapshot) {
14
+ return snapshot.activePlans.length > 0 ? snapshot.activePlans : snapshot.plans
15
+ }
16
+ export function classifyPlan(plan) {
17
+ if (!plan) {
18
+ return {
19
+ status: 'none',
20
+ details: [],
21
+ }
22
+ }
23
+
24
+ const details = [
25
+ ...plan.missingFiles.map((file) => `缺少 ${file}`),
26
+ ...plan.templateIssues,
27
+ ]
28
+
29
+ if (details.length > 0) {
30
+ return {
31
+ status: 'incomplete',
32
+ details,
33
+ }
34
+ }
35
+
36
+ if (plan.taskSummary.total === 0) {
37
+ return {
38
+ status: 'missing-task-checklist',
39
+ details: ['tasks.md 尚未形成可执行 checklist'],
40
+ }
41
+ }
42
+
43
+ if (plan.taskSummary.open > 0) {
44
+ return {
45
+ status: 'in-progress',
46
+ details: plan.taskSummary.items
47
+ .filter((item) => item.status === 'open')
48
+ .slice(0, 3)
49
+ .map((item) => item.text),
50
+ openCount: plan.taskSummary.open,
51
+ }
52
+ }
53
+
54
+ return {
55
+ status: 'closed',
56
+ details: [],
57
+ }
58
+ }
59
+
60
+ export function determineVerifyMode(plan) {
61
+ if (!plan) return null
62
+
63
+ if (plan.contractIssues.length > 0) {
64
+ return {
65
+ mode: 'metadata-first',
66
+ reason: '方案包缺少可信的结构化 contract',
67
+ guidance: '验证分流:当前还不适合直接进入 reviewer / tester;先回到 ~plan / ~prd 补齐 `contract.json`,明确 `verifyMode`、`reviewerFocus` 和 `testerFocus`。',
68
+ }
69
+ }
70
+
71
+ if (plan.contract?.verifyMode === 'review-first') {
72
+ const reviewerFocus = plan.contract.reviewerFocus.join(';')
73
+ const testerFocus = plan.contract.testerFocus.join(';')
74
+ return {
75
+ mode: 'review-first',
76
+ reason: '方案 contract 已明确要求审查优先',
77
+ guidance: `验证分流:当前更适合审查优先;先执行 reviewer / hello-review 范围审查,再交给 tester / hello-verify 跑完整验证。${reviewerFocus ? ` reviewer 重点:${reviewerFocus}。` : ''}${testerFocus ? ` tester 重点:${testerFocus}。` : ''}`.trim(),
78
+ }
79
+ }
80
+
81
+ if (plan.taskSummary.underSpecifiedCount > 0) {
82
+ return {
83
+ mode: 'metadata-first',
84
+ reason: 'tasks.md 仍缺少可信的任务元数据',
85
+ guidance: '验证分流:当前还不适合直接进入 reviewer / tester;先补齐 tasks.md 中每个任务的“涉及文件”“完成标准”和“验证方式”。',
86
+ }
87
+ }
88
+
89
+ return {
90
+ mode: 'test-first',
91
+ reason: '方案 contract 已明确验证主路径,且任务 contract 已完整',
92
+ guidance: `验证分流:当前更适合测试优先;先执行 tester / hello-verify 跑完整验证,再针对失败点或关键边界补充 hello-review。${plan.contract?.testerFocus?.length ? ` tester 重点:${plan.contract.testerFocus.join(';')}。` : ''}`.trim(),
93
+ }
94
+ }
95
+
96
+ function collectStateSyncIssues(snapshot) {
97
+ const issues = []
98
+ const hasPlans = snapshot.plans.length > 0
99
+ const state = snapshot.state
100
+
101
+ if (!hasPlans) {
102
+ return issues
103
+ }
104
+
105
+ if (!state.exists) {
106
+ issues.push('当前已存在方案包,但 `.helloagents/STATE.md` 缺失')
107
+ return issues
108
+ }
109
+
110
+ if (!state.referencedPlanDir) {
111
+ issues.push('当前已存在方案包,但 `STATE.md` 未记录活跃方案路径')
112
+ }
113
+ if (!state.sections['主线目标']) {
114
+ issues.push('`STATE.md` 缺少“主线目标”')
115
+ }
116
+ if (!state.sections['正在做什么']) {
117
+ issues.push('`STATE.md` 缺少“正在做什么”')
118
+ }
119
+ if (!state.sections['下一步']) {
120
+ issues.push('`STATE.md` 缺少“下一步”')
121
+ }
122
+
123
+ return issues
124
+ }
125
+
126
+ export function buildVerifyModeHintFromSnapshot(snapshot) {
127
+ const plan = getTargetPlans(snapshot)[0]
128
+ if (!plan) return ''
129
+ if (!plan.planSections['风险与验证'] && plan.taskSummary.total === 0) return ''
130
+ return determineVerifyMode(plan)?.guidance || ''
131
+ }
132
+ export function buildStateSyncHintFromSnapshot(snapshot) {
133
+ const issues = collectStateSyncIssues(snapshot)
134
+ if (issues.length === 0) return ''
135
+ return `STATE.md 提醒:${issues.join(';')};继续项目级流程、收尾或进入压缩前先同步恢复快照。`
136
+ }
137
+
138
+ export function buildStateRoleHintFromSnapshot(snapshot) {
139
+ if (!snapshot.state.exists || snapshot.plans.length > 0) return ''
140
+ return '恢复约束:当前仅检测到 `.helloagents/STATE.md`;先以当前用户消息、显式命令和代码事实确认主线,STATE.md 只用于找回上次停在哪,不是当前任务的自动授权或唯一事实源。'
141
+ }
142
+
143
+ export function buildUiContractHint(cwd, snapshot) {
144
+ const targetPlans = getTargetPlans(snapshot)
145
+ const hasDesignContract = existsSync(getProjectDesignContractPath(cwd))
146
+ const hasPrdUiArtifact = targetPlans.some((plan) => existsSync(join(plan.dirPath, 'prd', '03-ui-design.md')))
147
+ const hasUiContract = targetPlans.some((plan) => plan.contract?.ui?.required)
148
+ const styleAdvisorRequired = targetPlans.some((plan) => getAdvisorRequirement(plan.contract).styleRequired)
149
+ const visualValidationRequired = targetPlans.some((plan) => getVisualValidationRequirement(plan.contract).required)
150
+
151
+ if (!hasDesignContract && !hasPrdUiArtifact && !hasUiContract) {
152
+ return ''
153
+ }
154
+
155
+ const extraHints = []
156
+ if (styleAdvisorRequired) {
157
+ extraHints.push('若当前 UI contract 要求 style advisor,收尾前需复用 `.helloagents/.ralph-advisor.json` 留下独立复查证据')
158
+ }
159
+ if (visualValidationRequired) {
160
+ extraHints.push('若当前 UI contract 要求视觉验收,收尾前需写 `.helloagents/.ralph-visual.json` 记录关键视口、状态与结论')
161
+ }
162
+ return `UI 约束提示:如本次属于视觉/交互链路,设计决策优先级固定为:当前活跃 plan.md / prd/03-ui-design.md → ${describeProjectStoreFile(cwd, 'DESIGN.md')} → hello-ui。${extraHints.length > 0 ? ` ${extraHints.join(';')}。` : ''}`
163
+ }
164
+
165
+ export { normalizeTaskFile, readStateSnapshot, listPlanPackages, getWorkflowSnapshot }