helloagents 3.0.33 → 3.0.37
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 -4
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +3 -4
- package/README.md +78 -74
- package/README_CN.md +78 -74
- package/bootstrap-lite.md +9 -11
- package/bootstrap.md +21 -23
- package/gemini-extension.json +1 -1
- package/install.ps1 +27 -4
- package/install.sh +27 -3
- package/package.json +2 -2
- package/scripts/capability-registry.mjs +5 -3
- package/scripts/cli-doctor-codex.mjs +153 -1
- package/scripts/cli-doctor-render.mjs +2 -1
- package/scripts/cli-doctor.mjs +3 -3
- package/scripts/cli-hosts.mjs +1 -1
- package/scripts/cli-lifecycle-hosts.mjs +124 -54
- package/scripts/cli-lifecycle.mjs +50 -15
- package/scripts/cli-messages.mjs +7 -7
- package/scripts/cli-runtime-root.mjs +9 -1
- 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-sound.mjs +2 -1
- package/scripts/notify.mjs +2 -2
- package/scripts/plan-contract.mjs +10 -14
- package/scripts/project-session-cleanup.mjs +91 -31
- package/scripts/qa-review-state.mjs +313 -0
- package/scripts/ralph-loop.mjs +32 -13
- package/scripts/runtime-artifacts.mjs +2 -2
- package/scripts/runtime-scope.mjs +14 -13
- package/scripts/runtime-ttl.mjs +7 -4
- package/scripts/session-capsule.mjs +75 -13
- package/scripts/session-token.mjs +44 -9
- 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
- /package/hooks/{hooks.json → hooks-gemini.json} +0 -0
|
@@ -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
|
}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
readSessionArtifact,
|
|
8
8
|
writeSessionArtifact,
|
|
9
9
|
} from './session-capsule.mjs'
|
|
10
|
-
import { EVIDENCE_MAX_AGE_MS, LONG_RUNNING_TTL_HOURS } from './runtime-ttl.mjs'
|
|
10
|
+
import { EVIDENCE_MAX_AGE_MS, LONG_RUNNING_TTL_HOURS, STANDARD_RUNTIME_TTL_HOURS } from './runtime-ttl.mjs'
|
|
11
11
|
|
|
12
12
|
export { EVIDENCE_MAX_AGE_MS }
|
|
13
13
|
|
|
@@ -87,7 +87,7 @@ export function validateEvidenceTimestamp(evidence, now, label) {
|
|
|
87
87
|
required: true,
|
|
88
88
|
status: 'stale-time',
|
|
89
89
|
evidence,
|
|
90
|
-
details: [`${label}超过 ${LONG_RUNNING_TTL_HOURS}
|
|
90
|
+
details: [`${label}超过 ${STANDARD_RUNTIME_TTL_HOURS} 小时(长任务上限:${LONG_RUNNING_TTL_HOURS} 小时)`],
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
return null
|
|
@@ -4,7 +4,7 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync,
|
|
|
4
4
|
import { dirname, join, normalize, resolve } from 'node:path'
|
|
5
5
|
import { homedir } from 'node:os'
|
|
6
6
|
|
|
7
|
-
import { resolveSessionToken } from './session-token.mjs'
|
|
7
|
+
import { resolveProjectSessionToken, resolveSessionToken } from './session-token.mjs'
|
|
8
8
|
import { USER_RUNTIME_MAX_AGE_MS } from './runtime-ttl.mjs'
|
|
9
9
|
import { cleanupUserRuntimeRoot, getUserRuntimeRoot } from './runtime-user-cleanup.mjs'
|
|
10
10
|
import { FULL_CARRIER_PROFILE_MARKER } from './cli-utils.mjs'
|
|
@@ -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'
|
|
@@ -324,21 +323,14 @@ function findProjectActivationDir(cwd) {
|
|
|
324
323
|
|
|
325
324
|
function resolvePayloadSessionToken(payload = {}) {
|
|
326
325
|
if (payload?._helloagentsSessionAlias) return ''
|
|
327
|
-
return
|
|
326
|
+
return resolveProjectSessionToken({
|
|
328
327
|
payload,
|
|
329
328
|
env: {},
|
|
330
|
-
ppid: 0,
|
|
331
|
-
allowPpidFallback: false,
|
|
332
329
|
})
|
|
333
330
|
}
|
|
334
331
|
|
|
335
332
|
function resolveEnvSessionToken(env = process.env) {
|
|
336
|
-
return
|
|
337
|
-
payload: {},
|
|
338
|
-
env,
|
|
339
|
-
ppid: 0,
|
|
340
|
-
allowPpidFallback: false,
|
|
341
|
-
})
|
|
333
|
+
return resolveProjectSessionToken({ payload: {}, env })
|
|
342
334
|
}
|
|
343
335
|
|
|
344
336
|
function resolveTransientSessionToken({ payload = {}, env = process.env, ppid = process.ppid } = {}) {
|
|
@@ -438,11 +430,21 @@ function chooseProjectSession({ payload, env, activationDir, projectRoot, worksp
|
|
|
438
430
|
return { session: DEFAULT_STATE_SESSION_TOKEN, sessionMode: 'default' }
|
|
439
431
|
}
|
|
440
432
|
|
|
433
|
+
function removeLegacyProjectArtifacts(activationDir) {
|
|
434
|
+
if (!activationDir) return
|
|
435
|
+
const artifactsDir = join(activationDir, PROJECT_ARTIFACTS_DIR_NAME)
|
|
436
|
+
if (!existsSync(artifactsDir)) return
|
|
437
|
+
try {
|
|
438
|
+
rmSync(artifactsDir, { recursive: true, force: true })
|
|
439
|
+
} catch {}
|
|
440
|
+
}
|
|
441
|
+
|
|
441
442
|
export function getProjectSessionScope(cwd, options = {}) {
|
|
442
443
|
const normalizedCwd = normalizePath(cwd || process.cwd())
|
|
443
444
|
const projectRoot = getProjectRoot(normalizedCwd)
|
|
444
445
|
const { payload = {}, env = process.env } = normalizeRuntimeOptions(options)
|
|
445
446
|
const activationDir = getProjectActivationDir(projectRoot)
|
|
447
|
+
removeLegacyProjectArtifacts(activationDir)
|
|
446
448
|
const workspace = resolveWorkspaceName(projectRoot)
|
|
447
449
|
const { session, sessionMode } = chooseProjectSession({
|
|
448
450
|
payload,
|
|
@@ -463,7 +465,6 @@ export function getProjectSessionScope(cwd, options = {}) {
|
|
|
463
465
|
activationDir,
|
|
464
466
|
sessionDir,
|
|
465
467
|
statePath: join(sessionDir, 'STATE.md'),
|
|
466
|
-
capsulePath: join(sessionDir, CAPSULE_FILE_NAME),
|
|
467
468
|
eventsPath: join(sessionDir, EVENTS_FILE_NAME),
|
|
468
469
|
artifactsDir: join(sessionDir, PROJECT_ARTIFACTS_DIR_NAME),
|
|
469
470
|
key: `${projectRoot}::${workspace}::${session}`,
|
|
@@ -493,7 +494,7 @@ function buildTransientRuntimeDir(cwd, options = {}) {
|
|
|
493
494
|
session: token,
|
|
494
495
|
sessionMode: token === DEFAULT_STATE_SESSION_TOKEN ? 'default' : 'transient-session',
|
|
495
496
|
sessionDir: join(getUserRuntimeRoot(), hash),
|
|
496
|
-
|
|
497
|
+
statePath: join(getUserRuntimeRoot(), hash, 'STATE.md'),
|
|
497
498
|
eventsPath: join(getUserRuntimeRoot(), hash, EVENTS_FILE_NAME),
|
|
498
499
|
artifactsDir: join(getUserRuntimeRoot(), hash, PROJECT_ARTIFACTS_DIR_NAME),
|
|
499
500
|
key: `${normalizedCwd}::transient::${token}`,
|
package/scripts/runtime-ttl.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export const LONG_RUNNING_TTL_HOURS = 720
|
|
2
2
|
export const LONG_RUNNING_TTL_MS = LONG_RUNNING_TTL_HOURS * 60 * 60 * 1000
|
|
3
3
|
|
|
4
|
-
export const
|
|
5
|
-
export const
|
|
6
|
-
|
|
7
|
-
export const
|
|
4
|
+
export const STANDARD_RUNTIME_TTL_HOURS = 72
|
|
5
|
+
export const STANDARD_RUNTIME_TTL_MS = STANDARD_RUNTIME_TTL_HOURS * 60 * 60 * 1000
|
|
6
|
+
|
|
7
|
+
export const ROUTE_CONTEXT_TTL_MS = STANDARD_RUNTIME_TTL_MS
|
|
8
|
+
export const TURN_STATE_TTL_MS = STANDARD_RUNTIME_TTL_MS
|
|
9
|
+
export const EVIDENCE_MAX_AGE_MS = STANDARD_RUNTIME_TTL_MS
|
|
10
|
+
export const USER_RUNTIME_MAX_AGE_MS = STANDARD_RUNTIME_TTL_MS
|
|
@@ -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
|
|
|
@@ -67,8 +68,23 @@ function getScope(cwd, options = {}) {
|
|
|
67
68
|
return getRuntimeScope(cwd, normalizedOptions)
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
function shouldMaterializeSessionState(options = {}) {
|
|
72
|
+
const normalizedOptions = normalizeOptions(options)
|
|
73
|
+
if (normalizedOptions.ensureProjectLocal === true) return true
|
|
74
|
+
if (normalizedOptions.project === true) return true
|
|
75
|
+
if (normalizedOptions.traceEvents === true) return true
|
|
76
|
+
|
|
77
|
+
const payload = normalizedOptions.payload || {}
|
|
78
|
+
if (payload.traceEvents === true || payload._helloagentsTraceEvents === true) return true
|
|
79
|
+
|
|
80
|
+
const raw = String(normalizedOptions.env?.HELLOAGENTS_TRACE_EVENTS || process.env.HELLOAGENTS_TRACE_EVENTS || '')
|
|
81
|
+
.trim()
|
|
82
|
+
.toLowerCase()
|
|
83
|
+
return raw === '1' || raw === 'true' || raw === 'yes'
|
|
84
|
+
}
|
|
85
|
+
|
|
70
86
|
export function getSessionCapsulePath(cwd = process.cwd(), options = {}) {
|
|
71
|
-
return getScope(cwd, options).
|
|
87
|
+
return getScope(cwd, options).statePath
|
|
72
88
|
}
|
|
73
89
|
|
|
74
90
|
export function getSessionEventsPath(cwd = process.cwd(), options = {}) {
|
|
@@ -93,8 +109,9 @@ export function getSessionArtifactRelativePath(cwd, fileName, options = {}) {
|
|
|
93
109
|
|
|
94
110
|
export function readSessionCapsule(cwd = process.cwd(), options = {}) {
|
|
95
111
|
const scope = getScope(cwd, options)
|
|
96
|
-
const
|
|
97
|
-
|
|
112
|
+
const { metadata } = readStateDocument(scope.statePath)
|
|
113
|
+
const capsule = metadata && typeof metadata === 'object' ? metadata : null
|
|
114
|
+
if (!capsule || Array.isArray(capsule)) return buildEmptyCapsule(scope)
|
|
98
115
|
return {
|
|
99
116
|
...buildEmptyCapsule(scope),
|
|
100
117
|
...capsule,
|
|
@@ -109,7 +126,33 @@ export function readSessionCapsule(cwd = process.cwd(), options = {}) {
|
|
|
109
126
|
}
|
|
110
127
|
|
|
111
128
|
export function writeSessionCapsule(cwd, capsule, options = {}) {
|
|
112
|
-
const
|
|
129
|
+
const normalizedOptions = normalizeOptions(options)
|
|
130
|
+
const scope = getScope(cwd, normalizedOptions)
|
|
131
|
+
const shouldMaterialize = shouldMaterializeSessionState(normalizedOptions)
|
|
132
|
+
const currentDocument = readStateDocument(scope.statePath)
|
|
133
|
+
const hasBody = Boolean(currentDocument.body && currentDocument.body.trim())
|
|
134
|
+
if (!hasBody && !shouldMaterialize && !existsSync(scope.statePath)) {
|
|
135
|
+
return {
|
|
136
|
+
...buildEmptyCapsule(scope),
|
|
137
|
+
...capsule,
|
|
138
|
+
scope: scope.scope,
|
|
139
|
+
key: scope.key,
|
|
140
|
+
cwd: scope.cwd,
|
|
141
|
+
branch: scope.branch,
|
|
142
|
+
workspace: scope.workspace || scope.branch,
|
|
143
|
+
session: scope.session,
|
|
144
|
+
sessionMode: scope.sessionMode,
|
|
145
|
+
updatedAt: new Date().toISOString(),
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!hasBody && shouldMaterialize && !existsSync(scope.statePath)) {
|
|
149
|
+
ensureProjectLocalRuntime(cwd, {
|
|
150
|
+
...normalizedOptions,
|
|
151
|
+
stateSeed: normalizedOptions.stateSeed && typeof normalizedOptions.stateSeed === 'object'
|
|
152
|
+
? normalizedOptions.stateSeed
|
|
153
|
+
: {},
|
|
154
|
+
})
|
|
155
|
+
}
|
|
113
156
|
const nextCapsule = {
|
|
114
157
|
...buildEmptyCapsule(scope),
|
|
115
158
|
...capsule,
|
|
@@ -122,9 +165,12 @@ export function writeSessionCapsule(cwd, capsule, options = {}) {
|
|
|
122
165
|
sessionMode: scope.sessionMode,
|
|
123
166
|
updatedAt: new Date().toISOString(),
|
|
124
167
|
}
|
|
125
|
-
|
|
168
|
+
writeStateDocument(scope.statePath, {
|
|
169
|
+
metadata: nextCapsule,
|
|
170
|
+
body: currentDocument.body,
|
|
171
|
+
})
|
|
126
172
|
writeActiveProjectSession(scope, {
|
|
127
|
-
env:
|
|
173
|
+
env: normalizedOptions.env,
|
|
128
174
|
})
|
|
129
175
|
return nextCapsule
|
|
130
176
|
}
|
|
@@ -151,8 +197,8 @@ export function writeCapsuleSection(cwd, section, value, options = {}) {
|
|
|
151
197
|
}
|
|
152
198
|
|
|
153
199
|
export function clearCapsuleSection(cwd, section, options = {}) {
|
|
154
|
-
const
|
|
155
|
-
if (!existsSync(
|
|
200
|
+
const statePath = getSessionCapsulePath(cwd, options)
|
|
201
|
+
if (!existsSync(statePath)) return false
|
|
156
202
|
|
|
157
203
|
const capsule = readSessionCapsule(cwd, options)
|
|
158
204
|
if (!Object.prototype.hasOwnProperty.call(capsule, section)) return false
|
|
@@ -180,6 +226,13 @@ export function appendSessionEvent(cwd, eventPayload, options = {}) {
|
|
|
180
226
|
const eventName = eventPayload?.event || ''
|
|
181
227
|
if (!eventName) return ''
|
|
182
228
|
|
|
229
|
+
writeActiveProjectSession(scope, {
|
|
230
|
+
host: eventPayload.host || '',
|
|
231
|
+
source: eventPayload.source || eventName,
|
|
232
|
+
env: scopedOptions.env,
|
|
233
|
+
})
|
|
234
|
+
if (!shouldRecordSessionEvents(scopedOptions)) return ''
|
|
235
|
+
|
|
183
236
|
mkdirSync(dirname(scope.eventsPath), { recursive: true })
|
|
184
237
|
const payload = {
|
|
185
238
|
ts: new Date().toISOString(),
|
|
@@ -192,17 +245,13 @@ export function appendSessionEvent(cwd, eventPayload, options = {}) {
|
|
|
192
245
|
encoding: 'utf-8',
|
|
193
246
|
flag: 'a',
|
|
194
247
|
})
|
|
195
|
-
writeActiveProjectSession(scope, {
|
|
196
|
-
host: eventPayload.host || '',
|
|
197
|
-
source: eventPayload.source || eventName,
|
|
198
|
-
env: scopedOptions.env,
|
|
199
|
-
})
|
|
200
248
|
return scope.eventsPath
|
|
201
249
|
}
|
|
202
250
|
|
|
203
251
|
export function resetSessionEvents(cwd, options = {}) {
|
|
204
252
|
const scope = getScope(cwd, options)
|
|
205
253
|
if (scope.scope === 'project-session' && !scope.active) return ''
|
|
254
|
+
if (!shouldRecordSessionEvents(options)) return ''
|
|
206
255
|
mkdirSync(dirname(scope.eventsPath), { recursive: true })
|
|
207
256
|
writeFileSync(scope.eventsPath, '', 'utf-8')
|
|
208
257
|
return scope.eventsPath
|
|
@@ -242,3 +291,16 @@ export function clearSessionArtifact(cwd, fileName, options = {}) {
|
|
|
242
291
|
export function removeSessionCapsule(cwd, options = {}) {
|
|
243
292
|
removeRuntimeFile(getSessionCapsulePath(cwd, options))
|
|
244
293
|
}
|
|
294
|
+
|
|
295
|
+
function shouldRecordSessionEvents(options = {}) {
|
|
296
|
+
const normalizedOptions = normalizeOptions(options)
|
|
297
|
+
const payload = normalizedOptions.payload || {}
|
|
298
|
+
if (normalizedOptions.traceEvents === true || payload.traceEvents === true || payload._helloagentsTraceEvents === true) {
|
|
299
|
+
return true
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const raw = String(normalizedOptions.env?.HELLOAGENTS_TRACE_EVENTS || process.env.HELLOAGENTS_TRACE_EVENTS || '')
|
|
303
|
+
.trim()
|
|
304
|
+
.toLowerCase()
|
|
305
|
+
return raw === '1' || raw === 'true' || raw === 'yes'
|
|
306
|
+
}
|