helloagents 3.0.33 → 3.0.35
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/plugin.json +2 -2
- package/.codex-plugin/plugin.json +3 -4
- package/README.md +70 -71
- package/README_CN.md +70 -71
- package/bootstrap-lite.md +9 -11
- package/bootstrap.md +21 -23
- package/gemini-extension.json +1 -1
- package/install.ps1 +21 -3
- package/install.sh +19 -2
- package/package.json +2 -2
- package/scripts/capability-registry.mjs +5 -3
- package/scripts/cli-doctor-codex.mjs +150 -1
- package/scripts/cli-doctor-render.mjs +2 -1
- package/scripts/cli-lifecycle-hosts.mjs +76 -34
- package/scripts/cli-lifecycle.mjs +50 -15
- package/scripts/cli-messages.mjs +5 -5
- package/scripts/delivery-gate-messages.mjs +5 -4
- package/scripts/delivery-gate.mjs +11 -22
- package/scripts/guard.mjs +1 -1
- package/scripts/notify-closeout.mjs +61 -22
- package/scripts/notify-context.mjs +5 -5
- package/scripts/notify-route.mjs +1 -1
- package/scripts/notify.mjs +2 -2
- package/scripts/plan-contract.mjs +10 -14
- package/scripts/project-session-cleanup.mjs +45 -31
- package/scripts/qa-review-state.mjs +313 -0
- package/scripts/ralph-loop.mjs +32 -13
- package/scripts/runtime-scope.mjs +1 -3
- package/scripts/session-capsule.mjs +51 -13
- package/scripts/state-document.mjs +77 -0
- package/scripts/workflow-core.mjs +13 -19
- package/scripts/workflow-plan-files.mjs +1 -1
- package/scripts/workflow-recommendation.mjs +55 -67
- package/scripts/workflow-state.mjs +8 -8
- package/skills/commands/auto/SKILL.md +12 -12
- package/skills/commands/build/SKILL.md +9 -10
- package/skills/commands/commit/SKILL.md +1 -1
- package/skills/commands/help/SKILL.md +11 -13
- package/skills/commands/init/SKILL.md +18 -9
- package/skills/commands/loop/SKILL.md +70 -96
- package/skills/commands/plan/SKILL.md +7 -8
- package/skills/commands/prd/SKILL.md +3 -3
- package/skills/commands/qa/SKILL.md +49 -0
- package/skills/hello-ui/SKILL.md +3 -3
- package/skills/helloagents/SKILL.md +11 -14
- package/skills/qa-review/SKILL.md +92 -0
- package/templates/plans/contract.json +4 -7
- package/templates/plans/plan.md +1 -1
- package/templates/plans/tasks.md +1 -1
- package/templates/verify.yaml +1 -1
- package/scripts/review-state.mjs +0 -193
- package/scripts/verify-state.mjs +0 -175
- package/skills/commands/global/SKILL.md +0 -71
- package/skills/commands/verify/SKILL.md +0 -46
- package/skills/commands/wiki/SKILL.md +0 -57
- package/skills/hello-review/SKILL.md +0 -42
- package/skills/hello-verify/SKILL.md +0 -144
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { existsSync, readFileSync } 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 {
|
|
7
|
+
captureWorkspaceFingerprint,
|
|
8
|
+
clearRuntimeEvidence,
|
|
9
|
+
getRuntimeEvidencePath,
|
|
10
|
+
getRuntimeEvidenceRelativePath,
|
|
11
|
+
readRuntimeEvidence,
|
|
12
|
+
validateEvidenceFingerprint,
|
|
13
|
+
validateEvidenceTimestamp,
|
|
14
|
+
writeRuntimeEvidence,
|
|
15
|
+
} from './runtime-artifacts.mjs'
|
|
16
|
+
import { getProjectVerifyYamlPath } from './project-storage.mjs'
|
|
17
|
+
|
|
18
|
+
export const QA_REVIEW_EVIDENCE_FILE_NAME = 'qa-review.json'
|
|
19
|
+
const VALID_QA_MODES = new Set(['standard', 'deep'])
|
|
20
|
+
const VALID_QA_OUTCOMES = new Set(['clean', 'findings'])
|
|
21
|
+
const SHELL_OPERATORS = /[;&|`$(){}\n\r]/
|
|
22
|
+
|
|
23
|
+
function normalizeStringArray(values) {
|
|
24
|
+
if (!Array.isArray(values)) return []
|
|
25
|
+
return [...new Set(values
|
|
26
|
+
.map((value) => (typeof value === 'string' ? value.trim() : ''))
|
|
27
|
+
.filter(Boolean))]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeQaMode(value) {
|
|
31
|
+
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
32
|
+
return VALID_QA_MODES.has(normalized) ? normalized : ''
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeQaOutcome(value) {
|
|
36
|
+
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''
|
|
37
|
+
return VALID_QA_OUTCOMES.has(normalized) ? normalized : ''
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeCommands(values) {
|
|
41
|
+
return normalizeStringArray(values)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function loadVerifyYaml(cwd) {
|
|
45
|
+
const filePath = getProjectVerifyYamlPath(cwd)
|
|
46
|
+
if (!existsSync(filePath)) return null
|
|
47
|
+
try {
|
|
48
|
+
const content = readFileSync(filePath, 'utf-8')
|
|
49
|
+
const commands = []
|
|
50
|
+
let inCommands = false
|
|
51
|
+
for (const line of content.split('\n')) {
|
|
52
|
+
const trimmed = line.trim()
|
|
53
|
+
if (trimmed.startsWith('commands:')) {
|
|
54
|
+
inCommands = true
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
if (!inCommands) continue
|
|
58
|
+
if (trimmed.startsWith('- ') && !trimmed.startsWith('# ')) {
|
|
59
|
+
const command = trimmed.slice(2).trim().replace(/^["']|["']$/g, '')
|
|
60
|
+
if (command && !command.startsWith('#')) commands.push(command)
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
if (trimmed && !trimmed.startsWith('#')) break
|
|
64
|
+
}
|
|
65
|
+
return commands.length > 0 ? commands : null
|
|
66
|
+
} catch {
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function detectFromPackageJson(cwd) {
|
|
72
|
+
const filePath = join(cwd, 'package.json')
|
|
73
|
+
if (!existsSync(filePath)) return []
|
|
74
|
+
try {
|
|
75
|
+
const scripts = JSON.parse(readFileSync(filePath, 'utf-8')).scripts || {}
|
|
76
|
+
return ['lint', 'typecheck', 'type-check', 'test', 'build']
|
|
77
|
+
.filter((key) => key in scripts)
|
|
78
|
+
.map((key) => `npm run ${key}`)
|
|
79
|
+
} catch {
|
|
80
|
+
return []
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function detectFromPyproject(cwd) {
|
|
85
|
+
const filePath = join(cwd, 'pyproject.toml')
|
|
86
|
+
if (!existsSync(filePath)) return []
|
|
87
|
+
try {
|
|
88
|
+
const content = readFileSync(filePath, 'utf-8')
|
|
89
|
+
const commands = []
|
|
90
|
+
if (content.includes('[tool.ruff')) commands.push('ruff check .')
|
|
91
|
+
if (content.includes('[tool.mypy')) commands.push('mypy .')
|
|
92
|
+
if (content.includes('[tool.pytest')) commands.push('pytest --tb=short -q')
|
|
93
|
+
return commands
|
|
94
|
+
} catch {
|
|
95
|
+
return []
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function detectCommands(cwd) {
|
|
100
|
+
const yamlCommands = loadVerifyYaml(cwd)
|
|
101
|
+
if (yamlCommands?.length) return yamlCommands
|
|
102
|
+
const packageJsonCommands = detectFromPackageJson(cwd)
|
|
103
|
+
if (packageJsonCommands.length > 0) return packageJsonCommands
|
|
104
|
+
return detectFromPyproject(cwd)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function hasUnsafeQaCommand(commands = []) {
|
|
108
|
+
return commands.some((command) => SHELL_OPERATORS.test(command))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getQaReviewEvidencePath(cwd, options = {}) {
|
|
112
|
+
return getRuntimeEvidencePath(cwd, QA_REVIEW_EVIDENCE_FILE_NAME, options)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function readQaReviewEvidence(cwd, options = {}) {
|
|
116
|
+
return readRuntimeEvidence(cwd, QA_REVIEW_EVIDENCE_FILE_NAME, options)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function clearQaReviewEvidence(cwd, options = {}) {
|
|
120
|
+
clearRuntimeEvidence(cwd, QA_REVIEW_EVIDENCE_FILE_NAME, options)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function normalizeQaReviewEvidence(input = {}) {
|
|
124
|
+
return {
|
|
125
|
+
source: typeof input.source === 'string' && input.source.trim() ? input.source.trim() : 'manual',
|
|
126
|
+
originCommand: typeof input.originCommand === 'string' ? input.originCommand.trim() : '',
|
|
127
|
+
qaMode: normalizeQaMode(input.qaMode) || 'standard',
|
|
128
|
+
scope: typeof input.scope === 'string' ? input.scope.trim() : '',
|
|
129
|
+
outcome: normalizeQaOutcome(input.outcome),
|
|
130
|
+
conclusion: typeof input.conclusion === 'string' ? input.conclusion.trim() : '',
|
|
131
|
+
findings: normalizeStringArray(input.findings),
|
|
132
|
+
fileReferences: normalizeStringArray(input.fileReferences),
|
|
133
|
+
commands: normalizeCommands(input.commands),
|
|
134
|
+
fastOnly: Boolean(input.fastOnly),
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function writeQaReviewEvidence(cwd, {
|
|
139
|
+
source = 'manual',
|
|
140
|
+
originCommand = '',
|
|
141
|
+
qaMode = 'standard',
|
|
142
|
+
scope = '',
|
|
143
|
+
outcome = '',
|
|
144
|
+
conclusion = '',
|
|
145
|
+
findings = [],
|
|
146
|
+
fileReferences = [],
|
|
147
|
+
commands = [],
|
|
148
|
+
fastOnly = false,
|
|
149
|
+
} = {}, options = {}) {
|
|
150
|
+
const normalized = normalizeQaReviewEvidence({
|
|
151
|
+
source,
|
|
152
|
+
originCommand,
|
|
153
|
+
qaMode,
|
|
154
|
+
scope,
|
|
155
|
+
outcome,
|
|
156
|
+
conclusion,
|
|
157
|
+
findings,
|
|
158
|
+
fileReferences,
|
|
159
|
+
commands,
|
|
160
|
+
fastOnly,
|
|
161
|
+
})
|
|
162
|
+
const payload = {
|
|
163
|
+
schemaVersion: 2,
|
|
164
|
+
updatedAt: new Date().toISOString(),
|
|
165
|
+
source: normalized.source,
|
|
166
|
+
originCommand: normalized.originCommand,
|
|
167
|
+
qaMode: normalized.qaMode,
|
|
168
|
+
scope: normalized.scope,
|
|
169
|
+
outcome: normalized.outcome,
|
|
170
|
+
conclusion: normalized.conclusion,
|
|
171
|
+
findings: normalized.findings,
|
|
172
|
+
fileReferences: normalized.fileReferences,
|
|
173
|
+
commands: normalized.commands,
|
|
174
|
+
fastOnly: normalized.fastOnly,
|
|
175
|
+
fingerprint: captureWorkspaceFingerprint(cwd),
|
|
176
|
+
}
|
|
177
|
+
writeRuntimeEvidence(cwd, QA_REVIEW_EVIDENCE_FILE_NAME, payload, options)
|
|
178
|
+
appendReplayEvent(cwd, {
|
|
179
|
+
event: 'qa_review_evidence_written',
|
|
180
|
+
source: normalized.source,
|
|
181
|
+
skillName: normalized.originCommand,
|
|
182
|
+
payload: options.payload || {},
|
|
183
|
+
details: {
|
|
184
|
+
qaMode: normalized.qaMode,
|
|
185
|
+
scope: normalized.scope,
|
|
186
|
+
outcome: normalized.outcome,
|
|
187
|
+
conclusion: normalized.conclusion,
|
|
188
|
+
findings: normalized.findings,
|
|
189
|
+
fileReferences: normalized.fileReferences,
|
|
190
|
+
commands: normalized.commands,
|
|
191
|
+
fastOnly: normalized.fastOnly,
|
|
192
|
+
},
|
|
193
|
+
artifacts: [getRuntimeEvidenceRelativePath(cwd, QA_REVIEW_EVIDENCE_FILE_NAME, options)],
|
|
194
|
+
})
|
|
195
|
+
return payload
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function readRequiredQaReviewEvidence(cwd, options = {}) {
|
|
199
|
+
const evidence = readQaReviewEvidence(cwd, options)
|
|
200
|
+
if (evidence) return { evidence }
|
|
201
|
+
return {
|
|
202
|
+
error: {
|
|
203
|
+
required: true,
|
|
204
|
+
status: 'missing',
|
|
205
|
+
details: ['缺少当前工作流的成功 qa-review 证据'],
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function validateQaReviewTimestamp(evidence, now) {
|
|
211
|
+
return validateEvidenceTimestamp(evidence, now, 'qa-review 证据')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function validateQaReviewFingerprint(cwd, evidence) {
|
|
215
|
+
return validateEvidenceFingerprint(cwd, evidence, '成功 qa-review 证据')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function validateQaReviewEvidence(commands, evidence) {
|
|
219
|
+
if (!normalizeQaOutcome(evidence.outcome) || !String(evidence.conclusion || '').trim()) {
|
|
220
|
+
return {
|
|
221
|
+
required: true,
|
|
222
|
+
status: 'invalid',
|
|
223
|
+
evidence,
|
|
224
|
+
commands,
|
|
225
|
+
details: ['qa-review 证据必须记录明确的 outcome 和 conclusion'],
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (commands.length > 0 && normalizeCommands(evidence.commands).length === 0) {
|
|
229
|
+
return {
|
|
230
|
+
required: true,
|
|
231
|
+
status: 'invalid',
|
|
232
|
+
evidence,
|
|
233
|
+
commands,
|
|
234
|
+
details: ['qa-review 证据必须记录本次实际执行的验证命令'],
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (Boolean(evidence.fastOnly)) {
|
|
238
|
+
return {
|
|
239
|
+
required: true,
|
|
240
|
+
status: 'fast-only',
|
|
241
|
+
evidence,
|
|
242
|
+
commands,
|
|
243
|
+
details: ['最新 qa-review 证据只覆盖快速命令检查,未完成完整 qa-review 闭环'],
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (normalizeQaOutcome(evidence.outcome) !== 'clean') {
|
|
247
|
+
return {
|
|
248
|
+
required: true,
|
|
249
|
+
status: 'blocked',
|
|
250
|
+
evidence,
|
|
251
|
+
commands,
|
|
252
|
+
details: ['最新 qa-review 证据仍记录阻断问题'],
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return null
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function getQaReviewEvidenceStatus(cwd, { required = false, now = Date.now(), ...options } = {}) {
|
|
259
|
+
const commands = detectCommands(cwd)
|
|
260
|
+
if (!required) {
|
|
261
|
+
return {
|
|
262
|
+
required: false,
|
|
263
|
+
status: 'not-applicable',
|
|
264
|
+
commands,
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const requiredEvidence = readRequiredQaReviewEvidence(cwd, options)
|
|
269
|
+
if (requiredEvidence.error) return { ...requiredEvidence.error, commands }
|
|
270
|
+
|
|
271
|
+
const { evidence } = requiredEvidence
|
|
272
|
+
const timestampError = validateQaReviewTimestamp(evidence, now)
|
|
273
|
+
if (timestampError) return { ...timestampError, commands }
|
|
274
|
+
|
|
275
|
+
const fingerprintError = validateQaReviewFingerprint(cwd, evidence)
|
|
276
|
+
if (fingerprintError) return { ...fingerprintError, commands }
|
|
277
|
+
|
|
278
|
+
const evidenceError = validateQaReviewEvidence(commands, evidence)
|
|
279
|
+
if (evidenceError) return evidenceError
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
required: true,
|
|
283
|
+
status: 'valid',
|
|
284
|
+
evidence,
|
|
285
|
+
commands,
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function readStdinJson() {
|
|
290
|
+
try {
|
|
291
|
+
return JSON.parse(readFileSync(0, 'utf-8'))
|
|
292
|
+
} catch {
|
|
293
|
+
return {}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function main() {
|
|
298
|
+
const command = process.argv[2] || ''
|
|
299
|
+
if (command !== 'write') return
|
|
300
|
+
|
|
301
|
+
const input = readStdinJson()
|
|
302
|
+
const cwd = input.cwd || process.cwd()
|
|
303
|
+
const payload = writeQaReviewEvidence(cwd, input, { payload: input })
|
|
304
|
+
process.stdout.write(JSON.stringify({
|
|
305
|
+
suppressOutput: true,
|
|
306
|
+
path: getQaReviewEvidencePath(cwd, { payload: input }),
|
|
307
|
+
payload,
|
|
308
|
+
}))
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
312
|
+
main()
|
|
313
|
+
}
|
package/scripts/ralph-loop.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* HelloAGENTS
|
|
3
|
+
* HelloAGENTS QA Gate — Quality verification gate
|
|
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
|
*/
|
|
@@ -9,7 +9,13 @@ import { join } from 'node:path';
|
|
|
9
9
|
import { execSync } from 'node:child_process';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
clearQaReviewEvidence,
|
|
14
|
+
detectCommands,
|
|
15
|
+
getQaReviewEvidenceStatus,
|
|
16
|
+
hasUnsafeQaCommand,
|
|
17
|
+
writeQaReviewEvidence,
|
|
18
|
+
} from './qa-review-state.mjs';
|
|
13
19
|
import {
|
|
14
20
|
getRuntimeEvidencePath,
|
|
15
21
|
readRuntimeEvidence,
|
|
@@ -70,7 +76,7 @@ function hasGitChanges(cwd) {
|
|
|
70
76
|
function runVerify(commands, cwd) {
|
|
71
77
|
const failures = [];
|
|
72
78
|
for (const cmd of commands) {
|
|
73
|
-
if (
|
|
79
|
+
if (hasUnsafeQaCommand([cmd])) {
|
|
74
80
|
failures.push({ cmd, output: '已阻止:验证命令不允许使用 shell 组合操作符' });
|
|
75
81
|
continue;
|
|
76
82
|
}
|
|
@@ -219,17 +225,30 @@ export function evaluateRalphLoop(data = {}, runtime = {}) {
|
|
|
219
225
|
const failures = runVerify(commands, cwd);
|
|
220
226
|
if (failures.length === 0) {
|
|
221
227
|
resetBreaker(cwd, runtimeOptions);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
228
|
+
const qaStatus = getQaReviewEvidenceStatus(cwd, {
|
|
229
|
+
required: true,
|
|
230
|
+
...runtimeOptions,
|
|
231
|
+
});
|
|
232
|
+
if (qaStatus.status !== 'valid') {
|
|
233
|
+
writeQaReviewEvidence(cwd, {
|
|
234
|
+
source: isSubagent ? 'subagent' : 'ralph-loop',
|
|
235
|
+
originCommand: 'qa',
|
|
236
|
+
qaMode: 'standard',
|
|
237
|
+
scope: isSubagent ? 'subagent-fast-check' : 'runtime-fast-check',
|
|
238
|
+
outcome: 'clean',
|
|
239
|
+
conclusion: '自动命令检查通过。',
|
|
240
|
+
findings: [],
|
|
241
|
+
fileReferences: [],
|
|
242
|
+
commands: detectCommands(cwd),
|
|
243
|
+
fastOnly: true,
|
|
244
|
+
}, runtimeOptions);
|
|
245
|
+
}
|
|
227
246
|
|
|
228
247
|
if (isSubagent) {
|
|
229
248
|
return {
|
|
230
249
|
hookSpecificOutput: {
|
|
231
250
|
hookEventName,
|
|
232
|
-
additionalContext: '
|
|
251
|
+
additionalContext: '子代理快速 QA 命令检查通过(lint/typecheck)。请控制器继续完整 qa-review。',
|
|
233
252
|
},
|
|
234
253
|
suppressOutput: true,
|
|
235
254
|
};
|
|
@@ -239,7 +258,7 @@ export function evaluateRalphLoop(data = {}, runtime = {}) {
|
|
|
239
258
|
return {
|
|
240
259
|
hookSpecificOutput: {
|
|
241
260
|
hookEventName,
|
|
242
|
-
additionalContext: '⚠️ [
|
|
261
|
+
additionalContext: '⚠️ [QA Gate] 验证通过但未检测到代码变更(git diff 为空)。如果确实完成了编码任务,请确认变更已保存。',
|
|
243
262
|
},
|
|
244
263
|
suppressOutput: true,
|
|
245
264
|
};
|
|
@@ -248,7 +267,7 @@ export function evaluateRalphLoop(data = {}, runtime = {}) {
|
|
|
248
267
|
return { suppressOutput: true };
|
|
249
268
|
}
|
|
250
269
|
|
|
251
|
-
|
|
270
|
+
clearQaReviewEvidence(cwd, runtimeOptions);
|
|
252
271
|
const breaker = readBreaker(cwd, runtimeOptions);
|
|
253
272
|
breaker.consecutive_failures += 1;
|
|
254
273
|
breaker.last_failure = new Date().toISOString();
|
|
@@ -260,7 +279,7 @@ export function evaluateRalphLoop(data = {}, runtime = {}) {
|
|
|
260
279
|
const details = failures.map(f => `\u2717 ${f.cmd}\n${f.output}`).join('\n\n');
|
|
261
280
|
return {
|
|
262
281
|
decision: 'block',
|
|
263
|
-
reason: `[
|
|
282
|
+
reason: `[QA Gate] 验证失败:\n\n${details}\n\n请先修复以上问题,再报告完成。${breakerWarning}`,
|
|
264
283
|
suppressOutput: true,
|
|
265
284
|
};
|
|
266
285
|
}
|
|
@@ -278,7 +297,7 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
|
278
297
|
} catch (error) {
|
|
279
298
|
process.stdout.write(JSON.stringify({
|
|
280
299
|
decision: 'block',
|
|
281
|
-
reason: `[
|
|
300
|
+
reason: `[QA Gate] 验证脚本执行异常,已暂停完成通知。\n原因:${error?.message || error}`,
|
|
282
301
|
suppressOutput: true,
|
|
283
302
|
}));
|
|
284
303
|
}
|
|
@@ -12,7 +12,6 @@ import { FULL_CARRIER_PROFILE_MARKER } from './cli-utils.mjs'
|
|
|
12
12
|
export const PROJECT_DIR_NAME = '.helloagents'
|
|
13
13
|
export const PROJECT_SESSIONS_DIR_NAME = 'sessions'
|
|
14
14
|
export const PROJECT_ARTIFACTS_DIR_NAME = 'artifacts'
|
|
15
|
-
export const CAPSULE_FILE_NAME = 'capsule.json'
|
|
16
15
|
export const EVENTS_FILE_NAME = 'events.jsonl'
|
|
17
16
|
export const ACTIVE_SESSION_FILE_NAME = 'active.json'
|
|
18
17
|
export const DEFAULT_STATE_SESSION_TOKEN = 'default'
|
|
@@ -463,7 +462,6 @@ export function getProjectSessionScope(cwd, options = {}) {
|
|
|
463
462
|
activationDir,
|
|
464
463
|
sessionDir,
|
|
465
464
|
statePath: join(sessionDir, 'STATE.md'),
|
|
466
|
-
capsulePath: join(sessionDir, CAPSULE_FILE_NAME),
|
|
467
465
|
eventsPath: join(sessionDir, EVENTS_FILE_NAME),
|
|
468
466
|
artifactsDir: join(sessionDir, PROJECT_ARTIFACTS_DIR_NAME),
|
|
469
467
|
key: `${projectRoot}::${workspace}::${session}`,
|
|
@@ -493,7 +491,7 @@ function buildTransientRuntimeDir(cwd, options = {}) {
|
|
|
493
491
|
session: token,
|
|
494
492
|
sessionMode: token === DEFAULT_STATE_SESSION_TOKEN ? 'default' : 'transient-session',
|
|
495
493
|
sessionDir: join(getUserRuntimeRoot(), hash),
|
|
496
|
-
|
|
494
|
+
statePath: join(getUserRuntimeRoot(), hash, 'STATE.md'),
|
|
497
495
|
eventsPath: join(getUserRuntimeRoot(), hash, EVENTS_FILE_NAME),
|
|
498
496
|
artifactsDir: join(getUserRuntimeRoot(), hash, PROJECT_ARTIFACTS_DIR_NAME),
|
|
499
497
|
key: `${normalizedCwd}::transient::${token}`,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
writeActiveProjectSession,
|
|
11
11
|
writeJsonFileAtomic,
|
|
12
12
|
} from './runtime-scope.mjs'
|
|
13
|
+
import { readStateDocument, writeStateDocument } from './state-document.mjs'
|
|
13
14
|
|
|
14
15
|
export { getRuntimeScope }
|
|
15
16
|
|
|
@@ -68,7 +69,7 @@ function getScope(cwd, options = {}) {
|
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
export function getSessionCapsulePath(cwd = process.cwd(), options = {}) {
|
|
71
|
-
return getScope(cwd, options).
|
|
72
|
+
return getScope(cwd, options).statePath
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
export function getSessionEventsPath(cwd = process.cwd(), options = {}) {
|
|
@@ -93,8 +94,9 @@ export function getSessionArtifactRelativePath(cwd, fileName, options = {}) {
|
|
|
93
94
|
|
|
94
95
|
export function readSessionCapsule(cwd = process.cwd(), options = {}) {
|
|
95
96
|
const scope = getScope(cwd, options)
|
|
96
|
-
const
|
|
97
|
-
|
|
97
|
+
const { metadata } = readStateDocument(scope.statePath)
|
|
98
|
+
const capsule = metadata && typeof metadata === 'object' ? metadata : null
|
|
99
|
+
if (!capsule || Array.isArray(capsule)) return buildEmptyCapsule(scope)
|
|
98
100
|
return {
|
|
99
101
|
...buildEmptyCapsule(scope),
|
|
100
102
|
...capsule,
|
|
@@ -109,7 +111,24 @@ export function readSessionCapsule(cwd = process.cwd(), options = {}) {
|
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
export function writeSessionCapsule(cwd, capsule, options = {}) {
|
|
112
|
-
const
|
|
114
|
+
const normalizedOptions = normalizeOptions(options)
|
|
115
|
+
const scope = getScope(cwd, normalizedOptions)
|
|
116
|
+
const currentDocument = readStateDocument(scope.statePath)
|
|
117
|
+
const hasBody = Boolean(currentDocument.body && currentDocument.body.trim())
|
|
118
|
+
if (!hasBody && normalizedOptions.ensureProjectLocal !== true && !existsSync(scope.statePath)) {
|
|
119
|
+
return {
|
|
120
|
+
...buildEmptyCapsule(scope),
|
|
121
|
+
...capsule,
|
|
122
|
+
scope: scope.scope,
|
|
123
|
+
key: scope.key,
|
|
124
|
+
cwd: scope.cwd,
|
|
125
|
+
branch: scope.branch,
|
|
126
|
+
workspace: scope.workspace || scope.branch,
|
|
127
|
+
session: scope.session,
|
|
128
|
+
sessionMode: scope.sessionMode,
|
|
129
|
+
updatedAt: new Date().toISOString(),
|
|
130
|
+
}
|
|
131
|
+
}
|
|
113
132
|
const nextCapsule = {
|
|
114
133
|
...buildEmptyCapsule(scope),
|
|
115
134
|
...capsule,
|
|
@@ -122,9 +141,12 @@ export function writeSessionCapsule(cwd, capsule, options = {}) {
|
|
|
122
141
|
sessionMode: scope.sessionMode,
|
|
123
142
|
updatedAt: new Date().toISOString(),
|
|
124
143
|
}
|
|
125
|
-
|
|
144
|
+
writeStateDocument(scope.statePath, {
|
|
145
|
+
metadata: nextCapsule,
|
|
146
|
+
body: currentDocument.body,
|
|
147
|
+
})
|
|
126
148
|
writeActiveProjectSession(scope, {
|
|
127
|
-
env:
|
|
149
|
+
env: normalizedOptions.env,
|
|
128
150
|
})
|
|
129
151
|
return nextCapsule
|
|
130
152
|
}
|
|
@@ -151,8 +173,8 @@ export function writeCapsuleSection(cwd, section, value, options = {}) {
|
|
|
151
173
|
}
|
|
152
174
|
|
|
153
175
|
export function clearCapsuleSection(cwd, section, options = {}) {
|
|
154
|
-
const
|
|
155
|
-
if (!existsSync(
|
|
176
|
+
const statePath = getSessionCapsulePath(cwd, options)
|
|
177
|
+
if (!existsSync(statePath)) return false
|
|
156
178
|
|
|
157
179
|
const capsule = readSessionCapsule(cwd, options)
|
|
158
180
|
if (!Object.prototype.hasOwnProperty.call(capsule, section)) return false
|
|
@@ -180,6 +202,13 @@ export function appendSessionEvent(cwd, eventPayload, options = {}) {
|
|
|
180
202
|
const eventName = eventPayload?.event || ''
|
|
181
203
|
if (!eventName) return ''
|
|
182
204
|
|
|
205
|
+
writeActiveProjectSession(scope, {
|
|
206
|
+
host: eventPayload.host || '',
|
|
207
|
+
source: eventPayload.source || eventName,
|
|
208
|
+
env: scopedOptions.env,
|
|
209
|
+
})
|
|
210
|
+
if (!shouldRecordSessionEvents(scopedOptions)) return ''
|
|
211
|
+
|
|
183
212
|
mkdirSync(dirname(scope.eventsPath), { recursive: true })
|
|
184
213
|
const payload = {
|
|
185
214
|
ts: new Date().toISOString(),
|
|
@@ -192,17 +221,13 @@ export function appendSessionEvent(cwd, eventPayload, options = {}) {
|
|
|
192
221
|
encoding: 'utf-8',
|
|
193
222
|
flag: 'a',
|
|
194
223
|
})
|
|
195
|
-
writeActiveProjectSession(scope, {
|
|
196
|
-
host: eventPayload.host || '',
|
|
197
|
-
source: eventPayload.source || eventName,
|
|
198
|
-
env: scopedOptions.env,
|
|
199
|
-
})
|
|
200
224
|
return scope.eventsPath
|
|
201
225
|
}
|
|
202
226
|
|
|
203
227
|
export function resetSessionEvents(cwd, options = {}) {
|
|
204
228
|
const scope = getScope(cwd, options)
|
|
205
229
|
if (scope.scope === 'project-session' && !scope.active) return ''
|
|
230
|
+
if (!shouldRecordSessionEvents(options)) return ''
|
|
206
231
|
mkdirSync(dirname(scope.eventsPath), { recursive: true })
|
|
207
232
|
writeFileSync(scope.eventsPath, '', 'utf-8')
|
|
208
233
|
return scope.eventsPath
|
|
@@ -242,3 +267,16 @@ export function clearSessionArtifact(cwd, fileName, options = {}) {
|
|
|
242
267
|
export function removeSessionCapsule(cwd, options = {}) {
|
|
243
268
|
removeRuntimeFile(getSessionCapsulePath(cwd, options))
|
|
244
269
|
}
|
|
270
|
+
|
|
271
|
+
function shouldRecordSessionEvents(options = {}) {
|
|
272
|
+
const normalizedOptions = normalizeOptions(options)
|
|
273
|
+
const payload = normalizedOptions.payload || {}
|
|
274
|
+
if (normalizedOptions.traceEvents === true || payload.traceEvents === true || payload._helloagentsTraceEvents === true) {
|
|
275
|
+
return true
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const raw = String(normalizedOptions.env?.HELLOAGENTS_TRACE_EVENTS || process.env.HELLOAGENTS_TRACE_EVENTS || '')
|
|
279
|
+
.trim()
|
|
280
|
+
.toLowerCase()
|
|
281
|
+
return raw === '1' || raw === 'true' || raw === 'yes'
|
|
282
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { dirname } from 'node:path'
|
|
3
|
+
|
|
4
|
+
const STATE_META_BEGIN = '<!-- HELLOAGENTS_STATE_META'
|
|
5
|
+
const STATE_META_END = 'HELLOAGENTS_STATE_META -->'
|
|
6
|
+
|
|
7
|
+
function normalizeText(content = '') {
|
|
8
|
+
return String(content || '').replace(/^\uFEFF/, '')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function splitLines(content = '') {
|
|
12
|
+
return normalizeText(content).replace(/\r\n/g, '\n').split('\n')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function parseStateDocument(content = '') {
|
|
16
|
+
const lines = splitLines(content)
|
|
17
|
+
if (lines[0]?.trim() !== STATE_META_BEGIN) {
|
|
18
|
+
return {
|
|
19
|
+
hasMetadata: false,
|
|
20
|
+
metadata: null,
|
|
21
|
+
body: normalizeText(content),
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const endIndex = lines.findIndex((line, index) => index > 0 && line.trim() === STATE_META_END)
|
|
26
|
+
if (endIndex < 0) {
|
|
27
|
+
return {
|
|
28
|
+
hasMetadata: false,
|
|
29
|
+
metadata: null,
|
|
30
|
+
body: normalizeText(content),
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const metadataText = lines.slice(1, endIndex).join('\n').trim()
|
|
35
|
+
const body = lines.slice(endIndex + 1).join('\n').replace(/^\n+/, '')
|
|
36
|
+
try {
|
|
37
|
+
return {
|
|
38
|
+
hasMetadata: true,
|
|
39
|
+
metadata: JSON.parse(metadataText),
|
|
40
|
+
body,
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
return {
|
|
44
|
+
hasMetadata: false,
|
|
45
|
+
metadata: null,
|
|
46
|
+
body,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function readStateDocument(filePath) {
|
|
52
|
+
if (!filePath || !existsSync(filePath)) {
|
|
53
|
+
return {
|
|
54
|
+
hasMetadata: false,
|
|
55
|
+
metadata: null,
|
|
56
|
+
body: '',
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return parseStateDocument(readFileSync(filePath, 'utf-8'))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function composeStateDocument({ metadata = {}, body = '' } = {}) {
|
|
64
|
+
const normalizedBody = normalizeText(body).replace(/^\n+/, '')
|
|
65
|
+
return [
|
|
66
|
+
STATE_META_BEGIN,
|
|
67
|
+
JSON.stringify(metadata, null, 2),
|
|
68
|
+
STATE_META_END,
|
|
69
|
+
'',
|
|
70
|
+
normalizedBody,
|
|
71
|
+
].join('\n').replace(/\n+$/, '\n')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function writeStateDocument(filePath, { metadata = {}, body = '' } = {}) {
|
|
75
|
+
mkdirSync(dirname(filePath), { recursive: true })
|
|
76
|
+
writeFileSync(filePath, composeStateDocument({ metadata, body }), 'utf-8')
|
|
77
|
+
}
|