helloagents 3.0.3-beta.1 → 3.0.8-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 (75) 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 +157 -57
  5. package/README_CN.md +157 -57
  6. package/bootstrap-lite.md +125 -50
  7. package/bootstrap.md +169 -123
  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 +1 -1
  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 +94 -0
  17. package/scripts/cli-codex.mjs +90 -222
  18. package/scripts/cli-config.mjs +1 -0
  19. package/scripts/cli-doctor-render.mjs +28 -0
  20. package/scripts/cli-doctor.mjs +370 -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/cli-toml.mjs +30 -0
  26. package/scripts/closeout-state.mjs +213 -0
  27. package/scripts/delivery-gate.mjs +256 -0
  28. package/scripts/guard-rules.mjs +147 -0
  29. package/scripts/guard.mjs +218 -168
  30. package/scripts/notify-context.mjs +78 -23
  31. package/scripts/notify-events.mjs +5 -1
  32. package/scripts/notify-route.mjs +111 -0
  33. package/scripts/notify-shared.mjs +0 -2
  34. package/scripts/notify-source.mjs +113 -0
  35. package/scripts/notify-ui.mjs +40 -6
  36. package/scripts/notify.mjs +137 -65
  37. package/scripts/plan-contract.mjs +210 -0
  38. package/scripts/project-storage.mjs +235 -0
  39. package/scripts/ralph-loop.mjs +9 -58
  40. package/scripts/replay-state.mjs +210 -0
  41. package/scripts/review-state.mjs +220 -0
  42. package/scripts/runtime-context.mjs +74 -0
  43. package/scripts/turn-state.mjs +173 -0
  44. package/scripts/verify-state.mjs +226 -0
  45. package/scripts/visual-state.mjs +244 -0
  46. package/scripts/workflow-core.mjs +165 -0
  47. package/scripts/workflow-plan-files.mjs +249 -0
  48. package/scripts/workflow-recommendation.mjs +335 -0
  49. package/scripts/workflow-state.mjs +113 -0
  50. package/skills/_meta/SKILL.md +1 -1
  51. package/skills/commands/auto/SKILL.md +48 -67
  52. package/skills/commands/build/SKILL.md +67 -0
  53. package/skills/commands/clean/SKILL.md +10 -8
  54. package/skills/commands/commit/SKILL.md +8 -4
  55. package/skills/commands/help/SKILL.md +18 -11
  56. package/skills/commands/idea/SKILL.md +55 -0
  57. package/skills/commands/init/SKILL.md +16 -8
  58. package/skills/commands/loop/SKILL.md +6 -5
  59. package/skills/commands/plan/SKILL.md +118 -0
  60. package/skills/commands/prd/SKILL.md +22 -15
  61. package/skills/commands/verify/SKILL.md +32 -9
  62. package/skills/commands/wiki/SKILL.md +11 -11
  63. package/skills/hello-review/SKILL.md +9 -0
  64. package/skills/hello-subagent/SKILL.md +5 -3
  65. package/skills/hello-ui/SKILL.md +36 -8
  66. package/skills/hello-verify/SKILL.md +12 -3
  67. package/skills/helloagents/SKILL.md +36 -20
  68. package/templates/DESIGN.md +25 -4
  69. package/templates/STATE.md +3 -0
  70. package/templates/plans/contract.json +48 -0
  71. package/templates/plans/plan.md +23 -0
  72. package/templates/plans/tasks.md +3 -3
  73. package/skills/commands/design/SKILL.md +0 -108
  74. package/skills/commands/review/SKILL.md +0 -16
  75. package/templates/plans/design.md +0 -14
@@ -0,0 +1,222 @@
1
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { join } from 'node:path'
4
+
5
+ import { appendReplayEvent } from './replay-state.mjs'
6
+ import { captureWorkspaceFingerprint } from './verify-state.mjs'
7
+
8
+ export const ADVISOR_EVIDENCE_FILE_NAME = '.ralph-advisor.json'
9
+ const ADVISOR_EVIDENCE_MAX_AGE_MS = 30 * 60 * 1000
10
+ const VALID_ADVISOR_OUTCOMES = new Set(['clean', 'findings'])
11
+ const VALID_SOURCES = new Set(['claude', 'codex', 'gemini'])
12
+
13
+ function normalizeStringArray(values) {
14
+ if (!Array.isArray(values)) return []
15
+ return [...new Set(values.map((value) => (typeof value === 'string' ? value.trim() : '')).filter(Boolean))]
16
+ }
17
+
18
+ function normalizeSources(values) {
19
+ return normalizeStringArray(values).filter((value) => VALID_SOURCES.has(value))
20
+ }
21
+
22
+ function normalizeOutcome(value) {
23
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
24
+ return VALID_ADVISOR_OUTCOMES.has(normalized) ? normalized : ''
25
+ }
26
+
27
+ export function getAdvisorEvidencePath(cwd) {
28
+ return join(cwd, '.helloagents', ADVISOR_EVIDENCE_FILE_NAME)
29
+ }
30
+
31
+ export function readAdvisorEvidence(cwd) {
32
+ try {
33
+ return JSON.parse(readFileSync(getAdvisorEvidencePath(cwd), 'utf-8'))
34
+ } catch {
35
+ return null
36
+ }
37
+ }
38
+
39
+ export function clearAdvisorEvidence(cwd) {
40
+ rmSync(getAdvisorEvidencePath(cwd), { force: true })
41
+ }
42
+
43
+ export function normalizeAdvisorEvidence(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
+ focus: normalizeStringArray(input.focus),
49
+ preferredSources: normalizeSources(input.preferredSources),
50
+ consultedSources: normalizeSources(input.consultedSources),
51
+ outcome: normalizeOutcome(input.outcome),
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 writeAdvisorEvidence(cwd, input = {}) {
59
+ mkdirSync(join(cwd, '.helloagents'), { recursive: true })
60
+ const normalized = normalizeAdvisorEvidence(input)
61
+ const payload = {
62
+ updatedAt: new Date().toISOString(),
63
+ ...normalized,
64
+ fingerprint: captureWorkspaceFingerprint(cwd),
65
+ }
66
+ writeFileSync(getAdvisorEvidencePath(cwd), `${JSON.stringify(payload, null, 2)}\n`, 'utf-8')
67
+ appendReplayEvent(cwd, {
68
+ event: 'advisor_evidence_written',
69
+ source: normalized.source,
70
+ skillName: normalized.originCommand,
71
+ details: {
72
+ reason: normalized.reason,
73
+ focus: normalized.focus,
74
+ preferredSources: normalized.preferredSources,
75
+ consultedSources: normalized.consultedSources,
76
+ outcome: normalized.outcome,
77
+ },
78
+ artifacts: ['.helloagents/.ralph-advisor.json'],
79
+ })
80
+ return payload
81
+ }
82
+
83
+ function readRequiredAdvisorEvidence(cwd, required) {
84
+ if (!required) {
85
+ return {
86
+ required: false,
87
+ status: 'not-applicable',
88
+ }
89
+ }
90
+
91
+ const evidence = readAdvisorEvidence(cwd)
92
+ if (evidence) return { evidence }
93
+ return {
94
+ error: {
95
+ required: true,
96
+ status: 'missing',
97
+ details: ['missing advisor evidence required by the active contract'],
98
+ },
99
+ }
100
+ }
101
+
102
+ function validateAdvisorTimestamp(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: ['advisor evidence timestamp is invalid'],
110
+ }
111
+ }
112
+ if (now - updatedAt > ADVISOR_EVIDENCE_MAX_AGE_MS) {
113
+ return {
114
+ required: true,
115
+ status: 'stale-time',
116
+ evidence,
117
+ details: ['advisor evidence is older than 30 minutes'],
118
+ }
119
+ }
120
+ return null
121
+ }
122
+
123
+ function validateAdvisorFingerprint(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 advisor evidence'],
135
+ }
136
+ }
137
+ return null
138
+ }
139
+
140
+ function validateAdvisorContent(evidence, focus = []) {
141
+ if (!normalizeOutcome(evidence.outcome) || !String(evidence.summary || '').trim() || !String(evidence.reason || '').trim()) {
142
+ return {
143
+ required: true,
144
+ status: 'invalid',
145
+ evidence,
146
+ details: ['advisor evidence must record explicit outcome, reason, and summary'],
147
+ }
148
+ }
149
+ if (normalizeSources(evidence.consultedSources).length === 0) {
150
+ return {
151
+ required: true,
152
+ status: 'invalid',
153
+ evidence,
154
+ details: ['advisor evidence must record at least one consulted source'],
155
+ }
156
+ }
157
+ if (normalizeStringArray(focus).length > 0 && normalizeStringArray(evidence.focus).length === 0) {
158
+ return {
159
+ required: true,
160
+ status: 'invalid',
161
+ evidence,
162
+ details: ['advisor evidence must retain the requested advisor focus'],
163
+ }
164
+ }
165
+ if (normalizeOutcome(evidence.outcome) !== 'clean') {
166
+ return {
167
+ required: true,
168
+ status: 'blocked',
169
+ evidence,
170
+ details: ['latest advisor evidence still records blocking findings'],
171
+ }
172
+ }
173
+ return null
174
+ }
175
+
176
+ export function getAdvisorEvidenceStatus(cwd, { required = false, focus = [], now = Date.now() } = {}) {
177
+ const requiredEvidence = readRequiredAdvisorEvidence(cwd, required)
178
+ if ('status' in requiredEvidence) return requiredEvidence
179
+ if (requiredEvidence.error) return requiredEvidence.error
180
+
181
+ const { evidence } = requiredEvidence
182
+ const timestampError = validateAdvisorTimestamp(evidence, now)
183
+ if (timestampError) return timestampError
184
+
185
+ const fingerprintError = validateAdvisorFingerprint(cwd, evidence)
186
+ if (fingerprintError) return fingerprintError
187
+
188
+ const contentError = validateAdvisorContent(evidence, focus)
189
+ if (contentError) return contentError
190
+
191
+ return {
192
+ required: true,
193
+ status: 'valid',
194
+ evidence,
195
+ }
196
+ }
197
+
198
+ function readStdinJson() {
199
+ try {
200
+ return JSON.parse(readFileSync(0, 'utf-8'))
201
+ } catch {
202
+ return {}
203
+ }
204
+ }
205
+
206
+ function main() {
207
+ const command = process.argv[2] || ''
208
+ if (command !== 'write') return
209
+
210
+ const input = readStdinJson()
211
+ const cwd = input.cwd || process.cwd()
212
+ const payload = writeAdvisorEvidence(cwd, input)
213
+ process.stdout.write(JSON.stringify({
214
+ suppressOutput: true,
215
+ path: getAdvisorEvidencePath(cwd),
216
+ payload,
217
+ }))
218
+ }
219
+
220
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
221
+ main()
222
+ }
@@ -0,0 +1,59 @@
1
+ import { existsSync } from 'node:fs'
2
+
3
+ import { getAdvisorRequirement, getVisualValidationRequirement } from './plan-contract.mjs'
4
+ import { describeProjectStoreFile, getProjectDesignContractPath } from './project-storage.mjs'
5
+ import { getWorkflowRecommendation, getWorkflowSnapshot } from './workflow-state.mjs'
6
+
7
+ function getPrimaryPlan(snapshot) {
8
+ return snapshot.activePlans[0] || snapshot.plans[0] || null
9
+ }
10
+
11
+ export function selectCapabilities({ cwd, skillName = '' }) {
12
+ const snapshot = getWorkflowSnapshot(cwd)
13
+ const recommendation = getWorkflowRecommendation(cwd)
14
+ const plan = getPrimaryPlan(snapshot)
15
+ const advisorRequirement = getAdvisorRequirement(plan?.contract)
16
+ const visualRequirement = getVisualValidationRequirement(plan?.contract)
17
+ const capabilities = []
18
+
19
+ if (skillName === 'plan' || skillName === 'prd' || recommendation?.nextCommand === 'plan') {
20
+ capabilities.push({
21
+ id: 'plan-contract',
22
+ description: '结构化契约:仅在规划/PRD 场景使用 `scripts/plan-contract.mjs write` 写 `contract.json`,不要只把验证路径留在自然语言说明里。',
23
+ })
24
+ }
25
+ if (advisorRequirement.required) {
26
+ capabilities.push({
27
+ id: 'advisor-artifact',
28
+ description: advisorRequirement.styleRequired
29
+ ? '风格 advisor:当前 UI 契约要求进入收尾前复查设计方向,并复用 `.helloagents/.ralph-advisor.json` 记录 reason、focus、consultedSources 与结论。'
30
+ : '独立 advisor:当前契约要求进入收尾前写 `.helloagents/.ralph-advisor.json`,记录 advisor reason、focus、consultedSources 与结论。',
31
+ })
32
+ }
33
+ if (plan?.contract?.verifyMode === 'review-first') {
34
+ capabilities.push({
35
+ id: 'review-evaluator',
36
+ description: '审查优先:当前验证主路径是 review-first,先做 hello-review,再做 hello-verify。',
37
+ })
38
+ }
39
+ if (plan?.contract?.ui?.required || existsSync(getProjectDesignContractPath(cwd))) {
40
+ capabilities.push({
41
+ id: 'design-contract',
42
+ description: `UI 契约:仅在 UI 场景按需读取当前 plan.md / prd/03-ui-design.md、${describeProjectStoreFile(cwd, 'DESIGN.md')} 与 hello-ui,不全局常驻。`,
43
+ })
44
+ }
45
+ if (visualRequirement.required) {
46
+ capabilities.push({
47
+ id: 'visual-evaluator',
48
+ description: '视觉验收:当前 UI 契约要求进入收尾前写 `.helloagents/.ralph-visual.json`,记录 tooling、screensChecked、statesChecked、status 与 summary。',
49
+ })
50
+ }
51
+
52
+ return capabilities
53
+ }
54
+
55
+ export function buildCapabilityHint({ cwd, skillName = '' }) {
56
+ const capabilities = selectCapabilities({ cwd, skillName })
57
+ if (capabilities.length === 0) return ''
58
+ return `按需能力:${capabilities.map((entry) => `${entry.id}=${entry.description}`).join(' ')}`
59
+ }
@@ -0,0 +1,59 @@
1
+ import { copyFileSync, existsSync, readdirSync } from 'node:fs'
2
+ import { dirname, join } from 'node:path'
3
+
4
+ import { removeIfExists, safeRead } from './cli-utils.mjs'
5
+
6
+ const CODEX_BACKUP_TIMESTAMP_RE = /^\d{8}-\d{6}$/
7
+
8
+ function formatBackupTimestamp(date = new Date()) {
9
+ const pad = (value, size = 2) => String(value).padStart(size, '0')
10
+ return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`
11
+ }
12
+
13
+ export function createTimestampedBackupPath(filePath, backupBaseName) {
14
+ return join(dirname(filePath), `${backupBaseName}_${formatBackupTimestamp()}.bak`)
15
+ }
16
+
17
+ function listTimestampedBackups(directory, backupBaseName) {
18
+ if (!existsSync(directory)) return []
19
+ return readdirSync(directory)
20
+ .filter((name) => name.startsWith(`${backupBaseName}_`) && name.endsWith('.bak'))
21
+ .filter((name) => CODEX_BACKUP_TIMESTAMP_RE.test(name.slice(backupBaseName.length + 1, -4)))
22
+ .sort()
23
+ }
24
+
25
+ function getLatestTimestampedBackupPath(filePath, backupBaseName) {
26
+ const backups = listTimestampedBackups(dirname(filePath), backupBaseName)
27
+ const latest = backups.at(-1)
28
+ return latest ? join(dirname(filePath), latest) : ''
29
+ }
30
+
31
+ function readLatestTimestampedBackup(filePath, backupBaseName) {
32
+ const backupPath = getLatestTimestampedBackupPath(filePath, backupBaseName)
33
+ return backupPath ? safeRead(backupPath) || '' : ''
34
+ }
35
+
36
+ function removeLatestTimestampedBackup(filePath, backupBaseName) {
37
+ const backupPath = getLatestTimestampedBackupPath(filePath, backupBaseName)
38
+ if (backupPath) removeIfExists(backupPath)
39
+ }
40
+
41
+ export function ensureTimestampedBackup(filePath, backupBaseName) {
42
+ if (!existsSync(filePath)) return ''
43
+ const existingBackup = getLatestTimestampedBackupPath(filePath, backupBaseName)
44
+ if (existingBackup) return existingBackup
45
+ const backupPath = createTimestampedBackupPath(filePath, backupBaseName)
46
+ copyFileSync(filePath, backupPath)
47
+ return backupPath
48
+ }
49
+
50
+ export function readCodexBackup(filePath, backupBaseName) {
51
+ const latest = readLatestTimestampedBackup(filePath, backupBaseName)
52
+ if (latest) return latest
53
+ return safeRead(`${filePath}.bak`) || ''
54
+ }
55
+
56
+ export function removeCodexBackup(filePath, backupBaseName) {
57
+ removeLatestTimestampedBackup(filePath, backupBaseName)
58
+ removeIfExists(`${filePath}.bak`)
59
+ }
@@ -0,0 +1,94 @@
1
+ import {
2
+ prependTopLevelTomlBlocks,
3
+ removeTopLevelTomlBlock,
4
+ stripTomlSection,
5
+ } from './cli-toml.mjs'
6
+
7
+ export const CODEX_PLUGIN_CONFIG_HEADER = '[plugins."helloagents@local-plugins"]'
8
+ export const CODEX_MANAGED_TOML_COMMENT = '# helloagents-managed'
9
+
10
+ function normalizePath(value = '') {
11
+ return String(value || '').replace(/\\/g, '/')
12
+ }
13
+
14
+ export function upsertCodexPluginConfig(text) {
15
+ const stripped = stripTomlSection(text, CODEX_PLUGIN_CONFIG_HEADER).text.trimEnd()
16
+ const block = `${CODEX_PLUGIN_CONFIG_HEADER}\nenabled = true`
17
+ return stripped ? `${stripped}\n\n${block}\n` : `${block}\n`
18
+ }
19
+
20
+ export function removeCodexPluginConfig(text) {
21
+ return stripTomlSection(text, CODEX_PLUGIN_CONFIG_HEADER).text
22
+ }
23
+
24
+ export function isManagedCodexModelInstruction(line = '') {
25
+ return line.includes('model_instructions_file')
26
+ && line.includes(CODEX_MANAGED_TOML_COMMENT)
27
+ }
28
+
29
+ export function isManagedCodexNotify(line = '') {
30
+ return line.includes('codex-notify')
31
+ }
32
+
33
+ export function isManagedCodexBackupInstruction(line = '') {
34
+ return line.includes(CODEX_MANAGED_TOML_COMMENT)
35
+ }
36
+
37
+ export function isManagedCodexHooks(line = '') {
38
+ return /^\s*codex_hooks\s*=\s*true(?:\s+#.*)?\s*$/i.test(String(line || ''))
39
+ }
40
+
41
+ function formatManagedCodexModelInstructionsValue(filePath) {
42
+ return `"${normalizePath(filePath)}" ${CODEX_MANAGED_TOML_COMMENT}`
43
+ }
44
+
45
+ function formatManagedCodexModelInstructionsLine(filePath) {
46
+ return `model_instructions_file = ${formatManagedCodexModelInstructionsValue(filePath)}`
47
+ }
48
+
49
+ function formatManagedCodexNotifyValue(notifyScriptPath) {
50
+ return `["node", "${normalizePath(notifyScriptPath)}", "codex-notify"]`
51
+ }
52
+
53
+ function formatManagedCodexNotifyLine(notifyScriptPath) {
54
+ return `notify = ${formatManagedCodexNotifyValue(notifyScriptPath)}`
55
+ }
56
+
57
+ function removeTopLevelLinesBeingReplaced(toml, lines) {
58
+ let next = toml
59
+
60
+ for (const line of lines.map((value) => String(value || '').trim()).filter(Boolean)) {
61
+ const key = line.slice(0, line.indexOf('=')).trim()
62
+ if (!key) continue
63
+ next = removeTopLevelTomlBlock(next, key)
64
+ }
65
+
66
+ return next
67
+ }
68
+
69
+ function upsertOrderedCodexTopLevelLines(toml, lines) {
70
+ return prependTopLevelTomlBlocks(
71
+ removeTopLevelLinesBeingReplaced(toml, lines),
72
+ lines,
73
+ )
74
+ }
75
+
76
+ export function installCodexModelInstructions(toml, filePath) {
77
+ return upsertOrderedCodexTopLevelLines(toml, [
78
+ formatManagedCodexModelInstructionsLine(filePath),
79
+ ])
80
+ }
81
+
82
+ export function installCodexManagedTopLevelConfig(toml, { modelInstructionsPath, notifyScriptPath }) {
83
+ return upsertOrderedCodexTopLevelLines(toml, [
84
+ formatManagedCodexModelInstructionsLine(modelInstructionsPath),
85
+ formatManagedCodexNotifyLine(notifyScriptPath),
86
+ ])
87
+ }
88
+
89
+ export function restoreCodexTopLevelConfig(toml, { modelInstructionsLine = '', notifyLine = '' }) {
90
+ return upsertOrderedCodexTopLevelLines(toml, [
91
+ modelInstructionsLine,
92
+ notifyLine,
93
+ ])
94
+ }