helloagents 3.0.2-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 +147 -45
  5. package/README_CN.md +148 -46
  6. package/bootstrap-lite.md +104 -46
  7. package/bootstrap.md +143 -112
  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 +19 -11
  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 +59 -0
  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,249 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
2
+ import { isAbsolute, join, normalize } from 'node:path'
3
+
4
+ import { getPlanContractIssues, readPlanContract } from './plan-contract.mjs'
5
+ import { getProjectPlansDir, getProjectStatePath, resolveProjectPlanDir } from './project-storage.mjs'
6
+
7
+ const PLAN_TEMPLATE_MARKERS = {
8
+ 'requirements.md': [
9
+ /# \{项目\/功能名称\} — 需求/,
10
+ /\[解决什么问题?给谁用?\]/,
11
+ /\[必须有什么?不需要什么?\]/,
12
+ ],
13
+ 'plan.md': [
14
+ /# \{项目\/功能名称\} — 实施规划/,
15
+ /\[本次要解决的问题、范围边界、验收目标\]/,
16
+ /\[关键决策及理由\]/,
17
+ /\[功能完成时必须为真的条件、关键验收点、reviewer \/ tester 关注边界\]/,
18
+ ],
19
+ 'tasks.md': [
20
+ /# \{项目\/功能名称\} — 任务分解/,
21
+ /\[按执行顺序排列,每个任务独立可验证\]/,
22
+ /- \[ \] 任务1:描述/,
23
+ ],
24
+ }
25
+
26
+ function readText(filePath) {
27
+ try {
28
+ return readFileSync(filePath, 'utf-8')
29
+ } catch {
30
+ return ''
31
+ }
32
+ }
33
+
34
+ function parseMarkdownSections(content = '') {
35
+ const sections = {}
36
+ let currentTitle = ''
37
+ let currentLines = []
38
+
39
+ for (const line of content.split(/\r?\n/)) {
40
+ const match = line.match(/^##\s+(.+?)\s*$/)
41
+ if (match) {
42
+ if (currentTitle) {
43
+ sections[currentTitle] = currentLines.join('\n').trim()
44
+ }
45
+ currentTitle = match[1].trim()
46
+ currentLines = []
47
+ continue
48
+ }
49
+ if (currentTitle) {
50
+ currentLines.push(line)
51
+ }
52
+ }
53
+
54
+ if (currentTitle) {
55
+ sections[currentTitle] = currentLines.join('\n').trim()
56
+ }
57
+
58
+ return sections
59
+ }
60
+
61
+ function normalizePlanRef(rawValue) {
62
+ return (rawValue || '')
63
+ .replace(/[`'"]/g, '')
64
+ .split(/\r?\n/)[0]
65
+ .trim()
66
+ }
67
+
68
+ function resolvePlanDir(cwd, rawValue) {
69
+ const value = normalizePlanRef(rawValue)
70
+ if (!value || value === '(无)' || value === '(无)') return ''
71
+
72
+ if (isAbsolute(value)) {
73
+ return normalize(value.replace(/[\\/]+$/, ''))
74
+ }
75
+
76
+ const match = value.match(/(?:\.helloagents[\\/])?plans[\\/][^\s/\\]+/)
77
+ if (!match) return ''
78
+ return resolveProjectPlanDir(cwd, match[0])
79
+ }
80
+
81
+ function splitTaskMetaValues(rawValue = '') {
82
+ return rawValue
83
+ .split(/[,,、]/)
84
+ .map((value) => value.replace(/[`'"]/g, '').trim())
85
+ .filter(Boolean)
86
+ }
87
+
88
+ export function normalizeTaskFile(filePath = '') {
89
+ return filePath.replace(/\\/g, '/').replace(/[`'"]/g, '').trim().toLowerCase()
90
+ }
91
+
92
+ function extractTaskFiles(text = '') {
93
+ const explicitMatch = text.match(/涉及文件\s*[::]\s*([^));;]+)/)
94
+ if (explicitMatch) {
95
+ return splitTaskMetaValues(explicitMatch[1])
96
+ }
97
+
98
+ const matches = text.match(/(?:[A-Za-z0-9_.-]+[\\/])+[A-Za-z0-9_.-]+(?:\.[A-Za-z0-9_.-]+)?/g) || []
99
+ return [...new Set(matches.map((value) => value.trim()))]
100
+ }
101
+
102
+ function extractTaskValidation(text = '') {
103
+ const validationMatch = text.match(/验证方式\s*[::]\s*([^))]+)/)
104
+ return validationMatch ? validationMatch[1].trim() : ''
105
+ }
106
+
107
+ function extractTaskAcceptance(text = '') {
108
+ const acceptanceMatch = text.match(/完成标准\s*[::]\s*([^));;]+)/)
109
+ return acceptanceMatch ? acceptanceMatch[1].trim() : ''
110
+ }
111
+
112
+ function summarizeTasks(taskPath) {
113
+ if (!existsSync(taskPath)) {
114
+ return {
115
+ exists: false,
116
+ total: 0,
117
+ open: 0,
118
+ done: 0,
119
+ skipped: 0,
120
+ cancelled: 0,
121
+ items: [],
122
+ }
123
+ }
124
+
125
+ const items = []
126
+ for (const rawLine of readText(taskPath).split(/\r?\n/)) {
127
+ const line = rawLine.trim()
128
+ const match = line.match(/^-\s+\[([^\]])\]\s+(.+)$/)
129
+ if (!match) continue
130
+ const marker = match[1]
131
+ const text = match[2].trim()
132
+ let status = 'open'
133
+
134
+ if (marker === '√') status = 'done'
135
+ else if (marker === '-') status = 'skipped'
136
+ else if (marker.toUpperCase() === 'X') status = 'cancelled'
137
+
138
+ items.push({
139
+ status,
140
+ text,
141
+ files: extractTaskFiles(text),
142
+ acceptance: extractTaskAcceptance(text),
143
+ validation: extractTaskValidation(text),
144
+ })
145
+ }
146
+
147
+ return {
148
+ exists: true,
149
+ total: items.length,
150
+ open: items.filter((item) => item.status === 'open').length,
151
+ done: items.filter((item) => item.status === 'done').length,
152
+ skipped: items.filter((item) => item.status === 'skipped').length,
153
+ cancelled: items.filter((item) => item.status === 'cancelled').length,
154
+ underSpecifiedCount: items.filter((item) => item.files.length === 0 || !item.acceptance || !item.validation).length,
155
+ underSpecifiedOpenCount: items.filter((item) => item.status === 'open' && (item.files.length === 0 || !item.acceptance || !item.validation)).length,
156
+ underSpecifiedItems: items.filter((item) => item.files.length === 0 || !item.acceptance || !item.validation),
157
+ items,
158
+ }
159
+ }
160
+
161
+ function findTemplateIssues(fileName, filePath) {
162
+ if (!existsSync(filePath)) return []
163
+ const content = readText(filePath)
164
+ return (PLAN_TEMPLATE_MARKERS[fileName] || [])
165
+ .filter((pattern) => pattern.test(content))
166
+ .map(() => `${fileName} still contains template placeholders`)
167
+ }
168
+
169
+ function comparePlanEntries(a, b) {
170
+ return a.planName.localeCompare(b.planName)
171
+ }
172
+
173
+ export function readStateSnapshot(cwd) {
174
+ const statePath = getProjectStatePath(cwd)
175
+ const content = readText(statePath)
176
+ const sections = parseMarkdownSections(content)
177
+ const referencedPlanDir = resolvePlanDir(cwd, sections['方案'])
178
+
179
+ return {
180
+ statePath,
181
+ exists: existsSync(statePath),
182
+ content,
183
+ sections,
184
+ referencedPlanDir,
185
+ }
186
+ }
187
+
188
+ export function listPlanPackages(cwd) {
189
+ const plansDir = getProjectPlansDir(cwd)
190
+ if (!existsSync(plansDir)) return []
191
+
192
+ return readdirSync(plansDir, { withFileTypes: true })
193
+ .filter((entry) => entry.isDirectory())
194
+ .map((entry) => {
195
+ const dirPath = join(plansDir, entry.name)
196
+ const filePaths = {
197
+ requirementsPath: join(dirPath, 'requirements.md'),
198
+ planPath: join(dirPath, 'plan.md'),
199
+ taskPath: join(dirPath, 'tasks.md'),
200
+ }
201
+ const missingFiles = Object.entries({
202
+ 'requirements.md': filePaths.requirementsPath,
203
+ 'plan.md': filePaths.planPath,
204
+ 'tasks.md': filePaths.taskPath,
205
+ })
206
+ .filter(([, pathValue]) => !existsSync(pathValue))
207
+ .map(([name]) => name)
208
+ const taskSummary = summarizeTasks(filePaths.taskPath)
209
+ const planSections = parseMarkdownSections(readText(filePaths.planPath))
210
+ const contract = readPlanContract(dirPath)
211
+ const contractIssues = getPlanContractIssues(contract)
212
+ const templateIssues = [
213
+ ...findTemplateIssues('requirements.md', filePaths.requirementsPath),
214
+ ...findTemplateIssues('plan.md', filePaths.planPath),
215
+ ...findTemplateIssues('tasks.md', filePaths.taskPath),
216
+ ]
217
+
218
+ return {
219
+ planName: entry.name,
220
+ dirPath,
221
+ relativePath: `.helloagents/plans/${entry.name}`,
222
+ missingFiles,
223
+ planSections,
224
+ taskSummary,
225
+ contract,
226
+ contractIssues,
227
+ templateIssues,
228
+ }
229
+ })
230
+ .sort(comparePlanEntries)
231
+ }
232
+
233
+ export function getWorkflowSnapshot(cwd) {
234
+ const state = readStateSnapshot(cwd)
235
+ const plans = listPlanPackages(cwd).map((entry) => ({
236
+ ...entry,
237
+ referencedByState: state.referencedPlanDir ? normalize(entry.dirPath) === normalize(state.referencedPlanDir) : false,
238
+ }))
239
+
240
+ const activePlans = state.referencedPlanDir
241
+ ? plans.filter((entry) => entry.referencedByState)
242
+ : plans
243
+
244
+ return {
245
+ state,
246
+ plans,
247
+ activePlans,
248
+ }
249
+ }
@@ -0,0 +1,335 @@
1
+ import { getCloseoutEvidenceStatus } from './closeout-state.mjs'
2
+ import { getAdvisorEvidenceStatus } from './advisor-state.mjs'
3
+ import { getAdvisorRequirement, getVisualValidationRequirement } from './plan-contract.mjs'
4
+ import { getReviewEvidenceStatus } from './review-state.mjs'
5
+ import { getVisualEvidenceStatus } from './visual-state.mjs'
6
+ import { getVerifyEvidenceStatus } from './verify-state.mjs'
7
+ import {
8
+ classifyPlan,
9
+ determineVerifyMode,
10
+ getTargetPlans,
11
+ normalizeTaskFile,
12
+ } from './workflow-core.mjs'
13
+
14
+ function getClosedPlanEvidenceStatus(cwd, plan) {
15
+ const verifyMode = determineVerifyMode(plan)
16
+ const advisorRequirement = getAdvisorRequirement(plan.contract)
17
+ const visualRequirement = getVisualValidationRequirement(plan.contract)
18
+ const verificationStatus = getVerifyEvidenceStatus(cwd)
19
+ const reviewStatus = getReviewEvidenceStatus(cwd, {
20
+ required: verifyMode?.mode === 'review-first',
21
+ })
22
+ const advisorStatus = getAdvisorEvidenceStatus(cwd, {
23
+ required: advisorRequirement.required,
24
+ focus: advisorRequirement.focus,
25
+ })
26
+ const visualStatus = getVisualEvidenceStatus(cwd, {
27
+ required: visualRequirement.required,
28
+ screens: visualRequirement.screens,
29
+ states: visualRequirement.states,
30
+ })
31
+ const verifyReady = !verificationStatus.required || verificationStatus.status === 'valid'
32
+ const reviewReady = !reviewStatus.required || reviewStatus.status === 'valid'
33
+ const advisorReady = !advisorStatus.required || advisorStatus.status === 'valid'
34
+ const visualReady = !visualStatus.required || visualStatus.status === 'valid'
35
+ const closeoutStatus = getCloseoutEvidenceStatus(cwd, {
36
+ required: verifyReady && reviewReady && advisorReady && visualReady,
37
+ })
38
+
39
+ return {
40
+ verifyMode,
41
+ advisorRequirement,
42
+ visualRequirement,
43
+ verificationStatus,
44
+ reviewStatus,
45
+ advisorStatus,
46
+ visualStatus,
47
+ closeoutStatus,
48
+ verifyReady,
49
+ reviewReady,
50
+ advisorReady,
51
+ visualReady,
52
+ closeoutReady: !closeoutStatus.required || closeoutStatus.status === 'valid',
53
+ }
54
+ }
55
+
56
+ function buildConsolidateAction(recommendation) {
57
+ if (recommendation.mode === 'closeout-pending') {
58
+ return {
59
+ phase: 'consolidate',
60
+ mode: recommendation.mode,
61
+ routeHint: recommendation.guidance,
62
+ gateHint: '交付门控:审查与验证证据已满足;先写 `.helloagents/.ralph-closeout.json` 记录需求覆盖与交付清单,再完成 STATE.md / 归档后才可交付。',
63
+ }
64
+ }
65
+
66
+ return {
67
+ phase: 'consolidate',
68
+ mode: recommendation.mode || 'ready',
69
+ routeHint: recommendation.guidance,
70
+ gateHint: '交付门控:当前已具备 closeout evidence;完成 STATE.md、知识沉淀与归档后即可交付。',
71
+ }
72
+ }
73
+
74
+ function buildVerifyAction(plan, verifyMode) {
75
+ if (!verifyMode) return null
76
+ const advisorRequirement = getAdvisorRequirement(plan.contract)
77
+ const visualRequirement = getVisualValidationRequirement(plan.contract)
78
+ const extraChecks = []
79
+ if (advisorRequirement.required) {
80
+ extraChecks.push('完成独立 advisor / style advisor 复查并写入 `.helloagents/.ralph-advisor.json`')
81
+ }
82
+ if (visualRequirement.required) {
83
+ extraChecks.push('完成视觉验收并写入 `.helloagents/.ralph-visual.json`')
84
+ }
85
+ const gateSuffix = extraChecks.length > 0 ? ` ${extraChecks.join(',')},再进入 CONSOLIDATE。` : ''
86
+ if (verifyMode.mode === 'review-first') {
87
+ return {
88
+ phase: 'verify',
89
+ mode: verifyMode.mode,
90
+ routeHint: verifyMode.guidance,
91
+ gateHint: `交付门控:进入 CONSOLIDATE 前,必须先完成 reviewer / hello-review 范围审查,再完成 tester / hello-verify 全量验证,并留下最新验证证据;两步都通过后才可交付。${gateSuffix}`.trim(),
92
+ }
93
+ }
94
+ if (verifyMode.mode === 'metadata-first') {
95
+ return {
96
+ phase: 'verify',
97
+ mode: verifyMode.mode,
98
+ routeHint: verifyMode.guidance,
99
+ gateHint: plan.contractIssues.length > 0
100
+ ? '交付门控:当前还不能进入 CONSOLIDATE;先补齐 `contract.json` 中的 `verifyMode`、`reviewerFocus`、`testerFocus`,再进入 reviewer / tester。'
101
+ : '交付门控:当前还不能进入 CONSOLIDATE;先补齐 tasks.md 中每个任务的“涉及文件”“完成标准”和“验证方式”,再进入 reviewer / tester。',
102
+ }
103
+ }
104
+
105
+ return {
106
+ phase: 'verify',
107
+ mode: verifyMode.mode,
108
+ routeHint: verifyMode.guidance,
109
+ gateHint: `交付门控:进入 CONSOLIDATE 前,先完成 tester / hello-verify 全量验证并留下最新验证证据,再针对失败点或关键边界补充 hello-review;确认通过后才可交付。${gateSuffix}`.trim(),
110
+ }
111
+ }
112
+
113
+ export function buildDeliveryActionFromSnapshot(snapshot, cwd, recommendation = buildRecommendation(snapshot, cwd)) {
114
+ if (!recommendation) return null
115
+
116
+ if (recommendation.stage === 'consolidate') {
117
+ return buildConsolidateAction(recommendation)
118
+ }
119
+
120
+ const plan = getTargetPlans(snapshot)[0]
121
+ if (recommendation.nextCommand === 'verify' && plan) {
122
+ return buildVerifyAction(plan, determineVerifyMode(plan))
123
+ }
124
+ if (recommendation.nextCommand === 'build') {
125
+ return {
126
+ phase: 'build',
127
+ gateHint: '交付门控:当前还不能报告完成;先回到 ~build 完成剩余任务,再进入 ~verify。',
128
+ }
129
+ }
130
+ if (recommendation.nextCommand === 'plan') {
131
+ return {
132
+ phase: 'plan',
133
+ gateHint: '交付门控:当前还不能报告完成;先回到 ~plan 修复或补齐当前方案包,再进入 ~build / ~verify。',
134
+ }
135
+ }
136
+
137
+ return null
138
+ }
139
+
140
+ export function buildDeliveryGateHintFromSnapshot(snapshot, cwd, recommendation = buildRecommendation(snapshot, cwd)) {
141
+ return buildDeliveryActionFromSnapshot(snapshot, cwd, recommendation)?.gateHint || ''
142
+ }
143
+
144
+ function buildPlanRecommendation(scopeLabel, plan, classification) {
145
+ return {
146
+ scopeLabel,
147
+ plan,
148
+ status: classification.status,
149
+ details: classification.details,
150
+ nextCommand: 'plan',
151
+ nextPath: '~plan -> ~build / ~verify',
152
+ summary: classification.status === 'incomplete'
153
+ ? `${scopeLabel} "${plan.planName}" 仍不完整(${classification.details.join(';')})。`
154
+ : `${scopeLabel} "${plan.planName}" 尚未形成可执行任务清单。`,
155
+ guidance: classification.status === 'incomplete'
156
+ ? '优先先走 ~plan 修复或补全当前方案包,再进入实现或验证;不要把不完整 artifact 直接当成可交付依据。'
157
+ : '先回到 ~plan 补齐 tasks.md 的原子任务,再进入实现、验证或收尾。',
158
+ }
159
+ }
160
+
161
+ function buildInProgressRecommendation(scopeLabel, plan, classification) {
162
+ return {
163
+ scopeLabel,
164
+ plan,
165
+ status: classification.status,
166
+ details: classification.details,
167
+ nextCommand: 'build',
168
+ nextPath: '~build -> ~verify',
169
+ summary: `${scopeLabel} "${plan.planName}" 仍有 ${classification.openCount} 个未完成任务。`,
170
+ guidance: '若用户是在继续当前功能、落实既有方案、或让你“继续做完”,优先复用现有 requirements.md / plan.md / tasks.md 进入 ~build;完成当前实现后再进入 ~verify。除非用户明确要求重规划或现有方案已失效,不要重新回到 ~idea。',
171
+ }
172
+ }
173
+
174
+ function buildClosedRecommendation(scopeLabel, plan, cwd) {
175
+ const closedPlanEvidence = getClosedPlanEvidenceStatus(cwd, plan)
176
+ if (closedPlanEvidence.verifyMode?.mode === 'metadata-first') {
177
+ return {
178
+ scopeLabel,
179
+ plan,
180
+ status: 'closed',
181
+ nextCommand: 'verify',
182
+ nextPath: '~verify -> CONSOLIDATE',
183
+ summary: `${scopeLabel} "${plan.planName}" 的任务已全部闭合,但验证 contract 仍未结构化。`,
184
+ guidance: closedPlanEvidence.verifyMode.guidance,
185
+ }
186
+ }
187
+
188
+ if (
189
+ closedPlanEvidence.advisorStatus.required
190
+ && closedPlanEvidence.advisorStatus.status !== 'valid'
191
+ && closedPlanEvidence.visualStatus.required
192
+ && closedPlanEvidence.visualStatus.status !== 'valid'
193
+ ) {
194
+ return {
195
+ scopeLabel,
196
+ plan,
197
+ status: 'closed',
198
+ nextCommand: 'verify',
199
+ nextPath: '~verify -> CONSOLIDATE',
200
+ summary: `${scopeLabel} "${plan.planName}" 的任务已闭合,但当前 UI contract 仍要求独立 advisor 复查与视觉验收。`,
201
+ guidance: '先在 ~verify 阶段完成独立 advisor / style advisor 复查,并写入 `.helloagents/.ralph-advisor.json`;再完成视觉验收并写入 `.helloagents/.ralph-visual.json`,记录 reason、tooling、screensChecked、statesChecked、status 与 summary;两项都通过后再进入 CONSOLIDATE。',
202
+ }
203
+ }
204
+
205
+ if (closedPlanEvidence.advisorStatus.required && closedPlanEvidence.advisorStatus.status !== 'valid') {
206
+ return {
207
+ scopeLabel,
208
+ plan,
209
+ status: 'closed',
210
+ nextCommand: 'verify',
211
+ nextPath: '~verify -> CONSOLIDATE',
212
+ summary: `${scopeLabel} "${plan.planName}" 的任务已闭合,但当前 contract 仍要求独立 advisor 复查。`,
213
+ guidance: '先在 ~verify 阶段完成独立 advisor / style advisor 复查,并写入 `.helloagents/.ralph-advisor.json` 记录复查原因、focus、来源与结论;advisor 通过后再进入 CONSOLIDATE。',
214
+ }
215
+ }
216
+
217
+ if (closedPlanEvidence.visualStatus.required && closedPlanEvidence.visualStatus.status !== 'valid') {
218
+ return {
219
+ scopeLabel,
220
+ plan,
221
+ status: 'closed',
222
+ nextCommand: 'verify',
223
+ nextPath: '~verify -> CONSOLIDATE',
224
+ summary: `${scopeLabel} "${plan.planName}" 的任务已闭合,但当前 UI contract 仍要求视觉验收。`,
225
+ guidance: '先在 ~verify 阶段完成视觉验收,并写入 `.helloagents/.ralph-visual.json` 记录 reason、tooling、screensChecked、statesChecked、status 与 summary;视觉验收通过后再进入 CONSOLIDATE。',
226
+ }
227
+ }
228
+
229
+ if (closedPlanEvidence.verifyReady && closedPlanEvidence.reviewReady && closedPlanEvidence.advisorReady && closedPlanEvidence.visualReady) {
230
+ return {
231
+ scopeLabel,
232
+ plan,
233
+ status: 'closed',
234
+ stage: 'consolidate',
235
+ mode: closedPlanEvidence.closeoutReady ? 'ready' : 'closeout-pending',
236
+ nextCommand: 'verify',
237
+ nextPath: 'CONSOLIDATE',
238
+ summary: closedPlanEvidence.closeoutReady
239
+ ? `${scopeLabel} "${plan.planName}" 的任务与交付证据已闭合。`
240
+ : `${scopeLabel} "${plan.planName}" 的任务、审查与验证已闭合。`,
241
+ guidance: closedPlanEvidence.closeoutReady
242
+ ? '当前进入 CONSOLIDATE:完成 `STATE.md`、知识沉淀与方案归档后即可交付;不要无故重开新的方案包或重新跑一遍无关验证。'
243
+ : '当前进入 CONSOLIDATE:先写 `.helloagents/.ralph-closeout.json` 记录需求覆盖与交付清单,再同步 `STATE.md` / 归档后交付。',
244
+ }
245
+ }
246
+
247
+ return {
248
+ scopeLabel,
249
+ plan,
250
+ status: 'closed',
251
+ nextCommand: 'verify',
252
+ nextPath: '~verify -> CONSOLIDATE',
253
+ summary: `${scopeLabel} "${plan.planName}" 的任务已全部闭合。`,
254
+ guidance: '若用户是在做收尾、验真、复查或准备交付,优先走 ~verify 或 CONSOLIDATE;不要无故重开新的方案包。',
255
+ }
256
+ }
257
+
258
+ export function buildRecommendation(snapshot, cwd = process.cwd()) {
259
+ const plan = getTargetPlans(snapshot)[0]
260
+ if (!plan) return null
261
+
262
+ const classification = classifyPlan(plan)
263
+ const scopeLabel = snapshot.activePlans.length > 0 ? '当前活跃方案包' : '当前存在的方案包'
264
+
265
+ if (classification.status === 'incomplete' || classification.status === 'missing-task-checklist') {
266
+ return buildPlanRecommendation(scopeLabel, plan, classification)
267
+ }
268
+ if (classification.status === 'in-progress') {
269
+ return buildInProgressRecommendation(scopeLabel, plan, classification)
270
+ }
271
+ if (classification.status === 'closed') {
272
+ return buildClosedRecommendation(scopeLabel, plan, cwd)
273
+ }
274
+ return null
275
+ }
276
+
277
+ function findDisjointOpenTaskPair(plan) {
278
+ const openTasks = plan?.taskSummary?.items?.filter((item) => item.status === 'open') || []
279
+ const tasksWithFiles = openTasks.filter((item) => item.files.length > 0)
280
+
281
+ for (let i = 0; i < tasksWithFiles.length; i += 1) {
282
+ const left = tasksWithFiles[i]
283
+ const leftFiles = new Set(left.files.map(normalizeTaskFile))
284
+ for (let j = i + 1; j < tasksWithFiles.length; j += 1) {
285
+ const right = tasksWithFiles[j]
286
+ const overlaps = right.files.some((filePath) => leftFiles.has(normalizeTaskFile(filePath)))
287
+ if (!overlaps) {
288
+ return [left, right]
289
+ }
290
+ }
291
+ }
292
+
293
+ return null
294
+ }
295
+
296
+ function buildBuildOrchestrationHint(plan) {
297
+ if (plan.taskSummary.underSpecifiedOpenCount > 0) {
298
+ return '编排提示:当前开放任务里仍有条目缺少“涉及文件”“完成标准”或“验证方式”;并行分派或进入可信交付前,先补齐 tasks.md。'
299
+ }
300
+
301
+ const openTasks = plan.taskSummary.items.filter((item) => item.status === 'open')
302
+ if (openTasks.length < 2) return ''
303
+
304
+ const pair = findDisjointOpenTaskPair(plan)
305
+ if (pair) {
306
+ const describeTask = (task) => `${task.text}(${task.files.slice(0, 2).join(', ')}${task.validation ? `;验证:${task.validation}` : ''})`
307
+ return `编排提示:检测到可并行的开放任务;如需提速,可先读取 hello-subagent 再按 tasks.md 分派。任务A:${describeTask(pair[0])};任务B:${describeTask(pair[1])}。`
308
+ }
309
+ if (openTasks.every((item) => item.files.length === 0)) {
310
+ return '编排提示:当前有多个开放任务,但 tasks.md 尚未写清 contract 元数据;考虑子代理并行前先补足文件路径、完成标准与验证方式。'
311
+ }
312
+ return '编排提示:当前仍有多个开放任务,但文件范围存在重叠;暂不建议并行子代理,优先串行推进。'
313
+ }
314
+
315
+ export function buildOrchestrationHintFromSnapshot(snapshot, cwd, recommendation = buildRecommendation(snapshot, cwd)) {
316
+ const plan = getTargetPlans(snapshot)[0]
317
+ if (!plan || !recommendation) return ''
318
+
319
+ if (recommendation.nextCommand === 'build') {
320
+ return buildBuildOrchestrationHint(plan)
321
+ }
322
+ if (recommendation.nextCommand === 'verify' && plan.taskSummary.total >= 1) {
323
+ const action = buildDeliveryActionFromSnapshot(snapshot, cwd, recommendation)
324
+ if (action?.phase === 'verify') {
325
+ return `编排提示:当前已进入收尾;${[action.routeHint, action.gateHint].filter(Boolean).join(' ')}`
326
+ }
327
+ }
328
+ if (recommendation.stage === 'consolidate') {
329
+ const action = buildDeliveryActionFromSnapshot(snapshot, cwd, recommendation)
330
+ if (action?.phase === 'consolidate') {
331
+ return `编排提示:当前已进入 CONSOLIDATE;${[action.routeHint, action.gateHint].filter(Boolean).join(' ')}`
332
+ }
333
+ }
334
+ return ''
335
+ }
@@ -0,0 +1,113 @@
1
+ import { basename } from 'node:path'
2
+
3
+ import {
4
+ buildStateRoleHintFromSnapshot,
5
+ buildStateSyncHintFromSnapshot,
6
+ buildUiContractHint,
7
+ buildVerifyModeHintFromSnapshot,
8
+ getWorkflowSnapshot,
9
+ readStateSnapshot,
10
+ listPlanPackages,
11
+ } from './workflow-core.mjs'
12
+ import {
13
+ buildDeliveryActionFromSnapshot,
14
+ buildDeliveryGateHintFromSnapshot,
15
+ buildOrchestrationHintFromSnapshot,
16
+ buildRecommendation,
17
+ } from './workflow-recommendation.mjs'
18
+
19
+ export function getDeliveryAction(cwd) {
20
+ const snapshot = getWorkflowSnapshot(cwd)
21
+ const recommendation = buildRecommendation(snapshot, cwd)
22
+ return buildDeliveryActionFromSnapshot(snapshot, cwd, recommendation)
23
+ }
24
+
25
+ export function getWorkflowRecommendation(cwd) {
26
+ return buildRecommendation(getWorkflowSnapshot(cwd), cwd)
27
+ }
28
+
29
+ export function buildStateSyncHint(cwd) {
30
+ return buildStateSyncHintFromSnapshot(getWorkflowSnapshot(cwd))
31
+ }
32
+
33
+ export function buildDeliveryGateHint(cwd) {
34
+ const snapshot = getWorkflowSnapshot(cwd)
35
+ return buildDeliveryGateHintFromSnapshot(snapshot, cwd, buildRecommendation(snapshot, cwd))
36
+ }
37
+
38
+ export function buildWorkflowRouteHint(cwd) {
39
+ const snapshot = getWorkflowSnapshot(cwd)
40
+ const recommendation = buildRecommendation(snapshot, cwd)
41
+ const stateSyncHint = buildStateSyncHintFromSnapshot(snapshot)
42
+ const stateRoleHint = buildStateRoleHintFromSnapshot(snapshot)
43
+ const orchestrationHint = buildOrchestrationHintFromSnapshot(snapshot, cwd, recommendation)
44
+ const uiContractHint = buildUiContractHint(cwd, snapshot)
45
+
46
+ if (!recommendation) {
47
+ return [stateRoleHint, stateSyncHint, uiContractHint].filter(Boolean).join(' ')
48
+ }
49
+
50
+ const suffix = [stateRoleHint, stateSyncHint, orchestrationHint, uiContractHint].filter(Boolean).join(' ')
51
+ if (recommendation.stage === 'consolidate') {
52
+ return `${recommendation.summary} 当前建议下一阶段:CONSOLIDATE。推荐路径:${recommendation.nextPath}。${recommendation.guidance}${suffix ? ` ${suffix}` : ''}`
53
+ }
54
+ return `${recommendation.summary} 当前建议下一命令:~${recommendation.nextCommand}。推荐路径:${recommendation.nextPath}。${recommendation.guidance}${suffix ? ` ${suffix}` : ''}`
55
+ }
56
+
57
+ function buildCommandRouteMessage(skillName, recommendation, verifyModeHint) {
58
+ if (skillName === 'auto') {
59
+ return recommendation.stage === 'consolidate'
60
+ ? `当前工作流约束:${recommendation.summary} 当前建议下一阶段:CONSOLIDATE。${recommendation.guidance}`
61
+ : `当前工作流约束:${recommendation.summary} 当前建议主路径:${recommendation.nextPath}。${recommendation.guidance}`
62
+ }
63
+ if (skillName === 'plan') {
64
+ if (recommendation.stage === 'consolidate') {
65
+ return `当前工作流约束:${recommendation.summary} 当前更推荐的下一阶段其实是 CONSOLIDATE。只有在用户明确要求重规划、改方向或新增范围时,才继续 ~plan。`
66
+ }
67
+ return recommendation.nextCommand === 'plan'
68
+ ? `当前工作流约束:${recommendation.summary} 当前建议下一命令:~plan。${recommendation.guidance}`
69
+ : `当前工作流约束:${recommendation.summary} 当前更推荐的下一命令其实是 ~${recommendation.nextCommand}。只有在用户明确要求重规划、改方向或新增范围时,才继续 ~plan。`
70
+ }
71
+ if (skillName === 'build') {
72
+ if (recommendation.stage === 'consolidate') {
73
+ return `当前工作流约束:${recommendation.summary} 当前更推荐的下一阶段其实是 CONSOLIDATE。只有在用户明确提出新增实现范围时,才继续 ~build。`
74
+ }
75
+ return recommendation.nextCommand === 'build'
76
+ ? `当前工作流约束:${recommendation.summary} 当前建议下一命令:~build。${recommendation.guidance}`
77
+ : `当前工作流约束:${recommendation.summary} 当前更推荐的下一命令其实是 ~${recommendation.nextCommand}。只有在用户明确提出新增实现范围时,才继续 ~build。`
78
+ }
79
+ if (skillName === 'verify') {
80
+ if (recommendation.stage === 'consolidate') {
81
+ return `当前工作流约束:${recommendation.summary} 当前建议下一阶段:CONSOLIDATE。${recommendation.guidance}`
82
+ }
83
+ return recommendation.nextCommand === 'verify'
84
+ ? `当前工作流约束:${recommendation.summary} 当前建议下一命令:~verify。${recommendation.guidance}`
85
+ : `当前工作流约束:${recommendation.summary} 当前更推荐的下一命令其实是 ~${recommendation.nextCommand}。即使执行 ~verify,也不能越过当前工作流边界。${verifyModeHint ? ` 若本次仅做阶段内审查或验真,${verifyModeHint}` : ''}`
86
+ }
87
+ return `当前工作流约束:${recommendation.summary} 当前建议下一命令:~${recommendation.nextCommand}。${recommendation.guidance}`
88
+ }
89
+
90
+ export function buildCommandRouteHint(skillName, cwd) {
91
+ const snapshot = getWorkflowSnapshot(cwd)
92
+ const recommendation = buildRecommendation(snapshot, cwd)
93
+ const contextHints = [
94
+ buildStateRoleHintFromSnapshot(snapshot),
95
+ buildStateSyncHintFromSnapshot(snapshot),
96
+ buildOrchestrationHintFromSnapshot(snapshot, cwd, recommendation),
97
+ buildUiContractHint(cwd, snapshot),
98
+ ].filter(Boolean)
99
+
100
+ if (!recommendation) {
101
+ return contextHints.join(' ')
102
+ }
103
+
104
+ const message = buildCommandRouteMessage(skillName, recommendation, buildVerifyModeHintFromSnapshot(snapshot))
105
+ return [message, ...contextHints].join(' ')
106
+ }
107
+
108
+ export { readStateSnapshot, listPlanPackages, getWorkflowSnapshot }
109
+
110
+ export function describePlanForLogs(planEntry) {
111
+ if (!planEntry) return ''
112
+ return basename(planEntry.dirPath)
113
+ }