@basou/core 0.28.0 → 0.30.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/dist/index.d.ts +319 -8
- package/dist/index.js +357 -12
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/schemas/event.schema.json +119 -0
- package/schemas/session-import.schema.json +120 -0
- package/schemas/session.schema.json +1 -0
package/dist/index.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
// src/adapters/
|
|
1
|
+
// src/adapters/command-lookup.ts
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
|
+
async function isOnPath(command) {
|
|
4
|
+
return new Promise((resolve3) => {
|
|
5
|
+
const child = spawn("which", [command], { stdio: "ignore" });
|
|
6
|
+
child.on("error", () => resolve3(false));
|
|
7
|
+
child.on("exit", (code) => resolve3(code === 0));
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/adapters/claude-code/claude-code-adapter.ts
|
|
3
12
|
var claudeCodeAdapterMetadata = {
|
|
4
13
|
kind: "claude-code-adapter",
|
|
5
14
|
version: "0.1.0"
|
|
@@ -10,17 +19,123 @@ async function resolveClaudeCodeCommand(lookup = isOnPath) {
|
|
|
10
19
|
}
|
|
11
20
|
throw new Error("Claude Code CLI not found in PATH. Install claude-code (or claude) first.");
|
|
12
21
|
}
|
|
13
|
-
async function isOnPath(command) {
|
|
14
|
-
return new Promise((resolve3) => {
|
|
15
|
-
const child = spawn("which", [command], { stdio: "ignore" });
|
|
16
|
-
child.on("error", () => resolve3(false));
|
|
17
|
-
child.on("exit", (code) => resolve3(code === 0));
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
22
|
function summarizeAdapterOutput(_stream, _raw) {
|
|
21
23
|
throw new Error("adapter_output summary is not implemented in this release");
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
// src/adapters/claude-code/settings-hook.ts
|
|
27
|
+
var STOP_HOOK_TIMEOUT_SECONDS = 20;
|
|
28
|
+
var BASOU_STOP_HOOK = /(?:\bbasou|(?:@basou|packages)\/cli\/dist\/index\.js['"]?)\s+hook\s+stop\b/;
|
|
29
|
+
function isBasouStopHookCommand(command) {
|
|
30
|
+
return BASOU_STOP_HOOK.test(command);
|
|
31
|
+
}
|
|
32
|
+
function shellQuote(value) {
|
|
33
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
34
|
+
}
|
|
35
|
+
function buildStopHookCommand(options) {
|
|
36
|
+
const flags = [];
|
|
37
|
+
if (options.block === true) flags.push("--block");
|
|
38
|
+
if (options.requireReview === true) flags.push("--require-review");
|
|
39
|
+
if (options.minEdits !== void 0) flags.push(`--min-edits ${options.minEdits}`);
|
|
40
|
+
const suffix = flags.length > 0 ? ` ${flags.join(" ")}` : "";
|
|
41
|
+
return `node ${shellQuote(options.cliEntry)} hook stop${suffix} 2>/dev/null || true`;
|
|
42
|
+
}
|
|
43
|
+
function isRecord(value) {
|
|
44
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
45
|
+
}
|
|
46
|
+
function cloneSettings(settings) {
|
|
47
|
+
if (settings === void 0 || settings === null) return {};
|
|
48
|
+
if (!isRecord(settings)) {
|
|
49
|
+
throw new Error("Claude settings is not a JSON object.");
|
|
50
|
+
}
|
|
51
|
+
return structuredClone(settings);
|
|
52
|
+
}
|
|
53
|
+
function upsertStopHook(settings, command) {
|
|
54
|
+
const root = cloneSettings(settings);
|
|
55
|
+
if (root.hooks === void 0) {
|
|
56
|
+
root.hooks = {};
|
|
57
|
+
} else if (!isRecord(root.hooks)) {
|
|
58
|
+
throw new Error("The 'hooks' key in Claude settings is not an object.");
|
|
59
|
+
}
|
|
60
|
+
const hooks = root.hooks;
|
|
61
|
+
if (hooks.Stop === void 0) {
|
|
62
|
+
hooks.Stop = [];
|
|
63
|
+
} else if (!Array.isArray(hooks.Stop)) {
|
|
64
|
+
throw new Error("The 'hooks.Stop' key in Claude settings is not an array.");
|
|
65
|
+
}
|
|
66
|
+
const stop = hooks.Stop;
|
|
67
|
+
for (const group of stop) {
|
|
68
|
+
if (!isRecord(group) || !Array.isArray(group.hooks)) continue;
|
|
69
|
+
for (const entry of group.hooks) {
|
|
70
|
+
if (!isRecord(entry)) continue;
|
|
71
|
+
if (typeof entry.command === "string" && isBasouStopHookCommand(entry.command)) {
|
|
72
|
+
const unchanged = entry.type === "command" && entry.command === command && entry.timeout === STOP_HOOK_TIMEOUT_SECONDS;
|
|
73
|
+
entry.type = "command";
|
|
74
|
+
entry.command = command;
|
|
75
|
+
entry.timeout = STOP_HOOK_TIMEOUT_SECONDS;
|
|
76
|
+
return { settings: root, action: unchanged ? "unchanged" : "updated" };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
stop.push({ hooks: [{ type: "command", command, timeout: STOP_HOOK_TIMEOUT_SECONDS }] });
|
|
81
|
+
return { settings: root, action: "installed" };
|
|
82
|
+
}
|
|
83
|
+
function removeStopHook(settings) {
|
|
84
|
+
const root = cloneSettings(settings);
|
|
85
|
+
if (!isRecord(root.hooks) || !Array.isArray(root.hooks.Stop)) {
|
|
86
|
+
return { settings: root, action: "absent" };
|
|
87
|
+
}
|
|
88
|
+
const hooks = root.hooks;
|
|
89
|
+
const stop = hooks.Stop;
|
|
90
|
+
let removed = false;
|
|
91
|
+
const newStop = [];
|
|
92
|
+
for (const group of stop) {
|
|
93
|
+
if (!isRecord(group) || !Array.isArray(group.hooks)) {
|
|
94
|
+
newStop.push(group);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const keptHooks = group.hooks.filter((entry) => {
|
|
98
|
+
if (isRecord(entry) && typeof entry.command === "string" && isBasouStopHookCommand(entry.command)) {
|
|
99
|
+
removed = true;
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
});
|
|
104
|
+
if (keptHooks.length === group.hooks.length) {
|
|
105
|
+
newStop.push(group);
|
|
106
|
+
} else if (keptHooks.length > 0) {
|
|
107
|
+
group.hooks = keptHooks;
|
|
108
|
+
newStop.push(group);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!removed) {
|
|
112
|
+
return { settings: root, action: "absent" };
|
|
113
|
+
}
|
|
114
|
+
if (newStop.length === 0) {
|
|
115
|
+
delete hooks.Stop;
|
|
116
|
+
} else {
|
|
117
|
+
hooks.Stop = newStop;
|
|
118
|
+
}
|
|
119
|
+
if (Object.keys(hooks).length === 0) {
|
|
120
|
+
delete root.hooks;
|
|
121
|
+
}
|
|
122
|
+
return { settings: root, action: "removed" };
|
|
123
|
+
}
|
|
124
|
+
function findBasouStopHookCommand(settings) {
|
|
125
|
+
if (!isRecord(settings) || !isRecord(settings.hooks) || !Array.isArray(settings.hooks.Stop)) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
for (const group of settings.hooks.Stop) {
|
|
129
|
+
if (!isRecord(group) || !Array.isArray(group.hooks)) continue;
|
|
130
|
+
for (const entry of group.hooks) {
|
|
131
|
+
if (isRecord(entry) && typeof entry.command === "string" && isBasouStopHookCommand(entry.command)) {
|
|
132
|
+
return entry.command;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
24
139
|
// src/adapters/claude-code/ask-user-question.ts
|
|
25
140
|
function readString(value) {
|
|
26
141
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
@@ -106,6 +221,14 @@ var CAPTURE_VERB = /(?:decision\s+(?:capture|record)|note)\b/;
|
|
|
106
221
|
var CAPTURE_COMMAND_PATTERN = new RegExp(
|
|
107
222
|
`(?:^|[\\n;&|(])\\s*${CAPTURE_INVOCATION.source}\\s+${CAPTURE_VERB.source}`
|
|
108
223
|
);
|
|
224
|
+
var SHIP_ACT_PATTERN = /(?:^|[\n;&|(])\s*(?:git\s+push|git\s+merge|gh\s+pr\s+(?:create|merge))(?![-\w])/;
|
|
225
|
+
var DRY_RUN_PUSH_PATTERN = /(?:^|[\n;&|(])\s*git\s+push\b[^\n;&|()]*?\s-(?:-dry-run|n)\b/;
|
|
226
|
+
function isShipAct(command) {
|
|
227
|
+
return SHIP_ACT_PATTERN.test(command) && !DRY_RUN_PUSH_PATTERN.test(command);
|
|
228
|
+
}
|
|
229
|
+
var REVIEW_RECORD_PATTERN = new RegExp(
|
|
230
|
+
`(?:^|[\\n;&|(])\\s*${CAPTURE_INVOCATION.source}\\s+review\\s+record\\b`
|
|
231
|
+
);
|
|
109
232
|
var FILE_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "NotebookEdit"]);
|
|
110
233
|
function evaluateStopHook(input) {
|
|
111
234
|
const minEdits = input.minEdits ?? DEFAULT_STOP_HOOK_MIN_EDITS;
|
|
@@ -113,6 +236,7 @@ function evaluateStopHook(input) {
|
|
|
113
236
|
return {
|
|
114
237
|
kind: "silent",
|
|
115
238
|
reason: "stop_hook_active",
|
|
239
|
+
review: { fires: false, reason: "stop_hook_active" },
|
|
116
240
|
commandCount: 0,
|
|
117
241
|
fileCount: 0,
|
|
118
242
|
decisionPointCount: 0
|
|
@@ -121,6 +245,8 @@ function evaluateStopHook(input) {
|
|
|
121
245
|
let commandCount = 0;
|
|
122
246
|
let fileCount = 0;
|
|
123
247
|
let captured = false;
|
|
248
|
+
let shipped = false;
|
|
249
|
+
let reviewed = false;
|
|
124
250
|
for (const record of input.records) {
|
|
125
251
|
if (readString2(record.type) !== "assistant") continue;
|
|
126
252
|
for (const tool of toolUsesOf2(record)) {
|
|
@@ -130,7 +256,11 @@ function evaluateStopHook(input) {
|
|
|
130
256
|
commandCount += 1;
|
|
131
257
|
const toolInput = isObject2(tool.input) ? tool.input : void 0;
|
|
132
258
|
const command = toolInput !== void 0 ? readString2(toolInput.command) : void 0;
|
|
133
|
-
if (command !== void 0
|
|
259
|
+
if (command !== void 0) {
|
|
260
|
+
if (CAPTURE_COMMAND_PATTERN.test(command)) captured = true;
|
|
261
|
+
if (isShipAct(command)) shipped = true;
|
|
262
|
+
if (REVIEW_RECORD_PATTERN.test(command)) reviewed = true;
|
|
263
|
+
}
|
|
134
264
|
} else if (FILE_EDIT_TOOLS.has(name)) {
|
|
135
265
|
fileCount += 1;
|
|
136
266
|
}
|
|
@@ -138,14 +268,21 @@ function evaluateStopHook(input) {
|
|
|
138
268
|
}
|
|
139
269
|
const decisionPointCount = countUncapturedDecisionPoints(input.records);
|
|
140
270
|
const counts = { commandCount, fileCount, decisionPointCount };
|
|
271
|
+
const review = evaluateReviewGate({ shipped, reviewed, fileCount, minEdits });
|
|
141
272
|
if (captured) {
|
|
142
|
-
return { kind: "silent", reason: "already_captured", ...counts };
|
|
273
|
+
return { kind: "silent", reason: "already_captured", review, ...counts };
|
|
143
274
|
}
|
|
144
275
|
const substantive = fileCount >= minEdits || decisionPointCount > 0;
|
|
145
276
|
if (!substantive) {
|
|
146
|
-
return { kind: "silent", reason: "not_substantive", ...counts };
|
|
277
|
+
return { kind: "silent", reason: "not_substantive", review, ...counts };
|
|
147
278
|
}
|
|
148
|
-
return { kind: "nudge", additionalContext: renderNudge(counts), ...counts };
|
|
279
|
+
return { kind: "nudge", additionalContext: renderNudge(counts), review, ...counts };
|
|
280
|
+
}
|
|
281
|
+
function evaluateReviewGate(input) {
|
|
282
|
+
if (!input.shipped) return { fires: false, reason: "no_ship_act" };
|
|
283
|
+
if (input.fileCount < input.minEdits) return { fires: false, reason: "not_substantive_code" };
|
|
284
|
+
if (input.reviewed) return { fires: false, reason: "already_reviewed" };
|
|
285
|
+
return { fires: true, additionalContext: renderReviewNudge() };
|
|
149
286
|
}
|
|
150
287
|
function renderNudge(counts) {
|
|
151
288
|
const did = [];
|
|
@@ -168,6 +305,14 @@ function renderNudge(counts) {
|
|
|
168
305
|
"If nothing is worth capturing, just stop \u2014 do not invent decisions."
|
|
169
306
|
].join("\n");
|
|
170
307
|
}
|
|
308
|
+
function renderReviewNudge() {
|
|
309
|
+
return [
|
|
310
|
+
"This session shipped code (a push / PR / merge) after substantive edits but recorded no review.",
|
|
311
|
+
"An adversarial / second-opinion review before shipping is the discipline here. If a review ran, record it now so it lands on the durable trail:",
|
|
312
|
+
' - run `basou review record` and pipe a JSON object: { "reviewer": "...", "target": "...", with optional "verdict" / "findings" / "blocked" } (an explicit "blocked": [] records that you blocked nothing).',
|
|
313
|
+
"If no review ran, that is the gap this is meant to catch \u2014 review before relying on this. If a review genuinely was not warranted, just stop \u2014 do not fabricate a review record."
|
|
314
|
+
].join("\n");
|
|
315
|
+
}
|
|
171
316
|
function readString2(value) {
|
|
172
317
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
173
318
|
}
|
|
@@ -494,6 +639,16 @@ function toolUses(record) {
|
|
|
494
639
|
return result;
|
|
495
640
|
}
|
|
496
641
|
|
|
642
|
+
// src/adapters/codex/codex-adapter.ts
|
|
643
|
+
var codexAdapterMetadata = {
|
|
644
|
+
kind: "codex-adapter",
|
|
645
|
+
version: "0.1.0"
|
|
646
|
+
};
|
|
647
|
+
async function resolveCodexCommand(lookup = isOnPath) {
|
|
648
|
+
if (await lookup("codex")) return { command: "codex" };
|
|
649
|
+
throw new Error("Codex CLI not found in PATH. Install codex first.");
|
|
650
|
+
}
|
|
651
|
+
|
|
497
652
|
// src/adapters/codex/rollout-importer.ts
|
|
498
653
|
var CODEX_IMPORT_SOURCE = "codex-import";
|
|
499
654
|
function codexRolloutToImportPayload(records, options) {
|
|
@@ -1084,6 +1239,25 @@ var NoteAddedEventSchema = BaseEventSchema.extend({
|
|
|
1084
1239
|
// surface. Optional so pre-existing note_added events remain valid.
|
|
1085
1240
|
kind: z3.enum(["note", "next_step"]).optional()
|
|
1086
1241
|
});
|
|
1242
|
+
var ReviewFindingSchema = z3.object({
|
|
1243
|
+
title: z3.string().min(1),
|
|
1244
|
+
severity: z3.enum(["high", "medium", "low"]).optional(),
|
|
1245
|
+
location: z3.string().min(1).optional(),
|
|
1246
|
+
summary: z3.string().min(1).optional()
|
|
1247
|
+
});
|
|
1248
|
+
var ReviewBlockedSchema = z3.object({
|
|
1249
|
+
title: z3.string().min(1),
|
|
1250
|
+
reason: z3.enum(["spec-deviation", "design-reversal"]),
|
|
1251
|
+
why: z3.string().min(1).optional()
|
|
1252
|
+
});
|
|
1253
|
+
var ReviewRecordedEventSchema = BaseEventSchema.extend({
|
|
1254
|
+
type: z3.literal("review_recorded"),
|
|
1255
|
+
reviewer: z3.string().min(1),
|
|
1256
|
+
target: z3.string().min(1),
|
|
1257
|
+
verdict: z3.enum(["pass", "needs-attention", "fail"]).optional(),
|
|
1258
|
+
findings: z3.array(ReviewFindingSchema).optional(),
|
|
1259
|
+
blocked: z3.array(ReviewBlockedSchema).optional()
|
|
1260
|
+
});
|
|
1087
1261
|
var AdapterOutputEventSchema = BaseEventSchema.extend({
|
|
1088
1262
|
type: z3.literal("adapter_output"),
|
|
1089
1263
|
stream: z3.enum(["stdout", "stderr"]),
|
|
@@ -1111,6 +1285,7 @@ var EventSchema = z3.discriminatedUnion("type", [
|
|
|
1111
1285
|
TaskDeletedEventSchema,
|
|
1112
1286
|
TaskArchivedEventSchema,
|
|
1113
1287
|
NoteAddedEventSchema,
|
|
1288
|
+
ReviewRecordedEventSchema,
|
|
1114
1289
|
AdapterOutputEventSchema
|
|
1115
1290
|
]);
|
|
1116
1291
|
|
|
@@ -1409,6 +1584,7 @@ var SessionStatusSchema = z4.enum([
|
|
|
1409
1584
|
var SessionSourceKindSchema = z4.enum([
|
|
1410
1585
|
"claude-code-adapter",
|
|
1411
1586
|
"claude-code-import",
|
|
1587
|
+
"codex-adapter",
|
|
1412
1588
|
"codex-import",
|
|
1413
1589
|
"human",
|
|
1414
1590
|
"import",
|
|
@@ -6755,6 +6931,159 @@ async function findReviewGaps(input) {
|
|
|
6755
6931
|
};
|
|
6756
6932
|
}
|
|
6757
6933
|
|
|
6934
|
+
// src/review/review-record.ts
|
|
6935
|
+
var VALID_VERDICTS = /* @__PURE__ */ new Set(["pass", "needs-attention", "fail"]);
|
|
6936
|
+
var VALID_SEVERITIES = /* @__PURE__ */ new Set(["high", "medium", "low"]);
|
|
6937
|
+
var VALID_BLOCK_REASONS = /* @__PURE__ */ new Set(["spec-deviation", "design-reversal"]);
|
|
6938
|
+
var ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
6939
|
+
"reviewer",
|
|
6940
|
+
"target",
|
|
6941
|
+
"verdict",
|
|
6942
|
+
"findings",
|
|
6943
|
+
"blocked"
|
|
6944
|
+
]);
|
|
6945
|
+
var ALLOWED_FINDING_KEYS = /* @__PURE__ */ new Set([
|
|
6946
|
+
"title",
|
|
6947
|
+
"severity",
|
|
6948
|
+
"location",
|
|
6949
|
+
"summary"
|
|
6950
|
+
]);
|
|
6951
|
+
var ALLOWED_BLOCKED_KEYS = /* @__PURE__ */ new Set(["title", "reason", "why"]);
|
|
6952
|
+
var REVIEW_RECORD_NO_INPUT_HINT = "No input: pipe a JSON object describing the review to stdin or pass --file <path>.";
|
|
6953
|
+
function parseReviewRecordInput(raw) {
|
|
6954
|
+
if (raw.trim().length === 0) {
|
|
6955
|
+
throw new Error(REVIEW_RECORD_NO_INPUT_HINT);
|
|
6956
|
+
}
|
|
6957
|
+
let parsed;
|
|
6958
|
+
try {
|
|
6959
|
+
parsed = JSON.parse(raw);
|
|
6960
|
+
} catch (error) {
|
|
6961
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
6962
|
+
throw new Error(`Input is not valid JSON: ${detail}`);
|
|
6963
|
+
}
|
|
6964
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
6965
|
+
throw new Error("Input must be a single JSON object describing one review.");
|
|
6966
|
+
}
|
|
6967
|
+
const obj = parsed;
|
|
6968
|
+
for (const key of Object.keys(obj)) {
|
|
6969
|
+
if (!ALLOWED_KEYS.has(key)) {
|
|
6970
|
+
throw new Error(
|
|
6971
|
+
`Unknown field '${key}'. Allowed: reviewer, target, verdict, findings, blocked.`
|
|
6972
|
+
);
|
|
6973
|
+
}
|
|
6974
|
+
}
|
|
6975
|
+
const reviewer = requireNonEmptyString(obj.reviewer, "reviewer");
|
|
6976
|
+
const target = requireNonEmptyString(obj.target, "target");
|
|
6977
|
+
const out = { reviewer, target };
|
|
6978
|
+
if (obj.verdict !== void 0) {
|
|
6979
|
+
if (typeof obj.verdict !== "string" || !VALID_VERDICTS.has(obj.verdict)) {
|
|
6980
|
+
throw new Error(`verdict must be one of pass, needs-attention, fail, got '${obj.verdict}'.`);
|
|
6981
|
+
}
|
|
6982
|
+
out.verdict = obj.verdict;
|
|
6983
|
+
}
|
|
6984
|
+
if (obj.findings !== void 0) {
|
|
6985
|
+
out.findings = parseFindings(obj.findings);
|
|
6986
|
+
}
|
|
6987
|
+
if (obj.blocked !== void 0) {
|
|
6988
|
+
out.blocked = parseBlocked(obj.blocked);
|
|
6989
|
+
}
|
|
6990
|
+
return out;
|
|
6991
|
+
}
|
|
6992
|
+
function parseFindings(value) {
|
|
6993
|
+
if (!Array.isArray(value)) {
|
|
6994
|
+
throw new Error("findings must be an array of objects.");
|
|
6995
|
+
}
|
|
6996
|
+
return value.map((item, i) => {
|
|
6997
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
6998
|
+
throw new Error(`findings[${i}] must be a JSON object.`);
|
|
6999
|
+
}
|
|
7000
|
+
const obj = item;
|
|
7001
|
+
for (const key of Object.keys(obj)) {
|
|
7002
|
+
if (!ALLOWED_FINDING_KEYS.has(key)) {
|
|
7003
|
+
throw new Error(
|
|
7004
|
+
`findings[${i}]: unknown field '${key}'. Allowed: title, severity, location, summary.`
|
|
7005
|
+
);
|
|
7006
|
+
}
|
|
7007
|
+
}
|
|
7008
|
+
const finding = {
|
|
7009
|
+
title: requireNonEmptyString(obj.title, `findings[${i}].title`)
|
|
7010
|
+
};
|
|
7011
|
+
if (obj.severity !== void 0) {
|
|
7012
|
+
if (typeof obj.severity !== "string" || !VALID_SEVERITIES.has(obj.severity)) {
|
|
7013
|
+
throw new Error(
|
|
7014
|
+
`findings[${i}].severity must be one of high, medium, low, got '${obj.severity}'.`
|
|
7015
|
+
);
|
|
7016
|
+
}
|
|
7017
|
+
finding.severity = obj.severity;
|
|
7018
|
+
}
|
|
7019
|
+
if (obj.location !== void 0) {
|
|
7020
|
+
finding.location = requireNonEmptyString(obj.location, `findings[${i}].location`);
|
|
7021
|
+
}
|
|
7022
|
+
if (obj.summary !== void 0) {
|
|
7023
|
+
finding.summary = requireNonEmptyString(obj.summary, `findings[${i}].summary`);
|
|
7024
|
+
}
|
|
7025
|
+
return finding;
|
|
7026
|
+
});
|
|
7027
|
+
}
|
|
7028
|
+
function parseBlocked(value) {
|
|
7029
|
+
if (!Array.isArray(value)) {
|
|
7030
|
+
throw new Error("blocked must be an array of objects.");
|
|
7031
|
+
}
|
|
7032
|
+
return value.map((item, i) => {
|
|
7033
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
7034
|
+
throw new Error(`blocked[${i}] must be a JSON object.`);
|
|
7035
|
+
}
|
|
7036
|
+
const obj = item;
|
|
7037
|
+
for (const key of Object.keys(obj)) {
|
|
7038
|
+
if (!ALLOWED_BLOCKED_KEYS.has(key)) {
|
|
7039
|
+
throw new Error(`blocked[${i}]: unknown field '${key}'. Allowed: title, reason, why.`);
|
|
7040
|
+
}
|
|
7041
|
+
}
|
|
7042
|
+
if (typeof obj.reason !== "string" || !VALID_BLOCK_REASONS.has(obj.reason)) {
|
|
7043
|
+
throw new Error(
|
|
7044
|
+
`blocked[${i}].reason must be one of spec-deviation, design-reversal, got '${obj.reason}'.`
|
|
7045
|
+
);
|
|
7046
|
+
}
|
|
7047
|
+
const blocked = {
|
|
7048
|
+
title: requireNonEmptyString(obj.title, `blocked[${i}].title`),
|
|
7049
|
+
reason: obj.reason
|
|
7050
|
+
};
|
|
7051
|
+
if (obj.why !== void 0) {
|
|
7052
|
+
blocked.why = requireNonEmptyString(obj.why, `blocked[${i}].why`);
|
|
7053
|
+
}
|
|
7054
|
+
return blocked;
|
|
7055
|
+
});
|
|
7056
|
+
}
|
|
7057
|
+
function requireNonEmptyString(value, field) {
|
|
7058
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
7059
|
+
throw new Error(`${field} must be a non-empty string.`);
|
|
7060
|
+
}
|
|
7061
|
+
return value;
|
|
7062
|
+
}
|
|
7063
|
+
function buildReviewRecordedEvent(input) {
|
|
7064
|
+
const { review } = input;
|
|
7065
|
+
return {
|
|
7066
|
+
schema_version: "0.1.0",
|
|
7067
|
+
id: input.eventId,
|
|
7068
|
+
session_id: input.sessionId,
|
|
7069
|
+
occurred_at: input.occurredAt,
|
|
7070
|
+
source: "local-cli",
|
|
7071
|
+
type: "review_recorded",
|
|
7072
|
+
reviewer: review.reviewer,
|
|
7073
|
+
target: review.target,
|
|
7074
|
+
...review.verdict !== void 0 ? { verdict: review.verdict } : {},
|
|
7075
|
+
...review.findings !== void 0 ? { findings: review.findings } : {},
|
|
7076
|
+
...review.blocked !== void 0 ? { blocked: review.blocked } : {}
|
|
7077
|
+
};
|
|
7078
|
+
}
|
|
7079
|
+
var LABEL_FRAGMENT_MAX = 40;
|
|
7080
|
+
function buildReviewRecordLabel(review) {
|
|
7081
|
+
return `Ad-hoc review: ${truncate(review.reviewer)} -> ${truncate(review.target)}`;
|
|
7082
|
+
}
|
|
7083
|
+
function truncate(value) {
|
|
7084
|
+
return value.length > LABEL_FRAGMENT_MAX ? `${value.slice(0, LABEL_FRAGMENT_MAX - 3)}...` : value;
|
|
7085
|
+
}
|
|
7086
|
+
|
|
6758
7087
|
// src/runtime/child-process-runner.ts
|
|
6759
7088
|
import { spawn as spawn2 } from "child_process";
|
|
6760
7089
|
var DEFAULT_KILL_GRACE_MS = 5e3;
|
|
@@ -7137,6 +7466,8 @@ var GENERATED_START = "<!-- BASOU:GENERATED:START -->";
|
|
|
7137
7466
|
var GENERATED_END = "<!-- BASOU:GENERATED:END -->";
|
|
7138
7467
|
var PROTOCOL_START = "<!-- BASOU:PROTOCOLS:START -->";
|
|
7139
7468
|
var PROTOCOL_END = "<!-- BASOU:PROTOCOLS:END -->";
|
|
7469
|
+
var ORIENTATION_START = "<!-- BASOU:ORIENTATION:START -->";
|
|
7470
|
+
var ORIENTATION_END = "<!-- BASOU:ORIENTATION:END -->";
|
|
7140
7471
|
var DEFAULT_MARKERS = { start: GENERATED_START, end: GENERATED_END };
|
|
7141
7472
|
async function readMarkdownFile(filePath) {
|
|
7142
7473
|
try {
|
|
@@ -7661,9 +7992,13 @@ export {
|
|
|
7661
7992
|
IsoTimestampSchema,
|
|
7662
7993
|
JSON_SCHEMA_VERSION,
|
|
7663
7994
|
ManifestSchema,
|
|
7995
|
+
ORIENTATION_END,
|
|
7996
|
+
ORIENTATION_START,
|
|
7664
7997
|
PROTOCOL_END,
|
|
7665
7998
|
PROTOCOL_START,
|
|
7999
|
+
REVIEW_RECORD_NO_INPUT_HINT,
|
|
7666
8000
|
RiskLevelSchema,
|
|
8001
|
+
STOP_HOOK_TIMEOUT_SECONDS,
|
|
7667
8002
|
STUCK_THRESHOLD_MS,
|
|
7668
8003
|
SchemaVersionSchema,
|
|
7669
8004
|
SessionIdSchema,
|
|
@@ -7690,7 +8025,10 @@ export {
|
|
|
7690
8025
|
assertBasouRootSafe,
|
|
7691
8026
|
basouPaths,
|
|
7692
8027
|
buildJsonSchemas,
|
|
8028
|
+
buildReviewRecordLabel,
|
|
8029
|
+
buildReviewRecordedEvent,
|
|
7693
8030
|
buildStatusSnapshot,
|
|
8031
|
+
buildStopHookCommand,
|
|
7694
8032
|
chainEvents,
|
|
7695
8033
|
chainRawJsonLines,
|
|
7696
8034
|
classifyFilesBySourceRoot,
|
|
@@ -7698,6 +8036,7 @@ export {
|
|
|
7698
8036
|
classifySuspect,
|
|
7699
8037
|
claudeCodeAdapterMetadata,
|
|
7700
8038
|
claudeTranscriptToImportPayload,
|
|
8039
|
+
codexAdapterMetadata,
|
|
7701
8040
|
codexRolloutToImportPayload,
|
|
7702
8041
|
computeWorkStats,
|
|
7703
8042
|
createAdHocSessionWithEvent,
|
|
@@ -7712,6 +8051,7 @@ export {
|
|
|
7712
8051
|
enumerateTaskIds,
|
|
7713
8052
|
evaluateStopHook,
|
|
7714
8053
|
finalizeSessionYaml,
|
|
8054
|
+
findBasouStopHookCommand,
|
|
7715
8055
|
findErrorCode,
|
|
7716
8056
|
findReviewGaps,
|
|
7717
8057
|
formatDurationMs,
|
|
@@ -7721,6 +8061,7 @@ export {
|
|
|
7721
8061
|
importSessionFromJson,
|
|
7722
8062
|
inspectChainTail,
|
|
7723
8063
|
instructionMode,
|
|
8064
|
+
isBasouStopHookCommand,
|
|
7724
8065
|
isGitNotFound,
|
|
7725
8066
|
isImportDerivedSource,
|
|
7726
8067
|
isLazyExpired,
|
|
@@ -7737,6 +8078,7 @@ export {
|
|
|
7737
8078
|
overwriteYamlFile,
|
|
7738
8079
|
parseDuration,
|
|
7739
8080
|
parseMarkers,
|
|
8081
|
+
parseReviewRecordInput,
|
|
7740
8082
|
pathBasename,
|
|
7741
8083
|
planArchive,
|
|
7742
8084
|
planGitignore,
|
|
@@ -7759,6 +8101,7 @@ export {
|
|
|
7759
8101
|
refreshTaskLinkedSessions,
|
|
7760
8102
|
reimportPreservingId,
|
|
7761
8103
|
removeMarkerSection,
|
|
8104
|
+
removeStopHook,
|
|
7762
8105
|
renderDecisions,
|
|
7763
8106
|
renderHandoff,
|
|
7764
8107
|
renderOrientation,
|
|
@@ -7768,6 +8111,7 @@ export {
|
|
|
7768
8111
|
replayEvents,
|
|
7769
8112
|
resolveBasouRepositoryRoot,
|
|
7770
8113
|
resolveClaudeCodeCommand,
|
|
8114
|
+
resolveCodexCommand,
|
|
7771
8115
|
resolveRepositoryRoot,
|
|
7772
8116
|
resolveSessionId,
|
|
7773
8117
|
resolveTaskId,
|
|
@@ -7788,6 +8132,7 @@ export {
|
|
|
7788
8132
|
ulid,
|
|
7789
8133
|
unknownManifestKeys,
|
|
7790
8134
|
updateTaskStatusWithEvent,
|
|
8135
|
+
upsertStopHook,
|
|
7791
8136
|
verifyEventsChain,
|
|
7792
8137
|
writeEventsBulk,
|
|
7793
8138
|
writeManifest,
|