@basou/core 0.29.0 → 0.31.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 +356 -94
- package/dist/index.js +269 -31
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schemas/approval.schema.json +3 -2
- package/schemas/event.schema.json +139 -20
- package/schemas/manifest.schema.json +2 -12
- package/schemas/session-import.schema.json +144 -22
- package/schemas/session.schema.json +14 -7
- package/schemas/status.schema.json +1 -1
- package/schemas/task.schema.json +5 -3
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,13 +19,6 @@ 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
|
}
|
|
@@ -33,6 +35,7 @@ function shellQuote(value) {
|
|
|
33
35
|
function buildStopHookCommand(options) {
|
|
34
36
|
const flags = [];
|
|
35
37
|
if (options.block === true) flags.push("--block");
|
|
38
|
+
if (options.requireReview === true) flags.push("--require-review");
|
|
36
39
|
if (options.minEdits !== void 0) flags.push(`--min-edits ${options.minEdits}`);
|
|
37
40
|
const suffix = flags.length > 0 ? ` ${flags.join(" ")}` : "";
|
|
38
41
|
return `node ${shellQuote(options.cliEntry)} hook stop${suffix} 2>/dev/null || true`;
|
|
@@ -218,6 +221,14 @@ var CAPTURE_VERB = /(?:decision\s+(?:capture|record)|note)\b/;
|
|
|
218
221
|
var CAPTURE_COMMAND_PATTERN = new RegExp(
|
|
219
222
|
`(?:^|[\\n;&|(])\\s*${CAPTURE_INVOCATION.source}\\s+${CAPTURE_VERB.source}`
|
|
220
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
|
+
);
|
|
221
232
|
var FILE_EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "NotebookEdit"]);
|
|
222
233
|
function evaluateStopHook(input) {
|
|
223
234
|
const minEdits = input.minEdits ?? DEFAULT_STOP_HOOK_MIN_EDITS;
|
|
@@ -225,6 +236,7 @@ function evaluateStopHook(input) {
|
|
|
225
236
|
return {
|
|
226
237
|
kind: "silent",
|
|
227
238
|
reason: "stop_hook_active",
|
|
239
|
+
review: { fires: false, reason: "stop_hook_active" },
|
|
228
240
|
commandCount: 0,
|
|
229
241
|
fileCount: 0,
|
|
230
242
|
decisionPointCount: 0
|
|
@@ -233,6 +245,8 @@ function evaluateStopHook(input) {
|
|
|
233
245
|
let commandCount = 0;
|
|
234
246
|
let fileCount = 0;
|
|
235
247
|
let captured = false;
|
|
248
|
+
let shipped = false;
|
|
249
|
+
let reviewed = false;
|
|
236
250
|
for (const record of input.records) {
|
|
237
251
|
if (readString2(record.type) !== "assistant") continue;
|
|
238
252
|
for (const tool of toolUsesOf2(record)) {
|
|
@@ -242,7 +256,11 @@ function evaluateStopHook(input) {
|
|
|
242
256
|
commandCount += 1;
|
|
243
257
|
const toolInput = isObject2(tool.input) ? tool.input : void 0;
|
|
244
258
|
const command = toolInput !== void 0 ? readString2(toolInput.command) : void 0;
|
|
245
|
-
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
|
+
}
|
|
246
264
|
} else if (FILE_EDIT_TOOLS.has(name)) {
|
|
247
265
|
fileCount += 1;
|
|
248
266
|
}
|
|
@@ -250,14 +268,21 @@ function evaluateStopHook(input) {
|
|
|
250
268
|
}
|
|
251
269
|
const decisionPointCount = countUncapturedDecisionPoints(input.records);
|
|
252
270
|
const counts = { commandCount, fileCount, decisionPointCount };
|
|
271
|
+
const review = evaluateReviewGate({ shipped, reviewed, fileCount, minEdits });
|
|
253
272
|
if (captured) {
|
|
254
|
-
return { kind: "silent", reason: "already_captured", ...counts };
|
|
273
|
+
return { kind: "silent", reason: "already_captured", review, ...counts };
|
|
255
274
|
}
|
|
256
275
|
const substantive = fileCount >= minEdits || decisionPointCount > 0;
|
|
257
276
|
if (!substantive) {
|
|
258
|
-
return { kind: "silent", reason: "not_substantive", ...counts };
|
|
277
|
+
return { kind: "silent", reason: "not_substantive", review, ...counts };
|
|
259
278
|
}
|
|
260
|
-
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() };
|
|
261
286
|
}
|
|
262
287
|
function renderNudge(counts) {
|
|
263
288
|
const did = [];
|
|
@@ -280,6 +305,14 @@ function renderNudge(counts) {
|
|
|
280
305
|
"If nothing is worth capturing, just stop \u2014 do not invent decisions."
|
|
281
306
|
].join("\n");
|
|
282
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
|
+
}
|
|
283
316
|
function readString2(value) {
|
|
284
317
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
285
318
|
}
|
|
@@ -606,6 +639,16 @@ function toolUses(record) {
|
|
|
606
639
|
return result;
|
|
607
640
|
}
|
|
608
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
|
+
|
|
609
652
|
// src/adapters/codex/rollout-importer.ts
|
|
610
653
|
var CODEX_IMPORT_SOURCE = "codex-import";
|
|
611
654
|
function codexRolloutToImportPayload(records, options) {
|
|
@@ -867,7 +910,10 @@ import { z as z2 } from "zod";
|
|
|
867
910
|
|
|
868
911
|
// src/schemas/shared.schema.ts
|
|
869
912
|
import { z } from "zod";
|
|
870
|
-
var SchemaVersionSchema = z.
|
|
913
|
+
var SchemaVersionSchema = z.string().regex(/^0\.\d+\.\d+$/, {
|
|
914
|
+
message: "unsupported .basou format version: this basou reads format major 0 (0.x.y). If this workspace was written by a newer basou, upgrade basou to open it."
|
|
915
|
+
});
|
|
916
|
+
var CacheVersionSchema = z.literal("0.1.0");
|
|
871
917
|
var IsoTimestampSchema = z.string().datetime({ offset: true });
|
|
872
918
|
var createPrefixedIdSchema = (prefix) => {
|
|
873
919
|
const refiner = (value) => isValidPrefixedId(value) && value.startsWith(`${prefix}_`);
|
|
@@ -887,14 +933,14 @@ var EventSourceSchema = z.string().min(1);
|
|
|
887
933
|
|
|
888
934
|
// src/schemas/approval.schema.ts
|
|
889
935
|
var ApprovalStatusSchema = z2.enum(["pending", "approved", "rejected", "expired"]);
|
|
890
|
-
var ApprovalSchema = z2.
|
|
936
|
+
var ApprovalSchema = z2.looseObject({
|
|
891
937
|
schema_version: SchemaVersionSchema,
|
|
892
938
|
id: ApprovalIdSchema,
|
|
893
939
|
session_id: SessionIdSchema,
|
|
894
940
|
created_at: IsoTimestampSchema,
|
|
895
941
|
status: ApprovalStatusSchema,
|
|
896
942
|
risk_level: RiskLevelSchema,
|
|
897
|
-
action: z2.
|
|
943
|
+
action: z2.looseObject({ kind: z2.string() }).passthrough(),
|
|
898
944
|
reason: z2.string(),
|
|
899
945
|
expires_at: IsoTimestampSchema.nullable().default(null),
|
|
900
946
|
// The four fields below are null while `status === "pending"` and set
|
|
@@ -1196,6 +1242,25 @@ var NoteAddedEventSchema = BaseEventSchema.extend({
|
|
|
1196
1242
|
// surface. Optional so pre-existing note_added events remain valid.
|
|
1197
1243
|
kind: z3.enum(["note", "next_step"]).optional()
|
|
1198
1244
|
});
|
|
1245
|
+
var ReviewFindingSchema = z3.object({
|
|
1246
|
+
title: z3.string().min(1),
|
|
1247
|
+
severity: z3.enum(["high", "medium", "low"]).optional(),
|
|
1248
|
+
location: z3.string().min(1).optional(),
|
|
1249
|
+
summary: z3.string().min(1).optional()
|
|
1250
|
+
});
|
|
1251
|
+
var ReviewBlockedSchema = z3.object({
|
|
1252
|
+
title: z3.string().min(1),
|
|
1253
|
+
reason: z3.enum(["spec-deviation", "design-reversal"]),
|
|
1254
|
+
why: z3.string().min(1).optional()
|
|
1255
|
+
});
|
|
1256
|
+
var ReviewRecordedEventSchema = BaseEventSchema.extend({
|
|
1257
|
+
type: z3.literal("review_recorded"),
|
|
1258
|
+
reviewer: z3.string().min(1),
|
|
1259
|
+
target: z3.string().min(1),
|
|
1260
|
+
verdict: z3.enum(["pass", "needs-attention", "fail"]).optional(),
|
|
1261
|
+
findings: z3.array(ReviewFindingSchema).optional(),
|
|
1262
|
+
blocked: z3.array(ReviewBlockedSchema).optional()
|
|
1263
|
+
});
|
|
1199
1264
|
var AdapterOutputEventSchema = BaseEventSchema.extend({
|
|
1200
1265
|
type: z3.literal("adapter_output"),
|
|
1201
1266
|
stream: z3.enum(["stdout", "stderr"]),
|
|
@@ -1223,6 +1288,7 @@ var EventSchema = z3.discriminatedUnion("type", [
|
|
|
1223
1288
|
TaskDeletedEventSchema,
|
|
1224
1289
|
TaskArchivedEventSchema,
|
|
1225
1290
|
NoteAddedEventSchema,
|
|
1291
|
+
ReviewRecordedEventSchema,
|
|
1226
1292
|
AdapterOutputEventSchema
|
|
1227
1293
|
]);
|
|
1228
1294
|
|
|
@@ -1521,12 +1587,13 @@ var SessionStatusSchema = z4.enum([
|
|
|
1521
1587
|
var SessionSourceKindSchema = z4.enum([
|
|
1522
1588
|
"claude-code-adapter",
|
|
1523
1589
|
"claude-code-import",
|
|
1590
|
+
"codex-adapter",
|
|
1524
1591
|
"codex-import",
|
|
1525
1592
|
"human",
|
|
1526
1593
|
"import",
|
|
1527
1594
|
"terminal"
|
|
1528
1595
|
]);
|
|
1529
|
-
var SessionSourceSchema = z4.
|
|
1596
|
+
var SessionSourceSchema = z4.looseObject({
|
|
1530
1597
|
kind: SessionSourceKindSchema,
|
|
1531
1598
|
version: z4.literal("0.1.0"),
|
|
1532
1599
|
// Optional id of the originating session in the SOURCE tool's own
|
|
@@ -1542,20 +1609,20 @@ var SessionSourceSchema = z4.object({
|
|
|
1542
1609
|
// fresh import or `--force`).
|
|
1543
1610
|
source_size_bytes: z4.number().int().nonnegative().optional()
|
|
1544
1611
|
});
|
|
1545
|
-
var InvocationSchema = z4.
|
|
1612
|
+
var InvocationSchema = z4.looseObject({
|
|
1546
1613
|
command: z4.string().min(1),
|
|
1547
1614
|
args: z4.array(z4.string()).default([]),
|
|
1548
1615
|
// Nullable to record signal-terminated runs where the child has no exit
|
|
1549
1616
|
// code; the same nullability is mirrored in CommandExecutedEventSchema.
|
|
1550
1617
|
exit_code: z4.number().int().nullable()
|
|
1551
1618
|
});
|
|
1552
|
-
var SessionMetricsSchema = z4.
|
|
1619
|
+
var SessionMetricsSchema = z4.looseObject({
|
|
1553
1620
|
output_tokens: z4.number().int().nonnegative().optional(),
|
|
1554
1621
|
input_tokens: z4.number().int().nonnegative().optional(),
|
|
1555
1622
|
cached_input_tokens: z4.number().int().nonnegative().optional(),
|
|
1556
1623
|
reasoning_output_tokens: z4.number().int().nonnegative().optional(),
|
|
1557
1624
|
active_time_ms: z4.number().int().nonnegative().optional(),
|
|
1558
|
-
active_intervals: z4.array(z4.
|
|
1625
|
+
active_intervals: z4.array(z4.looseObject({ start: IsoTimestampSchema, end: IsoTimestampSchema })).optional(),
|
|
1559
1626
|
active_gap_cap_ms: z4.number().int().nonnegative().optional(),
|
|
1560
1627
|
active_time_method: z4.string().optional(),
|
|
1561
1628
|
machine_active_time_ms: z4.number().int().nonnegative().optional()
|
|
@@ -1564,7 +1631,7 @@ var SessionIntegritySchema = z4.object({
|
|
|
1564
1631
|
head_hash: z4.string(),
|
|
1565
1632
|
event_count: z4.number().int().nonnegative()
|
|
1566
1633
|
}).strict();
|
|
1567
|
-
var SessionInnerSchema = z4.
|
|
1634
|
+
var SessionInnerSchema = z4.looseObject({
|
|
1568
1635
|
id: SessionIdSchema,
|
|
1569
1636
|
label: z4.string().optional(),
|
|
1570
1637
|
task_id: TaskIdSchema.nullable().optional(),
|
|
@@ -1582,7 +1649,7 @@ var SessionInnerSchema = z4.object({
|
|
|
1582
1649
|
metrics: SessionMetricsSchema.optional(),
|
|
1583
1650
|
integrity: SessionIntegritySchema.optional()
|
|
1584
1651
|
});
|
|
1585
|
-
var SessionSchema = z4.
|
|
1652
|
+
var SessionSchema = z4.looseObject({
|
|
1586
1653
|
schema_version: SchemaVersionSchema,
|
|
1587
1654
|
session: SessionInnerSchema
|
|
1588
1655
|
});
|
|
@@ -2082,12 +2149,16 @@ import * as fsp from "fs/promises";
|
|
|
2082
2149
|
// src/schemas/status.schema.ts
|
|
2083
2150
|
import { z as z5 } from "zod";
|
|
2084
2151
|
var StatusSchema = z5.object({
|
|
2085
|
-
|
|
2152
|
+
// status.json is a rebuildable cache: exact-match-or-rebuild, not the
|
|
2153
|
+
// durable forward-compat gate.
|
|
2154
|
+
schema_version: CacheVersionSchema,
|
|
2086
2155
|
generated_at: IsoTimestampSchema,
|
|
2087
2156
|
workspace: z5.object({
|
|
2088
2157
|
id: WorkspaceIdSchema,
|
|
2089
2158
|
name: z5.string().min(1),
|
|
2090
|
-
basou_version
|
|
2159
|
+
// Mirrors the manifest's basou_version, so it uses the same
|
|
2160
|
+
// forward-compatible format gate (accept 0.x.y) rather than a literal.
|
|
2161
|
+
basou_version: SchemaVersionSchema
|
|
2091
2162
|
}).strict(),
|
|
2092
2163
|
directories_present: z5.object({
|
|
2093
2164
|
sessions: z5.boolean(),
|
|
@@ -2447,7 +2518,7 @@ import { z as z8 } from "zod";
|
|
|
2447
2518
|
// src/schemas/task.schema.ts
|
|
2448
2519
|
import { z as z6 } from "zod";
|
|
2449
2520
|
var TaskStatusSchema = z6.enum(["planned", "in_progress", "done", "cancelled"]);
|
|
2450
|
-
var TaskInnerSchema = z6.
|
|
2521
|
+
var TaskInnerSchema = z6.looseObject({
|
|
2451
2522
|
id: TaskIdSchema,
|
|
2452
2523
|
title: z6.string().min(1),
|
|
2453
2524
|
label: z6.string().min(1).optional(),
|
|
@@ -2476,7 +2547,7 @@ var TaskInnerSchema = z6.object({
|
|
|
2476
2547
|
*/
|
|
2477
2548
|
linked_sessions: z6.array(SessionIdSchema).default([])
|
|
2478
2549
|
});
|
|
2479
|
-
var TaskSchema = z6.
|
|
2550
|
+
var TaskSchema = z6.looseObject({
|
|
2480
2551
|
schema_version: SchemaVersionSchema,
|
|
2481
2552
|
task: TaskInnerSchema
|
|
2482
2553
|
});
|
|
@@ -2726,7 +2797,8 @@ var TaskIndexEntrySchema = z7.object({
|
|
|
2726
2797
|
updated_at: IsoTimestampSchema
|
|
2727
2798
|
}).strict();
|
|
2728
2799
|
var TaskIndexSchema = z7.object({
|
|
2729
|
-
|
|
2800
|
+
// Rebuildable cache: exact-match-or-rebuild, not the durable forward-compat gate.
|
|
2801
|
+
schema_version: CacheVersionSchema,
|
|
2730
2802
|
tasks: z7.array(TaskIndexEntrySchema),
|
|
2731
2803
|
last_rebuilt_at: IsoTimestampSchema
|
|
2732
2804
|
}).strict();
|
|
@@ -4664,8 +4736,7 @@ import { lstat as lstat3 } from "fs/promises";
|
|
|
4664
4736
|
import { z as z9 } from "zod";
|
|
4665
4737
|
var ProjectSchema = z9.looseObject({
|
|
4666
4738
|
name: z9.string().optional(),
|
|
4667
|
-
description: z9.string().optional()
|
|
4668
|
-
repository_url: z9.string().nullable().optional()
|
|
4739
|
+
description: z9.string().optional()
|
|
4669
4740
|
});
|
|
4670
4741
|
var CapabilitiesSchema = z9.looseObject({
|
|
4671
4742
|
enabled: z9.array(z9.string())
|
|
@@ -4723,7 +4794,11 @@ var WorkspaceMetaSchema = z9.looseObject({
|
|
|
4723
4794
|
});
|
|
4724
4795
|
var ManifestSchema = z9.looseObject({
|
|
4725
4796
|
schema_version: SchemaVersionSchema,
|
|
4726
|
-
|
|
4797
|
+
// Same forward-compatible format gate as schema_version (accept 0.x.y, gate a
|
|
4798
|
+
// higher major with an upgrade error) rather than a hard literal. `basou_version`
|
|
4799
|
+
// is a format stamp, not the npm/product version; consolidating it with
|
|
4800
|
+
// schema_version is a candidate cleanup for the M4 freeze pass.
|
|
4801
|
+
basou_version: SchemaVersionSchema,
|
|
4727
4802
|
workspace: WorkspaceMetaSchema,
|
|
4728
4803
|
project: ProjectSchema,
|
|
4729
4804
|
capabilities: CapabilitiesSchema,
|
|
@@ -4747,8 +4822,7 @@ function createManifest(input) {
|
|
|
4747
4822
|
const workspaceId = input.workspaceId ?? prefixedUlid("ws");
|
|
4748
4823
|
const project = {
|
|
4749
4824
|
...input.projectName !== void 0 ? { name: input.projectName } : {},
|
|
4750
|
-
...input.projectDescription !== void 0 ? { description: input.projectDescription } : {}
|
|
4751
|
-
...input.repositoryUrl !== void 0 ? { repository_url: input.repositoryUrl } : {}
|
|
4825
|
+
...input.projectDescription !== void 0 ? { description: input.projectDescription } : {}
|
|
4752
4826
|
};
|
|
4753
4827
|
const manifest = {
|
|
4754
4828
|
schema_version: "0.1.0",
|
|
@@ -4778,6 +4852,7 @@ function createManifest(input) {
|
|
|
4778
4852
|
async function writeManifest(paths, manifest, options) {
|
|
4779
4853
|
const force = options?.force === true;
|
|
4780
4854
|
const validated = ManifestSchema.parse(manifest);
|
|
4855
|
+
delete validated.project.repository_url;
|
|
4781
4856
|
if (!force) {
|
|
4782
4857
|
let existed = false;
|
|
4783
4858
|
try {
|
|
@@ -6867,6 +6942,159 @@ async function findReviewGaps(input) {
|
|
|
6867
6942
|
};
|
|
6868
6943
|
}
|
|
6869
6944
|
|
|
6945
|
+
// src/review/review-record.ts
|
|
6946
|
+
var VALID_VERDICTS = /* @__PURE__ */ new Set(["pass", "needs-attention", "fail"]);
|
|
6947
|
+
var VALID_SEVERITIES = /* @__PURE__ */ new Set(["high", "medium", "low"]);
|
|
6948
|
+
var VALID_BLOCK_REASONS = /* @__PURE__ */ new Set(["spec-deviation", "design-reversal"]);
|
|
6949
|
+
var ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
6950
|
+
"reviewer",
|
|
6951
|
+
"target",
|
|
6952
|
+
"verdict",
|
|
6953
|
+
"findings",
|
|
6954
|
+
"blocked"
|
|
6955
|
+
]);
|
|
6956
|
+
var ALLOWED_FINDING_KEYS = /* @__PURE__ */ new Set([
|
|
6957
|
+
"title",
|
|
6958
|
+
"severity",
|
|
6959
|
+
"location",
|
|
6960
|
+
"summary"
|
|
6961
|
+
]);
|
|
6962
|
+
var ALLOWED_BLOCKED_KEYS = /* @__PURE__ */ new Set(["title", "reason", "why"]);
|
|
6963
|
+
var REVIEW_RECORD_NO_INPUT_HINT = "No input: pipe a JSON object describing the review to stdin or pass --file <path>.";
|
|
6964
|
+
function parseReviewRecordInput(raw) {
|
|
6965
|
+
if (raw.trim().length === 0) {
|
|
6966
|
+
throw new Error(REVIEW_RECORD_NO_INPUT_HINT);
|
|
6967
|
+
}
|
|
6968
|
+
let parsed;
|
|
6969
|
+
try {
|
|
6970
|
+
parsed = JSON.parse(raw);
|
|
6971
|
+
} catch (error) {
|
|
6972
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
6973
|
+
throw new Error(`Input is not valid JSON: ${detail}`);
|
|
6974
|
+
}
|
|
6975
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
6976
|
+
throw new Error("Input must be a single JSON object describing one review.");
|
|
6977
|
+
}
|
|
6978
|
+
const obj = parsed;
|
|
6979
|
+
for (const key of Object.keys(obj)) {
|
|
6980
|
+
if (!ALLOWED_KEYS.has(key)) {
|
|
6981
|
+
throw new Error(
|
|
6982
|
+
`Unknown field '${key}'. Allowed: reviewer, target, verdict, findings, blocked.`
|
|
6983
|
+
);
|
|
6984
|
+
}
|
|
6985
|
+
}
|
|
6986
|
+
const reviewer = requireNonEmptyString(obj.reviewer, "reviewer");
|
|
6987
|
+
const target = requireNonEmptyString(obj.target, "target");
|
|
6988
|
+
const out = { reviewer, target };
|
|
6989
|
+
if (obj.verdict !== void 0) {
|
|
6990
|
+
if (typeof obj.verdict !== "string" || !VALID_VERDICTS.has(obj.verdict)) {
|
|
6991
|
+
throw new Error(`verdict must be one of pass, needs-attention, fail, got '${obj.verdict}'.`);
|
|
6992
|
+
}
|
|
6993
|
+
out.verdict = obj.verdict;
|
|
6994
|
+
}
|
|
6995
|
+
if (obj.findings !== void 0) {
|
|
6996
|
+
out.findings = parseFindings(obj.findings);
|
|
6997
|
+
}
|
|
6998
|
+
if (obj.blocked !== void 0) {
|
|
6999
|
+
out.blocked = parseBlocked(obj.blocked);
|
|
7000
|
+
}
|
|
7001
|
+
return out;
|
|
7002
|
+
}
|
|
7003
|
+
function parseFindings(value) {
|
|
7004
|
+
if (!Array.isArray(value)) {
|
|
7005
|
+
throw new Error("findings must be an array of objects.");
|
|
7006
|
+
}
|
|
7007
|
+
return value.map((item, i) => {
|
|
7008
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
7009
|
+
throw new Error(`findings[${i}] must be a JSON object.`);
|
|
7010
|
+
}
|
|
7011
|
+
const obj = item;
|
|
7012
|
+
for (const key of Object.keys(obj)) {
|
|
7013
|
+
if (!ALLOWED_FINDING_KEYS.has(key)) {
|
|
7014
|
+
throw new Error(
|
|
7015
|
+
`findings[${i}]: unknown field '${key}'. Allowed: title, severity, location, summary.`
|
|
7016
|
+
);
|
|
7017
|
+
}
|
|
7018
|
+
}
|
|
7019
|
+
const finding = {
|
|
7020
|
+
title: requireNonEmptyString(obj.title, `findings[${i}].title`)
|
|
7021
|
+
};
|
|
7022
|
+
if (obj.severity !== void 0) {
|
|
7023
|
+
if (typeof obj.severity !== "string" || !VALID_SEVERITIES.has(obj.severity)) {
|
|
7024
|
+
throw new Error(
|
|
7025
|
+
`findings[${i}].severity must be one of high, medium, low, got '${obj.severity}'.`
|
|
7026
|
+
);
|
|
7027
|
+
}
|
|
7028
|
+
finding.severity = obj.severity;
|
|
7029
|
+
}
|
|
7030
|
+
if (obj.location !== void 0) {
|
|
7031
|
+
finding.location = requireNonEmptyString(obj.location, `findings[${i}].location`);
|
|
7032
|
+
}
|
|
7033
|
+
if (obj.summary !== void 0) {
|
|
7034
|
+
finding.summary = requireNonEmptyString(obj.summary, `findings[${i}].summary`);
|
|
7035
|
+
}
|
|
7036
|
+
return finding;
|
|
7037
|
+
});
|
|
7038
|
+
}
|
|
7039
|
+
function parseBlocked(value) {
|
|
7040
|
+
if (!Array.isArray(value)) {
|
|
7041
|
+
throw new Error("blocked must be an array of objects.");
|
|
7042
|
+
}
|
|
7043
|
+
return value.map((item, i) => {
|
|
7044
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
7045
|
+
throw new Error(`blocked[${i}] must be a JSON object.`);
|
|
7046
|
+
}
|
|
7047
|
+
const obj = item;
|
|
7048
|
+
for (const key of Object.keys(obj)) {
|
|
7049
|
+
if (!ALLOWED_BLOCKED_KEYS.has(key)) {
|
|
7050
|
+
throw new Error(`blocked[${i}]: unknown field '${key}'. Allowed: title, reason, why.`);
|
|
7051
|
+
}
|
|
7052
|
+
}
|
|
7053
|
+
if (typeof obj.reason !== "string" || !VALID_BLOCK_REASONS.has(obj.reason)) {
|
|
7054
|
+
throw new Error(
|
|
7055
|
+
`blocked[${i}].reason must be one of spec-deviation, design-reversal, got '${obj.reason}'.`
|
|
7056
|
+
);
|
|
7057
|
+
}
|
|
7058
|
+
const blocked = {
|
|
7059
|
+
title: requireNonEmptyString(obj.title, `blocked[${i}].title`),
|
|
7060
|
+
reason: obj.reason
|
|
7061
|
+
};
|
|
7062
|
+
if (obj.why !== void 0) {
|
|
7063
|
+
blocked.why = requireNonEmptyString(obj.why, `blocked[${i}].why`);
|
|
7064
|
+
}
|
|
7065
|
+
return blocked;
|
|
7066
|
+
});
|
|
7067
|
+
}
|
|
7068
|
+
function requireNonEmptyString(value, field) {
|
|
7069
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
7070
|
+
throw new Error(`${field} must be a non-empty string.`);
|
|
7071
|
+
}
|
|
7072
|
+
return value;
|
|
7073
|
+
}
|
|
7074
|
+
function buildReviewRecordedEvent(input) {
|
|
7075
|
+
const { review } = input;
|
|
7076
|
+
return {
|
|
7077
|
+
schema_version: "0.1.0",
|
|
7078
|
+
id: input.eventId,
|
|
7079
|
+
session_id: input.sessionId,
|
|
7080
|
+
occurred_at: input.occurredAt,
|
|
7081
|
+
source: "local-cli",
|
|
7082
|
+
type: "review_recorded",
|
|
7083
|
+
reviewer: review.reviewer,
|
|
7084
|
+
target: review.target,
|
|
7085
|
+
...review.verdict !== void 0 ? { verdict: review.verdict } : {},
|
|
7086
|
+
...review.findings !== void 0 ? { findings: review.findings } : {},
|
|
7087
|
+
...review.blocked !== void 0 ? { blocked: review.blocked } : {}
|
|
7088
|
+
};
|
|
7089
|
+
}
|
|
7090
|
+
var LABEL_FRAGMENT_MAX = 40;
|
|
7091
|
+
function buildReviewRecordLabel(review) {
|
|
7092
|
+
return `Ad-hoc review: ${truncate(review.reviewer)} -> ${truncate(review.target)}`;
|
|
7093
|
+
}
|
|
7094
|
+
function truncate(value) {
|
|
7095
|
+
return value.length > LABEL_FRAGMENT_MAX ? `${value.slice(0, LABEL_FRAGMENT_MAX - 3)}...` : value;
|
|
7096
|
+
}
|
|
7097
|
+
|
|
6870
7098
|
// src/runtime/child-process-runner.ts
|
|
6871
7099
|
import { spawn as spawn2 } from "child_process";
|
|
6872
7100
|
var DEFAULT_KILL_GRACE_MS = 5e3;
|
|
@@ -7249,6 +7477,8 @@ var GENERATED_START = "<!-- BASOU:GENERATED:START -->";
|
|
|
7249
7477
|
var GENERATED_END = "<!-- BASOU:GENERATED:END -->";
|
|
7250
7478
|
var PROTOCOL_START = "<!-- BASOU:PROTOCOLS:START -->";
|
|
7251
7479
|
var PROTOCOL_END = "<!-- BASOU:PROTOCOLS:END -->";
|
|
7480
|
+
var ORIENTATION_START = "<!-- BASOU:ORIENTATION:START -->";
|
|
7481
|
+
var ORIENTATION_END = "<!-- BASOU:ORIENTATION:END -->";
|
|
7252
7482
|
var DEFAULT_MARKERS = { start: GENERATED_START, end: GENERATED_END };
|
|
7253
7483
|
async function readMarkdownFile(filePath) {
|
|
7254
7484
|
try {
|
|
@@ -7773,8 +8003,11 @@ export {
|
|
|
7773
8003
|
IsoTimestampSchema,
|
|
7774
8004
|
JSON_SCHEMA_VERSION,
|
|
7775
8005
|
ManifestSchema,
|
|
8006
|
+
ORIENTATION_END,
|
|
8007
|
+
ORIENTATION_START,
|
|
7776
8008
|
PROTOCOL_END,
|
|
7777
8009
|
PROTOCOL_START,
|
|
8010
|
+
REVIEW_RECORD_NO_INPUT_HINT,
|
|
7778
8011
|
RiskLevelSchema,
|
|
7779
8012
|
STOP_HOOK_TIMEOUT_SECONDS,
|
|
7780
8013
|
STUCK_THRESHOLD_MS,
|
|
@@ -7803,6 +8036,8 @@ export {
|
|
|
7803
8036
|
assertBasouRootSafe,
|
|
7804
8037
|
basouPaths,
|
|
7805
8038
|
buildJsonSchemas,
|
|
8039
|
+
buildReviewRecordLabel,
|
|
8040
|
+
buildReviewRecordedEvent,
|
|
7806
8041
|
buildStatusSnapshot,
|
|
7807
8042
|
buildStopHookCommand,
|
|
7808
8043
|
chainEvents,
|
|
@@ -7812,6 +8047,7 @@ export {
|
|
|
7812
8047
|
classifySuspect,
|
|
7813
8048
|
claudeCodeAdapterMetadata,
|
|
7814
8049
|
claudeTranscriptToImportPayload,
|
|
8050
|
+
codexAdapterMetadata,
|
|
7815
8051
|
codexRolloutToImportPayload,
|
|
7816
8052
|
computeWorkStats,
|
|
7817
8053
|
createAdHocSessionWithEvent,
|
|
@@ -7853,6 +8089,7 @@ export {
|
|
|
7853
8089
|
overwriteYamlFile,
|
|
7854
8090
|
parseDuration,
|
|
7855
8091
|
parseMarkers,
|
|
8092
|
+
parseReviewRecordInput,
|
|
7856
8093
|
pathBasename,
|
|
7857
8094
|
planArchive,
|
|
7858
8095
|
planGitignore,
|
|
@@ -7885,6 +8122,7 @@ export {
|
|
|
7885
8122
|
replayEvents,
|
|
7886
8123
|
resolveBasouRepositoryRoot,
|
|
7887
8124
|
resolveClaudeCodeCommand,
|
|
8125
|
+
resolveCodexCommand,
|
|
7888
8126
|
resolveRepositoryRoot,
|
|
7889
8127
|
resolveSessionId,
|
|
7890
8128
|
resolveTaskId,
|