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, readdirSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { dirname, join, normalize, resolve } from 'node:path'
3
+ import { homedir } from 'node:os'
4
+
5
+ const RUNTIME_DIR = join(homedir(), '.helloagents', 'runtime')
6
+ const REPLAY_CONTEXT_PATH = join(RUNTIME_DIR, 'replay-context.json')
7
+ const REPLAY_SESSION_TTL_MS = 12 * 60 * 60 * 1000
8
+ const MAX_REPLAY_SESSIONS = 3
9
+
10
+ function normalizePath(filePath = '') {
11
+ return filePath ? normalize(resolve(filePath)) : ''
12
+ }
13
+
14
+ function ensureRuntimeDir() {
15
+ mkdirSync(dirname(REPLAY_CONTEXT_PATH), { recursive: true })
16
+ }
17
+
18
+ function readReplayContext() {
19
+ try {
20
+ return JSON.parse(readFileSync(REPLAY_CONTEXT_PATH, 'utf-8'))
21
+ } catch {
22
+ return {}
23
+ }
24
+ }
25
+
26
+ function writeReplayContext(context) {
27
+ ensureRuntimeDir()
28
+ writeFileSync(REPLAY_CONTEXT_PATH, `${JSON.stringify(context, null, 2)}\n`, 'utf-8')
29
+ }
30
+
31
+ function getReplayKey(cwd, host = '') {
32
+ return `${normalizePath(cwd)}::${host || 'unknown'}`
33
+ }
34
+
35
+ function findLatestReplaySession(context, cwd) {
36
+ const normalizedCwd = normalizePath(cwd)
37
+ const entries = Object.values(context)
38
+ .filter((entry) => entry?.cwd === normalizedCwd && entry.filePath && existsSync(entry.filePath))
39
+ .sort((left, right) => (right.updatedAt || 0) - (left.updatedAt || 0))
40
+ return entries[0] || null
41
+ }
42
+
43
+ function getProjectRoot(cwd) {
44
+ const projectRoot = join(cwd, '.helloagents')
45
+ return existsSync(projectRoot) ? projectRoot : ''
46
+ }
47
+
48
+ export function getReplayDir(cwd) {
49
+ const projectRoot = getProjectRoot(cwd)
50
+ return projectRoot ? join(projectRoot, 'replay') : ''
51
+ }
52
+
53
+ function ensureReplayDir(cwd) {
54
+ const replayDir = getReplayDir(cwd)
55
+ if (!replayDir) return ''
56
+ mkdirSync(replayDir, { recursive: true })
57
+ return replayDir
58
+ }
59
+
60
+ function listReplaySessionFiles(replayDir) {
61
+ if (!replayDir || !existsSync(replayDir)) return []
62
+ return readdirSync(replayDir)
63
+ .filter((name) => name.endsWith('.jsonl'))
64
+ .sort()
65
+ }
66
+
67
+ function trimReplaySessions(replayDir) {
68
+ const files = listReplaySessionFiles(replayDir)
69
+ const staleFiles = files.slice(0, Math.max(0, files.length - MAX_REPLAY_SESSIONS))
70
+ for (const fileName of staleFiles) {
71
+ rmSync(join(replayDir, fileName), { force: true })
72
+ }
73
+ }
74
+
75
+ function sanitizeReplayValue(value) {
76
+ if (typeof value === 'string') {
77
+ return value.replace(/\s+/g, ' ').trim().slice(0, 280)
78
+ }
79
+ if (Array.isArray(value)) {
80
+ return value
81
+ .slice(0, 8)
82
+ .map((entry) => sanitizeReplayValue(entry))
83
+ .filter((entry) => entry !== '' && entry !== undefined)
84
+ }
85
+ if (value && typeof value === 'object') {
86
+ const output = {}
87
+ for (const [key, entry] of Object.entries(value)) {
88
+ const sanitized = sanitizeReplayValue(entry)
89
+ if (
90
+ sanitized === ''
91
+ || sanitized === undefined
92
+ || sanitized === null
93
+ || (Array.isArray(sanitized) && sanitized.length === 0)
94
+ || (typeof sanitized === 'object' && !Array.isArray(sanitized) && Object.keys(sanitized).length === 0)
95
+ ) {
96
+ continue
97
+ }
98
+ output[key] = sanitized
99
+ }
100
+ return output
101
+ }
102
+ return value
103
+ }
104
+
105
+ function getReplaySession(cwd, { host = '', create = false, reset = false } = {}) {
106
+ const replayDir = ensureReplayDir(cwd)
107
+ if (!replayDir) return null
108
+
109
+ const key = getReplayKey(cwd, host)
110
+ const context = readReplayContext()
111
+ const current = context[key] || (!host ? findLatestReplaySession(context, cwd) : null)
112
+ const isExpired = !current?.updatedAt || (Date.now() - current.updatedAt > REPLAY_SESSION_TTL_MS)
113
+ const isMissing = !current?.filePath || !existsSync(current.filePath)
114
+
115
+ if (!reset && !isExpired && !isMissing) {
116
+ context[key] = {
117
+ ...current,
118
+ updatedAt: Date.now(),
119
+ }
120
+ writeReplayContext(context)
121
+ return context[key]
122
+ }
123
+
124
+ if (!create) return null
125
+
126
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-')
127
+ const suffix = Math.random().toString(36).slice(2, 8)
128
+ const sessionId = `${stamp}-${host || 'unknown'}-${suffix}`
129
+ const filePath = join(replayDir, `${sessionId}.jsonl`)
130
+ const next = {
131
+ cwd: normalizePath(cwd),
132
+ host: host || 'unknown',
133
+ sessionId,
134
+ filePath,
135
+ updatedAt: Date.now(),
136
+ }
137
+ context[key] = next
138
+ writeReplayContext(context)
139
+ return next
140
+ }
141
+
142
+ function buildReplayRecommendation(recommendation) {
143
+ if (!recommendation) return {}
144
+ return {
145
+ nextCommand: recommendation.nextCommand,
146
+ nextPath: recommendation.nextPath,
147
+ stage: recommendation.stage || '',
148
+ status: recommendation.status || '',
149
+ planName: recommendation.plan?.planName || '',
150
+ summary: recommendation.summary || '',
151
+ }
152
+ }
153
+
154
+ export function startReplaySession(cwd, {
155
+ host = '',
156
+ source = 'startup',
157
+ bootstrapFile = '',
158
+ installMode = '',
159
+ } = {}) {
160
+ const session = getReplaySession(cwd, { host, create: true, reset: true })
161
+ if (!session) return ''
162
+
163
+ appendReplayEvent(cwd, {
164
+ host,
165
+ event: 'session_started',
166
+ source,
167
+ bootstrapFile,
168
+ installMode,
169
+ sessionId: session.sessionId,
170
+ })
171
+ return session.filePath
172
+ }
173
+
174
+ export function appendReplayEvent(cwd, {
175
+ host = '',
176
+ event = '',
177
+ source = '',
178
+ skillName = '',
179
+ sourceSkillName = '',
180
+ recommendation = null,
181
+ reason = '',
182
+ artifacts = [],
183
+ details = {},
184
+ sessionId = '',
185
+ } = {}) {
186
+ if (!event) return ''
187
+ const session = getReplaySession(cwd, { host, create: true })
188
+ if (!session?.filePath) return ''
189
+
190
+ const payload = sanitizeReplayValue({
191
+ ts: new Date().toISOString(),
192
+ event,
193
+ host: host || session.host,
194
+ source,
195
+ sessionId: sessionId || session.sessionId,
196
+ skillName,
197
+ sourceSkillName,
198
+ recommendation: buildReplayRecommendation(recommendation),
199
+ reason,
200
+ artifacts,
201
+ details,
202
+ })
203
+
204
+ writeFileSync(session.filePath, `${JSON.stringify(payload)}\n`, {
205
+ encoding: 'utf-8',
206
+ flag: 'a',
207
+ })
208
+ trimReplaySessions(getReplayDir(cwd))
209
+ return session.filePath
210
+ }
@@ -0,0 +1,220 @@
1
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { join } from 'node:path'
4
+ import { captureWorkspaceFingerprint } from './verify-state.mjs'
5
+ import { appendReplayEvent } from './replay-state.mjs'
6
+
7
+ export const REVIEW_EVIDENCE_FILE_NAME = '.ralph-review.json'
8
+ const REVIEW_EVIDENCE_MAX_AGE_MS = 30 * 60 * 1000
9
+ const VALID_REVIEW_OUTCOMES = new Set(['clean', 'findings'])
10
+
11
+ function normalizeStringArray(values) {
12
+ if (!Array.isArray(values)) return []
13
+ return [...new Set(values
14
+ .map((value) => (typeof value === 'string' ? value.trim() : ''))
15
+ .filter(Boolean))]
16
+ }
17
+
18
+ function normalizeReviewOutcome(value) {
19
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
20
+ return VALID_REVIEW_OUTCOMES.has(normalized) ? normalized : ''
21
+ }
22
+
23
+ export function getReviewEvidencePath(cwd) {
24
+ return join(cwd, '.helloagents', REVIEW_EVIDENCE_FILE_NAME)
25
+ }
26
+
27
+ export function readReviewEvidence(cwd) {
28
+ try {
29
+ return JSON.parse(readFileSync(getReviewEvidencePath(cwd), 'utf-8'))
30
+ } catch {
31
+ return null
32
+ }
33
+ }
34
+
35
+ export function clearReviewEvidence(cwd) {
36
+ rmSync(getReviewEvidencePath(cwd), { force: true })
37
+ }
38
+
39
+ export function normalizeReviewEvidence(input = {}) {
40
+ return {
41
+ source: typeof input.source === 'string' && input.source.trim() ? input.source.trim() : 'manual',
42
+ originCommand: typeof input.originCommand === 'string' ? input.originCommand.trim() : '',
43
+ reviewMode: typeof input.reviewMode === 'string' ? input.reviewMode.trim() : '',
44
+ outcome: normalizeReviewOutcome(input.outcome),
45
+ conclusion: typeof input.conclusion === 'string' ? input.conclusion.trim() : '',
46
+ findings: normalizeStringArray(input.findings),
47
+ fileReferences: normalizeStringArray(input.fileReferences),
48
+ }
49
+ }
50
+
51
+ export function writeReviewEvidence(cwd, {
52
+ source = 'stop',
53
+ originCommand = '',
54
+ reviewMode = '',
55
+ outcome = '',
56
+ conclusion = '',
57
+ findings = [],
58
+ fileReferences = [],
59
+ } = {}) {
60
+ mkdirSync(join(cwd, '.helloagents'), { recursive: true })
61
+ const normalized = normalizeReviewEvidence({
62
+ source,
63
+ originCommand,
64
+ reviewMode,
65
+ outcome,
66
+ conclusion,
67
+ findings,
68
+ fileReferences,
69
+ })
70
+ const payload = {
71
+ updatedAt: new Date().toISOString(),
72
+ source: normalized.source,
73
+ originCommand: normalized.originCommand,
74
+ reviewMode: normalized.reviewMode,
75
+ conclusion: normalized.conclusion,
76
+ outcome: normalized.outcome,
77
+ findings: normalized.findings,
78
+ fileReferences: normalized.fileReferences,
79
+ fingerprint: captureWorkspaceFingerprint(cwd),
80
+ }
81
+ writeFileSync(getReviewEvidencePath(cwd), `${JSON.stringify(payload, null, 2)}\n`, 'utf-8')
82
+ appendReplayEvent(cwd, {
83
+ event: 'review_evidence_written',
84
+ source: normalized.source,
85
+ skillName: normalized.originCommand,
86
+ details: {
87
+ reviewMode: normalized.reviewMode,
88
+ outcome: normalized.outcome,
89
+ conclusion: normalized.conclusion,
90
+ findings: normalized.findings,
91
+ fileReferences: normalized.fileReferences,
92
+ },
93
+ artifacts: ['.helloagents/.ralph-review.json'],
94
+ })
95
+ return payload
96
+ }
97
+
98
+ function readRequiredReviewEvidence(cwd) {
99
+ const evidence = readReviewEvidence(cwd)
100
+ if (evidence) return { evidence }
101
+ return {
102
+ error: {
103
+ required: true,
104
+ status: 'missing',
105
+ details: ['missing successful review evidence for review-first closeout'],
106
+ },
107
+ }
108
+ }
109
+
110
+ function validateReviewTimestamp(evidence, now) {
111
+ const updatedAt = Date.parse(evidence.updatedAt || '')
112
+ if (!Number.isFinite(updatedAt)) {
113
+ return {
114
+ required: true,
115
+ status: 'invalid',
116
+ evidence,
117
+ details: ['review evidence timestamp is invalid'],
118
+ }
119
+ }
120
+ if (now - updatedAt > REVIEW_EVIDENCE_MAX_AGE_MS) {
121
+ return {
122
+ required: true,
123
+ status: 'stale-time',
124
+ evidence,
125
+ details: ['review evidence is older than 30 minutes'],
126
+ }
127
+ }
128
+ return null
129
+ }
130
+
131
+ function validateReviewFingerprint(cwd, evidence) {
132
+ const currentFingerprint = captureWorkspaceFingerprint(cwd)
133
+ if (
134
+ currentFingerprint.available
135
+ && evidence.fingerprint?.available
136
+ && currentFingerprint.combined !== evidence.fingerprint.combined
137
+ ) {
138
+ return {
139
+ required: true,
140
+ status: 'stale-diff',
141
+ evidence,
142
+ details: ['workspace diff changed after the last successful review evidence'],
143
+ }
144
+ }
145
+ return null
146
+ }
147
+
148
+ function validateReviewOutcome(evidence) {
149
+ if (!normalizeReviewOutcome(evidence.outcome) || !String(evidence.conclusion || '').trim()) {
150
+ return {
151
+ required: true,
152
+ status: 'invalid',
153
+ evidence,
154
+ details: ['review evidence must record explicit outcome and conclusion'],
155
+ }
156
+ }
157
+ if (normalizeReviewOutcome(evidence.outcome) !== 'clean') {
158
+ return {
159
+ required: true,
160
+ status: 'blocked',
161
+ evidence,
162
+ details: ['latest review evidence still records blocking findings'],
163
+ }
164
+ }
165
+ return null
166
+ }
167
+
168
+ export function getReviewEvidenceStatus(cwd, { required = false, now = Date.now() } = {}) {
169
+ if (!required) {
170
+ return {
171
+ required: false,
172
+ status: 'not-applicable',
173
+ }
174
+ }
175
+
176
+ const requiredEvidence = readRequiredReviewEvidence(cwd)
177
+ if (requiredEvidence.error) return requiredEvidence.error
178
+
179
+ const { evidence } = requiredEvidence
180
+ const timestampError = validateReviewTimestamp(evidence, now)
181
+ if (timestampError) return timestampError
182
+
183
+ const fingerprintError = validateReviewFingerprint(cwd, evidence)
184
+ if (fingerprintError) return fingerprintError
185
+
186
+ const outcomeError = validateReviewOutcome(evidence)
187
+ if (outcomeError) return outcomeError
188
+
189
+ return {
190
+ required: true,
191
+ status: 'valid',
192
+ evidence,
193
+ }
194
+ }
195
+
196
+ function readStdinJson() {
197
+ try {
198
+ return JSON.parse(readFileSync(0, 'utf-8'))
199
+ } catch {
200
+ return {}
201
+ }
202
+ }
203
+
204
+ function main() {
205
+ const command = process.argv[2] || ''
206
+ if (command !== 'write') return
207
+
208
+ const input = readStdinJson()
209
+ const cwd = input.cwd || process.cwd()
210
+ const payload = writeReviewEvidence(cwd, input)
211
+ process.stdout.write(JSON.stringify({
212
+ suppressOutput: true,
213
+ path: getReviewEvidencePath(cwd),
214
+ payload,
215
+ }))
216
+ }
217
+
218
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
219
+ main()
220
+ }
@@ -0,0 +1,74 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { dirname, join, normalize, resolve } from 'node:path'
3
+ import { homedir } from 'node:os'
4
+
5
+ const RUNTIME_DIR = join(homedir(), '.helloagents', 'runtime')
6
+ const ROUTE_CONTEXT_PATH = join(RUNTIME_DIR, 'route-context.json')
7
+ const ROUTE_CONTEXT_TTL_MS = 30 * 60 * 1000
8
+
9
+ function normalizePath(filePath = '') {
10
+ return filePath ? normalize(resolve(filePath)) : ''
11
+ }
12
+
13
+ function ensureRuntimeDir() {
14
+ mkdirSync(dirname(ROUTE_CONTEXT_PATH), { recursive: true })
15
+ }
16
+
17
+ export function clearRouteContext() {
18
+ rmSync(ROUTE_CONTEXT_PATH, { force: true })
19
+ }
20
+
21
+ export function writeRouteContext({ cwd, skillName, sourceSkillName = skillName }) {
22
+ ensureRuntimeDir()
23
+ const context = {
24
+ cwd: normalizePath(cwd),
25
+ skillName,
26
+ sourceSkillName,
27
+ zeroSideEffect: skillName === 'idea',
28
+ updatedAt: Date.now(),
29
+ }
30
+ writeFileSync(ROUTE_CONTEXT_PATH, `${JSON.stringify(context, null, 2)}\n`, 'utf-8')
31
+ }
32
+
33
+ export function readRouteContext() {
34
+ if (!existsSync(ROUTE_CONTEXT_PATH)) return null
35
+
36
+ try {
37
+ const context = JSON.parse(readFileSync(ROUTE_CONTEXT_PATH, 'utf-8'))
38
+ if (!context?.cwd || !context?.skillName || !context?.updatedAt) return null
39
+ if (Date.now() - context.updatedAt > ROUTE_CONTEXT_TTL_MS) {
40
+ clearRouteContext()
41
+ return null
42
+ }
43
+ return {
44
+ ...context,
45
+ cwd: normalizePath(context.cwd),
46
+ }
47
+ } catch {
48
+ return null
49
+ }
50
+ }
51
+
52
+ export function getApplicableRouteContext({ cwd = '', filePath = '' } = {}) {
53
+ const context = readRouteContext()
54
+ if (!context) return null
55
+
56
+ const normalizedCwd = normalizePath(cwd)
57
+ if (normalizedCwd && normalizedCwd === context.cwd) {
58
+ return context
59
+ }
60
+
61
+ const normalizedFilePath = normalizePath(filePath)
62
+ if (
63
+ normalizedFilePath
64
+ && (
65
+ normalizedFilePath === context.cwd
66
+ || normalizedFilePath.startsWith(`${context.cwd}\\`)
67
+ || normalizedFilePath.startsWith(`${context.cwd}/`)
68
+ )
69
+ ) {
70
+ return context
71
+ }
72
+
73
+ return null
74
+ }