@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,307 @@
|
|
|
1
|
+
import { evaluateTaskArtifactPolicy, normalizeOutputPolicy } from "./output-policy.js";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_FORCE_FULL_INTENTS = ["bug", "feature", "refactor"];
|
|
4
|
+
|
|
5
|
+
export function normalizeWorkflowPolicy(value) {
|
|
6
|
+
const policy = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
7
|
+
const enforcement = policy.enforcement && typeof policy.enforcement === "object" && !Array.isArray(policy.enforcement)
|
|
8
|
+
? policy.enforcement
|
|
9
|
+
: {};
|
|
10
|
+
const liteAllowedIf = policy.lite_allowed_if && typeof policy.lite_allowed_if === "object" && !Array.isArray(policy.lite_allowed_if)
|
|
11
|
+
? policy.lite_allowed_if
|
|
12
|
+
: {};
|
|
13
|
+
const forceFullIf = policy.force_full_if && typeof policy.force_full_if === "object" && !Array.isArray(policy.force_full_if)
|
|
14
|
+
? policy.force_full_if
|
|
15
|
+
: {};
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
default_mode: typeof policy.default_mode === "string" ? policy.default_mode : "full",
|
|
19
|
+
lite_allowed_if: {
|
|
20
|
+
single_file: liteAllowedIf.single_file !== false,
|
|
21
|
+
low_risk: liteAllowedIf.low_risk !== false,
|
|
22
|
+
docs_only: liteAllowedIf.docs_only !== false,
|
|
23
|
+
no_behavior_change: liteAllowedIf.no_behavior_change !== false,
|
|
24
|
+
no_policy_change: liteAllowedIf.no_policy_change !== false,
|
|
25
|
+
no_output_artifacts: liteAllowedIf.no_output_artifacts !== false
|
|
26
|
+
},
|
|
27
|
+
force_full_if: {
|
|
28
|
+
intents: Array.isArray(forceFullIf.intents)
|
|
29
|
+
? forceFullIf.intents.map((item) => String(item))
|
|
30
|
+
: DEFAULT_FORCE_FULL_INTENTS,
|
|
31
|
+
multi_file_scope: forceFullIf.multi_file_scope !== false,
|
|
32
|
+
config_changed: forceFullIf.config_changed !== false,
|
|
33
|
+
protocol_changed: forceFullIf.protocol_changed !== false,
|
|
34
|
+
host_adapter_changed: forceFullIf.host_adapter_changed !== false,
|
|
35
|
+
output_artifact_required: forceFullIf.output_artifact_required !== false,
|
|
36
|
+
high_risk: forceFullIf.high_risk !== false,
|
|
37
|
+
override_used: forceFullIf.override_used !== false
|
|
38
|
+
},
|
|
39
|
+
enforcement: {
|
|
40
|
+
mode: typeof enforcement.mode === "string" ? enforcement.mode : "recommend",
|
|
41
|
+
upgrade_only: enforcement.upgrade_only !== false
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function evaluateTaskWorkflowDecision(taskState, options = {}) {
|
|
47
|
+
const workflowPolicy = normalizeWorkflowPolicy(options.workflowPolicy);
|
|
48
|
+
const outputPolicy = normalizeOutputPolicy(options.outputPolicy);
|
|
49
|
+
const previousDecision = normalizeWorkflowDecision(options.previousDecision ?? taskState?.workflow_decision);
|
|
50
|
+
const context = buildWorkflowContext(taskState, options);
|
|
51
|
+
const artifactRequirements = evaluateTaskArtifactPolicy(taskState, outputPolicy);
|
|
52
|
+
const requiredArtifacts = Object.values(artifactRequirements)
|
|
53
|
+
.filter((artifact) => artifact.required)
|
|
54
|
+
.map((artifact) => artifact.name);
|
|
55
|
+
|
|
56
|
+
const fullReasons = collectFullReasons(context, workflowPolicy, requiredArtifacts);
|
|
57
|
+
const liteReasons = collectLiteReasons(context, workflowPolicy, requiredArtifacts);
|
|
58
|
+
const recommendedMode = fullReasons.length > 0 || liteReasons.length === 0
|
|
59
|
+
? "full"
|
|
60
|
+
: "lite";
|
|
61
|
+
const recommendedReasons = recommendedMode === "full" ? fullReasons : liteReasons;
|
|
62
|
+
|
|
63
|
+
let effectiveMode = recommendedMode;
|
|
64
|
+
let upgradedFrom = null;
|
|
65
|
+
let reasons = recommendedReasons;
|
|
66
|
+
|
|
67
|
+
if (workflowPolicy.enforcement.upgrade_only && previousDecision?.effective_mode === "full" && recommendedMode === "lite") {
|
|
68
|
+
effectiveMode = "full";
|
|
69
|
+
reasons = Array.isArray(previousDecision.reasons) && previousDecision.reasons.length > 0
|
|
70
|
+
? previousDecision.reasons
|
|
71
|
+
: ["upgrade_only_preserved_full"];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (previousDecision?.effective_mode === "lite" && effectiveMode === "full") {
|
|
75
|
+
upgradedFrom = "lite";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
recommended_mode: recommendedMode,
|
|
80
|
+
effective_mode: effectiveMode,
|
|
81
|
+
upgraded_from: upgradedFrom,
|
|
82
|
+
reasons,
|
|
83
|
+
enforcement_mode: workflowPolicy.enforcement.mode,
|
|
84
|
+
evaluated_at: new Date().toISOString()
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function buildWorkflowWarning(decision) {
|
|
89
|
+
if (!decision || typeof decision !== "object") {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (decision.enforcement_mode !== "warn") {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (decision.effective_mode !== "full") {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const reasons = Array.isArray(decision.reasons)
|
|
102
|
+
? decision.reasons.filter((item) => typeof item === "string" && item.trim().length > 0)
|
|
103
|
+
: [];
|
|
104
|
+
if (reasons.length === 0) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (decision.upgraded_from === "lite") {
|
|
109
|
+
return `当前任务已从 lite 升级为 full workflow,建议按完整链路收口(原因: ${reasons.join(", ")})`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return `当前任务命中 full workflow,建议按完整链路收口(原因: ${reasons.join(", ")})`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildWorkflowContext(taskState, options) {
|
|
116
|
+
const contract = taskState?.confirmed_contract ?? {};
|
|
117
|
+
const draft = taskState?.task_draft ?? {};
|
|
118
|
+
const scopeSource = Array.isArray(options.actualScope) && options.actualScope.length > 0
|
|
119
|
+
? options.actualScope
|
|
120
|
+
: (contract.scope ?? draft.scope);
|
|
121
|
+
const scope = normalizeStringArray(scopeSource);
|
|
122
|
+
const goal = String(contract.goal ?? draft.goal ?? "");
|
|
123
|
+
const intent = String(contract.intent ?? draft.intent ?? "unknown");
|
|
124
|
+
const riskLevel = String(contract.risk_level ?? draft?.derived?.risk_level ?? "medium");
|
|
125
|
+
const overrideUsed = Array.isArray(taskState?.override_history) && taskState.override_history.length > 0;
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
goal,
|
|
129
|
+
intent,
|
|
130
|
+
risk_level: riskLevel,
|
|
131
|
+
scope,
|
|
132
|
+
multi_file_scope: inferMultiFileScope(scope),
|
|
133
|
+
single_file_scope: inferSingleFileScope(scope),
|
|
134
|
+
docs_only: inferDocsOnly(scope),
|
|
135
|
+
config_changed: inferConfigChanged(scope),
|
|
136
|
+
protocol_changed: inferProtocolChanged(scope),
|
|
137
|
+
host_adapter_changed: inferHostAdapterChanged(scope),
|
|
138
|
+
override_used: overrideUsed
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function collectFullReasons(context, workflowPolicy, requiredArtifacts) {
|
|
143
|
+
const reasons = [];
|
|
144
|
+
const rules = workflowPolicy.force_full_if;
|
|
145
|
+
|
|
146
|
+
if (rules.intents.includes(context.intent)) {
|
|
147
|
+
reasons.push(`intent:${context.intent}`);
|
|
148
|
+
}
|
|
149
|
+
if (rules.multi_file_scope && context.multi_file_scope) {
|
|
150
|
+
reasons.push("multi_file_scope");
|
|
151
|
+
}
|
|
152
|
+
if (rules.config_changed && context.config_changed) {
|
|
153
|
+
reasons.push("config_changed");
|
|
154
|
+
}
|
|
155
|
+
if (rules.protocol_changed && context.protocol_changed) {
|
|
156
|
+
reasons.push("protocol_changed");
|
|
157
|
+
}
|
|
158
|
+
if (rules.host_adapter_changed && context.host_adapter_changed) {
|
|
159
|
+
reasons.push("host_adapter_changed");
|
|
160
|
+
}
|
|
161
|
+
if (rules.output_artifact_required && requiredArtifacts.length > 0) {
|
|
162
|
+
reasons.push(`output_artifact_required:${requiredArtifacts.join(",")}`);
|
|
163
|
+
}
|
|
164
|
+
if (rules.high_risk && context.risk_level === "high") {
|
|
165
|
+
reasons.push("high_risk");
|
|
166
|
+
}
|
|
167
|
+
if (rules.override_used && context.override_used) {
|
|
168
|
+
reasons.push("override_used");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return reasons;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function collectLiteReasons(context, workflowPolicy, requiredArtifacts) {
|
|
175
|
+
const conditions = workflowPolicy.lite_allowed_if;
|
|
176
|
+
const reasons = [];
|
|
177
|
+
|
|
178
|
+
if (conditions.single_file && !context.single_file_scope) {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
if (conditions.single_file) {
|
|
182
|
+
reasons.push("single_file_scope");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (conditions.low_risk && context.risk_level !== "low") {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
if (conditions.low_risk) {
|
|
189
|
+
reasons.push("low_risk");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (conditions.docs_only && !context.docs_only) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
if (conditions.docs_only) {
|
|
196
|
+
reasons.push("docs_only");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (conditions.no_behavior_change && !context.docs_only) {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
if (conditions.no_behavior_change) {
|
|
203
|
+
reasons.push("no_behavior_change");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (conditions.no_policy_change && (context.config_changed || context.protocol_changed || context.host_adapter_changed)) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
if (conditions.no_policy_change) {
|
|
210
|
+
reasons.push("no_policy_change");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (conditions.no_output_artifacts && requiredArtifacts.length > 0) {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
if (conditions.no_output_artifacts) {
|
|
217
|
+
reasons.push("no_output_artifacts");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (context.override_used) {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return reasons;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function inferSingleFileScope(scope) {
|
|
228
|
+
if (scope.length !== 1) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
return !looksLikeDirectory(scope[0]);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function inferMultiFileScope(scope) {
|
|
235
|
+
if (scope.length > 1) {
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
return scope.some((item) => looksLikeDirectory(item));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function inferDocsOnly(scope) {
|
|
242
|
+
if (scope.length === 0) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
return scope.every((item) => {
|
|
246
|
+
const normalized = String(item).replace(/^\.\//, "");
|
|
247
|
+
return normalized === "CHANGELOG.md" ||
|
|
248
|
+
normalized.startsWith("docs/") ||
|
|
249
|
+
normalized.endsWith(".md");
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function inferConfigChanged(scope) {
|
|
254
|
+
return scope.some((item) => {
|
|
255
|
+
const normalized = String(item).replace(/^\.\//, "");
|
|
256
|
+
return normalized === "harness.yaml" ||
|
|
257
|
+
normalized === "package.json" ||
|
|
258
|
+
normalized.endsWith(".schema.json");
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function inferProtocolChanged(scope) {
|
|
263
|
+
return scope.some((item) => {
|
|
264
|
+
const normalized = String(item).replace(/^\.\//, "");
|
|
265
|
+
return normalized.startsWith("packages/protocol/") ||
|
|
266
|
+
normalized === "AGENTS.md" ||
|
|
267
|
+
normalized === "CLAUDE.md" ||
|
|
268
|
+
normalized === "GEMINI.md";
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function inferHostAdapterChanged(scope) {
|
|
273
|
+
return scope.some((item) => {
|
|
274
|
+
const normalized = String(item).replace(/^\.\//, "");
|
|
275
|
+
return normalized.startsWith(".codex/") ||
|
|
276
|
+
normalized.startsWith(".claude/") ||
|
|
277
|
+
normalized.startsWith("packages/protocol/adapters/");
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function looksLikeDirectory(value) {
|
|
282
|
+
const normalized = String(value ?? "");
|
|
283
|
+
return normalized.endsWith("/") || normalized.endsWith("/**") || !pathLikeLeaf(normalized);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function pathLikeLeaf(value) {
|
|
287
|
+
const normalized = String(value ?? "");
|
|
288
|
+
const lastSegment = normalized.split("/").pop() ?? "";
|
|
289
|
+
return lastSegment.includes(".");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function normalizeStringArray(value) {
|
|
293
|
+
if (!Array.isArray(value)) {
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return value
|
|
298
|
+
.map((item) => String(item ?? "").trim())
|
|
299
|
+
.filter(Boolean);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function normalizeWorkflowDecision(value) {
|
|
303
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
return value;
|
|
307
|
+
}
|