helloagents 3.0.12 → 3.0.15-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 +169 -30
- package/README_CN.md +169 -30
- package/bootstrap-lite.md +27 -20
- package/bootstrap.md +30 -23
- package/cli.mjs +119 -11
- package/gemini-extension.json +1 -1
- package/install.ps1 +125 -0
- package/install.sh +118 -0
- package/package.json +23 -4
- package/scripts/advisor-state.mjs +36 -63
- package/scripts/capability-registry.mjs +3 -3
- package/scripts/cli-branch.mjs +84 -0
- package/scripts/cli-codex-config.mjs +11 -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 +92 -27
- package/scripts/cli-lifecycle.mjs +9 -7
- package/scripts/cli-messages.mjs +34 -16
- package/scripts/cli-runtime-carrier.mjs +36 -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-gates.mjs +2 -0
- package/scripts/notify-route.mjs +9 -7
- package/scripts/notify-ui.mjs +46 -33
- package/scripts/notify.mjs +60 -32
- 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 +2 -2
- 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 +3 -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 +4 -4
- package/skills/hello-verify/SKILL.md +10 -7
- package/skills/helloagents/SKILL.md +12 -7
- package/templates/context.md +6 -0
- package/templates/plans/plan.md +3 -0
- package/templates/plans/tasks.md +8 -3
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { DEFAULTS } from './cli-config.mjs'
|
|
4
|
+
import { safeJson } from './cli-utils.mjs'
|
|
5
|
+
|
|
6
|
+
const CARRIER_SETTING_KEYS = [
|
|
7
|
+
'output_language',
|
|
8
|
+
'output_format',
|
|
9
|
+
'notify_level',
|
|
10
|
+
'ralph_loop_enabled',
|
|
11
|
+
'guard_enabled',
|
|
12
|
+
'kb_create_mode',
|
|
13
|
+
'project_store_mode',
|
|
14
|
+
'commit_attribution',
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
function pickCarrierSettings(settings) {
|
|
18
|
+
const merged = { ...DEFAULTS, ...(settings || {}) }
|
|
19
|
+
return Object.fromEntries(CARRIER_SETTING_KEYS.map((key) => [key, merged[key]]))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function readCarrierSettings(home) {
|
|
23
|
+
return pickCarrierSettings(safeJson(join(home, '.helloagents', 'helloagents.json')) || {})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildRuntimeCarrier(bootstrapContent, settings = {}) {
|
|
27
|
+
const normalized = String(bootstrapContent || '').trim()
|
|
28
|
+
if (!normalized) return ''
|
|
29
|
+
|
|
30
|
+
const carrierSettings = pickCarrierSettings(settings)
|
|
31
|
+
const snapshot = Object.keys(carrierSettings).length
|
|
32
|
+
? `\n\n## 当前用户设置\n\`\`\`json\n${JSON.stringify(carrierSettings, null, 2)}\n\`\`\``
|
|
33
|
+
: ''
|
|
34
|
+
|
|
35
|
+
return `${normalized}${snapshot}\n`
|
|
36
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { mkdtempSync, realpathSync, renameSync } from 'node:fs'
|
|
2
|
+
import { dirname, join, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { copyEntries, ensureDir, removeIfExists } from './cli-utils.mjs'
|
|
5
|
+
|
|
6
|
+
export const RUNTIME_ROOT_ENTRIES = [
|
|
7
|
+
'.claude-plugin',
|
|
8
|
+
'.codex-plugin',
|
|
9
|
+
'assets',
|
|
10
|
+
'bootstrap-lite.md',
|
|
11
|
+
'bootstrap.md',
|
|
12
|
+
'cli.mjs',
|
|
13
|
+
'gemini-extension.json',
|
|
14
|
+
'hooks',
|
|
15
|
+
'install.ps1',
|
|
16
|
+
'install.sh',
|
|
17
|
+
'LICENSE.md',
|
|
18
|
+
'package.json',
|
|
19
|
+
'README.md',
|
|
20
|
+
'README_CN.md',
|
|
21
|
+
'scripts',
|
|
22
|
+
'skills',
|
|
23
|
+
'templates',
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
/** Return the stable per-user runtime copy used by host integrations. */
|
|
27
|
+
export function getStableRuntimeRoot(home) {
|
|
28
|
+
return join(home, '.helloagents', 'helloagents')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizePath(path) {
|
|
32
|
+
const resolved = resolve(path)
|
|
33
|
+
try {
|
|
34
|
+
return realpathSync(resolved)
|
|
35
|
+
} catch {
|
|
36
|
+
return resolved
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function samePath(left, right) {
|
|
41
|
+
const a = normalizePath(left)
|
|
42
|
+
const b = normalizePath(right)
|
|
43
|
+
return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Sync package runtime files into the stable root without copying repo-only files. */
|
|
47
|
+
export function syncRuntimeRoot(sourceRoot, runtimeRoot) {
|
|
48
|
+
const source = resolve(sourceRoot)
|
|
49
|
+
const target = resolve(runtimeRoot)
|
|
50
|
+
if (samePath(source, target)) {
|
|
51
|
+
return { synced: false, root: target }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const parent = dirname(target)
|
|
55
|
+
ensureDir(parent)
|
|
56
|
+
const staging = mkdtempSync(join(parent, '.helloagents-runtime-'))
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
copyEntries(source, staging, RUNTIME_ROOT_ENTRIES)
|
|
60
|
+
removeIfExists(target)
|
|
61
|
+
renameSync(staging, target)
|
|
62
|
+
return { synced: true, root: target }
|
|
63
|
+
} catch (error) {
|
|
64
|
+
removeIfExists(staging)
|
|
65
|
+
throw error
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Remove the stable runtime copy while leaving user settings under ~/.helloagents intact. */
|
|
70
|
+
export function removeRuntimeRoot(runtimeRoot) {
|
|
71
|
+
removeIfExists(runtimeRoot)
|
|
72
|
+
}
|
package/scripts/cli-toml.mjs
CHANGED
|
@@ -164,85 +164,6 @@ export function ensureTopLevelTomlLine(text, key, line) {
|
|
|
164
164
|
return upsertTopLevelTomlKey(text, key, value);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
export function readTomlKeyInSection(text, headerLine, key) {
|
|
168
|
-
const lines = String(text || '').replace(/\r\n/g, '\n').split('\n');
|
|
169
|
-
const headerIndex = lines.findIndex((line) => line.trim() === headerLine);
|
|
170
|
-
if (headerIndex < 0) return '';
|
|
171
|
-
|
|
172
|
-
const keyRe = new RegExp(`^\\s*${key}\\s*=.*$`);
|
|
173
|
-
for (let index = headerIndex + 1; index < lines.length; index += 1) {
|
|
174
|
-
const line = lines[index];
|
|
175
|
-
if (isTomlTableHeader(line)) break;
|
|
176
|
-
if (keyRe.test(line)) return line.trim();
|
|
177
|
-
}
|
|
178
|
-
return '';
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function removeTomlKeyInSection(text, headerLine, key) {
|
|
182
|
-
const lines = String(text || '').replace(/\r\n/g, '\n').split('\n');
|
|
183
|
-
const headerIndex = lines.findIndex((line) => line.trim() === headerLine);
|
|
184
|
-
if (headerIndex < 0) return normalizeToml(text);
|
|
185
|
-
|
|
186
|
-
const keyRe = new RegExp(`^\\s*${key}\\s*=`);
|
|
187
|
-
const nextLines = [];
|
|
188
|
-
let removed = false;
|
|
189
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
190
|
-
const line = lines[index];
|
|
191
|
-
if (index > headerIndex && isTomlTableHeader(line)) {
|
|
192
|
-
nextLines.push(...lines.slice(index));
|
|
193
|
-
break;
|
|
194
|
-
}
|
|
195
|
-
if (index > headerIndex && keyRe.test(line)) {
|
|
196
|
-
removed = true;
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
nextLines.push(line);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (!removed) return normalizeToml(text);
|
|
203
|
-
return normalizeToml(nextLines.join('\n'));
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export function upsertTomlKeyInSection(text, headerLine, key, value) {
|
|
207
|
-
const lines = String(text || '').replace(/\r\n/g, '\n').split('\n');
|
|
208
|
-
const headerIndex = lines.findIndex((line) => line.trim() === headerLine);
|
|
209
|
-
|
|
210
|
-
if (headerIndex < 0) {
|
|
211
|
-
const base = normalizeToml(text).trimEnd();
|
|
212
|
-
return base
|
|
213
|
-
? `${base}\n\n${headerLine}\n${key} = ${value}\n`
|
|
214
|
-
: `${headerLine}\n${key} = ${value}\n`;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
let endIndex = headerIndex + 1;
|
|
218
|
-
while (endIndex < lines.length && !isTomlTableHeader(lines[endIndex])) {
|
|
219
|
-
endIndex += 1;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const keyRe = new RegExp(`^\\s*${key}\\s*=`);
|
|
223
|
-
let updated = false;
|
|
224
|
-
for (let index = headerIndex + 1; index < endIndex; index += 1) {
|
|
225
|
-
if (keyRe.test(lines[index])) {
|
|
226
|
-
lines[index] = `${key} = ${value}`;
|
|
227
|
-
updated = true;
|
|
228
|
-
break;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (!updated) {
|
|
233
|
-
lines.splice(endIndex, 0, `${key} = ${value}`);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return normalizeToml(lines.join('\n'));
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export function ensureTomlKeyInSection(text, headerLine, key, line) {
|
|
240
|
-
const normalized = String(line || '').trim();
|
|
241
|
-
if (!normalized) return normalizeToml(text);
|
|
242
|
-
const value = normalized.slice(normalized.indexOf('=') + 1).trim();
|
|
243
|
-
return upsertTomlKeyInSection(text, headerLine, key, value);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
167
|
export function stripTomlSection(text, headerLine) {
|
|
247
168
|
const lines = String(text || '').replace(/\r\n/g, '\n').split('\n');
|
|
248
169
|
const kept = [];
|
package/scripts/cli-utils.mjs
CHANGED
|
@@ -130,10 +130,36 @@ export function cleanSettingsHooks(settingsPath, cleanPermissions = false) {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
function rewriteHookCommandToCli(command = '', pathVar = '') {
|
|
134
|
+
const replacements = new Map([
|
|
135
|
+
[`node "${pathVar}/scripts/notify.mjs"`, 'helloagents-js notify'],
|
|
136
|
+
[`node "${pathVar}/scripts/guard.mjs"`, 'helloagents-js guard'],
|
|
137
|
+
[`node "${pathVar}/scripts/ralph-loop.mjs"`, 'helloagents-js ralph-loop'],
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
let next = command;
|
|
141
|
+
for (const [from, to] of replacements) {
|
|
142
|
+
next = next.replaceAll(from, to);
|
|
143
|
+
}
|
|
144
|
+
return next;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function rewriteHookCommands(value, pathVar) {
|
|
148
|
+
if (Array.isArray(value)) return value.map((item) => rewriteHookCommands(item, pathVar));
|
|
149
|
+
if (value && typeof value === 'object') {
|
|
150
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
|
|
151
|
+
key,
|
|
152
|
+
key === 'command' && typeof entry === 'string'
|
|
153
|
+
? rewriteHookCommandToCli(entry, pathVar)
|
|
154
|
+
: rewriteHookCommands(entry, pathVar),
|
|
155
|
+
]));
|
|
156
|
+
}
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Read hooks source file and rewrite standby hooks to the stable CLI entrypoint. */
|
|
161
|
+
export function loadHooksWithCliEntry(pkgRoot, hooksFile, pathVar) {
|
|
135
162
|
const src = safeRead(join(pkgRoot, 'hooks', hooksFile));
|
|
136
163
|
if (!src) return null;
|
|
137
|
-
|
|
138
|
-
return JSON.parse(src.replace(new RegExp(pathVar.replace(/[{}$]/g, '\\$&'), 'g'), absRoot));
|
|
164
|
+
return rewriteHookCommands(JSON.parse(src), pathVar);
|
|
139
165
|
}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import { join } from 'node:path'
|
|
4
|
-
import { captureWorkspaceFingerprint } from './verify-state.mjs'
|
|
5
3
|
import { appendReplayEvent } from './replay-state.mjs'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
import {
|
|
5
|
+
captureWorkspaceFingerprint,
|
|
6
|
+
clearRuntimeEvidence,
|
|
7
|
+
getRuntimeEvidencePath,
|
|
8
|
+
getRuntimeEvidenceRelativePath,
|
|
9
|
+
readRuntimeEvidence,
|
|
10
|
+
validateEvidenceFingerprint,
|
|
11
|
+
validateEvidenceTimestamp,
|
|
12
|
+
writeRuntimeEvidence,
|
|
13
|
+
} from './runtime-artifacts.mjs'
|
|
14
|
+
|
|
15
|
+
export const CLOSEOUT_EVIDENCE_FILE_NAME = 'closeout.json'
|
|
9
16
|
const ALLOWED_STATUSES = new Set(['PASS', 'BLOCKED'])
|
|
10
17
|
|
|
11
18
|
function normalizeEntry(entry = {}) {
|
|
@@ -15,20 +22,16 @@ function normalizeEntry(entry = {}) {
|
|
|
15
22
|
}
|
|
16
23
|
}
|
|
17
24
|
|
|
18
|
-
export function getCloseoutEvidencePath(cwd) {
|
|
19
|
-
return
|
|
25
|
+
export function getCloseoutEvidencePath(cwd, options = {}) {
|
|
26
|
+
return getRuntimeEvidencePath(cwd, CLOSEOUT_EVIDENCE_FILE_NAME, options)
|
|
20
27
|
}
|
|
21
28
|
|
|
22
|
-
export function readCloseoutEvidence(cwd) {
|
|
23
|
-
|
|
24
|
-
return JSON.parse(readFileSync(getCloseoutEvidencePath(cwd), 'utf-8'))
|
|
25
|
-
} catch {
|
|
26
|
-
return null
|
|
27
|
-
}
|
|
29
|
+
export function readCloseoutEvidence(cwd, options = {}) {
|
|
30
|
+
return readRuntimeEvidence(cwd, CLOSEOUT_EVIDENCE_FILE_NAME, options)
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
export function clearCloseoutEvidence(cwd) {
|
|
31
|
-
|
|
33
|
+
export function clearCloseoutEvidence(cwd, options = {}) {
|
|
34
|
+
clearRuntimeEvidence(cwd, CLOSEOUT_EVIDENCE_FILE_NAME, options)
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
export function normalizeCloseoutEvidence(input = {}) {
|
|
@@ -40,8 +43,7 @@ export function normalizeCloseoutEvidence(input = {}) {
|
|
|
40
43
|
}
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
export function writeCloseoutEvidence(cwd, input = {}) {
|
|
44
|
-
mkdirSync(join(cwd, '.helloagents'), { recursive: true })
|
|
46
|
+
export function writeCloseoutEvidence(cwd, input = {}, options = {}) {
|
|
45
47
|
const normalized = normalizeCloseoutEvidence(input)
|
|
46
48
|
const payload = {
|
|
47
49
|
updatedAt: new Date().toISOString(),
|
|
@@ -51,51 +53,35 @@ export function writeCloseoutEvidence(cwd, input = {}) {
|
|
|
51
53
|
deliveryChecklist: normalized.deliveryChecklist,
|
|
52
54
|
fingerprint: captureWorkspaceFingerprint(cwd),
|
|
53
55
|
}
|
|
54
|
-
|
|
56
|
+
writeRuntimeEvidence(cwd, CLOSEOUT_EVIDENCE_FILE_NAME, payload, options)
|
|
55
57
|
appendReplayEvent(cwd, {
|
|
56
58
|
event: 'closeout_evidence_written',
|
|
57
59
|
source: normalized.source || 'manual',
|
|
58
60
|
skillName: normalized.originCommand,
|
|
61
|
+
payload: options.payload || {},
|
|
59
62
|
details: {
|
|
60
63
|
requirementsCoverage: normalized.requirementsCoverage,
|
|
61
64
|
deliveryChecklist: normalized.deliveryChecklist,
|
|
62
65
|
},
|
|
63
|
-
artifacts: [
|
|
66
|
+
artifacts: [getRuntimeEvidenceRelativePath(cwd, CLOSEOUT_EVIDENCE_FILE_NAME, options)],
|
|
64
67
|
})
|
|
65
68
|
return payload
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
function readRequiredCloseoutEvidence(cwd) {
|
|
69
|
-
const evidence = readCloseoutEvidence(cwd)
|
|
71
|
+
function readRequiredCloseoutEvidence(cwd, options = {}) {
|
|
72
|
+
const evidence = readCloseoutEvidence(cwd, options)
|
|
70
73
|
if (evidence) return { evidence }
|
|
71
74
|
return {
|
|
72
75
|
error: {
|
|
73
76
|
required: true,
|
|
74
77
|
status: 'missing',
|
|
75
|
-
details: ['
|
|
78
|
+
details: ['缺少需求覆盖和交付清单的收尾证据'],
|
|
76
79
|
},
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
function validateCloseoutTimestamp(evidence, now) {
|
|
81
|
-
|
|
82
|
-
if (!Number.isFinite(updatedAt)) {
|
|
83
|
-
return {
|
|
84
|
-
required: true,
|
|
85
|
-
status: 'invalid',
|
|
86
|
-
evidence,
|
|
87
|
-
details: ['closeout evidence timestamp is invalid'],
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
if (now - updatedAt > CLOSEOUT_EVIDENCE_MAX_AGE_MS) {
|
|
91
|
-
return {
|
|
92
|
-
required: true,
|
|
93
|
-
status: 'stale-time',
|
|
94
|
-
evidence,
|
|
95
|
-
details: ['closeout evidence is older than 30 minutes'],
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return null
|
|
84
|
+
return validateEvidenceTimestamp(evidence, now, '收尾证据')
|
|
99
85
|
}
|
|
100
86
|
|
|
101
87
|
function validateCloseoutEntries(evidence) {
|
|
@@ -112,7 +98,7 @@ function validateCloseoutEntries(evidence) {
|
|
|
112
98
|
required: true,
|
|
113
99
|
status: 'invalid',
|
|
114
100
|
evidence,
|
|
115
|
-
details: ['
|
|
101
|
+
details: ['收尾证据必须记录需求覆盖和交付清单,并包含明确的 PASS/BLOCKED 状态和 summary'],
|
|
116
102
|
}
|
|
117
103
|
}
|
|
118
104
|
if (requirementsCoverage.status !== 'PASS') {
|
|
@@ -120,7 +106,7 @@ function validateCloseoutEntries(evidence) {
|
|
|
120
106
|
required: true,
|
|
121
107
|
status: 'blocked',
|
|
122
108
|
evidence,
|
|
123
|
-
details: ['
|
|
109
|
+
details: ['最新收尾证据中的需求覆盖未标记为 PASS'],
|
|
124
110
|
}
|
|
125
111
|
}
|
|
126
112
|
if (deliveryChecklist.status !== 'PASS') {
|
|
@@ -128,7 +114,7 @@ function validateCloseoutEntries(evidence) {
|
|
|
128
114
|
required: true,
|
|
129
115
|
status: 'blocked',
|
|
130
116
|
evidence,
|
|
131
|
-
details: ['
|
|
117
|
+
details: ['最新收尾证据中的交付清单未标记为 PASS'],
|
|
132
118
|
}
|
|
133
119
|
}
|
|
134
120
|
return {
|
|
@@ -138,23 +124,10 @@ function validateCloseoutEntries(evidence) {
|
|
|
138
124
|
}
|
|
139
125
|
|
|
140
126
|
function validateCloseoutFingerprint(cwd, evidence) {
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
currentFingerprint.available
|
|
144
|
-
&& evidence.fingerprint?.available
|
|
145
|
-
&& currentFingerprint.combined !== evidence.fingerprint.combined
|
|
146
|
-
) {
|
|
147
|
-
return {
|
|
148
|
-
required: true,
|
|
149
|
-
status: 'stale-diff',
|
|
150
|
-
evidence,
|
|
151
|
-
details: ['workspace diff changed after the last successful closeout evidence'],
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return null
|
|
127
|
+
return validateEvidenceFingerprint(cwd, evidence, '成功收尾证据')
|
|
155
128
|
}
|
|
156
129
|
|
|
157
|
-
export function getCloseoutEvidenceStatus(cwd, { required = false, now = Date.now() } = {}) {
|
|
130
|
+
export function getCloseoutEvidenceStatus(cwd, { required = false, now = Date.now(), ...options } = {}) {
|
|
158
131
|
if (!required) {
|
|
159
132
|
return {
|
|
160
133
|
required: false,
|
|
@@ -162,7 +135,7 @@ export function getCloseoutEvidenceStatus(cwd, { required = false, now = Date.no
|
|
|
162
135
|
}
|
|
163
136
|
}
|
|
164
137
|
|
|
165
|
-
const requiredEvidence = readRequiredCloseoutEvidence(cwd)
|
|
138
|
+
const requiredEvidence = readRequiredCloseoutEvidence(cwd, options)
|
|
166
139
|
if (requiredEvidence.error) return requiredEvidence.error
|
|
167
140
|
|
|
168
141
|
const { evidence } = requiredEvidence
|
|
@@ -200,10 +173,10 @@ function main() {
|
|
|
200
173
|
|
|
201
174
|
const input = readStdinJson()
|
|
202
175
|
const cwd = input.cwd || process.cwd()
|
|
203
|
-
const payload = writeCloseoutEvidence(cwd, input)
|
|
176
|
+
const payload = writeCloseoutEvidence(cwd, input, { payload: input })
|
|
204
177
|
process.stdout.write(JSON.stringify({
|
|
205
178
|
suppressOutput: true,
|
|
206
|
-
path: getCloseoutEvidencePath(cwd),
|
|
179
|
+
path: getCloseoutEvidencePath(cwd, { payload: input }),
|
|
207
180
|
payload,
|
|
208
181
|
}))
|
|
209
182
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export function buildUnderSpecifiedDetails(entry) {
|
|
2
|
+
return entry.taskSummary.underSpecifiedItems
|
|
3
|
+
.slice(0, 3)
|
|
4
|
+
.map((item) => {
|
|
5
|
+
const missing = []
|
|
6
|
+
if (item.files.length === 0) missing.push('缺少涉及文件')
|
|
7
|
+
if (!item.acceptance) missing.push('缺少完成标准')
|
|
8
|
+
if (!item.validation) missing.push('缺少验证方式')
|
|
9
|
+
return `${item.text}(${missing.join('、')})`
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function issueHeading(issue) {
|
|
14
|
+
switch (issue.type) {
|
|
15
|
+
case 'missing-files':
|
|
16
|
+
return '方案包缺少必需文件'
|
|
17
|
+
case 'template-placeholders':
|
|
18
|
+
return '方案包仍包含模板占位内容'
|
|
19
|
+
case 'missing-task-checklist':
|
|
20
|
+
return '方案包没有可执行任务'
|
|
21
|
+
case 'unfinished-tasks':
|
|
22
|
+
return '方案包仍有未完成任务'
|
|
23
|
+
case 'under-specified-tasks':
|
|
24
|
+
return '任务缺少可交付元数据'
|
|
25
|
+
case 'missing-contract':
|
|
26
|
+
return '方案包缺少可信的结构化契约'
|
|
27
|
+
case 'missing-verify-evidence':
|
|
28
|
+
return '当前工作流缺少最新验证证据'
|
|
29
|
+
case 'missing-review-evidence':
|
|
30
|
+
return '当前工作流缺少最新审查证据'
|
|
31
|
+
case 'missing-advisor-evidence':
|
|
32
|
+
return '当前工作流缺少最新 advisor 证据'
|
|
33
|
+
case 'missing-visual-evidence':
|
|
34
|
+
return '当前工作流缺少最新视觉验收证据'
|
|
35
|
+
case 'missing-closeout-evidence':
|
|
36
|
+
return '当前工作流缺少最新收尾证据'
|
|
37
|
+
default:
|
|
38
|
+
return '方案包尚未达到交付条件'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function buildDeliveryBlockReason(issues, recommendation, gateHint) {
|
|
43
|
+
const lines = ['[Delivery Gate] 当前工作流尚未闭合,暂不能交付:']
|
|
44
|
+
|
|
45
|
+
for (const issue of issues) {
|
|
46
|
+
lines.push(`- ${issue.planName}: ${issueHeading(issue)}`)
|
|
47
|
+
for (const detail of issue.details) {
|
|
48
|
+
lines.push(` - ${detail}`)
|
|
49
|
+
}
|
|
50
|
+
if (issue.extraCount) {
|
|
51
|
+
lines.push(` - 另有 ${issue.extraCount} 项`)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
lines.push('')
|
|
56
|
+
if (recommendation?.nextPath) {
|
|
57
|
+
lines.push(`建议路径:${recommendation.nextPath}`)
|
|
58
|
+
}
|
|
59
|
+
if (issues.some((issue) => issue.type === 'missing-closeout-evidence')) {
|
|
60
|
+
lines.push('下一步收尾:先写入当前会话 `artifacts/closeout.json`,记录 `requirementsCoverage` 和 `deliveryChecklist`,再报告完成。')
|
|
61
|
+
}
|
|
62
|
+
if (issues.some((issue) => issue.type === 'missing-visual-evidence')) {
|
|
63
|
+
lines.push('下一步视觉验收:先写入当前会话 `artifacts/visual.json`,记录 `tooling`、`screensChecked`、`statesChecked`、`status` 和 `summary`,再报告完成。')
|
|
64
|
+
}
|
|
65
|
+
if (gateHint) {
|
|
66
|
+
lines.push(gateHint)
|
|
67
|
+
}
|
|
68
|
+
lines.push('暂不要报告完成。先完成剩余任务、明确关闭任务,或修复方案包,使其成为可信的交付记录。')
|
|
69
|
+
return lines.join('\n')
|
|
70
|
+
}
|
|
@@ -12,24 +12,13 @@ import { getVisualEvidenceStatus } from './visual-state.mjs'
|
|
|
12
12
|
import { buildDeliveryGateHint, getDeliveryAction, getWorkflowRecommendation, getWorkflowSnapshot } from './workflow-state.mjs'
|
|
13
13
|
import { getReviewEvidenceStatus } from './review-state.mjs'
|
|
14
14
|
import { getVerifyEvidenceStatus } from './verify-state.mjs'
|
|
15
|
+
import { buildDeliveryBlockReason, buildUnderSpecifiedDetails } from './delivery-gate-messages.mjs'
|
|
15
16
|
|
|
16
17
|
function selectGatePlans(snapshot) {
|
|
17
18
|
if (snapshot.activePlans.length > 0) return snapshot.activePlans
|
|
18
19
|
return snapshot.plans
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
function buildUnderSpecifiedDetails(entry) {
|
|
22
|
-
return entry.taskSummary.underSpecifiedItems
|
|
23
|
-
.slice(0, 3)
|
|
24
|
-
.map((item) => {
|
|
25
|
-
const missing = []
|
|
26
|
-
if (item.files.length === 0) missing.push('missing files')
|
|
27
|
-
if (!item.acceptance) missing.push('missing acceptance')
|
|
28
|
-
if (!item.validation) missing.push('missing validation')
|
|
29
|
-
return `${item.text} (${missing.join(', ')})`
|
|
30
|
-
})
|
|
31
|
-
}
|
|
32
|
-
|
|
33
22
|
function collectTaskMetadataIssues(entry, issues) {
|
|
34
23
|
if (entry.taskSummary.underSpecifiedCount === 0) return
|
|
35
24
|
issues.push({
|
|
@@ -48,7 +37,7 @@ function collectPlanIssues(planEntries) {
|
|
|
48
37
|
issues.push({
|
|
49
38
|
type: 'missing-files',
|
|
50
39
|
planName: entry.planName,
|
|
51
|
-
details: entry.missingFiles.map((file) =>
|
|
40
|
+
details: entry.missingFiles.map((file) => `缺少 ${file}`),
|
|
52
41
|
})
|
|
53
42
|
}
|
|
54
43
|
|
|
@@ -64,7 +53,7 @@ function collectPlanIssues(planEntries) {
|
|
|
64
53
|
issues.push({
|
|
65
54
|
type: 'missing-task-checklist',
|
|
66
55
|
planName: entry.planName,
|
|
67
|
-
details: ['tasks.md
|
|
56
|
+
details: ['tasks.md 没有可执行检查项'],
|
|
68
57
|
})
|
|
69
58
|
continue
|
|
70
59
|
}
|
|
@@ -140,65 +129,6 @@ function collectGateIssues(planEntries, verificationStatus, reviewStatus, adviso
|
|
|
140
129
|
return issues
|
|
141
130
|
}
|
|
142
131
|
|
|
143
|
-
function issueHeading(issue) {
|
|
144
|
-
switch (issue.type) {
|
|
145
|
-
case 'missing-files':
|
|
146
|
-
return 'active plan package is missing required artifacts'
|
|
147
|
-
case 'template-placeholders':
|
|
148
|
-
return 'active plan package still contains template placeholders'
|
|
149
|
-
case 'missing-task-checklist':
|
|
150
|
-
return 'active plan package has no executable tasks'
|
|
151
|
-
case 'unfinished-tasks':
|
|
152
|
-
return 'active plan package still has unfinished tasks'
|
|
153
|
-
case 'under-specified-tasks':
|
|
154
|
-
return 'active plan package has under-specified task metadata'
|
|
155
|
-
case 'missing-contract':
|
|
156
|
-
return 'active plan package is missing a trustworthy structured contract'
|
|
157
|
-
case 'missing-verify-evidence':
|
|
158
|
-
return 'current workflow is missing fresh verification evidence'
|
|
159
|
-
case 'missing-review-evidence':
|
|
160
|
-
return 'current workflow is missing fresh review evidence'
|
|
161
|
-
case 'missing-advisor-evidence':
|
|
162
|
-
return 'current workflow is missing fresh advisor evidence'
|
|
163
|
-
case 'missing-visual-evidence':
|
|
164
|
-
return 'current workflow is missing fresh visual validation evidence'
|
|
165
|
-
case 'missing-closeout-evidence':
|
|
166
|
-
return 'current workflow is missing fresh closeout evidence'
|
|
167
|
-
default:
|
|
168
|
-
return 'active plan package is not ready for delivery'
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function buildBlockReason(issues, recommendation, gateHint) {
|
|
173
|
-
const lines = ['[Delivery Gate] Delivery is blocked because the current workflow state is not closed yet:']
|
|
174
|
-
|
|
175
|
-
for (const issue of issues) {
|
|
176
|
-
lines.push(`- ${issue.planName}: ${issueHeading(issue)}`)
|
|
177
|
-
for (const detail of issue.details) {
|
|
178
|
-
lines.push(` - ${detail}`)
|
|
179
|
-
}
|
|
180
|
-
if (issue.extraCount) {
|
|
181
|
-
lines.push(` - ...and ${issue.extraCount} more`)
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
lines.push('')
|
|
186
|
-
if (recommendation?.nextPath) {
|
|
187
|
-
lines.push(`Recommended path: ${recommendation.nextPath}`)
|
|
188
|
-
}
|
|
189
|
-
if (issues.some((issue) => issue.type === 'missing-closeout-evidence')) {
|
|
190
|
-
lines.push('Next closeout step: write `.helloagents/.ralph-closeout.json` with `requirementsCoverage` and `deliveryChecklist` before reporting completion.')
|
|
191
|
-
}
|
|
192
|
-
if (issues.some((issue) => issue.type === 'missing-visual-evidence')) {
|
|
193
|
-
lines.push('Next visual step: write `.helloagents/.ralph-visual.json` with `tooling`, `screensChecked`, `statesChecked`, `status`, and `summary` before reporting completion.')
|
|
194
|
-
}
|
|
195
|
-
if (gateHint) {
|
|
196
|
-
lines.push(gateHint)
|
|
197
|
-
}
|
|
198
|
-
lines.push('Do not report completion yet. First finish or explicitly close the remaining tasks, or repair the active plan package so it becomes a trustworthy delivery record.')
|
|
199
|
-
return lines.join('\n')
|
|
200
|
-
}
|
|
201
|
-
|
|
202
132
|
function main() {
|
|
203
133
|
let data = {}
|
|
204
134
|
try {
|
|
@@ -208,11 +138,12 @@ function main() {
|
|
|
208
138
|
const workflowOptions = { payload: data }
|
|
209
139
|
const snapshot = getWorkflowSnapshot(cwd, workflowOptions)
|
|
210
140
|
const recommendation = getWorkflowRecommendation(cwd, workflowOptions)
|
|
211
|
-
const verificationStatus = getVerifyEvidenceStatus(cwd)
|
|
141
|
+
const verificationStatus = getVerifyEvidenceStatus(cwd, workflowOptions)
|
|
212
142
|
const deliveryAction = getDeliveryAction(cwd, workflowOptions)
|
|
213
143
|
const gatePlans = selectGatePlans(snapshot)
|
|
214
144
|
const reviewStatus = getReviewEvidenceStatus(cwd, {
|
|
215
145
|
required: deliveryAction?.phase === 'verify' && deliveryAction?.mode === 'review-first',
|
|
146
|
+
...workflowOptions,
|
|
216
147
|
})
|
|
217
148
|
if (gatePlans.length === 0) {
|
|
218
149
|
process.stdout.write(JSON.stringify({ suppressOutput: true }))
|
|
@@ -223,12 +154,14 @@ function main() {
|
|
|
223
154
|
const advisorStatus = getAdvisorEvidenceStatus(cwd, {
|
|
224
155
|
required: advisorRequirements.some((entry) => entry.required),
|
|
225
156
|
focus: advisorRequirements.flatMap((entry) => entry.focus || []),
|
|
157
|
+
...workflowOptions,
|
|
226
158
|
})
|
|
227
159
|
const visualRequirements = gatePlans.map((entry) => getVisualValidationRequirement(entry.contract))
|
|
228
160
|
const visualStatus = getVisualEvidenceStatus(cwd, {
|
|
229
161
|
required: visualRequirements.some((entry) => entry.required),
|
|
230
162
|
screens: visualRequirements.flatMap((entry) => entry.screens || []),
|
|
231
163
|
states: visualRequirements.flatMap((entry) => entry.states || []),
|
|
164
|
+
...workflowOptions,
|
|
232
165
|
})
|
|
233
166
|
const closeoutRequired = (
|
|
234
167
|
gatePlans.every((entry) => entry.missingFiles.length === 0 && entry.templateIssues.length === 0 && entry.taskSummary.total > 0 && entry.taskSummary.open === 0 && entry.taskSummary.underSpecifiedCount === 0)
|
|
@@ -239,6 +172,7 @@ function main() {
|
|
|
239
172
|
)
|
|
240
173
|
const closeoutStatus = getCloseoutEvidenceStatus(cwd, {
|
|
241
174
|
required: closeoutRequired,
|
|
175
|
+
...workflowOptions,
|
|
242
176
|
})
|
|
243
177
|
|
|
244
178
|
const issues = collectGateIssues(gatePlans, verificationStatus, reviewStatus, advisorStatus, visualStatus, closeoutStatus)
|
|
@@ -249,7 +183,7 @@ function main() {
|
|
|
249
183
|
|
|
250
184
|
process.stdout.write(JSON.stringify({
|
|
251
185
|
decision: 'block',
|
|
252
|
-
reason:
|
|
186
|
+
reason: buildDeliveryBlockReason(issues, recommendation, buildDeliveryGateHint(cwd, workflowOptions)),
|
|
253
187
|
suppressOutput: true,
|
|
254
188
|
}))
|
|
255
189
|
}
|