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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +147 -45
- package/README_CN.md +148 -46
- package/bootstrap-lite.md +104 -46
- package/bootstrap.md +143 -112
- package/cli.mjs +80 -427
- package/gemini-extension.json +1 -1
- package/hooks/hooks-claude.json +10 -0
- package/hooks/hooks.json +10 -0
- package/package.json +2 -12
- package/scripts/advisor-state.mjs +222 -0
- package/scripts/capability-registry.mjs +59 -0
- package/scripts/cli-codex-backup.mjs +59 -0
- package/scripts/cli-codex-config.mjs +100 -0
- package/scripts/cli-codex.mjs +34 -156
- package/scripts/cli-config.mjs +1 -0
- package/scripts/cli-doctor-render.mjs +28 -0
- package/scripts/cli-doctor.mjs +367 -0
- package/scripts/cli-host-detect.mjs +94 -0
- package/scripts/cli-lifecycle-hosts.mjs +123 -0
- package/scripts/cli-lifecycle.mjs +213 -0
- package/scripts/cli-messages.mjs +76 -52
- package/scripts/closeout-state.mjs +213 -0
- package/scripts/delivery-gate.mjs +256 -0
- package/scripts/guard-rules.mjs +122 -0
- package/scripts/guard.mjs +190 -168
- package/scripts/notify-context.mjs +77 -17
- package/scripts/notify-events.mjs +5 -1
- package/scripts/notify-route.mjs +111 -0
- package/scripts/notify-shared.mjs +0 -2
- package/scripts/notify-source.mjs +113 -0
- package/scripts/notify-ui.mjs +40 -6
- package/scripts/notify.mjs +120 -59
- package/scripts/plan-contract.mjs +210 -0
- package/scripts/project-storage.mjs +235 -0
- package/scripts/ralph-loop.mjs +9 -58
- package/scripts/replay-state.mjs +210 -0
- package/scripts/review-state.mjs +220 -0
- package/scripts/runtime-context.mjs +74 -0
- package/scripts/verify-state.mjs +226 -0
- package/scripts/visual-state.mjs +244 -0
- package/scripts/workflow-core.mjs +165 -0
- package/scripts/workflow-plan-files.mjs +249 -0
- package/scripts/workflow-recommendation.mjs +335 -0
- package/scripts/workflow-state.mjs +113 -0
- package/skills/commands/auto/SKILL.md +37 -71
- package/skills/commands/build/SKILL.md +67 -0
- package/skills/commands/clean/SKILL.md +10 -8
- package/skills/commands/commit/SKILL.md +8 -4
- package/skills/commands/help/SKILL.md +19 -11
- package/skills/commands/idea/SKILL.md +55 -0
- package/skills/commands/init/SKILL.md +6 -3
- package/skills/commands/loop/SKILL.md +6 -5
- package/skills/commands/plan/SKILL.md +116 -0
- package/skills/commands/prd/SKILL.md +20 -15
- package/skills/commands/verify/SKILL.md +32 -9
- package/skills/commands/wiki/SKILL.md +59 -0
- package/skills/hello-review/SKILL.md +9 -0
- package/skills/hello-subagent/SKILL.md +4 -3
- package/skills/hello-ui/SKILL.md +36 -8
- package/skills/hello-verify/SKILL.md +10 -2
- package/skills/helloagents/SKILL.md +24 -13
- package/templates/DESIGN.md +25 -4
- package/templates/STATE.md +3 -0
- package/templates/plans/contract.json +48 -0
- package/templates/plans/plan.md +23 -0
- package/templates/plans/tasks.md +3 -3
- package/skills/commands/design/SKILL.md +0 -108
- package/skills/commands/review/SKILL.md +0 -16
- 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
|
+
}
|