helloagents 3.0.12 → 3.0.16-beta.1
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 +6 -4
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +182 -35
- package/README_CN.md +184 -37
- package/bootstrap-lite.md +32 -26
- package/bootstrap.md +35 -29
- package/cli.mjs +119 -11
- package/gemini-extension.json +1 -1
- package/install.ps1 +128 -0
- package/install.sh +121 -0
- package/package.json +23 -4
- package/scripts/advisor-state.mjs +36 -63
- package/scripts/capability-registry.mjs +4 -4
- package/scripts/cli-branch.mjs +84 -0
- package/scripts/cli-codex-config.mjs +14 -20
- package/scripts/cli-codex.mjs +32 -38
- package/scripts/cli-doctor-render.mjs +4 -0
- package/scripts/cli-doctor.mjs +40 -30
- package/scripts/cli-host-detect.mjs +0 -1
- package/scripts/cli-hosts.mjs +16 -8
- package/scripts/cli-lifecycle-hosts.mjs +119 -32
- package/scripts/cli-lifecycle.mjs +24 -13
- package/scripts/cli-messages.mjs +34 -16
- package/scripts/cli-runtime-carrier.mjs +15 -0
- package/scripts/cli-runtime-root.mjs +72 -0
- package/scripts/cli-toml.mjs +0 -79
- package/scripts/cli-utils.mjs +30 -4
- package/scripts/closeout-state.mjs +35 -62
- package/scripts/delivery-gate-messages.mjs +70 -0
- package/scripts/delivery-gate.mjs +9 -75
- package/scripts/guard-rules.mjs +42 -42
- package/scripts/guard.mjs +44 -24
- package/scripts/notify-context.mjs +19 -28
- package/scripts/notify-events.mjs +3 -1
- package/scripts/notify-gates.mjs +2 -0
- package/scripts/notify-route.mjs +9 -7
- package/scripts/notify-ui.mjs +42 -32
- package/scripts/notify.mjs +72 -36
- package/scripts/project-storage.mjs +35 -66
- package/scripts/ralph-loop.mjs +36 -31
- package/scripts/replay-state.mjs +31 -128
- package/scripts/review-state.mjs +34 -61
- package/scripts/runtime-artifacts.mjs +95 -0
- package/scripts/runtime-context.mjs +35 -29
- package/scripts/runtime-scope.mjs +313 -0
- package/scripts/session-capsule.mjs +202 -0
- package/scripts/turn-state-cli.mjs +17 -0
- package/scripts/turn-state.mjs +185 -66
- package/scripts/turn-stop-gate.mjs +24 -6
- package/scripts/verify-state.mjs +34 -85
- package/scripts/visual-state.mjs +38 -65
- package/scripts/workflow-core.mjs +3 -3
- package/scripts/workflow-plan-files.mjs +1 -1
- package/scripts/workflow-recommendation.mjs +17 -13
- package/scripts/workflow-state.mjs +5 -5
- package/skills/commands/build/SKILL.md +1 -1
- package/skills/commands/commit/SKILL.md +1 -1
- package/skills/commands/help/SKILL.md +5 -3
- package/skills/commands/loop/SKILL.md +1 -1
- package/skills/commands/plan/SKILL.md +8 -6
- package/skills/commands/prd/SKILL.md +5 -3
- package/skills/commands/verify/SKILL.md +5 -5
- package/skills/hello-debug/SKILL.md +20 -3
- package/skills/hello-review/SKILL.md +2 -2
- package/skills/hello-subagent/SKILL.md +2 -2
- package/skills/hello-test/SKILL.md +6 -2
- package/skills/hello-ui/SKILL.md +7 -7
- package/skills/hello-verify/SKILL.md +10 -7
- package/skills/helloagents/SKILL.md +14 -9
- package/templates/context.md +6 -0
- package/templates/plans/plan.md +3 -0
- package/templates/plans/tasks.md +8 -3
package/scripts/turn-state.mjs
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { homedir } from 'node:os'
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { normalize, resolve } from 'node:path'
|
|
4
3
|
import { fileURLToPath } from 'node:url'
|
|
5
4
|
|
|
6
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
appendSessionEvent,
|
|
7
|
+
clearCapsuleSection,
|
|
8
|
+
getSessionCapsulePath,
|
|
9
|
+
getRuntimeScope,
|
|
10
|
+
readCapsuleSection,
|
|
11
|
+
writeCapsuleSection,
|
|
12
|
+
} from './session-capsule.mjs'
|
|
7
13
|
|
|
8
|
-
const TURN_STATE_PATH = join(homedir(), '.helloagents', 'runtime', 'turn-state.json')
|
|
9
14
|
const TURN_STATE_TTL_MS = 30 * 60 * 1000
|
|
10
15
|
const VALID_KINDS = new Set(['complete', 'waiting', 'blocked', 'progress'])
|
|
11
16
|
const VALID_ROLES = new Set(['main', 'subagent'])
|
|
@@ -19,38 +24,31 @@ const VALID_REASON_CATEGORIES = new Set([
|
|
|
19
24
|
'external-dependency',
|
|
20
25
|
'error',
|
|
21
26
|
])
|
|
27
|
+
const HELP_TEXT = `Usage:
|
|
28
|
+
helloagents-turn-state write --kind complete --role main
|
|
29
|
+
helloagents-turn-state write --kind waiting --role main --reason-category missing-input --reason "..."
|
|
30
|
+
echo {"kind":"complete","role":"main"} | helloagents-turn-state write
|
|
31
|
+
helloagents-turn-state read [--cwd <path>]
|
|
32
|
+
helloagents-turn-state clear [--cwd <path>]
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
--cwd <path>
|
|
36
|
+
--kind <complete|waiting|blocked|progress>
|
|
37
|
+
--role <main|subagent>
|
|
38
|
+
--phase <name>
|
|
39
|
+
--source <name>
|
|
40
|
+
--reason-category <category>
|
|
41
|
+
--reason <text>
|
|
42
|
+
--requires-delivery-gate
|
|
43
|
+
--blocker-target <text>
|
|
44
|
+
--blocker-evidence <text>
|
|
45
|
+
--blocker-required-action <text>
|
|
46
|
+
`
|
|
22
47
|
|
|
23
48
|
function normalizePath(filePath = '') {
|
|
24
49
|
return filePath ? normalize(resolve(filePath)) : ''
|
|
25
50
|
}
|
|
26
51
|
|
|
27
|
-
function ensureRuntimeDir() {
|
|
28
|
-
mkdirSync(dirname(TURN_STATE_PATH), { recursive: true })
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function readStore() {
|
|
32
|
-
try {
|
|
33
|
-
return JSON.parse(readFileSync(TURN_STATE_PATH, 'utf-8'))
|
|
34
|
-
} catch {
|
|
35
|
-
return {}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function writeStore(store) {
|
|
40
|
-
const keys = Object.keys(store)
|
|
41
|
-
if (keys.length === 0) {
|
|
42
|
-
rmSync(TURN_STATE_PATH, { force: true })
|
|
43
|
-
return
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
ensureRuntimeDir()
|
|
47
|
-
writeFileSync(TURN_STATE_PATH, `${JSON.stringify(store, null, 2)}\n`, 'utf-8')
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function getTurnStateKey(cwd = process.cwd()) {
|
|
51
|
-
return normalizePath(cwd)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
52
|
function normalizeTurnState(input = {}) {
|
|
55
53
|
const kind = typeof input.kind === 'string' ? input.kind.trim().toLowerCase() : ''
|
|
56
54
|
const role = typeof input.role === 'string' ? input.role.trim().toLowerCase() : 'main'
|
|
@@ -58,6 +56,7 @@ function normalizeTurnState(input = {}) {
|
|
|
58
56
|
? input.reasonCategory.trim().toLowerCase()
|
|
59
57
|
: ''
|
|
60
58
|
const reason = typeof input.reason === 'string' ? input.reason.trim() : ''
|
|
59
|
+
const blocker = normalizeBlocker(input.blocker)
|
|
61
60
|
|
|
62
61
|
return {
|
|
63
62
|
kind: VALID_KINDS.has(kind) ? kind : '',
|
|
@@ -67,59 +66,64 @@ function normalizeTurnState(input = {}) {
|
|
|
67
66
|
requiresDeliveryGate: Boolean(input.requiresDeliveryGate),
|
|
68
67
|
reasonCategory: VALID_REASON_CATEGORIES.has(reasonCategory) ? reasonCategory : '',
|
|
69
68
|
reason,
|
|
69
|
+
...(blocker ? { blocker } : {}),
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
function
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
function normalizeBlocker(input = {}) {
|
|
74
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return null
|
|
75
|
+
|
|
76
|
+
const target = typeof input.target === 'string' ? input.target.trim() : ''
|
|
77
|
+
const evidence = typeof input.evidence === 'string' ? input.evidence.trim() : ''
|
|
78
|
+
const requiredAction = typeof input.requiredAction === 'string'
|
|
79
|
+
? input.requiredAction.trim()
|
|
80
|
+
: ''
|
|
77
81
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (!key) return false
|
|
81
|
-
const store = readStore()
|
|
82
|
-
if (!(key in store)) return false
|
|
83
|
-
delete store[key]
|
|
84
|
-
writeStore(store)
|
|
85
|
-
return true
|
|
82
|
+
if (!target && !evidence && !requiredAction) return null
|
|
83
|
+
return { target, evidence, requiredAction }
|
|
86
84
|
}
|
|
87
85
|
|
|
88
|
-
export function
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
export function clearTurnState(cwd = process.cwd(), options = {}) {
|
|
87
|
+
return clearCapsuleSection(cwd, 'turn', options)
|
|
88
|
+
}
|
|
91
89
|
|
|
92
|
-
|
|
93
|
-
const entry =
|
|
90
|
+
export function readTurnState(cwd = process.cwd(), { now = Date.now(), ...options } = {}) {
|
|
91
|
+
const entry = readCapsuleSection(cwd, 'turn', options)
|
|
94
92
|
if (!entry?.cwd || !entry?.kind || !entry?.updatedAt) {
|
|
95
|
-
if (entry) pruneInvalidEntry(store, key)
|
|
96
93
|
return null
|
|
97
94
|
}
|
|
98
95
|
|
|
99
96
|
const updatedAt = Date.parse(entry.updatedAt)
|
|
100
97
|
if (!Number.isFinite(updatedAt) || (now - updatedAt > TURN_STATE_TTL_MS)) {
|
|
101
|
-
|
|
98
|
+
clearTurnState(cwd, options)
|
|
102
99
|
return null
|
|
103
100
|
}
|
|
104
101
|
|
|
105
102
|
const normalized = normalizeTurnState(entry)
|
|
106
103
|
if (!normalized.kind) {
|
|
107
|
-
|
|
104
|
+
clearTurnState(cwd, options)
|
|
108
105
|
return null
|
|
109
106
|
}
|
|
110
107
|
|
|
111
108
|
return {
|
|
112
109
|
cwd: normalizePath(entry.cwd),
|
|
110
|
+
key: entry.key || '',
|
|
111
|
+
path: getSessionCapsulePath(cwd, options),
|
|
113
112
|
updatedAt: entry.updatedAt,
|
|
114
113
|
...normalized,
|
|
115
114
|
}
|
|
116
115
|
}
|
|
117
116
|
|
|
118
117
|
export function writeTurnState(cwd = process.cwd(), input = {}) {
|
|
119
|
-
const
|
|
118
|
+
const runtimeOptions = {
|
|
119
|
+
payload: input.payload && typeof input.payload === 'object' ? input.payload : input,
|
|
120
|
+
env: input.env || process.env,
|
|
121
|
+
ppid: input.ppid ?? process.ppid,
|
|
122
|
+
}
|
|
123
|
+
const scope = getRuntimeScope(cwd, runtimeOptions)
|
|
120
124
|
const normalized = normalizeTurnState(input)
|
|
121
|
-
if (!
|
|
122
|
-
throw new Error('turn-state requires
|
|
125
|
+
if (!normalized.kind) {
|
|
126
|
+
throw new Error('turn-state write requires a valid kind. Example: helloagents-turn-state write --kind complete --role main')
|
|
123
127
|
}
|
|
124
128
|
if (
|
|
125
129
|
(normalized.kind === 'waiting' || normalized.kind === 'blocked')
|
|
@@ -128,16 +132,16 @@ export function writeTurnState(cwd = process.cwd(), input = {}) {
|
|
|
128
132
|
throw new Error('turn-state waiting/blocked requires reasonCategory and reason')
|
|
129
133
|
}
|
|
130
134
|
|
|
131
|
-
const store = readStore()
|
|
132
135
|
const payload = {
|
|
133
|
-
cwd:
|
|
136
|
+
cwd: normalizePath(cwd),
|
|
137
|
+
key: scope.key,
|
|
138
|
+
scope: scope.scope,
|
|
134
139
|
updatedAt: new Date().toISOString(),
|
|
135
140
|
...normalized,
|
|
136
141
|
}
|
|
137
|
-
|
|
138
|
-
writeStore(store)
|
|
142
|
+
writeCapsuleSection(cwd, 'turn', payload, runtimeOptions)
|
|
139
143
|
|
|
140
|
-
|
|
144
|
+
appendSessionEvent(cwd, {
|
|
141
145
|
event: 'turn_state_written',
|
|
142
146
|
source: normalized.source,
|
|
143
147
|
details: {
|
|
@@ -154,23 +158,130 @@ export function writeTurnState(cwd = process.cwd(), input = {}) {
|
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
function readStdinJson() {
|
|
161
|
+
if (process.stdin.isTTY) return {}
|
|
157
162
|
try {
|
|
158
|
-
|
|
163
|
+
const text = readFileSync(0, 'utf-8').trim()
|
|
164
|
+
return text ? JSON.parse(text) : {}
|
|
159
165
|
} catch {
|
|
160
166
|
return {}
|
|
161
167
|
}
|
|
162
168
|
}
|
|
163
169
|
|
|
170
|
+
function normalizeOptionName(rawName = '') {
|
|
171
|
+
return rawName.replace(/^-+/, '').replace(/-([a-z])/g, (_, char) => char.toUpperCase())
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function readOptionValue(args, index, name) {
|
|
175
|
+
const raw = args[index]
|
|
176
|
+
const eqIndex = raw.indexOf('=')
|
|
177
|
+
if (eqIndex >= 0) {
|
|
178
|
+
return { value: raw.slice(eqIndex + 1), nextIndex: index }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const next = args[index + 1]
|
|
182
|
+
if (next === undefined || next.startsWith('--')) {
|
|
183
|
+
return { value: true, nextIndex: index }
|
|
184
|
+
}
|
|
185
|
+
return { value: next, nextIndex: index + 1 }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function assignCliOption(input, name, value) {
|
|
189
|
+
const key = normalizeOptionName(name)
|
|
190
|
+
const aliases = {
|
|
191
|
+
reasonCategory: 'reasonCategory',
|
|
192
|
+
requiresDeliveryGate: 'requiresDeliveryGate',
|
|
193
|
+
blockerTarget: 'blocker.target',
|
|
194
|
+
blockerEvidence: 'blocker.evidence',
|
|
195
|
+
blockerRequiredAction: 'blocker.requiredAction',
|
|
196
|
+
}
|
|
197
|
+
const target = aliases[key] || key
|
|
198
|
+
const allowed = new Set([
|
|
199
|
+
'cwd',
|
|
200
|
+
'kind',
|
|
201
|
+
'role',
|
|
202
|
+
'phase',
|
|
203
|
+
'source',
|
|
204
|
+
'reasonCategory',
|
|
205
|
+
'reason',
|
|
206
|
+
'requiresDeliveryGate',
|
|
207
|
+
'blocker.target',
|
|
208
|
+
'blocker.evidence',
|
|
209
|
+
'blocker.requiredAction',
|
|
210
|
+
])
|
|
211
|
+
if (!allowed.has(target)) {
|
|
212
|
+
throw new Error(`unknown turn-state option: --${name}`)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (target.startsWith('blocker.')) {
|
|
216
|
+
input.blocker = input.blocker || {}
|
|
217
|
+
input.blocker[target.slice('blocker.'.length)] = String(value)
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
input[target] = target === 'requiresDeliveryGate'
|
|
222
|
+
? value === true || String(value).toLowerCase() === 'true'
|
|
223
|
+
: String(value)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function parseCliArgs(args = []) {
|
|
227
|
+
const input = {}
|
|
228
|
+
let wantsHelp = false
|
|
229
|
+
|
|
230
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
231
|
+
const raw = args[index]
|
|
232
|
+
if (raw === '--help' || raw === '-h') {
|
|
233
|
+
wantsHelp = true
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
if (!raw.startsWith('--')) {
|
|
237
|
+
throw new Error(`unexpected turn-state argument: ${raw}`)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const optionName = raw.slice(2).split('=')[0]
|
|
241
|
+
const { value, nextIndex } = readOptionValue(args, index, optionName)
|
|
242
|
+
assignCliOption(input, optionName, value)
|
|
243
|
+
index = nextIndex
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return { input, wantsHelp }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function mergeInputs(stdinInput, cliInput) {
|
|
250
|
+
return {
|
|
251
|
+
...stdinInput,
|
|
252
|
+
...cliInput,
|
|
253
|
+
blocker: {
|
|
254
|
+
...(stdinInput.blocker || {}),
|
|
255
|
+
...(cliInput.blocker || {}),
|
|
256
|
+
},
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function printHelp() {
|
|
261
|
+
process.stdout.write(HELP_TEXT)
|
|
262
|
+
}
|
|
263
|
+
|
|
164
264
|
function main() {
|
|
165
265
|
const command = process.argv[2] || ''
|
|
166
|
-
|
|
266
|
+
if (!command || command === '--help' || command === '-h' || command === 'help') {
|
|
267
|
+
printHelp()
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const { input: cliInput, wantsHelp } = parseCliArgs(process.argv.slice(3))
|
|
272
|
+
if (wantsHelp) {
|
|
273
|
+
printHelp()
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const input = mergeInputs(readStdinJson(), cliInput)
|
|
167
278
|
const cwd = input.cwd || process.cwd()
|
|
168
279
|
|
|
169
280
|
if (command === 'write') {
|
|
170
281
|
const payload = writeTurnState(cwd, input)
|
|
171
282
|
process.stdout.write(JSON.stringify({
|
|
172
283
|
suppressOutput: true,
|
|
173
|
-
path:
|
|
284
|
+
path: getSessionCapsulePath(cwd, input),
|
|
174
285
|
payload,
|
|
175
286
|
}))
|
|
176
287
|
return
|
|
@@ -179,7 +290,7 @@ function main() {
|
|
|
179
290
|
if (command === 'clear') {
|
|
180
291
|
process.stdout.write(JSON.stringify({
|
|
181
292
|
suppressOutput: true,
|
|
182
|
-
cleared: clearTurnState(cwd),
|
|
293
|
+
cleared: clearTurnState(cwd, input),
|
|
183
294
|
}))
|
|
184
295
|
return
|
|
185
296
|
}
|
|
@@ -187,11 +298,19 @@ function main() {
|
|
|
187
298
|
if (command === 'read') {
|
|
188
299
|
process.stdout.write(JSON.stringify({
|
|
189
300
|
suppressOutput: true,
|
|
190
|
-
state: readTurnState(cwd),
|
|
301
|
+
state: readTurnState(cwd, input),
|
|
191
302
|
}))
|
|
303
|
+
return
|
|
192
304
|
}
|
|
305
|
+
|
|
306
|
+
throw new Error(`unknown turn-state command: ${command}`)
|
|
193
307
|
}
|
|
194
308
|
|
|
195
309
|
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
196
|
-
|
|
310
|
+
try {
|
|
311
|
+
main()
|
|
312
|
+
} catch (error) {
|
|
313
|
+
process.stderr.write(`${error.message}\n\n${HELP_TEXT}`)
|
|
314
|
+
process.exit(1)
|
|
315
|
+
}
|
|
197
316
|
}
|
|
@@ -30,7 +30,7 @@ function buildWorkflowHint(cwd) {
|
|
|
30
30
|
if (!recommendation) return ''
|
|
31
31
|
return [
|
|
32
32
|
`当前工作流:${recommendation.summary}`,
|
|
33
|
-
|
|
33
|
+
`应执行路径:${recommendation.nextPath}`,
|
|
34
34
|
recommendation.guidance,
|
|
35
35
|
].filter(Boolean).join('\n')
|
|
36
36
|
}
|
|
@@ -43,16 +43,27 @@ function buildBlockReason(routeContext, detail, cwd) {
|
|
|
43
43
|
detail,
|
|
44
44
|
workflowHint,
|
|
45
45
|
'若无真实阻塞,请继续沿当前路径执行。',
|
|
46
|
-
`若确需停下,先调用 \`
|
|
46
|
+
`若确需停下,先调用 \`helloagents-turn-state write --kind waiting --role main --reason-category <category> --reason "..."\` 写结构化状态;阻塞则把 \`waiting\` 改为 \`blocked\`。`,
|
|
47
47
|
`允许的 \`reasonCategory\`:${ALLOWED_STOP_REASON_CATEGORIES.join(' | ')}。`,
|
|
48
48
|
].filter(Boolean).join('\n')
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function getMainTurnState(cwd) {
|
|
52
|
-
const turnState = readTurnState(cwd)
|
|
51
|
+
function getMainTurnState(cwd, payload = {}) {
|
|
52
|
+
const turnState = readTurnState(cwd, { payload })
|
|
53
53
|
return turnState?.role === 'main' ? turnState : null
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function hasStructuredBlocker(turnState) {
|
|
57
|
+
const blocker = turnState?.blocker
|
|
58
|
+
return Boolean(
|
|
59
|
+
blocker
|
|
60
|
+
&& typeof blocker === 'object'
|
|
61
|
+
&& blocker.target
|
|
62
|
+
&& blocker.evidence
|
|
63
|
+
&& blocker.requiredAction,
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
56
67
|
function validateTurnState(routeContext, turnState, cwd) {
|
|
57
68
|
if (!turnState) {
|
|
58
69
|
return buildBlockReason(routeContext, '缺少主代理 turn-state。', cwd)
|
|
@@ -62,6 +73,13 @@ function validateTurnState(routeContext, turnState, cwd) {
|
|
|
62
73
|
}
|
|
63
74
|
if (turnState.kind === 'waiting' || turnState.kind === 'blocked') {
|
|
64
75
|
if (turnState.reasonCategory && turnState.reason) {
|
|
76
|
+
if (!hasStructuredBlocker(turnState)) {
|
|
77
|
+
return buildBlockReason(
|
|
78
|
+
routeContext,
|
|
79
|
+
'当前 waiting/blocked 缺少结构化 `blocker.target`、`blocker.evidence` 或 `blocker.requiredAction`,不能证明存在可核实的真实阻塞。',
|
|
80
|
+
cwd,
|
|
81
|
+
)
|
|
82
|
+
}
|
|
65
83
|
return ''
|
|
66
84
|
}
|
|
67
85
|
return buildBlockReason(
|
|
@@ -76,14 +94,14 @@ function validateTurnState(routeContext, turnState, cwd) {
|
|
|
76
94
|
function main() {
|
|
77
95
|
const payload = readStdinJson()
|
|
78
96
|
const cwd = payload.cwd || process.cwd()
|
|
79
|
-
const routeContext = getApplicableRouteContext({ cwd })
|
|
97
|
+
const routeContext = getApplicableRouteContext({ cwd, payload })
|
|
80
98
|
|
|
81
99
|
if (!routeContext || !ENFORCED_COMMANDS.has(routeContext.skillName)) {
|
|
82
100
|
process.stdout.write(JSON.stringify({ decision: 'continue' }))
|
|
83
101
|
return
|
|
84
102
|
}
|
|
85
103
|
|
|
86
|
-
const reason = validateTurnState(routeContext, getMainTurnState(cwd), cwd)
|
|
104
|
+
const reason = validateTurnState(routeContext, getMainTurnState(cwd, payload), cwd)
|
|
87
105
|
process.stdout.write(JSON.stringify(reason ? { decision: 'block', reason } : { decision: 'continue' }))
|
|
88
106
|
}
|
|
89
107
|
|
package/scripts/verify-state.mjs
CHANGED
|
@@ -1,27 +1,33 @@
|
|
|
1
|
-
import { existsSync,
|
|
2
|
-
import { execSync } from 'node:child_process'
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
3
2
|
import { join } from 'node:path'
|
|
4
3
|
import { appendReplayEvent } from './replay-state.mjs'
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
import {
|
|
5
|
+
getProjectVerifyYamlPath,
|
|
6
|
+
} from './project-storage.mjs'
|
|
7
|
+
import {
|
|
8
|
+
captureWorkspaceFingerprint,
|
|
9
|
+
clearRuntimeEvidence,
|
|
10
|
+
getRuntimeEvidencePath,
|
|
11
|
+
getRuntimeEvidenceRelativePath,
|
|
12
|
+
readRuntimeEvidence,
|
|
13
|
+
validateEvidenceFingerprint,
|
|
14
|
+
validateEvidenceTimestamp,
|
|
15
|
+
writeRuntimeEvidence,
|
|
16
|
+
} from './runtime-artifacts.mjs'
|
|
17
|
+
|
|
18
|
+
export const VERIFY_EVIDENCE_FILE_NAME = 'verify.json'
|
|
9
19
|
const SHELL_OPERATORS = /[;&|`$(){}\n\r]/
|
|
10
20
|
|
|
11
|
-
export function getVerifyEvidencePath(cwd) {
|
|
12
|
-
return
|
|
21
|
+
export function getVerifyEvidencePath(cwd, options = {}) {
|
|
22
|
+
return getRuntimeEvidencePath(cwd, VERIFY_EVIDENCE_FILE_NAME, options)
|
|
13
23
|
}
|
|
14
24
|
|
|
15
|
-
export function readVerifyEvidence(cwd) {
|
|
16
|
-
|
|
17
|
-
return JSON.parse(readFileSync(getVerifyEvidencePath(cwd), 'utf-8'))
|
|
18
|
-
} catch {
|
|
19
|
-
return null
|
|
20
|
-
}
|
|
25
|
+
export function readVerifyEvidence(cwd, options = {}) {
|
|
26
|
+
return readRuntimeEvidence(cwd, VERIFY_EVIDENCE_FILE_NAME, options)
|
|
21
27
|
}
|
|
22
28
|
|
|
23
|
-
export function clearVerifyEvidence(cwd) {
|
|
24
|
-
|
|
29
|
+
export function clearVerifyEvidence(cwd, options = {}) {
|
|
30
|
+
clearRuntimeEvidence(cwd, VERIFY_EVIDENCE_FILE_NAME, options)
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
function loadVerifyYaml(cwd) {
|
|
@@ -89,34 +95,7 @@ export function hasUnsafeVerifyCommand(commands = []) {
|
|
|
89
95
|
return commands.some((cmd) => SHELL_OPERATORS.test(cmd))
|
|
90
96
|
}
|
|
91
97
|
|
|
92
|
-
function
|
|
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 })
|
|
98
|
+
export function writeVerifyEvidence(cwd, { commands = [], fastOnly = false, source = 'ralph-loop' } = {}, options = {}) {
|
|
120
99
|
const payload = {
|
|
121
100
|
updatedAt: new Date().toISOString(),
|
|
122
101
|
commands,
|
|
@@ -124,15 +103,16 @@ export function writeVerifyEvidence(cwd, { commands = [], fastOnly = false, sour
|
|
|
124
103
|
source,
|
|
125
104
|
fingerprint: captureWorkspaceFingerprint(cwd),
|
|
126
105
|
}
|
|
127
|
-
|
|
106
|
+
writeRuntimeEvidence(cwd, VERIFY_EVIDENCE_FILE_NAME, payload, options)
|
|
128
107
|
appendReplayEvent(cwd, {
|
|
129
108
|
event: 'verify_evidence_written',
|
|
130
109
|
source,
|
|
110
|
+
payload: options.payload || {},
|
|
131
111
|
details: {
|
|
132
112
|
commands,
|
|
133
113
|
fastOnly,
|
|
134
114
|
},
|
|
135
|
-
artifacts: [
|
|
115
|
+
artifacts: [getRuntimeEvidenceRelativePath(cwd, VERIFY_EVIDENCE_FILE_NAME, options)],
|
|
136
116
|
})
|
|
137
117
|
}
|
|
138
118
|
|
|
@@ -142,7 +122,7 @@ function validateVerifyEvidencePresence(commands, evidence) {
|
|
|
142
122
|
required: true,
|
|
143
123
|
status: 'missing',
|
|
144
124
|
commands,
|
|
145
|
-
details: ['
|
|
125
|
+
details: ['缺少当前工作流的成功验证证据'],
|
|
146
126
|
}
|
|
147
127
|
}
|
|
148
128
|
|
|
@@ -153,51 +133,20 @@ function validateVerifyEvidenceFreshness(cwd, commands, evidence, now) {
|
|
|
153
133
|
status: 'fast-only',
|
|
154
134
|
commands,
|
|
155
135
|
evidence,
|
|
156
|
-
details: ['
|
|
136
|
+
details: ['最新验证证据只覆盖子代理快速检查'],
|
|
157
137
|
}
|
|
158
138
|
}
|
|
159
139
|
|
|
160
|
-
const
|
|
161
|
-
|
|
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
|
|
140
|
+
const timestampError = validateEvidenceTimestamp(evidence, now, '验证证据')
|
|
141
|
+
return timestampError ? { ...timestampError, commands } : null
|
|
180
142
|
}
|
|
181
143
|
|
|
182
144
|
function validateVerifyFingerprint(cwd, commands, evidence) {
|
|
183
|
-
const
|
|
184
|
-
|
|
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
|
|
145
|
+
const fingerprintError = validateEvidenceFingerprint(cwd, evidence, '成功验证证据')
|
|
146
|
+
return fingerprintError ? { ...fingerprintError, commands } : null
|
|
198
147
|
}
|
|
199
148
|
|
|
200
|
-
export function getVerifyEvidenceStatus(cwd, now = Date.now()) {
|
|
149
|
+
export function getVerifyEvidenceStatus(cwd, { now = Date.now(), ...options } = {}) {
|
|
201
150
|
const commands = detectCommands(cwd)
|
|
202
151
|
if (!commands.length) {
|
|
203
152
|
return {
|
|
@@ -207,7 +156,7 @@ export function getVerifyEvidenceStatus(cwd, now = Date.now()) {
|
|
|
207
156
|
}
|
|
208
157
|
}
|
|
209
158
|
|
|
210
|
-
const evidence = readVerifyEvidence(cwd)
|
|
159
|
+
const evidence = readVerifyEvidence(cwd, options)
|
|
211
160
|
const missingError = validateVerifyEvidencePresence(commands, evidence)
|
|
212
161
|
if (missingError) return missingError
|
|
213
162
|
|