@brawnen/agent-harness-cli 0.1.0
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/LICENSE +21 -0
- package/README.md +224 -0
- package/README.zh-CN.md +232 -0
- package/bin/agent-harness.js +6 -0
- package/package.json +46 -0
- package/src/commands/audit.js +110 -0
- package/src/commands/delivery.js +497 -0
- package/src/commands/docs.js +251 -0
- package/src/commands/gate.js +236 -0
- package/src/commands/init.js +711 -0
- package/src/commands/report.js +272 -0
- package/src/commands/state.js +274 -0
- package/src/commands/status.js +493 -0
- package/src/commands/task.js +316 -0
- package/src/commands/verify.js +173 -0
- package/src/index.js +101 -0
- package/src/lib/audit-store.js +80 -0
- package/src/lib/delivery-policy.js +219 -0
- package/src/lib/output-policy.js +266 -0
- package/src/lib/project-config.js +235 -0
- package/src/lib/runtime-paths.js +46 -0
- package/src/lib/state-store.js +510 -0
- package/src/lib/task-core.js +490 -0
- package/src/lib/workflow-policy.js +307 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { evaluateTaskDeliveryReadiness, summarizeDeliveryReadiness } from "../lib/delivery-policy.js";
|
|
5
|
+
import { evaluateTaskArtifactPolicy, inspectOutputPolicyWorkspace, normalizeOutputPolicy } from "../lib/output-policy.js";
|
|
6
|
+
import { loadProjectConfig } from "../lib/project-config.js";
|
|
7
|
+
import {
|
|
8
|
+
hasRuntimeSetup,
|
|
9
|
+
resolveRuntimeDirName,
|
|
10
|
+
runtimeRelativeCandidates,
|
|
11
|
+
runtimeRelativePathForCwd
|
|
12
|
+
} from "../lib/runtime-paths.js";
|
|
13
|
+
import { getActiveTask } from "../lib/state-store.js";
|
|
14
|
+
import { buildWorkflowWarning, evaluateTaskWorkflowDecision, normalizeWorkflowPolicy } from "../lib/workflow-policy.js";
|
|
15
|
+
|
|
16
|
+
const REQUIRED_TEMPLATE_FILES = [
|
|
17
|
+
"bug.md",
|
|
18
|
+
"explore.md",
|
|
19
|
+
"feature.md"
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export function runStatus(argv) {
|
|
23
|
+
if (argv.length > 0) {
|
|
24
|
+
console.error(`status 不接受额外参数: ${argv.join(" ")}`);
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const cwd = process.cwd();
|
|
29
|
+
const projectName = path.basename(cwd);
|
|
30
|
+
const detectedHosts = detectHosts(cwd);
|
|
31
|
+
const checks = [];
|
|
32
|
+
const warnings = [];
|
|
33
|
+
let exitCode = 0;
|
|
34
|
+
|
|
35
|
+
const harnessConfig = inspectHarnessConfig(cwd);
|
|
36
|
+
pushCheck(checks, harnessConfig);
|
|
37
|
+
exitCode = maxExitCode(exitCode, harnessConfig.severity);
|
|
38
|
+
|
|
39
|
+
const deliveryPolicyCheck = inspectDeliveryPolicy(cwd);
|
|
40
|
+
pushCheck(checks, deliveryPolicyCheck);
|
|
41
|
+
exitCode = maxExitCode(exitCode, deliveryPolicyCheck.severity);
|
|
42
|
+
|
|
43
|
+
const outputPolicyCheck = inspectOutputPolicy(cwd);
|
|
44
|
+
pushCheck(checks, outputPolicyCheck);
|
|
45
|
+
exitCode = maxExitCode(exitCode, outputPolicyCheck.severity);
|
|
46
|
+
|
|
47
|
+
const artifactHintsCheck = inspectActiveTaskArtifactHints(cwd);
|
|
48
|
+
pushCheck(checks, artifactHintsCheck);
|
|
49
|
+
exitCode = maxExitCode(exitCode, artifactHintsCheck.severity);
|
|
50
|
+
|
|
51
|
+
const workflowModeCheck = inspectWorkflowMode(cwd);
|
|
52
|
+
pushCheck(checks, workflowModeCheck);
|
|
53
|
+
exitCode = maxExitCode(exitCode, workflowModeCheck.severity);
|
|
54
|
+
|
|
55
|
+
const hosts = detectedHosts.length > 0 ? detectedHosts : ["claude-code", "codex", "gemini-cli"];
|
|
56
|
+
for (const host of hosts) {
|
|
57
|
+
const hostCheck = inspectHostRules(cwd, host, detectedHosts.length === 0);
|
|
58
|
+
pushCheck(checks, hostCheck);
|
|
59
|
+
exitCode = maxExitCode(exitCode, hostCheck.severity);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const templatesCheck = inspectTemplates(cwd);
|
|
63
|
+
pushCheck(checks, templatesCheck);
|
|
64
|
+
exitCode = maxExitCode(exitCode, templatesCheck.severity);
|
|
65
|
+
|
|
66
|
+
const runtimeMode = detectRuntimeMode(cwd);
|
|
67
|
+
const codexHooksCheck = inspectCodexHooks(cwd, hosts.includes("codex"));
|
|
68
|
+
pushCheck(checks, codexHooksCheck);
|
|
69
|
+
exitCode = maxExitCode(exitCode, codexHooksCheck.severity);
|
|
70
|
+
|
|
71
|
+
const claudeHooksCheck = inspectClaudeHooks(cwd, runtimeMode, hosts.includes("claude-code"));
|
|
72
|
+
pushCheck(checks, claudeHooksCheck);
|
|
73
|
+
exitCode = maxExitCode(exitCode, claudeHooksCheck.severity);
|
|
74
|
+
|
|
75
|
+
const runtimeDirsCheck = inspectRuntimeDirectories(cwd, runtimeMode);
|
|
76
|
+
pushCheck(checks, runtimeDirsCheck);
|
|
77
|
+
exitCode = maxExitCode(exitCode, runtimeDirsCheck.severity);
|
|
78
|
+
|
|
79
|
+
const gitignoreCheck = inspectGitignore(cwd, runtimeMode);
|
|
80
|
+
pushCheck(checks, gitignoreCheck);
|
|
81
|
+
exitCode = maxExitCode(exitCode, gitignoreCheck.severity);
|
|
82
|
+
|
|
83
|
+
for (const check of checks) {
|
|
84
|
+
if (check.severity === "warn") {
|
|
85
|
+
warnings.push(check.message);
|
|
86
|
+
} else if (check.severity === "fail") {
|
|
87
|
+
warnings.push(check.message);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
printStatus(projectName, checks, warnings);
|
|
92
|
+
return exitCode;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function inspectHarnessConfig(cwd) {
|
|
96
|
+
const config = loadProjectConfig(cwd);
|
|
97
|
+
if (!config) {
|
|
98
|
+
return fail("harness.yaml", "缺失,项目尚未初始化");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const schemaVersion = config.version ?? "unknown";
|
|
102
|
+
const mode = config.default_mode ?? "unknown";
|
|
103
|
+
|
|
104
|
+
return ok("harness.yaml", `version=${schemaVersion}, default_mode=${mode}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function inspectDeliveryPolicy(cwd) {
|
|
108
|
+
const config = loadProjectConfig(cwd);
|
|
109
|
+
if (!config) {
|
|
110
|
+
return skip("delivery_policy", "harness.yaml 缺失,无法检查");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const deliveryPolicy = config.delivery_policy ?? {};
|
|
114
|
+
const commit = deliveryPolicy.commit ?? null;
|
|
115
|
+
const push = deliveryPolicy.push ?? null;
|
|
116
|
+
|
|
117
|
+
if (!commit && !push) {
|
|
118
|
+
return warn("delivery_policy", "未配置 commit/push 策略");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const activeTask = getActiveTask(cwd);
|
|
122
|
+
if (!activeTask) {
|
|
123
|
+
const summary = [];
|
|
124
|
+
if (commit) {
|
|
125
|
+
summary.push(`commit=${commit.mode ?? "unknown"} via=${commit.via ?? "unknown"}`);
|
|
126
|
+
}
|
|
127
|
+
if (push) {
|
|
128
|
+
summary.push(`push=${push.mode ?? "unknown"} via=${push.via ?? "unknown"}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return ok("delivery_policy", `${summary.join(", ")};当前无 active task,暂不计算 readiness`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const outputPolicy = normalizeOutputPolicy(config.output_policy ?? {});
|
|
135
|
+
const readiness = evaluateTaskDeliveryReadiness(cwd, activeTask, {
|
|
136
|
+
deliveryPolicy,
|
|
137
|
+
reportPolicy: outputPolicy.report
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return ok("delivery_policy", `active_task=${activeTask.task_id};${summarizeDeliveryReadiness(readiness)}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function inspectOutputPolicy(cwd) {
|
|
144
|
+
const config = loadProjectConfig(cwd);
|
|
145
|
+
if (!config) {
|
|
146
|
+
return skip("output_policy", "harness.yaml 缺失,无法检查");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const outputPolicy = config.output_policy ?? {};
|
|
150
|
+
const normalizedPolicy = normalizeOutputPolicy(outputPolicy);
|
|
151
|
+
const report = normalizedPolicy.report ?? null;
|
|
152
|
+
|
|
153
|
+
if (!report) {
|
|
154
|
+
return warn("output_policy", "未配置 output_policy.report");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const inspection = inspectOutputPolicyWorkspace(cwd, normalizedPolicy);
|
|
158
|
+
if (inspection.warnings.length > 0) {
|
|
159
|
+
return warn("output_policy", `${inspection.summary};${inspection.warnings.join(";")}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return ok("output_policy", inspection.summary);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function inspectActiveTaskArtifactHints(cwd) {
|
|
166
|
+
const config = loadProjectConfig(cwd);
|
|
167
|
+
if (!config) {
|
|
168
|
+
return skip("artifact_hints", "harness.yaml 缺失,无法检查");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const activeTask = getActiveTask(cwd);
|
|
172
|
+
if (!activeTask) {
|
|
173
|
+
return skip("artifact_hints", "当前无 active task");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const outputPolicy = normalizeOutputPolicy(config.output_policy ?? {});
|
|
177
|
+
const requirements = evaluateTaskArtifactPolicy(activeTask, outputPolicy);
|
|
178
|
+
const requiredArtifacts = Object.values(requirements).filter((artifact) => artifact.required);
|
|
179
|
+
|
|
180
|
+
if (requiredArtifacts.length === 0) {
|
|
181
|
+
return ok("artifact_hints", `active_task=${activeTask.task_id};当前无需额外输出工件`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const names = requiredArtifacts.map((artifact) => artifact.name);
|
|
185
|
+
const hints = [];
|
|
186
|
+
if (names.includes("changelog")) {
|
|
187
|
+
hints.push(`更新 ${outputPolicy.changelog.file}`);
|
|
188
|
+
}
|
|
189
|
+
if (names.includes("design_note")) {
|
|
190
|
+
const suggestedPath = path.posix.join(outputPolicy.design_note.directory, `${activeTask.task_id}-design-note.md`);
|
|
191
|
+
hints.push(`docs scaffold --type design-note --task-id ${activeTask.task_id} --path ${suggestedPath}`);
|
|
192
|
+
}
|
|
193
|
+
if (names.includes("adr")) {
|
|
194
|
+
const suggestedPath = path.posix.join(outputPolicy.adr.directory, `${activeTask.task_id}-adr.md`);
|
|
195
|
+
hints.push(`docs scaffold --type adr --task-id ${activeTask.task_id} --path ${suggestedPath}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return warn("artifact_hints", `active_task=${activeTask.task_id};建议补齐 ${names.join(", ")};${hints.join(";")}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function inspectWorkflowMode(cwd) {
|
|
202
|
+
const config = loadProjectConfig(cwd);
|
|
203
|
+
if (!config) {
|
|
204
|
+
return skip("workflow_mode", "harness.yaml 缺失,无法检查");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const activeTask = getActiveTask(cwd);
|
|
208
|
+
if (!activeTask) {
|
|
209
|
+
return skip("workflow_mode", "当前无 active task");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const decision = evaluateTaskWorkflowDecision(activeTask, {
|
|
213
|
+
workflowPolicy: normalizeWorkflowPolicy(config.workflow_policy),
|
|
214
|
+
outputPolicy: normalizeOutputPolicy(config.output_policy),
|
|
215
|
+
previousDecision: activeTask.workflow_decision
|
|
216
|
+
});
|
|
217
|
+
const reasons = Array.isArray(decision.reasons) && decision.reasons.length > 0
|
|
218
|
+
? decision.reasons.join(", ")
|
|
219
|
+
: "none";
|
|
220
|
+
const upgraded = decision.upgraded_from ? `;upgraded_from=${decision.upgraded_from}` : "";
|
|
221
|
+
const warningMessage = buildWorkflowWarning(decision);
|
|
222
|
+
const summary = `active_task=${activeTask.task_id};recommended=${decision.recommended_mode};effective=${decision.effective_mode}${upgraded};reasons=${reasons}`;
|
|
223
|
+
|
|
224
|
+
if (warningMessage) {
|
|
225
|
+
return warn("workflow_mode", `${summary};warning=${warningMessage}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return ok("workflow_mode", summary);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function inspectHostRules(cwd, host, fallback) {
|
|
232
|
+
const hostMap = {
|
|
233
|
+
"claude-code": "CLAUDE.md",
|
|
234
|
+
codex: "AGENTS.md",
|
|
235
|
+
"gemini-cli": "GEMINI.md"
|
|
236
|
+
};
|
|
237
|
+
const fileName = hostMap[host];
|
|
238
|
+
const fullPath = path.join(cwd, fileName);
|
|
239
|
+
|
|
240
|
+
if (!fs.existsSync(fullPath)) {
|
|
241
|
+
if (fallback) {
|
|
242
|
+
return warn(fileName, "未检测到该宿主文件");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return fail(fileName, "缺失宿主规则文件");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
249
|
+
const marker = content.match(/<!-- agent-harness:start version="([^"]+)" rules="([^"]+)" -->/);
|
|
250
|
+
if (!marker) {
|
|
251
|
+
return fail(fileName, "存在文件,但未注入 agent-harness 规则块");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const [, version, rules] = marker;
|
|
255
|
+
return ok(fileName, `规则块存在(version=${version}, rules=${rules})`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function inspectTemplates(cwd) {
|
|
259
|
+
const missing = REQUIRED_TEMPLATE_FILES.filter((file) => {
|
|
260
|
+
return !runtimeRelativeCandidates("tasks", file)
|
|
261
|
+
.some((candidate) => fs.existsSync(path.join(cwd, candidate)));
|
|
262
|
+
});
|
|
263
|
+
if (missing.length > 0) {
|
|
264
|
+
return warn("runtime/tasks", `缺少模板: ${missing.join(", ")}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return ok("runtime/tasks", "bug / feature / explore 模板已就绪");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function detectRuntimeMode(cwd) {
|
|
271
|
+
if (hasRuntimeSetup(cwd) || fs.existsSync(path.join(cwd, ".claude", "settings.json"))) {
|
|
272
|
+
return "full";
|
|
273
|
+
}
|
|
274
|
+
return "protocol-only";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function inspectCodexHooks(cwd, hasCodexHost) {
|
|
278
|
+
if (!hasCodexHost) {
|
|
279
|
+
return skip(".codex/hooks", "当前项目未检测到 Codex 宿主");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const configPath = path.join(cwd, ".codex", "config.toml");
|
|
283
|
+
const hooksPath = path.join(cwd, ".codex", "hooks.json");
|
|
284
|
+
|
|
285
|
+
if (!fs.existsSync(configPath) && !fs.existsSync(hooksPath)) {
|
|
286
|
+
return warn(".codex/hooks", "未发现 .codex/config.toml 和 hooks.json");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!fs.existsSync(configPath)) {
|
|
290
|
+
return warn(".codex/hooks", "缺少 .codex/config.toml,trusted project 下不会默认启用 codex_hooks");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!fs.existsSync(hooksPath)) {
|
|
294
|
+
return warn(".codex/hooks", "缺少 .codex/hooks.json");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const configContent = fs.readFileSync(configPath, "utf8");
|
|
298
|
+
const hooksEnabled = /\bcodex_hooks\s*=\s*true\b/.test(configContent);
|
|
299
|
+
if (!hooksEnabled) {
|
|
300
|
+
return warn(".codex/hooks", ".codex/config.toml 存在,但未开启 features.codex_hooks = true");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let parsedHooks;
|
|
304
|
+
try {
|
|
305
|
+
parsedHooks = JSON.parse(fs.readFileSync(hooksPath, "utf8"));
|
|
306
|
+
} catch {
|
|
307
|
+
return warn(".codex/hooks", "hooks.json 存在,但 JSON 解析失败");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const checks = [
|
|
311
|
+
hasCodexHookCommand(parsedHooks, "UserPromptSubmit", "user_prompt_submit_intake.js"),
|
|
312
|
+
hasCodexHookCommand(parsedHooks, "SessionStart", "session_start_restore.js"),
|
|
313
|
+
hasCodexHookCommand(parsedHooks, "PostToolUse", "post_tool_use_record_evidence.js")
|
|
314
|
+
];
|
|
315
|
+
|
|
316
|
+
if (checks.some((item) => item === false)) {
|
|
317
|
+
return warn(".codex/hooks", "hooks.json 存在,但 agent-harness Codex hooks 不完整");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return ok(".codex/hooks", "Codex hooks 已配置;trusted project 默认启用,untrusted 请显式使用 codex --enable codex_hooks");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function inspectClaudeHooks(cwd, runtimeMode, hasClaudeHost) {
|
|
324
|
+
const settingsPath = path.join(cwd, ".claude", "settings.json");
|
|
325
|
+
|
|
326
|
+
if (!hasClaudeHost) {
|
|
327
|
+
return skip(".claude/settings.json", "当前项目未检测到 Claude Code 宿主");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (!fs.existsSync(settingsPath)) {
|
|
331
|
+
if (runtimeMode === "protocol-only") {
|
|
332
|
+
return skip(".claude/settings.json", "protocol-only 模式,无需 hooks");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return warn(".claude/settings.json", "未发现 Claude Code hooks");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let parsed;
|
|
339
|
+
try {
|
|
340
|
+
parsed = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
341
|
+
} catch {
|
|
342
|
+
return warn(".claude/settings.json", "文件存在,但 JSON 解析失败");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const commands = Object.values(parsed.hooks ?? {})
|
|
346
|
+
.flatMap((entries) => entries ?? [])
|
|
347
|
+
.flatMap((entry) => entry.hooks ?? [])
|
|
348
|
+
.map((hook) => hook.command)
|
|
349
|
+
.filter(Boolean);
|
|
350
|
+
|
|
351
|
+
const hasPreTool = commands.some((command) => command.includes("agent-harness gate before-tool"));
|
|
352
|
+
const hasPostTool = commands.some((command) => command.includes("agent-harness state update"));
|
|
353
|
+
|
|
354
|
+
if (hasPreTool && hasPostTool) {
|
|
355
|
+
return ok(".claude/settings.json", "Claude Code hooks 已配置");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return warn(".claude/settings.json", "hooks 存在,但 agent-harness 命令不完整");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function hasCodexHookCommand(parsedHooks, eventName, commandFragment) {
|
|
362
|
+
const eventEntries = parsedHooks?.hooks?.[eventName];
|
|
363
|
+
if (!Array.isArray(eventEntries)) {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return eventEntries
|
|
368
|
+
.flatMap((entry) => entry?.hooks ?? [])
|
|
369
|
+
.some((hook) => typeof hook?.command === "string" && hook.command.includes(commandFragment));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function inspectRuntimeDirectories(cwd, runtimeMode) {
|
|
373
|
+
if (runtimeMode === "protocol-only") {
|
|
374
|
+
return skip("runtime", "protocol-only 模式,无需运行时目录");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const runtimeDir = resolveRuntimeDirName(cwd);
|
|
378
|
+
const required = [
|
|
379
|
+
path.posix.join(runtimeDir, "README.md"),
|
|
380
|
+
path.posix.join(runtimeDir, "state", "tasks"),
|
|
381
|
+
path.posix.join(runtimeDir, "audit"),
|
|
382
|
+
path.posix.join(runtimeDir, "reports")
|
|
383
|
+
];
|
|
384
|
+
const missing = required.filter((file) => !fs.existsSync(path.join(cwd, file)));
|
|
385
|
+
|
|
386
|
+
if (missing.length > 0) {
|
|
387
|
+
return warn("runtime", `缺少: ${missing.join(", ")}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return ok("runtime", `运行时目录已就绪(${runtimeDir}/)`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function inspectGitignore(cwd, runtimeMode) {
|
|
394
|
+
const targetPath = path.join(cwd, ".gitignore");
|
|
395
|
+
if (!fs.existsSync(targetPath)) {
|
|
396
|
+
if (runtimeMode === "protocol-only") {
|
|
397
|
+
return skip(".gitignore", "protocol-only 模式,无需 runtime ignore");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return warn(".gitignore", "缺少 .gitignore");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const content = fs.readFileSync(targetPath, "utf8");
|
|
404
|
+
const required = [
|
|
405
|
+
`${runtimeRelativePathForCwd(cwd, "state")}/`,
|
|
406
|
+
`${runtimeRelativePathForCwd(cwd, "audit")}/`
|
|
407
|
+
];
|
|
408
|
+
const missing = required.filter((entry) => !content.includes(entry));
|
|
409
|
+
|
|
410
|
+
if (missing.length > 0) {
|
|
411
|
+
if (runtimeMode === "protocol-only") {
|
|
412
|
+
return skip(".gitignore", "protocol-only 模式,无需 runtime ignore");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return warn(".gitignore", `缺少忽略项: ${missing.join(", ")}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return ok(".gitignore", "runtime ignore 已配置");
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function detectHosts(cwd) {
|
|
422
|
+
const hosts = [];
|
|
423
|
+
if (fs.existsSync(path.join(cwd, "CLAUDE.md")) || fs.existsSync(path.join(cwd, ".claude"))) {
|
|
424
|
+
hosts.push("claude-code");
|
|
425
|
+
}
|
|
426
|
+
if (fs.existsSync(path.join(cwd, "AGENTS.md"))) {
|
|
427
|
+
hosts.push("codex");
|
|
428
|
+
}
|
|
429
|
+
if (fs.existsSync(path.join(cwd, "GEMINI.md"))) {
|
|
430
|
+
hosts.push("gemini-cli");
|
|
431
|
+
}
|
|
432
|
+
return hosts;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function pushCheck(checks, check) {
|
|
436
|
+
checks.push(check);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function printStatus(projectName, checks, warnings) {
|
|
440
|
+
console.log(`agent-harness status — ${projectName}`);
|
|
441
|
+
console.log("");
|
|
442
|
+
console.log("接入检查:");
|
|
443
|
+
for (const check of checks) {
|
|
444
|
+
console.log(` ${symbol(check.severity)} ${check.name.padEnd(22, " ")} ${check.message}`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (warnings.length > 0) {
|
|
448
|
+
console.log("");
|
|
449
|
+
console.log("提示:");
|
|
450
|
+
for (const item of warnings) {
|
|
451
|
+
console.log(` - ${item}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function symbol(severity) {
|
|
457
|
+
if (severity === "ok") {
|
|
458
|
+
return "[✓]";
|
|
459
|
+
}
|
|
460
|
+
if (severity === "warn") {
|
|
461
|
+
return "[!]";
|
|
462
|
+
}
|
|
463
|
+
if (severity === "fail") {
|
|
464
|
+
return "[x]";
|
|
465
|
+
}
|
|
466
|
+
return "[-]";
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function maxExitCode(current, severity) {
|
|
470
|
+
if (severity === "fail") {
|
|
471
|
+
return 2;
|
|
472
|
+
}
|
|
473
|
+
if (severity === "warn") {
|
|
474
|
+
return Math.max(current, 1);
|
|
475
|
+
}
|
|
476
|
+
return current;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function ok(name, message) {
|
|
480
|
+
return { name, message, severity: "ok" };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function warn(name, message) {
|
|
484
|
+
return { name, message, severity: "warn" };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function fail(name, message) {
|
|
488
|
+
return { name, message, severity: "fail" };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function skip(name, message) {
|
|
492
|
+
return { name, message, severity: "skip" };
|
|
493
|
+
}
|