helloagents 3.0.3-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 +134 -41
- package/README_CN.md +135 -42
- package/bootstrap-lite.md +104 -46
- package/bootstrap.md +143 -113
- 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 +17 -10
- 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 +7 -5
- 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,226 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { execSync } from 'node:child_process'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { appendReplayEvent } from './replay-state.mjs'
|
|
5
|
+
import { getProjectVerifyYamlPath } from './project-storage.mjs'
|
|
6
|
+
|
|
7
|
+
export const VERIFY_EVIDENCE_FILE_NAME = '.ralph-verify.json'
|
|
8
|
+
const VERIFY_EVIDENCE_MAX_AGE_MS = 30 * 60 * 1000
|
|
9
|
+
const SHELL_OPERATORS = /[;&|`$(){}\n\r]/
|
|
10
|
+
|
|
11
|
+
export function getVerifyEvidencePath(cwd) {
|
|
12
|
+
return join(cwd, '.helloagents', VERIFY_EVIDENCE_FILE_NAME)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function readVerifyEvidence(cwd) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(readFileSync(getVerifyEvidencePath(cwd), 'utf-8'))
|
|
18
|
+
} catch {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function clearVerifyEvidence(cwd) {
|
|
24
|
+
rmSync(getVerifyEvidencePath(cwd), { force: true })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function loadVerifyYaml(cwd) {
|
|
28
|
+
const f = getProjectVerifyYamlPath(cwd)
|
|
29
|
+
if (!existsSync(f)) return null
|
|
30
|
+
try {
|
|
31
|
+
const content = readFileSync(f, 'utf-8')
|
|
32
|
+
const cmds = []
|
|
33
|
+
let inCmds = false
|
|
34
|
+
for (const line of content.split('\n')) {
|
|
35
|
+
const s = line.trim()
|
|
36
|
+
if (s.startsWith('commands:')) { inCmds = true; continue }
|
|
37
|
+
if (inCmds) {
|
|
38
|
+
if (s.startsWith('- ') && !s.startsWith('# ')) {
|
|
39
|
+
const cmd = s.slice(2).trim().replace(/^["']|["']$/g, '')
|
|
40
|
+
if (cmd && !cmd.startsWith('#')) cmds.push(cmd)
|
|
41
|
+
} else if (s && !s.startsWith('#')) {
|
|
42
|
+
break
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return cmds.length ? cmds : null
|
|
47
|
+
} catch {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function detectFromPackageJson(cwd) {
|
|
53
|
+
const f = join(cwd, 'package.json')
|
|
54
|
+
if (!existsSync(f)) return []
|
|
55
|
+
try {
|
|
56
|
+
const scripts = JSON.parse(readFileSync(f, 'utf-8')).scripts || {}
|
|
57
|
+
return ['lint', 'typecheck', 'type-check', 'test', 'build']
|
|
58
|
+
.filter((k) => k in scripts)
|
|
59
|
+
.map((k) => `npm run ${k}`)
|
|
60
|
+
} catch {
|
|
61
|
+
return []
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function detectFromPyproject(cwd) {
|
|
66
|
+
const f = join(cwd, 'pyproject.toml')
|
|
67
|
+
if (!existsSync(f)) return []
|
|
68
|
+
try {
|
|
69
|
+
const content = readFileSync(f, 'utf-8')
|
|
70
|
+
const cmds = []
|
|
71
|
+
if (content.includes('[tool.ruff')) cmds.push('ruff check .')
|
|
72
|
+
if (content.includes('[tool.mypy')) cmds.push('mypy .')
|
|
73
|
+
if (content.includes('[tool.pytest')) cmds.push('pytest --tb=short -q')
|
|
74
|
+
return cmds
|
|
75
|
+
} catch {
|
|
76
|
+
return []
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function detectCommands(cwd) {
|
|
81
|
+
const yaml = loadVerifyYaml(cwd)
|
|
82
|
+
if (yaml?.length) return yaml
|
|
83
|
+
const pkg = detectFromPackageJson(cwd)
|
|
84
|
+
if (pkg.length) return pkg
|
|
85
|
+
return detectFromPyproject(cwd)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function hasUnsafeVerifyCommand(commands = []) {
|
|
89
|
+
return commands.some((cmd) => SHELL_OPERATORS.test(cmd))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readGitDiffStat(cwd, args) {
|
|
93
|
+
try {
|
|
94
|
+
return execSync(`git diff --stat ${args}`.trim(), {
|
|
95
|
+
cwd,
|
|
96
|
+
encoding: 'utf-8',
|
|
97
|
+
timeout: 10_000,
|
|
98
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
99
|
+
}).trim()
|
|
100
|
+
} catch {
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function captureWorkspaceFingerprint(cwd) {
|
|
106
|
+
const unstaged = readGitDiffStat(cwd, 'HEAD')
|
|
107
|
+
const staged = readGitDiffStat(cwd, '--cached')
|
|
108
|
+
const available = unstaged !== null || staged !== null
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
available,
|
|
112
|
+
unstaged: unstaged || '',
|
|
113
|
+
staged: staged || '',
|
|
114
|
+
combined: `${unstaged || ''}\n---\n${staged || ''}`.trim(),
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function writeVerifyEvidence(cwd, { commands = [], fastOnly = false, source = 'ralph-loop' } = {}) {
|
|
119
|
+
mkdirSync(join(cwd, '.helloagents'), { recursive: true })
|
|
120
|
+
const payload = {
|
|
121
|
+
updatedAt: new Date().toISOString(),
|
|
122
|
+
commands,
|
|
123
|
+
fastOnly,
|
|
124
|
+
source,
|
|
125
|
+
fingerprint: captureWorkspaceFingerprint(cwd),
|
|
126
|
+
}
|
|
127
|
+
writeFileSync(getVerifyEvidencePath(cwd), `${JSON.stringify(payload, null, 2)}\n`, 'utf-8')
|
|
128
|
+
appendReplayEvent(cwd, {
|
|
129
|
+
event: 'verify_evidence_written',
|
|
130
|
+
source,
|
|
131
|
+
details: {
|
|
132
|
+
commands,
|
|
133
|
+
fastOnly,
|
|
134
|
+
},
|
|
135
|
+
artifacts: ['.helloagents/.ralph-verify.json'],
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function validateVerifyEvidencePresence(commands, evidence) {
|
|
140
|
+
if (evidence) return null
|
|
141
|
+
return {
|
|
142
|
+
required: true,
|
|
143
|
+
status: 'missing',
|
|
144
|
+
commands,
|
|
145
|
+
details: ['missing successful verification evidence for the current workflow'],
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function validateVerifyEvidenceFreshness(cwd, commands, evidence, now) {
|
|
150
|
+
if (evidence.fastOnly) {
|
|
151
|
+
return {
|
|
152
|
+
required: true,
|
|
153
|
+
status: 'fast-only',
|
|
154
|
+
commands,
|
|
155
|
+
evidence,
|
|
156
|
+
details: ['latest verification evidence only covers subagent fast checks'],
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const updatedAt = Date.parse(evidence.updatedAt || '')
|
|
161
|
+
if (!Number.isFinite(updatedAt)) {
|
|
162
|
+
return {
|
|
163
|
+
required: true,
|
|
164
|
+
status: 'invalid',
|
|
165
|
+
commands,
|
|
166
|
+
evidence,
|
|
167
|
+
details: ['verification evidence timestamp is invalid'],
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (now - updatedAt > VERIFY_EVIDENCE_MAX_AGE_MS) {
|
|
171
|
+
return {
|
|
172
|
+
required: true,
|
|
173
|
+
status: 'stale-time',
|
|
174
|
+
commands,
|
|
175
|
+
evidence,
|
|
176
|
+
details: ['verification evidence is older than 30 minutes'],
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function validateVerifyFingerprint(cwd, commands, evidence) {
|
|
183
|
+
const currentFingerprint = captureWorkspaceFingerprint(cwd)
|
|
184
|
+
if (
|
|
185
|
+
currentFingerprint.available
|
|
186
|
+
&& evidence.fingerprint?.available
|
|
187
|
+
&& currentFingerprint.combined !== evidence.fingerprint.combined
|
|
188
|
+
) {
|
|
189
|
+
return {
|
|
190
|
+
required: true,
|
|
191
|
+
status: 'stale-diff',
|
|
192
|
+
commands,
|
|
193
|
+
evidence,
|
|
194
|
+
details: ['workspace diff changed after the last successful verification evidence'],
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return null
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function getVerifyEvidenceStatus(cwd, now = Date.now()) {
|
|
201
|
+
const commands = detectCommands(cwd)
|
|
202
|
+
if (!commands.length) {
|
|
203
|
+
return {
|
|
204
|
+
required: false,
|
|
205
|
+
status: 'not-applicable',
|
|
206
|
+
commands,
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const evidence = readVerifyEvidence(cwd)
|
|
211
|
+
const missingError = validateVerifyEvidencePresence(commands, evidence)
|
|
212
|
+
if (missingError) return missingError
|
|
213
|
+
|
|
214
|
+
const freshnessError = validateVerifyEvidenceFreshness(cwd, commands, evidence, now)
|
|
215
|
+
if (freshnessError) return freshnessError
|
|
216
|
+
|
|
217
|
+
const fingerprintError = validateVerifyFingerprint(cwd, commands, evidence)
|
|
218
|
+
if (fingerprintError) return fingerprintError
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
required: true,
|
|
222
|
+
status: 'valid',
|
|
223
|
+
commands,
|
|
224
|
+
evidence,
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
import { appendReplayEvent } from './replay-state.mjs'
|
|
6
|
+
import { captureWorkspaceFingerprint } from './verify-state.mjs'
|
|
7
|
+
|
|
8
|
+
export const VISUAL_EVIDENCE_FILE_NAME = '.ralph-visual.json'
|
|
9
|
+
const VISUAL_EVIDENCE_MAX_AGE_MS = 30 * 60 * 1000
|
|
10
|
+
const VALID_VISUAL_STATUSES = new Set(['PASS', 'BLOCKED'])
|
|
11
|
+
|
|
12
|
+
function normalizeStringArray(values) {
|
|
13
|
+
if (!Array.isArray(values)) return []
|
|
14
|
+
return [...new Set(values.map((value) => (typeof value === 'string' ? value.trim() : '')).filter(Boolean))]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeVisualStatus(value) {
|
|
18
|
+
const normalized = typeof value === 'string' ? value.trim().toUpperCase() : ''
|
|
19
|
+
return VALID_VISUAL_STATUSES.has(normalized) ? normalized : ''
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function findMissingCoverage(requested = [], completed = []) {
|
|
23
|
+
const completedSet = new Set(normalizeStringArray(completed))
|
|
24
|
+
return normalizeStringArray(requested).filter((entry) => !completedSet.has(entry))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getVisualEvidencePath(cwd) {
|
|
28
|
+
return join(cwd, '.helloagents', VISUAL_EVIDENCE_FILE_NAME)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function readVisualEvidence(cwd) {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(readFileSync(getVisualEvidencePath(cwd), 'utf-8'))
|
|
34
|
+
} catch {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function clearVisualEvidence(cwd) {
|
|
40
|
+
rmSync(getVisualEvidencePath(cwd), { force: true })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeVisualEvidence(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
|
+
tooling: normalizeStringArray(input.tooling),
|
|
49
|
+
screensChecked: normalizeStringArray(input.screensChecked),
|
|
50
|
+
statesChecked: normalizeStringArray(input.statesChecked),
|
|
51
|
+
status: normalizeVisualStatus(input.status),
|
|
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 writeVisualEvidence(cwd, input = {}) {
|
|
59
|
+
mkdirSync(join(cwd, '.helloagents'), { recursive: true })
|
|
60
|
+
const normalized = normalizeVisualEvidence(input)
|
|
61
|
+
const payload = {
|
|
62
|
+
updatedAt: new Date().toISOString(),
|
|
63
|
+
...normalized,
|
|
64
|
+
fingerprint: captureWorkspaceFingerprint(cwd),
|
|
65
|
+
}
|
|
66
|
+
writeFileSync(getVisualEvidencePath(cwd), `${JSON.stringify(payload, null, 2)}\n`, 'utf-8')
|
|
67
|
+
appendReplayEvent(cwd, {
|
|
68
|
+
event: 'visual_evidence_written',
|
|
69
|
+
source: normalized.source,
|
|
70
|
+
skillName: normalized.originCommand,
|
|
71
|
+
details: {
|
|
72
|
+
reason: normalized.reason,
|
|
73
|
+
tooling: normalized.tooling,
|
|
74
|
+
screensChecked: normalized.screensChecked,
|
|
75
|
+
statesChecked: normalized.statesChecked,
|
|
76
|
+
status: normalized.status,
|
|
77
|
+
},
|
|
78
|
+
artifacts: ['.helloagents/.ralph-visual.json'],
|
|
79
|
+
})
|
|
80
|
+
return payload
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readRequiredVisualEvidence(cwd, required) {
|
|
84
|
+
if (!required) {
|
|
85
|
+
return {
|
|
86
|
+
required: false,
|
|
87
|
+
status: 'not-applicable',
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const evidence = readVisualEvidence(cwd)
|
|
92
|
+
if (evidence) return { evidence }
|
|
93
|
+
return {
|
|
94
|
+
error: {
|
|
95
|
+
required: true,
|
|
96
|
+
status: 'missing',
|
|
97
|
+
details: ['missing visual validation evidence required by the active UI contract'],
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function validateVisualTimestamp(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: ['visual validation evidence timestamp is invalid'],
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (now - updatedAt > VISUAL_EVIDENCE_MAX_AGE_MS) {
|
|
113
|
+
return {
|
|
114
|
+
required: true,
|
|
115
|
+
status: 'stale-time',
|
|
116
|
+
evidence,
|
|
117
|
+
details: ['visual validation evidence is older than 30 minutes'],
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function validateVisualFingerprint(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 visual validation evidence'],
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function validateVisualContent(evidence, { screens = [], states = [] } = {}) {
|
|
141
|
+
const normalized = normalizeVisualEvidence(evidence)
|
|
142
|
+
if (!normalized.status || !normalized.summary || !normalized.reason) {
|
|
143
|
+
return {
|
|
144
|
+
required: true,
|
|
145
|
+
status: 'invalid',
|
|
146
|
+
evidence,
|
|
147
|
+
details: ['visual validation evidence must record explicit status, reason, and summary'],
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (normalized.tooling.length === 0) {
|
|
151
|
+
return {
|
|
152
|
+
required: true,
|
|
153
|
+
status: 'invalid',
|
|
154
|
+
evidence,
|
|
155
|
+
details: ['visual validation evidence must record the tooling used for the check'],
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (normalized.screensChecked.length === 0 && normalized.statesChecked.length === 0) {
|
|
159
|
+
return {
|
|
160
|
+
required: true,
|
|
161
|
+
status: 'invalid',
|
|
162
|
+
evidence,
|
|
163
|
+
details: ['visual validation evidence must record at least one checked screen or state'],
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const missingScreens = findMissingCoverage(screens, normalized.screensChecked)
|
|
168
|
+
if (missingScreens.length > 0) {
|
|
169
|
+
return {
|
|
170
|
+
required: true,
|
|
171
|
+
status: 'invalid',
|
|
172
|
+
evidence,
|
|
173
|
+
details: [`visual validation evidence does not cover requested screens: ${missingScreens.join(', ')}`],
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const missingStates = findMissingCoverage(states, normalized.statesChecked)
|
|
178
|
+
if (missingStates.length > 0) {
|
|
179
|
+
return {
|
|
180
|
+
required: true,
|
|
181
|
+
status: 'invalid',
|
|
182
|
+
evidence,
|
|
183
|
+
details: [`visual validation evidence does not cover requested states: ${missingStates.join(', ')}`],
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (normalized.status !== 'PASS') {
|
|
188
|
+
return {
|
|
189
|
+
required: true,
|
|
190
|
+
status: 'blocked',
|
|
191
|
+
evidence,
|
|
192
|
+
details: ['latest visual validation evidence still records blocking findings'],
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return null
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function getVisualEvidenceStatus(cwd, { required = false, screens = [], states = [], now = Date.now() } = {}) {
|
|
199
|
+
const requiredEvidence = readRequiredVisualEvidence(cwd, required)
|
|
200
|
+
if ('status' in requiredEvidence) return requiredEvidence
|
|
201
|
+
if (requiredEvidence.error) return requiredEvidence.error
|
|
202
|
+
|
|
203
|
+
const { evidence } = requiredEvidence
|
|
204
|
+
const timestampError = validateVisualTimestamp(evidence, now)
|
|
205
|
+
if (timestampError) return timestampError
|
|
206
|
+
|
|
207
|
+
const fingerprintError = validateVisualFingerprint(cwd, evidence)
|
|
208
|
+
if (fingerprintError) return fingerprintError
|
|
209
|
+
|
|
210
|
+
const contentError = validateVisualContent(evidence, { screens, states })
|
|
211
|
+
if (contentError) return contentError
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
required: true,
|
|
215
|
+
status: 'valid',
|
|
216
|
+
evidence: normalizeVisualEvidence(evidence),
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function readStdinJson() {
|
|
221
|
+
try {
|
|
222
|
+
return JSON.parse(readFileSync(0, 'utf-8'))
|
|
223
|
+
} catch {
|
|
224
|
+
return {}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function main() {
|
|
229
|
+
const command = process.argv[2] || ''
|
|
230
|
+
if (command !== 'write') return
|
|
231
|
+
|
|
232
|
+
const input = readStdinJson()
|
|
233
|
+
const cwd = input.cwd || process.cwd()
|
|
234
|
+
const payload = writeVisualEvidence(cwd, input)
|
|
235
|
+
process.stdout.write(JSON.stringify({
|
|
236
|
+
suppressOutput: true,
|
|
237
|
+
path: getVisualEvidencePath(cwd),
|
|
238
|
+
payload,
|
|
239
|
+
}))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
243
|
+
main()
|
|
244
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
getWorkflowSnapshot,
|
|
6
|
+
listPlanPackages,
|
|
7
|
+
normalizeTaskFile,
|
|
8
|
+
readStateSnapshot,
|
|
9
|
+
} from './workflow-plan-files.mjs'
|
|
10
|
+
import { getAdvisorRequirement, getVisualValidationRequirement } from './plan-contract.mjs'
|
|
11
|
+
import { describeProjectStoreFile, getProjectDesignContractPath } from './project-storage.mjs'
|
|
12
|
+
|
|
13
|
+
export function getTargetPlans(snapshot) {
|
|
14
|
+
return snapshot.activePlans.length > 0 ? snapshot.activePlans : snapshot.plans
|
|
15
|
+
}
|
|
16
|
+
export function classifyPlan(plan) {
|
|
17
|
+
if (!plan) {
|
|
18
|
+
return {
|
|
19
|
+
status: 'none',
|
|
20
|
+
details: [],
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const details = [
|
|
25
|
+
...plan.missingFiles.map((file) => `缺少 ${file}`),
|
|
26
|
+
...plan.templateIssues,
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
if (details.length > 0) {
|
|
30
|
+
return {
|
|
31
|
+
status: 'incomplete',
|
|
32
|
+
details,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (plan.taskSummary.total === 0) {
|
|
37
|
+
return {
|
|
38
|
+
status: 'missing-task-checklist',
|
|
39
|
+
details: ['tasks.md 尚未形成可执行 checklist'],
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (plan.taskSummary.open > 0) {
|
|
44
|
+
return {
|
|
45
|
+
status: 'in-progress',
|
|
46
|
+
details: plan.taskSummary.items
|
|
47
|
+
.filter((item) => item.status === 'open')
|
|
48
|
+
.slice(0, 3)
|
|
49
|
+
.map((item) => item.text),
|
|
50
|
+
openCount: plan.taskSummary.open,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
status: 'closed',
|
|
56
|
+
details: [],
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function determineVerifyMode(plan) {
|
|
61
|
+
if (!plan) return null
|
|
62
|
+
|
|
63
|
+
if (plan.contractIssues.length > 0) {
|
|
64
|
+
return {
|
|
65
|
+
mode: 'metadata-first',
|
|
66
|
+
reason: '方案包缺少可信的结构化 contract',
|
|
67
|
+
guidance: '验证分流:当前还不适合直接进入 reviewer / tester;先回到 ~plan / ~prd 补齐 `contract.json`,明确 `verifyMode`、`reviewerFocus` 和 `testerFocus`。',
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (plan.contract?.verifyMode === 'review-first') {
|
|
72
|
+
const reviewerFocus = plan.contract.reviewerFocus.join(';')
|
|
73
|
+
const testerFocus = plan.contract.testerFocus.join(';')
|
|
74
|
+
return {
|
|
75
|
+
mode: 'review-first',
|
|
76
|
+
reason: '方案 contract 已明确要求审查优先',
|
|
77
|
+
guidance: `验证分流:当前更适合审查优先;先执行 reviewer / hello-review 范围审查,再交给 tester / hello-verify 跑完整验证。${reviewerFocus ? ` reviewer 重点:${reviewerFocus}。` : ''}${testerFocus ? ` tester 重点:${testerFocus}。` : ''}`.trim(),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (plan.taskSummary.underSpecifiedCount > 0) {
|
|
82
|
+
return {
|
|
83
|
+
mode: 'metadata-first',
|
|
84
|
+
reason: 'tasks.md 仍缺少可信的任务元数据',
|
|
85
|
+
guidance: '验证分流:当前还不适合直接进入 reviewer / tester;先补齐 tasks.md 中每个任务的“涉及文件”“完成标准”和“验证方式”。',
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
mode: 'test-first',
|
|
91
|
+
reason: '方案 contract 已明确验证主路径,且任务 contract 已完整',
|
|
92
|
+
guidance: `验证分流:当前更适合测试优先;先执行 tester / hello-verify 跑完整验证,再针对失败点或关键边界补充 hello-review。${plan.contract?.testerFocus?.length ? ` tester 重点:${plan.contract.testerFocus.join(';')}。` : ''}`.trim(),
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function collectStateSyncIssues(snapshot) {
|
|
97
|
+
const issues = []
|
|
98
|
+
const hasPlans = snapshot.plans.length > 0
|
|
99
|
+
const state = snapshot.state
|
|
100
|
+
|
|
101
|
+
if (!hasPlans) {
|
|
102
|
+
return issues
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!state.exists) {
|
|
106
|
+
issues.push('当前已存在方案包,但 `.helloagents/STATE.md` 缺失')
|
|
107
|
+
return issues
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!state.referencedPlanDir) {
|
|
111
|
+
issues.push('当前已存在方案包,但 `STATE.md` 未记录活跃方案路径')
|
|
112
|
+
}
|
|
113
|
+
if (!state.sections['主线目标']) {
|
|
114
|
+
issues.push('`STATE.md` 缺少“主线目标”')
|
|
115
|
+
}
|
|
116
|
+
if (!state.sections['正在做什么']) {
|
|
117
|
+
issues.push('`STATE.md` 缺少“正在做什么”')
|
|
118
|
+
}
|
|
119
|
+
if (!state.sections['下一步']) {
|
|
120
|
+
issues.push('`STATE.md` 缺少“下一步”')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return issues
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function buildVerifyModeHintFromSnapshot(snapshot) {
|
|
127
|
+
const plan = getTargetPlans(snapshot)[0]
|
|
128
|
+
if (!plan) return ''
|
|
129
|
+
if (!plan.planSections['风险与验证'] && plan.taskSummary.total === 0) return ''
|
|
130
|
+
return determineVerifyMode(plan)?.guidance || ''
|
|
131
|
+
}
|
|
132
|
+
export function buildStateSyncHintFromSnapshot(snapshot) {
|
|
133
|
+
const issues = collectStateSyncIssues(snapshot)
|
|
134
|
+
if (issues.length === 0) return ''
|
|
135
|
+
return `STATE.md 提醒:${issues.join(';')};继续项目级流程、收尾或进入压缩前先同步恢复快照。`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function buildStateRoleHintFromSnapshot(snapshot) {
|
|
139
|
+
if (!snapshot.state.exists || snapshot.plans.length > 0) return ''
|
|
140
|
+
return '恢复约束:当前仅检测到 `.helloagents/STATE.md`;先以当前用户消息、显式命令和代码事实确认主线,STATE.md 只用于找回上次停在哪,不是当前任务的自动授权或唯一事实源。'
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function buildUiContractHint(cwd, snapshot) {
|
|
144
|
+
const targetPlans = getTargetPlans(snapshot)
|
|
145
|
+
const hasDesignContract = existsSync(getProjectDesignContractPath(cwd))
|
|
146
|
+
const hasPrdUiArtifact = targetPlans.some((plan) => existsSync(join(plan.dirPath, 'prd', '03-ui-design.md')))
|
|
147
|
+
const hasUiContract = targetPlans.some((plan) => plan.contract?.ui?.required)
|
|
148
|
+
const styleAdvisorRequired = targetPlans.some((plan) => getAdvisorRequirement(plan.contract).styleRequired)
|
|
149
|
+
const visualValidationRequired = targetPlans.some((plan) => getVisualValidationRequirement(plan.contract).required)
|
|
150
|
+
|
|
151
|
+
if (!hasDesignContract && !hasPrdUiArtifact && !hasUiContract) {
|
|
152
|
+
return ''
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const extraHints = []
|
|
156
|
+
if (styleAdvisorRequired) {
|
|
157
|
+
extraHints.push('若当前 UI contract 要求 style advisor,收尾前需复用 `.helloagents/.ralph-advisor.json` 留下独立复查证据')
|
|
158
|
+
}
|
|
159
|
+
if (visualValidationRequired) {
|
|
160
|
+
extraHints.push('若当前 UI contract 要求视觉验收,收尾前需写 `.helloagents/.ralph-visual.json` 记录关键视口、状态与结论')
|
|
161
|
+
}
|
|
162
|
+
return `UI 约束提示:如本次属于视觉/交互链路,设计决策优先级固定为:当前活跃 plan.md / prd/03-ui-design.md → ${describeProjectStoreFile(cwd, 'DESIGN.md')} → hello-ui。${extraHints.length > 0 ? ` ${extraHints.join(';')}。` : ''}`
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export { normalizeTaskFile, readStateSnapshot, listPlanPackages, getWorkflowSnapshot }
|