@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.
@@ -0,0 +1,272 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { readAuditEntries } from "../lib/audit-store.js";
5
+ import { evaluateTaskDeliveryReadiness, normalizeDeliveryPolicy } from "../lib/delivery-policy.js";
6
+ import { normalizeOutputPolicy, validateTaskOutputArtifacts } from "../lib/output-policy.js";
7
+ import { loadProjectConfig } from "../lib/project-config.js";
8
+ import { requireTaskState, resolveTaskId, updateTaskState } from "../lib/state-store.js";
9
+ import { buildWorkflowWarning, evaluateTaskWorkflowDecision, normalizeWorkflowPolicy } from "../lib/workflow-policy.js";
10
+ import { verifyTaskState } from "./verify.js";
11
+
12
+ const SCHEMA_VERSION = "0.3";
13
+
14
+ export function runReport(argv) {
15
+ const parsed = parseReportArgs(argv);
16
+ if (!parsed.ok) {
17
+ console.error(parsed.error);
18
+ return 1;
19
+ }
20
+
21
+ try {
22
+ const cwd = process.cwd();
23
+ const taskId = resolveTaskId(cwd, parsed.options.taskId);
24
+ const taskState = requireTaskState(cwd, taskId);
25
+ const projectConfig = loadProjectConfig(cwd);
26
+ const outputPolicy = normalizeOutputPolicy(projectConfig?.output_policy);
27
+ const reportPolicy = outputPolicy.report;
28
+ const verification = verifyTaskState(taskState, { reportPolicy });
29
+
30
+ if (!verification.allowed) {
31
+ console.log(`${JSON.stringify(verification, null, 2)}\n`);
32
+ return 1;
33
+ }
34
+
35
+ let outputArtifacts;
36
+ try {
37
+ outputArtifacts = validateTaskOutputArtifacts(cwd, taskState, outputPolicy, {
38
+ adr: parsed.options.adr,
39
+ changelog: parsed.options.changelogFile,
40
+ design_note: parsed.options.designNote
41
+ });
42
+ } catch (error) {
43
+ if (error?.code === "MISSING_OUTPUT_ARTIFACTS") {
44
+ console.error(buildMissingArtifactsMessage(taskId, error, outputPolicy));
45
+ return 1;
46
+ }
47
+ throw error;
48
+ }
49
+ const deliveryReadiness = evaluateTaskDeliveryReadiness(cwd, taskState, {
50
+ deliveryPolicy: normalizeDeliveryPolicy(projectConfig?.delivery_policy),
51
+ reportPolicy,
52
+ reportWillBeGenerated: true
53
+ });
54
+ const workflowDecision = evaluateTaskWorkflowDecision(taskState, {
55
+ workflowPolicy: normalizeWorkflowPolicy(projectConfig?.workflow_policy),
56
+ outputPolicy,
57
+ actualScope: parsed.options.actualScope,
58
+ outputArtifacts,
59
+ previousDecision: taskState.workflow_decision
60
+ });
61
+ const workflowWarning = buildWorkflowWarning(workflowDecision);
62
+ const report = buildReport(cwd, taskState, parsed.options, outputArtifacts, deliveryReadiness, workflowDecision, workflowWarning);
63
+ validateReportAgainstPolicy(report, reportPolicy);
64
+ writeReport(cwd, report, reportPolicy);
65
+
66
+ updateTaskState(cwd, taskId, {
67
+ current_phase: "close",
68
+ current_state: "done",
69
+ workflow_decision: workflowDecision
70
+ });
71
+
72
+ if (workflowWarning) {
73
+ console.error(`workflow warning: ${workflowWarning}`);
74
+ }
75
+ console.log(`${JSON.stringify(report, null, 2)}\n`);
76
+ return 0;
77
+ } catch (error) {
78
+ console.error(error.message);
79
+ return 1;
80
+ }
81
+ }
82
+
83
+ function parseReportArgs(argv) {
84
+ const options = {
85
+ adr: null,
86
+ actualScope: [],
87
+ changelogFile: null,
88
+ conclusion: null,
89
+ designNote: null,
90
+ nextSteps: [],
91
+ remainingRisks: [],
92
+ scopeDeviation: null,
93
+ taskId: null
94
+ };
95
+
96
+ for (let index = 0; index < argv.length; index += 1) {
97
+ const arg = argv[index];
98
+
99
+ if (arg === "--task-id") {
100
+ options.taskId = argv[index + 1] ?? null;
101
+ index += 1;
102
+ continue;
103
+ }
104
+
105
+ if (arg === "--conclusion") {
106
+ options.conclusion = argv[index + 1] ?? null;
107
+ index += 1;
108
+ continue;
109
+ }
110
+
111
+ if (arg === "--changelog-file") {
112
+ options.changelogFile = argv[index + 1] ?? null;
113
+ index += 1;
114
+ continue;
115
+ }
116
+
117
+ if (arg === "--design-note") {
118
+ options.designNote = argv[index + 1] ?? null;
119
+ index += 1;
120
+ continue;
121
+ }
122
+
123
+ if (arg === "--adr") {
124
+ options.adr = argv[index + 1] ?? null;
125
+ index += 1;
126
+ continue;
127
+ }
128
+
129
+ if (arg === "--actual-scope") {
130
+ options.actualScope.push(argv[index + 1] ?? "");
131
+ index += 1;
132
+ continue;
133
+ }
134
+
135
+ if (arg === "--scope-deviation") {
136
+ options.scopeDeviation = argv[index + 1] ?? null;
137
+ index += 1;
138
+ continue;
139
+ }
140
+
141
+ if (arg === "--risk") {
142
+ options.remainingRisks.push(argv[index + 1] ?? "");
143
+ index += 1;
144
+ continue;
145
+ }
146
+
147
+ if (arg === "--next-step") {
148
+ options.nextSteps.push(argv[index + 1] ?? "");
149
+ index += 1;
150
+ continue;
151
+ }
152
+
153
+ return { ok: false, error: `未知参数: ${arg}` };
154
+ }
155
+
156
+ if (!options.conclusion) {
157
+ return { ok: false, error: "需要 --conclusion 参数" };
158
+ }
159
+
160
+ return { ok: true, options };
161
+ }
162
+
163
+ function buildReport(cwd, taskState, options, outputArtifacts, deliveryReadiness, workflowDecision, workflowWarning) {
164
+ const contract = taskState.confirmed_contract ?? {};
165
+ const draft = taskState.task_draft ?? {};
166
+ const evidence = Array.isArray(taskState.evidence) ? taskState.evidence : [];
167
+ const auditEntries = readAuditEntries(cwd, taskState.task_id);
168
+
169
+ return {
170
+ schema_version: SCHEMA_VERSION,
171
+ task_id: taskState.task_id,
172
+ intent: contract.intent ?? draft.intent ?? "unknown",
173
+ conclusion: options.conclusion,
174
+ actual_scope: options.actualScope.length > 0 ? options.actualScope : (contract.scope ?? draft.scope ?? []),
175
+ scope_deviation: options.scopeDeviation ?? null,
176
+ evidence_summary: evidence.map((item) => {
177
+ const summary = {
178
+ type: item.type,
179
+ result: item.content
180
+ };
181
+
182
+ if (typeof item.passed === "boolean") {
183
+ summary.passed = item.passed;
184
+ }
185
+
186
+ return summary;
187
+ }),
188
+ remaining_risks: options.remainingRisks,
189
+ overrides_used: auditEntries
190
+ .filter((entry) => entry.event_type === "force_override" || entry.event_type === "manual_confirmation")
191
+ .map((entry) => entry.description),
192
+ output_artifacts: outputArtifacts,
193
+ delivery_readiness: deliveryReadiness,
194
+ workflow_decision: workflowDecision,
195
+ workflow_warning: workflowWarning,
196
+ next_steps: options.nextSteps,
197
+ completed_at: new Date().toISOString()
198
+ };
199
+ }
200
+
201
+ function validateReportAgainstPolicy(report, reportPolicy) {
202
+ if (reportPolicy.format !== "json") {
203
+ throw new Error(`当前仅支持 json 报告格式,收到: ${reportPolicy.format}`);
204
+ }
205
+
206
+ const missingSections = [];
207
+ for (const section of reportPolicy.required_sections) {
208
+ if (!isReportSectionSatisfied(report, section)) {
209
+ missingSections.push(section);
210
+ }
211
+ }
212
+
213
+ if (missingSections.length > 0) {
214
+ throw new Error(`报告缺少必需 section: ${missingSections.join(", ")}`);
215
+ }
216
+ }
217
+
218
+ function isReportSectionSatisfied(report, section) {
219
+ if (section === "task_conclusion") {
220
+ return typeof report.conclusion === "string" && report.conclusion.trim().length > 0;
221
+ }
222
+ if (section === "actual_scope") {
223
+ return Array.isArray(report.actual_scope) && report.actual_scope.length > 0;
224
+ }
225
+ if (section === "verification_evidence") {
226
+ return Array.isArray(report.evidence_summary) && report.evidence_summary.length > 0;
227
+ }
228
+ if (section === "remaining_risks") {
229
+ return Array.isArray(report.remaining_risks);
230
+ }
231
+ if (section === "next_steps") {
232
+ return Array.isArray(report.next_steps);
233
+ }
234
+
235
+ throw new Error(`未知的 output_policy.report.required_sections 配置项: ${section}`);
236
+ }
237
+
238
+ function writeReport(cwd, report, reportPolicy) {
239
+ const reportsDir = path.join(cwd, reportPolicy.directory);
240
+ fs.mkdirSync(reportsDir, { recursive: true });
241
+ const reportPath = path.join(reportsDir, `${report.task_id}.json`);
242
+ fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
243
+ }
244
+
245
+ function buildMissingArtifactsMessage(taskId, error, outputPolicy) {
246
+ const missing = Array.isArray(error?.missing_required) ? error.missing_required : [];
247
+ const lines = [
248
+ `缺少必需输出工件: ${missing.join(", ")}`
249
+ ];
250
+
251
+ for (const artifact of missing) {
252
+ if (artifact === "changelog") {
253
+ lines.push(`- changelog: 请更新 ${outputPolicy.changelog.file},然后重新执行 report`);
254
+ continue;
255
+ }
256
+
257
+ if (artifact === "design_note") {
258
+ const suggestedPath = path.posix.join(outputPolicy.design_note.directory, `${taskId}-design-note.md`);
259
+ lines.push(`- design_note: 可先执行 \`node packages/cli/bin/agent-harness.js docs scaffold --type design-note --task-id ${taskId} --path ${suggestedPath}\``);
260
+ lines.push(` 然后在 report 中补上 \`--design-note ${suggestedPath}\``);
261
+ continue;
262
+ }
263
+
264
+ if (artifact === "adr") {
265
+ const suggestedPath = path.posix.join(outputPolicy.adr.directory, `${taskId}-adr.md`);
266
+ lines.push(`- adr: 可先执行 \`node packages/cli/bin/agent-harness.js docs scaffold --type adr --task-id ${taskId} --path ${suggestedPath}\``);
267
+ lines.push(` 然后在 report 中补上 \`--adr ${suggestedPath}\``);
268
+ }
269
+ }
270
+
271
+ return `${lines.join("\n")}\n`;
272
+ }
@@ -0,0 +1,274 @@
1
+ import fs from "node:fs";
2
+
3
+ import {
4
+ getActiveTask,
5
+ getTaskState,
6
+ initTaskState,
7
+ loadStateIndex,
8
+ resolveTaskId,
9
+ updateTaskState
10
+ } from "../lib/state-store.js";
11
+
12
+ export function runState(argv) {
13
+ const [subcommand, ...rest] = argv;
14
+
15
+ if (!subcommand) {
16
+ console.error("缺少 state 子命令。可用: init, get, update, active");
17
+ return 1;
18
+ }
19
+
20
+ if (subcommand === "init") {
21
+ return runStateInit(rest);
22
+ }
23
+
24
+ if (subcommand === "get") {
25
+ return runStateGet(rest);
26
+ }
27
+
28
+ if (subcommand === "update") {
29
+ return runStateUpdate(rest);
30
+ }
31
+
32
+ if (subcommand === "active") {
33
+ return runStateActive(rest);
34
+ }
35
+
36
+ console.error(`未知 state 子命令: ${subcommand}。可用: init, get, update, active`);
37
+ return 1;
38
+ }
39
+
40
+ function runStateInit(argv) {
41
+ const parsed = parseStateInitArgs(argv);
42
+ if (!parsed.ok) {
43
+ console.error(parsed.error);
44
+ return 1;
45
+ }
46
+
47
+ try {
48
+ const taskDraft = loadDraft(parsed.options);
49
+ const result = initTaskState(process.cwd(), {
50
+ taskDraft,
51
+ taskId: parsed.options.taskId
52
+ });
53
+ printJson(result);
54
+ return 0;
55
+ } catch (error) {
56
+ console.error(error.message);
57
+ return 1;
58
+ }
59
+ }
60
+
61
+ function runStateGet(argv) {
62
+ const parsed = parseTaskIdArgs(argv);
63
+ if (!parsed.ok) {
64
+ console.error(parsed.error);
65
+ return 1;
66
+ }
67
+
68
+ try {
69
+ const taskId = resolveTaskId(process.cwd(), parsed.options.taskId);
70
+ const result = getTaskState(process.cwd(), taskId);
71
+ if (!result) {
72
+ console.error(`任务不存在: ${taskId}`);
73
+ return 1;
74
+ }
75
+
76
+ printJson(result);
77
+ return 0;
78
+ } catch (error) {
79
+ console.error(error.message);
80
+ return 1;
81
+ }
82
+ }
83
+
84
+ function runStateUpdate(argv) {
85
+ const parsed = parseStateUpdateArgs(argv);
86
+ if (!parsed.ok) {
87
+ console.error(parsed.error);
88
+ return 1;
89
+ }
90
+
91
+ try {
92
+ const taskId = resolveTaskId(process.cwd(), parsed.options.taskId);
93
+ const changes = {};
94
+
95
+ if (parsed.options.phase) {
96
+ changes.current_phase = parsed.options.phase;
97
+ }
98
+ if (parsed.options.state) {
99
+ changes.current_state = parsed.options.state;
100
+ }
101
+ if (parsed.options.evidence) {
102
+ changes.evidence = [JSON.parse(parsed.options.evidence)];
103
+ } else if (parsed.options.tool) {
104
+ changes.evidence = [{
105
+ type: "command_result",
106
+ content: `Tool: ${parsed.options.tool}`,
107
+ exit_code: parsed.options.exitCode ?? 0,
108
+ timestamp: new Date().toISOString()
109
+ }];
110
+ }
111
+
112
+ const result = updateTaskState(process.cwd(), taskId, changes);
113
+ printJson(result);
114
+ return 0;
115
+ } catch (error) {
116
+ console.error(error.message);
117
+ return 1;
118
+ }
119
+ }
120
+
121
+ function runStateActive(argv) {
122
+ if (argv.length > 0) {
123
+ console.error(`state active 不接受额外参数: ${argv.join(" ")}`);
124
+ return 1;
125
+ }
126
+
127
+ try {
128
+ const result = getActiveTask(process.cwd());
129
+ if (!result) {
130
+ const index = loadStateIndex(process.cwd());
131
+ if (!index.active_task_id) {
132
+ console.error("当前无活跃任务");
133
+ return 1;
134
+ }
135
+ }
136
+
137
+ printJson(result);
138
+ return 0;
139
+ } catch (error) {
140
+ console.error(error.message);
141
+ return 1;
142
+ }
143
+ }
144
+
145
+ function parseStateInitArgs(argv) {
146
+ const options = {
147
+ draft: null,
148
+ draftFile: null,
149
+ taskId: null
150
+ };
151
+
152
+ for (let index = 0; index < argv.length; index += 1) {
153
+ const arg = argv[index];
154
+
155
+ if (arg === "--draft") {
156
+ options.draft = argv[index + 1] ?? null;
157
+ index += 1;
158
+ continue;
159
+ }
160
+
161
+ if (arg === "--draft-file") {
162
+ options.draftFile = argv[index + 1] ?? null;
163
+ index += 1;
164
+ continue;
165
+ }
166
+
167
+ if (arg === "--task-id") {
168
+ options.taskId = argv[index + 1] ?? null;
169
+ index += 1;
170
+ continue;
171
+ }
172
+
173
+ return { ok: false, error: `未知参数: ${arg}` };
174
+ }
175
+
176
+ if (!options.draft && !options.draftFile) {
177
+ return { ok: false, error: "需要 --draft 或 --draft-file 参数" };
178
+ }
179
+
180
+ if (options.draft && options.draftFile) {
181
+ return { ok: false, error: "--draft 与 --draft-file 不能同时使用" };
182
+ }
183
+
184
+ return { ok: true, options };
185
+ }
186
+
187
+ function parseTaskIdArgs(argv) {
188
+ const options = { taskId: null };
189
+
190
+ for (let index = 0; index < argv.length; index += 1) {
191
+ const arg = argv[index];
192
+ if (arg === "--task-id") {
193
+ options.taskId = argv[index + 1] ?? null;
194
+ index += 1;
195
+ continue;
196
+ }
197
+
198
+ return { ok: false, error: `未知参数: ${arg}` };
199
+ }
200
+
201
+ return { ok: true, options };
202
+ }
203
+
204
+ function parseStateUpdateArgs(argv) {
205
+ const options = {
206
+ evidence: null,
207
+ exitCode: null,
208
+ phase: null,
209
+ state: null,
210
+ taskId: null,
211
+ tool: null
212
+ };
213
+
214
+ for (let index = 0; index < argv.length; index += 1) {
215
+ const arg = argv[index];
216
+
217
+ if (arg === "--task-id") {
218
+ options.taskId = argv[index + 1] ?? null;
219
+ index += 1;
220
+ continue;
221
+ }
222
+
223
+ if (arg === "--tool") {
224
+ options.tool = argv[index + 1] ?? null;
225
+ index += 1;
226
+ continue;
227
+ }
228
+
229
+ if (arg === "--exit-code") {
230
+ const value = argv[index + 1];
231
+ options.exitCode = value == null ? null : Number(value);
232
+ index += 1;
233
+ continue;
234
+ }
235
+
236
+ if (arg === "--phase") {
237
+ options.phase = argv[index + 1] ?? null;
238
+ index += 1;
239
+ continue;
240
+ }
241
+
242
+ if (arg === "--state") {
243
+ options.state = argv[index + 1] ?? null;
244
+ index += 1;
245
+ continue;
246
+ }
247
+
248
+ if (arg === "--evidence") {
249
+ options.evidence = argv[index + 1] ?? null;
250
+ index += 1;
251
+ continue;
252
+ }
253
+
254
+ return { ok: false, error: `未知参数: ${arg}` };
255
+ }
256
+
257
+ return { ok: true, options };
258
+ }
259
+
260
+ function loadDraft(options) {
261
+ if (options.draft) {
262
+ return JSON.parse(options.draft);
263
+ }
264
+
265
+ try {
266
+ return JSON.parse(fs.readFileSync(options.draftFile, "utf8"));
267
+ } catch {
268
+ throw new Error(`无法读取 task draft: ${options.draftFile}`);
269
+ }
270
+ }
271
+
272
+ function printJson(value) {
273
+ console.log(`${JSON.stringify(value, null, 2)}\n`);
274
+ }