@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,251 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { normalizeOutputPolicy } from "../lib/output-policy.js";
|
|
5
|
+
import { loadProjectConfig } from "../lib/project-config.js";
|
|
6
|
+
import { requireTaskState, resolveTaskId } from "../lib/state-store.js";
|
|
7
|
+
|
|
8
|
+
const VALID_TYPES = new Set(["design-note", "adr"]);
|
|
9
|
+
|
|
10
|
+
export function runDocs(argv) {
|
|
11
|
+
const [subcommand, ...rest] = argv;
|
|
12
|
+
|
|
13
|
+
if (!subcommand) {
|
|
14
|
+
console.error("缺少 docs 子命令。可用: scaffold");
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (subcommand === "scaffold") {
|
|
19
|
+
return runDocsScaffold(rest);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.error(`未知 docs 子命令: ${subcommand}。可用: scaffold`);
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function runDocsScaffold(argv) {
|
|
27
|
+
const parsed = parseDocsScaffoldArgs(argv);
|
|
28
|
+
if (!parsed.ok) {
|
|
29
|
+
console.error(parsed.error);
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const cwd = process.cwd();
|
|
35
|
+
const taskId = resolveTaskId(cwd, parsed.options.taskId);
|
|
36
|
+
const taskState = requireTaskState(cwd, taskId);
|
|
37
|
+
const projectConfig = loadProjectConfig(cwd);
|
|
38
|
+
const outputPolicy = normalizeOutputPolicy(projectConfig?.output_policy);
|
|
39
|
+
const plan = buildScaffoldPlan(cwd, taskState, outputPolicy, parsed.options);
|
|
40
|
+
|
|
41
|
+
if (!parsed.options.force && fs.existsSync(plan.absolutePath)) {
|
|
42
|
+
throw new Error(`文档已存在,请使用 --force 覆盖: ${plan.relativePath}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fs.mkdirSync(path.dirname(plan.absolutePath), { recursive: true });
|
|
46
|
+
fs.writeFileSync(plan.absolutePath, plan.content, "utf8");
|
|
47
|
+
|
|
48
|
+
console.log(`${JSON.stringify({
|
|
49
|
+
task_id: taskId,
|
|
50
|
+
type: parsed.options.type,
|
|
51
|
+
path: plan.relativePath,
|
|
52
|
+
title: plan.title
|
|
53
|
+
}, null, 2)}\n`);
|
|
54
|
+
return 0;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error(error.message);
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseDocsScaffoldArgs(argv) {
|
|
62
|
+
const options = {
|
|
63
|
+
force: false,
|
|
64
|
+
path: null,
|
|
65
|
+
taskId: null,
|
|
66
|
+
title: null,
|
|
67
|
+
type: null
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
71
|
+
const arg = argv[index];
|
|
72
|
+
if (arg === "--task-id") {
|
|
73
|
+
options.taskId = argv[index + 1] ?? null;
|
|
74
|
+
index += 1;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (arg === "--type") {
|
|
78
|
+
const value = argv[index + 1] ?? null;
|
|
79
|
+
if (!VALID_TYPES.has(value)) {
|
|
80
|
+
return { ok: false, error: "无效的 --type 参数。可选值: design-note, adr" };
|
|
81
|
+
}
|
|
82
|
+
options.type = value;
|
|
83
|
+
index += 1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (arg === "--path") {
|
|
87
|
+
options.path = argv[index + 1] ?? null;
|
|
88
|
+
index += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (arg === "--title") {
|
|
92
|
+
options.title = argv[index + 1] ?? null;
|
|
93
|
+
index += 1;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (arg === "--force") {
|
|
97
|
+
options.force = true;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { ok: false, error: `未知参数: ${arg}` };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!options.type) {
|
|
105
|
+
return { ok: false, error: "需要 --type 参数。可选值: design-note, adr" };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { ok: true, options };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildScaffoldPlan(cwd, taskState, outputPolicy, options) {
|
|
112
|
+
const context = buildTaskContext(taskState);
|
|
113
|
+
const relativePath = options.path ?? buildDefaultDocPath(options.type, taskState.task_id, context, outputPolicy);
|
|
114
|
+
const absolutePath = path.join(cwd, relativePath);
|
|
115
|
+
const title = options.title ?? buildDefaultTitle(options.type, context.goal);
|
|
116
|
+
const content = buildTemplate(options.type, title, context);
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
title,
|
|
120
|
+
relativePath,
|
|
121
|
+
absolutePath,
|
|
122
|
+
content
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function buildDefaultDocPath(type, taskId, context, outputPolicy) {
|
|
127
|
+
const directory = type === "design-note"
|
|
128
|
+
? outputPolicy.design_note.directory
|
|
129
|
+
: outputPolicy.adr.directory;
|
|
130
|
+
const datePrefix = new Date().toISOString().slice(0, 10);
|
|
131
|
+
const slug = slugify(taskId || context.goal || type);
|
|
132
|
+
const suffix = type === "design-note" ? "design-note" : "adr";
|
|
133
|
+
return path.posix.join(directory.replace(/\\/g, "/"), `${datePrefix}-${slug}-${suffix}.md`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function buildDefaultTitle(type, goal) {
|
|
137
|
+
const normalizedGoal = String(goal ?? "").trim();
|
|
138
|
+
if (!normalizedGoal) {
|
|
139
|
+
return type === "design-note" ? "Design Note" : "ADR";
|
|
140
|
+
}
|
|
141
|
+
return type === "design-note"
|
|
142
|
+
? `Design Note: ${normalizedGoal}`
|
|
143
|
+
: `ADR: ${normalizedGoal}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function buildTemplate(type, title, context) {
|
|
147
|
+
if (type === "adr") {
|
|
148
|
+
return buildAdrTemplate(title, context);
|
|
149
|
+
}
|
|
150
|
+
return buildDesignNoteTemplate(title, context);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildDesignNoteTemplate(title, context) {
|
|
154
|
+
return [
|
|
155
|
+
`# ${title}`,
|
|
156
|
+
"",
|
|
157
|
+
"## 背景",
|
|
158
|
+
"",
|
|
159
|
+
`- task_id: \`${context.task_id}\``,
|
|
160
|
+
`- intent: \`${context.intent}\``,
|
|
161
|
+
`- risk_level: \`${context.risk_level}\``,
|
|
162
|
+
"",
|
|
163
|
+
"## 目标",
|
|
164
|
+
"",
|
|
165
|
+
context.goal ? `- ${context.goal}` : "- 待补充",
|
|
166
|
+
"",
|
|
167
|
+
"## 作用范围",
|
|
168
|
+
"",
|
|
169
|
+
...toBulletLines(context.scope),
|
|
170
|
+
"",
|
|
171
|
+
"## 方案",
|
|
172
|
+
"",
|
|
173
|
+
"- 待补充",
|
|
174
|
+
"",
|
|
175
|
+
"## 风险与权衡",
|
|
176
|
+
"",
|
|
177
|
+
"- 待补充",
|
|
178
|
+
"",
|
|
179
|
+
"## 验证计划",
|
|
180
|
+
"",
|
|
181
|
+
"- 待补充",
|
|
182
|
+
""
|
|
183
|
+
].join("\n");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function buildAdrTemplate(title, context) {
|
|
187
|
+
return [
|
|
188
|
+
`# ${title}`,
|
|
189
|
+
"",
|
|
190
|
+
"## 状态",
|
|
191
|
+
"",
|
|
192
|
+
"Proposed",
|
|
193
|
+
"",
|
|
194
|
+
"## 背景",
|
|
195
|
+
"",
|
|
196
|
+
`- task_id: \`${context.task_id}\``,
|
|
197
|
+
`- intent: \`${context.intent}\``,
|
|
198
|
+
`- risk_level: \`${context.risk_level}\``,
|
|
199
|
+
context.goal ? `- goal: ${context.goal}` : "- goal: 待补充",
|
|
200
|
+
"",
|
|
201
|
+
"## 决策",
|
|
202
|
+
"",
|
|
203
|
+
"- 待补充",
|
|
204
|
+
"",
|
|
205
|
+
"## 后果",
|
|
206
|
+
"",
|
|
207
|
+
"- 正面影响:待补充",
|
|
208
|
+
"- 代价与风险:待补充",
|
|
209
|
+
"",
|
|
210
|
+
"## 影响范围",
|
|
211
|
+
"",
|
|
212
|
+
...toBulletLines(context.scope),
|
|
213
|
+
""
|
|
214
|
+
].join("\n");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function buildTaskContext(taskState) {
|
|
218
|
+
const contract = taskState?.confirmed_contract ?? {};
|
|
219
|
+
const draft = taskState?.task_draft ?? {};
|
|
220
|
+
return {
|
|
221
|
+
task_id: taskState?.task_id ?? "",
|
|
222
|
+
goal: String(contract.goal ?? draft.goal ?? "").trim(),
|
|
223
|
+
intent: String(contract.intent ?? draft.intent ?? "unknown"),
|
|
224
|
+
risk_level: String(contract.risk_level ?? draft?.derived?.risk_level ?? "medium"),
|
|
225
|
+
scope: normalizeStringArray(contract.scope ?? draft.scope)
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function normalizeStringArray(value) {
|
|
230
|
+
if (!Array.isArray(value)) {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
return value
|
|
234
|
+
.map((item) => String(item ?? "").trim())
|
|
235
|
+
.filter(Boolean);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function toBulletLines(items) {
|
|
239
|
+
if (items.length === 0) {
|
|
240
|
+
return ["- 待补充"];
|
|
241
|
+
}
|
|
242
|
+
return items.map((item) => `- ${item}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function slugify(value) {
|
|
246
|
+
const normalized = String(value ?? "")
|
|
247
|
+
.toLowerCase()
|
|
248
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
249
|
+
.replace(/^-+|-+$/g, "");
|
|
250
|
+
return normalized || "task";
|
|
251
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { appendAuditEntry } from "../lib/audit-store.js";
|
|
4
|
+
import { loadProjectConfig } from "../lib/project-config.js";
|
|
5
|
+
import { getTaskState, resolveActiveTaskId } from "../lib/state-store.js";
|
|
6
|
+
|
|
7
|
+
const EXIT_ALLOW = 0;
|
|
8
|
+
const EXIT_BLOCK = 1;
|
|
9
|
+
const EXIT_REQUIRE_CONFIRM = 2;
|
|
10
|
+
|
|
11
|
+
const WRITE_TOOLS = new Set(["Write", "Edit", "Bash", "NotebookEdit"]);
|
|
12
|
+
|
|
13
|
+
export function runGate(argv) {
|
|
14
|
+
const [subcommand, ...rest] = argv;
|
|
15
|
+
|
|
16
|
+
if (subcommand !== "before-tool") {
|
|
17
|
+
console.error("gate 目前只支持 before-tool");
|
|
18
|
+
return 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const parsed = parseBeforeToolArgs(rest);
|
|
22
|
+
if (!parsed.ok) {
|
|
23
|
+
console.error(parsed.error);
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const result = beforeTool(process.cwd(), parsed.options);
|
|
28
|
+
console.log(`${JSON.stringify(result, null, 2)}\n`);
|
|
29
|
+
return result.exit_code;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseBeforeToolArgs(argv) {
|
|
33
|
+
const options = {
|
|
34
|
+
filePath: null,
|
|
35
|
+
taskId: null,
|
|
36
|
+
tool: null
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
40
|
+
const arg = argv[index];
|
|
41
|
+
|
|
42
|
+
if (arg === "--tool") {
|
|
43
|
+
options.tool = argv[index + 1] ?? null;
|
|
44
|
+
index += 1;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (arg === "--task-id") {
|
|
49
|
+
options.taskId = argv[index + 1] ?? null;
|
|
50
|
+
index += 1;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (arg === "--file-path") {
|
|
55
|
+
options.filePath = argv[index + 1] ?? null;
|
|
56
|
+
index += 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { ok: false, error: `未知参数: ${arg}` };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!options.tool) {
|
|
64
|
+
return { ok: false, error: "需要 --tool 参数" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { ok: true, options };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function beforeTool(cwd, options) {
|
|
71
|
+
const taskId = options.taskId ?? resolveActiveTaskId(cwd);
|
|
72
|
+
const taskState = taskId ? getTaskState(cwd, taskId) : null;
|
|
73
|
+
const config = loadProjectConfig(cwd);
|
|
74
|
+
|
|
75
|
+
if (!taskState) {
|
|
76
|
+
if (taskId) {
|
|
77
|
+
appendAuditEntry(cwd, {
|
|
78
|
+
description: "State 文件不存在,降级允许执行",
|
|
79
|
+
event_type: "gate_violation",
|
|
80
|
+
phase: "execute",
|
|
81
|
+
risk_at_time: "unknown",
|
|
82
|
+
signal: "proceed_to_execute",
|
|
83
|
+
task_id: taskId
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return allow("State 文件不存在,降级允许");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const currentState = taskState.current_state;
|
|
90
|
+
const currentPhase = taskState.current_phase;
|
|
91
|
+
const filePath = normalizeFilePath(cwd, options.filePath);
|
|
92
|
+
const riskAssessment = deriveRiskAssessment(taskState, config, filePath);
|
|
93
|
+
const riskLevel = riskAssessment.level;
|
|
94
|
+
|
|
95
|
+
if (currentState === "needs_clarification" && isWriteTool(options.tool)) {
|
|
96
|
+
return block(cwd, taskState.task_id, currentPhase, riskLevel, "任务处于 needs_clarification 状态,禁止执行写入操作");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (currentState === "draft" && isWriteTool(options.tool)) {
|
|
100
|
+
return block(cwd, taskState.task_id, currentPhase, riskLevel, "任务合同未闭合(draft 状态),禁止执行写入操作");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (["blocked", "failed", "done", "suspended"].includes(currentState) && isWriteTool(options.tool)) {
|
|
104
|
+
return block(cwd, taskState.task_id, currentPhase, riskLevel, `任务处于 ${currentState} 状态,禁止执行写入操作`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (riskAssessment.requiresConfirmation && isWriteTool(options.tool) && !hasRiskConfirmation(taskState)) {
|
|
108
|
+
return requireConfirmation(cwd, taskState.task_id, currentPhase, "high", "任务命中高风险范围,需要用户确认后继续");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (filePath && config && isWriteTool(options.tool) && config.protected_paths.some((pattern) => pathMatch(filePath, pattern))) {
|
|
112
|
+
return block(cwd, taskState.task_id, currentPhase, riskLevel, `目标路径 ${filePath} 命中 protected_paths,禁止写入`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (filePath && isWriteTool(options.tool)) {
|
|
116
|
+
const scope = taskState?.confirmed_contract?.scope ?? taskState?.task_draft?.scope ?? [];
|
|
117
|
+
const pathScopes = scope.filter((item) => item.includes("/") || item.includes("*"));
|
|
118
|
+
if (pathScopes.length > 0 && !pathScopes.some((pattern) => pathMatch(filePath, pattern))) {
|
|
119
|
+
return block(cwd, taskState.task_id, currentPhase, riskLevel, `目标路径 ${filePath} 超出任务 scope: ${pathScopes.join(", ")}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return allow("门禁通过");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function deriveRiskAssessment(taskState, config, filePath) {
|
|
127
|
+
const baseLevel = normalizeRiskLevel(
|
|
128
|
+
taskState?.confirmed_contract?.risk_level ?? taskState?.task_draft?.derived?.risk_level ?? "medium"
|
|
129
|
+
);
|
|
130
|
+
const matchedRule = matchRiskRule(config?.risk_rules, filePath);
|
|
131
|
+
const effectiveLevel = pickHigherRiskLevel(baseLevel, matchedRule?.level ?? null);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
level: effectiveLevel,
|
|
135
|
+
requiresConfirmation: baseLevel === "high" || matchedRule?.requiresConfirmation === true
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function matchRiskRule(riskRules, filePath) {
|
|
140
|
+
if (!riskRules || typeof riskRules !== "object" || !filePath) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const level of ["high", "medium", "low"]) {
|
|
145
|
+
const rule = riskRules[level];
|
|
146
|
+
if (!rule || typeof rule !== "object") {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const patterns = Array.isArray(rule.path_matches) ? rule.path_matches : [];
|
|
151
|
+
if (patterns.some((pattern) => pathMatch(filePath, String(pattern)))) {
|
|
152
|
+
return {
|
|
153
|
+
level,
|
|
154
|
+
requiresConfirmation: rule.requires_confirmation === true
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function pickHigherRiskLevel(current, next) {
|
|
163
|
+
const order = ["low", "medium", "high"];
|
|
164
|
+
const currentIndex = order.indexOf(normalizeRiskLevel(current));
|
|
165
|
+
const nextIndex = order.indexOf(normalizeRiskLevel(next));
|
|
166
|
+
return currentIndex >= nextIndex ? normalizeRiskLevel(current) : normalizeRiskLevel(next);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function normalizeRiskLevel(value) {
|
|
170
|
+
if (value === "low" || value === "medium" || value === "high") {
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
return "medium";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function hasRiskConfirmation(taskState) {
|
|
177
|
+
return Array.isArray(taskState?.override_history) &&
|
|
178
|
+
taskState.override_history.some((item) => item.event_type === "manual_confirmation");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isWriteTool(toolName) {
|
|
182
|
+
return WRITE_TOOLS.has(toolName);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeFilePath(cwd, filePath) {
|
|
186
|
+
if (!filePath) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const absolute = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
191
|
+
return path.relative(cwd, absolute).replace(/\\/g, "/");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function pathMatch(filePath, pattern) {
|
|
195
|
+
const relative = filePath.replace(/^\.\//, "");
|
|
196
|
+
const normalizedPattern = pattern.replace(/^\.\//, "");
|
|
197
|
+
const regex = globToRegExp(normalizedPattern);
|
|
198
|
+
return regex.test(relative) || relative.startsWith(normalizedPattern.replace(/\*+$/, "").replace(/\/$/, ""));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function globToRegExp(pattern) {
|
|
202
|
+
const escaped = pattern
|
|
203
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
204
|
+
.replace(/\*\*/g, "::DOUBLE_STAR::")
|
|
205
|
+
.replace(/\*/g, "[^/]*")
|
|
206
|
+
.replace(/::DOUBLE_STAR::/g, ".*");
|
|
207
|
+
return new RegExp(`^${escaped}$`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function allow(reason) {
|
|
211
|
+
return { exit_code: EXIT_ALLOW, reason, signal: "proceed_to_execute" };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function block(cwd, taskId, phase, risk, reason) {
|
|
215
|
+
appendAuditEntry(cwd, {
|
|
216
|
+
description: reason,
|
|
217
|
+
event_type: "gate_violation",
|
|
218
|
+
phase,
|
|
219
|
+
risk_at_time: risk,
|
|
220
|
+
signal: "block_execution",
|
|
221
|
+
task_id: taskId
|
|
222
|
+
});
|
|
223
|
+
return { exit_code: EXIT_BLOCK, phase, reason, risk, signal: "block_execution" };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function requireConfirmation(cwd, taskId, phase, risk, reason) {
|
|
227
|
+
appendAuditEntry(cwd, {
|
|
228
|
+
description: reason,
|
|
229
|
+
event_type: "gate_violation",
|
|
230
|
+
phase,
|
|
231
|
+
risk_at_time: risk,
|
|
232
|
+
signal: "require_confirmation",
|
|
233
|
+
task_id: taskId
|
|
234
|
+
});
|
|
235
|
+
return { exit_code: EXIT_REQUIRE_CONFIRM, phase, reason, risk, signal: "require_confirmation" };
|
|
236
|
+
}
|