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,210 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { join, normalize } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { resolveProjectPlanDir } from './project-storage.mjs'
5
+
6
+ export const PLAN_CONTRACT_FILE_NAME = 'contract.json'
7
+ const VALID_VERIFY_MODES = new Set(['test-first', 'review-first'])
8
+ const VALID_ADVISOR_SOURCES = new Set(['claude', 'codex', 'gemini'])
9
+
10
+ function normalizeStringArray(values) {
11
+ if (!Array.isArray(values)) return []
12
+ return [...new Set(values
13
+ .map((value) => (typeof value === 'string' ? value.trim() : ''))
14
+ .filter(Boolean))]
15
+ }
16
+
17
+ function normalizeVerifyMode(value) {
18
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
19
+ return VALID_VERIFY_MODES.has(normalized) ? normalized : ''
20
+ }
21
+
22
+ function normalizeUiStyleAdvisorContract(input = {}) {
23
+ return {
24
+ required: Boolean(input.required),
25
+ reason: typeof input.reason === 'string' ? input.reason.trim() : '',
26
+ focus: normalizeStringArray(input.focus),
27
+ }
28
+ }
29
+
30
+ function normalizeUiVisualValidationContract(input = {}) {
31
+ return {
32
+ required: Boolean(input.required),
33
+ reason: typeof input.reason === 'string' ? input.reason.trim() : '',
34
+ screens: normalizeStringArray(input.screens),
35
+ states: normalizeStringArray(input.states),
36
+ }
37
+ }
38
+
39
+ function normalizeUiContract(input = {}) {
40
+ const styleAdvisor = normalizeUiStyleAdvisorContract(input.styleAdvisor)
41
+ const visualValidation = normalizeUiVisualValidationContract(input.visualValidation)
42
+ const sourcePriority = normalizeStringArray(input.sourcePriority)
43
+ const designContract = Boolean(input.designContract)
44
+
45
+ return {
46
+ required: Boolean(input.required)
47
+ || designContract
48
+ || sourcePriority.length > 0
49
+ || styleAdvisor.required
50
+ || visualValidation.required,
51
+ designContract,
52
+ sourcePriority,
53
+ styleAdvisor,
54
+ visualValidation,
55
+ }
56
+ }
57
+
58
+ function normalizeAdvisorSources(values) {
59
+ return normalizeStringArray(values).filter((value) => VALID_ADVISOR_SOURCES.has(value))
60
+ }
61
+
62
+ function normalizeAdvisorContract(input = {}) {
63
+ return {
64
+ required: Boolean(input.required),
65
+ reason: typeof input.reason === 'string' ? input.reason.trim() : '',
66
+ focus: normalizeStringArray(input.focus),
67
+ preferredSources: normalizeAdvisorSources(input.preferredSources),
68
+ }
69
+ }
70
+
71
+ function resolvePlanDir(cwd, input = {}) {
72
+ const rawPlanDir = typeof input.planDir === 'string' ? input.planDir.trim() : ''
73
+ if (!rawPlanDir) return ''
74
+ return resolveProjectPlanDir(cwd, rawPlanDir)
75
+ }
76
+
77
+ export function getPlanContractPath(planDir) {
78
+ return join(planDir, PLAN_CONTRACT_FILE_NAME)
79
+ }
80
+
81
+ export function readPlanContract(planDir) {
82
+ try {
83
+ return JSON.parse(readFileSync(getPlanContractPath(planDir), 'utf-8'))
84
+ } catch {
85
+ return null
86
+ }
87
+ }
88
+
89
+ export function normalizePlanContract(input = {}) {
90
+ return {
91
+ version: 1,
92
+ source: typeof input.source === 'string' && input.source.trim() ? input.source.trim() : 'manual',
93
+ originCommand: typeof input.originCommand === 'string' ? input.originCommand.trim() : '',
94
+ verifyMode: normalizeVerifyMode(input.verifyMode),
95
+ reviewerFocus: normalizeStringArray(input.reviewerFocus),
96
+ testerFocus: normalizeStringArray(input.testerFocus),
97
+ ui: normalizeUiContract(input.ui),
98
+ advisor: normalizeAdvisorContract(input.advisor),
99
+ }
100
+ }
101
+
102
+ export function getAdvisorRequirement(contract = null) {
103
+ const normalized = normalizePlanContract(contract || {})
104
+ const advisor = normalized.advisor || normalizeAdvisorContract()
105
+ const styleAdvisor = normalized.ui?.styleAdvisor || normalizeUiStyleAdvisorContract()
106
+
107
+ return {
108
+ required: Boolean(advisor.required || styleAdvisor.required),
109
+ genericRequired: advisor.required,
110
+ styleRequired: styleAdvisor.required,
111
+ reason: [advisor.reason, styleAdvisor.reason].filter(Boolean).join(';'),
112
+ focus: normalizeStringArray([...advisor.focus, ...styleAdvisor.focus]),
113
+ preferredSources: advisor.preferredSources,
114
+ }
115
+ }
116
+
117
+ export function getVisualValidationRequirement(contract = null) {
118
+ const normalized = normalizePlanContract(contract || {})
119
+ return normalized.ui?.visualValidation || normalizeUiVisualValidationContract()
120
+ }
121
+
122
+ export function getPlanContractIssues(contract = null) {
123
+ if (!contract) {
124
+ return ['missing contract.json']
125
+ }
126
+
127
+ const normalized = normalizePlanContract(contract)
128
+ const advisorRequirement = getAdvisorRequirement(normalized)
129
+ const visualValidation = getVisualValidationRequirement(normalized)
130
+ const issues = []
131
+ if (!normalizeVerifyMode(normalized.verifyMode)) {
132
+ issues.push('contract.json missing valid verifyMode')
133
+ }
134
+ if (normalizeStringArray(normalized.testerFocus).length === 0) {
135
+ issues.push('contract.json missing testerFocus')
136
+ }
137
+ if (normalizeVerifyMode(normalized.verifyMode) === 'review-first' && normalizeStringArray(normalized.reviewerFocus).length === 0) {
138
+ issues.push('contract.json missing reviewerFocus for review-first flow')
139
+ }
140
+ if (normalized.ui?.required && normalizeStringArray(normalized.ui.sourcePriority).length === 0) {
141
+ issues.push('contract.json missing ui.sourcePriority')
142
+ }
143
+ if (normalized.ui?.styleAdvisor?.required && !String(normalized.ui.styleAdvisor.reason || '').trim()) {
144
+ issues.push('contract.json missing ui.styleAdvisor.reason')
145
+ }
146
+ if (normalized.ui?.styleAdvisor?.required && normalizeStringArray(normalized.ui.styleAdvisor.focus).length === 0) {
147
+ issues.push('contract.json missing ui.styleAdvisor.focus')
148
+ }
149
+ if (visualValidation.required && !String(visualValidation.reason || '').trim()) {
150
+ issues.push('contract.json missing ui.visualValidation.reason')
151
+ }
152
+ if (visualValidation.required && visualValidation.screens.length === 0 && visualValidation.states.length === 0) {
153
+ issues.push('contract.json missing ui.visualValidation.screens or ui.visualValidation.states')
154
+ }
155
+ if (advisorRequirement.genericRequired && !String(normalized.advisor.reason || '').trim()) {
156
+ issues.push('contract.json missing advisor.reason')
157
+ }
158
+ if (advisorRequirement.genericRequired && normalizeStringArray(normalized.advisor.focus).length === 0) {
159
+ issues.push('contract.json missing advisor.focus')
160
+ }
161
+ if (advisorRequirement.genericRequired && normalizeAdvisorSources(normalized.advisor.preferredSources).length === 0) {
162
+ issues.push('contract.json missing advisor.preferredSources')
163
+ }
164
+ return issues
165
+ }
166
+
167
+ export function writePlanContract(planDir, input = {}) {
168
+ mkdirSync(planDir, { recursive: true })
169
+ const payload = {
170
+ updatedAt: new Date().toISOString(),
171
+ ...normalizePlanContract(input),
172
+ }
173
+ writeFileSync(getPlanContractPath(planDir), `${JSON.stringify(payload, null, 2)}\n`, 'utf-8')
174
+ return payload
175
+ }
176
+
177
+ function readStdinJson() {
178
+ try {
179
+ return JSON.parse(readFileSync(0, 'utf-8'))
180
+ } catch {
181
+ return {}
182
+ }
183
+ }
184
+
185
+ function main() {
186
+ const command = process.argv[2] || ''
187
+ if (command !== 'write') return
188
+
189
+ const input = readStdinJson()
190
+ const cwd = input.cwd || process.cwd()
191
+ const planDir = resolvePlanDir(cwd, input)
192
+ if (!planDir) {
193
+ process.stdout.write(JSON.stringify({
194
+ suppressOutput: true,
195
+ error: 'planDir is required',
196
+ }))
197
+ return
198
+ }
199
+
200
+ const payload = writePlanContract(planDir, input)
201
+ process.stdout.write(JSON.stringify({
202
+ suppressOutput: true,
203
+ path: getPlanContractPath(planDir),
204
+ payload,
205
+ }))
206
+ }
207
+
208
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
209
+ main()
210
+ }
@@ -0,0 +1,235 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import { createHash } from 'node:crypto'
3
+ import { existsSync, readFileSync } from 'node:fs'
4
+ import { homedir } from 'node:os'
5
+ import { basename, dirname, isAbsolute, join, normalize, resolve } from 'node:path'
6
+
7
+ import { DEFAULTS } from './cli-config.mjs'
8
+
9
+ export const PROJECT_DIR_NAME = '.helloagents'
10
+ const PROJECTS_DIR_NAME = 'projects'
11
+ const PROJECT_STORE_MODES = new Set(['local', 'repo-shared'])
12
+
13
+ function safeJson(filePath) {
14
+ try {
15
+ return JSON.parse(readFileSync(filePath, 'utf-8'))
16
+ } catch {
17
+ return null
18
+ }
19
+ }
20
+
21
+ function runGitRevParse(cwd, args = []) {
22
+ try {
23
+ return execFileSync('git', ['rev-parse', ...args], {
24
+ cwd,
25
+ encoding: 'utf-8',
26
+ timeout: 5_000,
27
+ stdio: ['ignore', 'pipe', 'ignore'],
28
+ }).trim()
29
+ } catch {
30
+ return ''
31
+ }
32
+ }
33
+
34
+ function resolveGitTopLevel(cwd) {
35
+ const absolute = runGitRevParse(cwd, ['--path-format=absolute', '--show-toplevel'])
36
+ if (absolute) return normalize(absolute)
37
+
38
+ const raw = runGitRevParse(cwd, ['--show-toplevel'])
39
+ return raw ? normalize(resolve(cwd, raw)) : ''
40
+ }
41
+
42
+ function resolveGitCommonDir(cwd, repoRoot = '') {
43
+ const absolute = runGitRevParse(cwd, ['--path-format=absolute', '--git-common-dir'])
44
+ if (absolute) return normalize(absolute)
45
+
46
+ const raw = runGitRevParse(cwd, ['--git-common-dir'])
47
+ if (!raw) return ''
48
+ if (isAbsolute(raw)) return normalize(raw)
49
+ return normalize(resolve(repoRoot || cwd, raw))
50
+ }
51
+
52
+ function sanitizeRepoName(value = '') {
53
+ const normalized = String(value).trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '')
54
+ return normalized || 'project'
55
+ }
56
+
57
+ function buildProjectKey(cwd) {
58
+ const repoRoot = resolveGitTopLevel(cwd)
59
+ const commonDir = resolveGitCommonDir(cwd, repoRoot)
60
+ const commonDirName = commonDir && basename(commonDir).toLowerCase() === '.git'
61
+ ? basename(dirname(commonDir))
62
+ : basename(commonDir || '')
63
+ const repoName = sanitizeRepoName(commonDirName || basename(repoRoot || cwd))
64
+ const keySource = commonDir || repoRoot || normalize(resolve(cwd))
65
+ const hash = createHash('sha1').update(keySource.toLowerCase()).digest('hex').slice(0, 12)
66
+
67
+ return {
68
+ repoName,
69
+ hash,
70
+ key: `${repoName}-${hash}`,
71
+ repoRoot,
72
+ commonDir,
73
+ keySource,
74
+ }
75
+ }
76
+
77
+ function normalizeStoreRelativePath(relativePath = '') {
78
+ return String(relativePath)
79
+ .replace(/[`'"]/g, '')
80
+ .trim()
81
+ .replace(/^\.helloagents[\\/]+/, '')
82
+ .replace(/\\/g, '/')
83
+ .replace(/^\/+/, '')
84
+ }
85
+
86
+ function formatPromptPath(pathValue = '') {
87
+ return pathValue ? normalize(pathValue).replace(/\\/g, '/') : ''
88
+ }
89
+
90
+ export function normalizeProjectStoreMode(value) {
91
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
92
+ return PROJECT_STORE_MODES.has(normalized) ? normalized : DEFAULTS.project_store_mode
93
+ }
94
+
95
+ export function getProjectConfigPath() {
96
+ return join(homedir(), PROJECT_DIR_NAME, 'helloagents.json')
97
+ }
98
+
99
+ export function getProjectStoreMode() {
100
+ const settings = safeJson(getProjectConfigPath()) || {}
101
+ return normalizeProjectStoreMode(settings.project_store_mode)
102
+ }
103
+
104
+ export function getProjectActivationDir(cwd) {
105
+ return join(cwd, PROJECT_DIR_NAME)
106
+ }
107
+
108
+ export function getProjectStatePath(cwd) {
109
+ return join(getProjectActivationDir(cwd), 'STATE.md')
110
+ }
111
+
112
+ export function isRepoSharedProjectStore(cwd) {
113
+ return getProjectStoreMode(cwd) === 'repo-shared'
114
+ }
115
+
116
+ export function getProjectStoreDir(cwd) {
117
+ if (!isRepoSharedProjectStore(cwd)) {
118
+ return getProjectActivationDir(cwd)
119
+ }
120
+
121
+ const projectKey = buildProjectKey(cwd)
122
+ return join(homedir(), PROJECT_DIR_NAME, PROJECTS_DIR_NAME, projectKey.key)
123
+ }
124
+
125
+ export function getProjectStoreSummary(cwd) {
126
+ const activationDir = getProjectActivationDir(cwd)
127
+ const storeDir = getProjectStoreDir(cwd)
128
+ const statePath = getProjectStatePath(cwd)
129
+ const projectKey = buildProjectKey(cwd)
130
+ const projectStoreMode = getProjectStoreMode(cwd)
131
+
132
+ return {
133
+ projectStoreMode,
134
+ activationDir,
135
+ storeDir,
136
+ statePath,
137
+ usesSharedStore: projectStoreMode === 'repo-shared',
138
+ projectKey: projectKey.key,
139
+ repoRoot: projectKey.repoRoot,
140
+ commonDir: projectKey.commonDir,
141
+ promptActivationDir: formatPromptPath(activationDir),
142
+ promptStoreDir: formatPromptPath(storeDir),
143
+ promptStatePath: formatPromptPath(statePath),
144
+ }
145
+ }
146
+
147
+ export function getProjectKnowledgeFilePath(cwd, fileName) {
148
+ return join(getProjectStoreDir(cwd), fileName)
149
+ }
150
+
151
+ export function getProjectDesignContractPath(cwd) {
152
+ return getProjectKnowledgeFilePath(cwd, 'DESIGN.md')
153
+ }
154
+
155
+ export function getProjectVerifyYamlPath(cwd) {
156
+ return getProjectKnowledgeFilePath(cwd, 'verify.yaml')
157
+ }
158
+
159
+ export function getProjectPlansDir(cwd) {
160
+ return join(getProjectStoreDir(cwd), 'plans')
161
+ }
162
+
163
+ export function resolveProjectPlanDir(cwd, rawPlanDir = '') {
164
+ const value = String(rawPlanDir).replace(/[`'"]/g, '').trim().replace(/[\\/]+$/, '')
165
+ if (!value) return ''
166
+
167
+ if (isAbsolute(value)) {
168
+ return normalize(value)
169
+ }
170
+
171
+ if (value.startsWith('.helloagents/')) {
172
+ return normalize(join(getProjectStoreDir(cwd), normalizeStoreRelativePath(value)))
173
+ }
174
+
175
+ if (value.startsWith('.helloagents\\')) {
176
+ return normalize(join(getProjectStoreDir(cwd), normalizeStoreRelativePath(value)))
177
+ }
178
+
179
+ if (value.startsWith('plans/')) {
180
+ return normalize(join(getProjectStoreDir(cwd), normalizeStoreRelativePath(value)))
181
+ }
182
+
183
+ if (value.startsWith('plans\\')) {
184
+ return normalize(join(getProjectStoreDir(cwd), normalizeStoreRelativePath(value)))
185
+ }
186
+
187
+ const fromCwd = normalize(join(cwd, value))
188
+ if (existsSync(fromCwd)) {
189
+ return fromCwd
190
+ }
191
+
192
+ return normalize(join(getProjectPlansDir(cwd), value))
193
+ }
194
+
195
+ export function describeProjectStoreFile(cwd, relativePath = '') {
196
+ const normalizedRelativePath = normalizeStoreRelativePath(relativePath)
197
+ const logicalPath = normalizedRelativePath ? `.helloagents/${normalizedRelativePath}` : '.helloagents/'
198
+ if (!isRepoSharedProjectStore(cwd)) {
199
+ return `\`${logicalPath}\``
200
+ }
201
+
202
+ const actualPath = formatPromptPath(join(getProjectStoreDir(cwd), normalizedRelativePath))
203
+ return `逻辑路径 \`${logicalPath}\`(实际存储:\`${actualPath}\`)`
204
+ }
205
+
206
+ export function buildProjectStorageHint(cwd) {
207
+ const summary = getProjectStoreSummary(cwd)
208
+ if (!summary.usesSharedStore) return ''
209
+ return `项目存储:\`project_store_mode=repo-shared\`;本地激活/运行态目录仍是 \`${summary.promptActivationDir}\`,知识库/方案目录改为 \`${summary.promptStoreDir}\`。`
210
+ }
211
+
212
+ export function buildProjectStorageBlock(cwd) {
213
+ const summary = getProjectStoreSummary(cwd)
214
+ if (!summary.usesSharedStore && !existsSync(summary.activationDir)) {
215
+ return ''
216
+ }
217
+
218
+ const details = {
219
+ project_store_mode: summary.projectStoreMode,
220
+ activation_dir: summary.promptActivationDir,
221
+ state_path: summary.promptStatePath,
222
+ knowledge_base_dir: summary.promptStoreDir,
223
+ uses_shared_store: summary.usesSharedStore,
224
+ }
225
+
226
+ return [
227
+ '## 当前项目存储',
228
+ '```json',
229
+ JSON.stringify(details, null, 2),
230
+ '```',
231
+ summary.usesSharedStore
232
+ ? '说明:`STATE.md` 与 `.ralph-*.json` 继续写本地激活目录;`context.md`、`guidelines.md`、`DESIGN.md`、`verify.yaml`、`modules/`、`plans/`、`archive/` 写知识库/方案目录。'
233
+ : '说明:当前使用项目本地 `.helloagents/` 作为激活目录、知识库目录和方案目录。',
234
+ ].join('\n')
235
+ }
@@ -4,10 +4,11 @@
4
4
  * Runs on SubagentStop (Claude Code) and Stop (Codex CLI).
5
5
  * Auto-detects lint/test commands and blocks if they fail.
6
6
  */
7
- import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';
7
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
8
8
  import { join } from 'node:path';
9
9
  import { execSync } from 'node:child_process';
10
10
  import { homedir } from 'node:os';
11
+ import { clearVerifyEvidence, detectCommands, hasUnsafeVerifyCommand, writeVerifyEvidence } from './verify-state.mjs';
11
12
 
12
13
  const CONFIG_FILE = join(homedir(), '.helloagents', 'helloagents.json');
13
14
  const CMD_TIMEOUT = 60_000; // 60s
@@ -25,60 +26,6 @@ function readSettings() {
25
26
  return {};
26
27
  }
27
28
 
28
- // ── Detect verification commands ──────────────────────────────────────
29
- function loadVerifyYaml(cwd) {
30
- const f = join(cwd, '.helloagents', 'verify.yaml');
31
- if (!existsSync(f)) return null;
32
- try {
33
- const content = readFileSync(f, 'utf-8');
34
- const cmds = [];
35
- let inCmds = false;
36
- for (const line of content.split('\n')) {
37
- const s = line.trim();
38
- if (s.startsWith('commands:')) { inCmds = true; continue; }
39
- if (inCmds) {
40
- if (s.startsWith('- ') && !s.startsWith('# ')) {
41
- const cmd = s.slice(2).trim().replace(/^["']|["']$/g, '');
42
- if (cmd && !cmd.startsWith('#')) cmds.push(cmd);
43
- } else if (s && !s.startsWith('#')) break;
44
- }
45
- }
46
- return cmds.length ? cmds : null;
47
- } catch { return null; }
48
- }
49
-
50
- function detectFromPackageJson(cwd) {
51
- const f = join(cwd, 'package.json');
52
- if (!existsSync(f)) return [];
53
- try {
54
- const scripts = JSON.parse(readFileSync(f, 'utf-8')).scripts || {};
55
- return ['lint', 'typecheck', 'type-check', 'test', 'build']
56
- .filter(k => k in scripts)
57
- .map(k => `npm run ${k}`);
58
- } catch { return []; }
59
- }
60
-
61
- function detectFromPyproject(cwd) {
62
- const f = join(cwd, 'pyproject.toml');
63
- if (!existsSync(f)) return [];
64
- try {
65
- const content = readFileSync(f, 'utf-8');
66
- const cmds = [];
67
- if (content.includes('[tool.ruff')) cmds.push('ruff check .');
68
- if (content.includes('[tool.mypy')) cmds.push('mypy .');
69
- if (content.includes('[tool.pytest')) cmds.push('pytest --tb=short -q');
70
- return cmds;
71
- } catch { return []; }
72
- }
73
-
74
- function detectCommands(cwd) {
75
- const yaml = loadVerifyYaml(cwd);
76
- if (yaml?.length) return yaml;
77
- const pkg = detectFromPackageJson(cwd);
78
- if (pkg.length) return pkg;
79
- return detectFromPyproject(cwd);
80
- }
81
-
82
29
  // ── Circuit Breaker (consecutive failure tracking) ───────────────────
83
30
  const BREAKER_FILE_NAME = '.ralph-breaker.json';
84
31
 
@@ -123,12 +70,10 @@ function hasGitChanges(cwd) {
123
70
  }
124
71
 
125
72
  // ── Run verification ──────────────────────────────────────────────────
126
- const SHELL_OPERATORS = /[;&|`$(){}\n\r]/;
127
-
128
73
  function runVerify(commands, cwd) {
129
74
  const failures = [];
130
75
  for (const cmd of commands) {
131
- if (SHELL_OPERATORS.test(cmd)) {
76
+ if (hasUnsafeVerifyCommand([cmd])) {
132
77
  failures.push({ cmd, output: 'Blocked: shell operators not allowed in verify commands' });
133
78
  continue;
134
79
  }
@@ -151,6 +96,11 @@ function runVerify(commands, cwd) {
151
96
 
152
97
  function handleSuccess(cwd, isSubagent) {
153
98
  resetBreaker(cwd);
99
+ writeVerifyEvidence(cwd, {
100
+ commands: detectCommands(cwd),
101
+ fastOnly: isSubagent,
102
+ source: isSubagent ? 'subagent' : 'stop',
103
+ });
154
104
 
155
105
  if (isSubagent) {
156
106
  process.stdout.write(JSON.stringify({
@@ -178,6 +128,7 @@ function handleSuccess(cwd, isSubagent) {
178
128
  }
179
129
 
180
130
  function handleFailure(failures, cwd) {
131
+ clearVerifyEvidence(cwd);
181
132
  const breaker = readBreaker(cwd);
182
133
  breaker.consecutive_failures += 1;
183
134
  breaker.last_failure = new Date().toISOString();