@basou/cli 0.44.0 → 0.46.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.js +768 -369
- package/dist/index.js.map +1 -1
- package/dist/program.d.ts +33 -1
- package/dist/program.js +770 -369
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/program.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/program.ts
|
|
2
2
|
import { createRequire } from "module";
|
|
3
|
+
import * as basouCore from "@basou/core";
|
|
3
4
|
import { Command } from "commander";
|
|
4
5
|
|
|
5
6
|
// src/commands/approval.ts
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
enumerateApprovals,
|
|
17
18
|
findErrorCode,
|
|
18
19
|
isLazyExpired,
|
|
20
|
+
LOCAL_CLI_EVENT_SOURCE,
|
|
19
21
|
linkYamlFile,
|
|
20
22
|
loadApproval,
|
|
21
23
|
prefixedUlid,
|
|
@@ -369,7 +371,7 @@ async function doRunApprovalResolve(idInput, options, ctx, decision) {
|
|
|
369
371
|
id: eventId,
|
|
370
372
|
session_id: approval.session_id,
|
|
371
373
|
occurred_at: occurredAt,
|
|
372
|
-
source:
|
|
374
|
+
source: LOCAL_CLI_EVENT_SOURCE,
|
|
373
375
|
type: "approval_approved",
|
|
374
376
|
approval_id: approval.id,
|
|
375
377
|
resolver: "local-cli",
|
|
@@ -382,7 +384,7 @@ async function doRunApprovalResolve(idInput, options, ctx, decision) {
|
|
|
382
384
|
id: eventId,
|
|
383
385
|
session_id: approval.session_id,
|
|
384
386
|
occurred_at: occurredAt,
|
|
385
|
-
source:
|
|
387
|
+
source: LOCAL_CLI_EVENT_SOURCE,
|
|
386
388
|
type: "approval_rejected",
|
|
387
389
|
approval_id: approval.id,
|
|
388
390
|
resolver: "local-cli",
|
|
@@ -861,12 +863,13 @@ import {
|
|
|
861
863
|
acquireLock as acquireLock2,
|
|
862
864
|
appendEventToExistingSession,
|
|
863
865
|
assertBasouRootSafe as assertBasouRootSafe2,
|
|
864
|
-
basouPaths as
|
|
866
|
+
basouPaths as basouPaths4,
|
|
865
867
|
classifyFilesBySourceRoot,
|
|
866
868
|
createAdHocSessionWithEvent,
|
|
867
869
|
EVENT_SCHEMA_VERSION as EVENT_SCHEMA_VERSION2,
|
|
868
870
|
findErrorCode as findErrorCode2,
|
|
869
871
|
isValidPrefixedId,
|
|
872
|
+
LOCAL_CLI_EVENT_SOURCE as LOCAL_CLI_EVENT_SOURCE2,
|
|
870
873
|
loadSessionEntries,
|
|
871
874
|
prefixedUlid as prefixedUlid2,
|
|
872
875
|
readManifest as readManifest2,
|
|
@@ -875,7 +878,7 @@ import {
|
|
|
875
878
|
resolveSessionId,
|
|
876
879
|
sanitizePath
|
|
877
880
|
} from "@basou/core";
|
|
878
|
-
import { InvalidArgumentError } from "commander";
|
|
881
|
+
import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
|
|
879
882
|
|
|
880
883
|
// src/lib/repo-root.ts
|
|
881
884
|
import { realpath, stat as stat2 } from "fs/promises";
|
|
@@ -1035,11 +1038,165 @@ async function realpathOrNull(p) {
|
|
|
1035
1038
|
}
|
|
1036
1039
|
}
|
|
1037
1040
|
|
|
1041
|
+
// src/commands/decision-gaps.ts
|
|
1042
|
+
import {
|
|
1043
|
+
basouPaths as basouPaths3,
|
|
1044
|
+
findDecisionGaps
|
|
1045
|
+
} from "@basou/core";
|
|
1046
|
+
import { InvalidArgumentError } from "commander";
|
|
1047
|
+
var DEFAULT_GAP_LIMIT = 20;
|
|
1048
|
+
function parseLimit(value) {
|
|
1049
|
+
const n = /^\d+$/u.test(value.trim()) ? Number(value.trim()) : Number.NaN;
|
|
1050
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
1051
|
+
throw new InvalidArgumentError("--limit must be a non-negative integer.");
|
|
1052
|
+
}
|
|
1053
|
+
return n;
|
|
1054
|
+
}
|
|
1055
|
+
function parseSince(value, now = /* @__PURE__ */ new Date()) {
|
|
1056
|
+
const duration = /^(\d+)([dhm])$/u.exec(value.trim());
|
|
1057
|
+
if (duration !== null) {
|
|
1058
|
+
const n = Number(duration[1]);
|
|
1059
|
+
const ms = { d: 864e5, h: 36e5, m: 6e4 }[duration[2]];
|
|
1060
|
+
return new Date(now.getTime() - n * ms).toISOString();
|
|
1061
|
+
}
|
|
1062
|
+
if (/^\d+$/u.test(value.trim())) {
|
|
1063
|
+
throw new InvalidArgumentError("--since needs a unit (7d, 36h, 90m) or a full ISO timestamp.");
|
|
1064
|
+
}
|
|
1065
|
+
const parsed = Date.parse(value);
|
|
1066
|
+
if (Number.isNaN(parsed)) {
|
|
1067
|
+
throw new InvalidArgumentError(
|
|
1068
|
+
"--since must be an ISO timestamp or a duration (7d, 36h, 90m)."
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
return new Date(parsed).toISOString();
|
|
1072
|
+
}
|
|
1073
|
+
function registerDecisionGapsCommand(decision) {
|
|
1074
|
+
decision.command("gaps").description("List recorded decisions that no task carries (read-only, advisory)").option(
|
|
1075
|
+
"--since <when>",
|
|
1076
|
+
"Only decisions recorded at or after this ISO instant or duration back (7d, 36h)"
|
|
1077
|
+
).option(
|
|
1078
|
+
"--limit <n>",
|
|
1079
|
+
`Maximum entries to list; the rest are counted (default ${DEFAULT_GAP_LIMIT}, 0 = no limit)`,
|
|
1080
|
+
parseLimit
|
|
1081
|
+
).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (opts) => {
|
|
1082
|
+
await runDecisionGaps(opts);
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
async function runDecisionGaps(options, ctx = {}) {
|
|
1086
|
+
try {
|
|
1087
|
+
await doRunDecisionGaps(options, ctx);
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
1090
|
+
process.exitCode = 1;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
async function doRunDecisionGaps(options, ctx) {
|
|
1094
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
1095
|
+
const repositoryRoot = await resolveBasouRootForCommand(cwd, "decision gaps");
|
|
1096
|
+
const paths = basouPaths3(repositoryRoot);
|
|
1097
|
+
const now = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
|
|
1098
|
+
const limit = options.limit ?? DEFAULT_GAP_LIMIT;
|
|
1099
|
+
const summary = await findDecisionGaps({
|
|
1100
|
+
paths,
|
|
1101
|
+
nowIso: now.toISOString(),
|
|
1102
|
+
...options.since !== void 0 ? { start: parseSince(options.since, now) } : {},
|
|
1103
|
+
...limit > 0 ? { limit } : {},
|
|
1104
|
+
onWarning: (w, sid) => printReplayWarning(w, sid),
|
|
1105
|
+
onSessionSkip: (sid, reason) => printSessionSkip(sid, reason)
|
|
1106
|
+
});
|
|
1107
|
+
if (options.json === true) {
|
|
1108
|
+
console.log(JSON.stringify(summary));
|
|
1109
|
+
} else {
|
|
1110
|
+
console.log(renderDecisionGaps(summary));
|
|
1111
|
+
}
|
|
1112
|
+
return summary;
|
|
1113
|
+
}
|
|
1114
|
+
function relAge(iso, now) {
|
|
1115
|
+
const ms = now.getTime() - Date.parse(iso);
|
|
1116
|
+
if (!Number.isFinite(ms)) return "(unknown)";
|
|
1117
|
+
if (ms < 0) return "clock ahead";
|
|
1118
|
+
const days = Math.floor(ms / 864e5);
|
|
1119
|
+
if (days >= 1) return `${days}d ago`;
|
|
1120
|
+
const hours = Math.floor(ms / 36e5);
|
|
1121
|
+
if (hours >= 1) return `${hours}h ago`;
|
|
1122
|
+
return `${Math.max(1, Math.floor(ms / 6e4))}m ago`;
|
|
1123
|
+
}
|
|
1124
|
+
var GRAPHEMES = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
1125
|
+
function oneLine(value, max) {
|
|
1126
|
+
const flat = [...GRAPHEMES.segment(value.replace(/\s+/gu, " ").trim())].map((s) => s.segment);
|
|
1127
|
+
return flat.length > max ? `${flat.slice(0, max - 1).join("")}\u2026` : flat.join("");
|
|
1128
|
+
}
|
|
1129
|
+
function gapLine(d, now) {
|
|
1130
|
+
return `- ${oneLine(d.title, 100)}
|
|
1131
|
+
${relAge(d.recordedAt, now)} \xB7 ${d.decisionId}`;
|
|
1132
|
+
}
|
|
1133
|
+
function incompleteLine(summary) {
|
|
1134
|
+
const { sessions, tasks, unknownReferences } = summary.incomplete;
|
|
1135
|
+
const parts = [];
|
|
1136
|
+
if (sessions > 0) {
|
|
1137
|
+
parts.push(
|
|
1138
|
+
`${sessions} session${sessions === 1 ? "" : "s"} could not be read in full, so a decision recorded there may be missing from these numbers, and a \`decision void\` recorded there may not have been applied`
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
if (tasks > 0) {
|
|
1142
|
+
parts.push(
|
|
1143
|
+
`${tasks} task file${tasks === 1 ? "" : "s"} could not be read, so a decision one of them carries is listed above as if nothing did`
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
if (unknownReferences > 0) {
|
|
1147
|
+
parts.push(
|
|
1148
|
+
`${unknownReferences} decision id${unknownReferences === 1 ? "" : "s"} named by a task ${unknownReferences === 1 ? "matches" : "match"} no decision in this store, and ${unknownReferences === 1 ? "was" : "were"} not counted as carrying anything`
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
return parts.length === 0 ? null : `\u26A0\uFE0F ${parts.join("; ")}.`;
|
|
1152
|
+
}
|
|
1153
|
+
function renderDecisionGaps(summary) {
|
|
1154
|
+
const now = new Date(summary.generatedAt);
|
|
1155
|
+
const { excluded, scope } = summary;
|
|
1156
|
+
const lines = ["# Decision gaps", ""];
|
|
1157
|
+
if (summary.populationCount === 0) {
|
|
1158
|
+
lines.push(
|
|
1159
|
+
`No decision recorded since ${scope.start} by \`basou decision capture\` or \`basou decision record\` is still open, so there is nothing to check yet. This list fills as decisions are recorded.`
|
|
1160
|
+
);
|
|
1161
|
+
} else if (summary.gaps.length === 0) {
|
|
1162
|
+
lines.push(
|
|
1163
|
+
`\u2705 Within what was checked, each of the ${summary.populationCount} open decision${summary.populationCount === 1 ? "" : "s"} in scope has a task carrying it.`
|
|
1164
|
+
);
|
|
1165
|
+
} else {
|
|
1166
|
+
const total = summary.gaps.length + summary.truncated;
|
|
1167
|
+
lines.push(`\u26A0\uFE0F Open decisions no task carries: ${total} of ${summary.populationCount} in scope`);
|
|
1168
|
+
lines.push("");
|
|
1169
|
+
for (const d of summary.gaps) lines.push(gapLine(d, now));
|
|
1170
|
+
if (summary.truncated > 0) {
|
|
1171
|
+
lines.push(` ... +${summary.truncated} more (--limit 0 to list them all, or --json)`);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
lines.push("");
|
|
1175
|
+
lines.push("## Scope");
|
|
1176
|
+
lines.push(
|
|
1177
|
+
`- Checked: decisions recorded at or after ${scope.start} with source \`${scope.source}\` (i.e. recorded by running basou, not derived from a transcript by an importer), still open, and not a track.`
|
|
1178
|
+
);
|
|
1179
|
+
lines.push(
|
|
1180
|
+
`- Not checked: ${excluded.byStart} recorded earlier, then ${excluded.bySource} recorded by something other than basou itself, then ${excluded.track} tracks (already shown every session by \`basou orient\` until closed), then ${excluded.voided} closed with \`basou decision void\`.`
|
|
1181
|
+
);
|
|
1182
|
+
lines.push(
|
|
1183
|
+
`- Task files read: ${summary.tasksScanned} (live and archived); ${summary.carried} of the decisions in scope are carried by one.`
|
|
1184
|
+
);
|
|
1185
|
+
const incomplete = incompleteLine(summary);
|
|
1186
|
+
if (incomplete !== null) lines.push(`- ${incomplete}`);
|
|
1187
|
+
lines.push("");
|
|
1188
|
+
lines.push(
|
|
1189
|
+
"Note: read-only advisory. A task carries a decision when it names that decision's full id; an abbreviated id names no one decision, because a captured batch shares a millisecond and its ids differ only at the end. It does not read what a decision means, and it does not enforce."
|
|
1190
|
+
);
|
|
1191
|
+
return lines.join("\n");
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1038
1194
|
// src/commands/decision.ts
|
|
1039
1195
|
var LABEL_TITLE_MAX = 80;
|
|
1040
1196
|
var LABEL_TRUNCATE_HEAD = LABEL_TITLE_MAX - 3;
|
|
1041
1197
|
function registerDecisionCommand(program) {
|
|
1042
1198
|
const decision = program.command("decision").description("Record human-authored decisions as events");
|
|
1199
|
+
registerDecisionGapsCommand(decision);
|
|
1043
1200
|
decision.command("record").description("Record a decision_recorded event").requiredOption("--title <text>", "Decision title", parseTitle).option("--rationale <text>", "Rationale for the decision", parseRationale).option(
|
|
1044
1201
|
"--rejected-reason <text>",
|
|
1045
1202
|
"Reason rejected alternatives were not chosen",
|
|
@@ -1161,7 +1318,7 @@ function warnTrackMarkerWithoutKind(decisions, markerWithoutKind) {
|
|
|
1161
1318
|
async function doRunDecisionRecord(options, ctx) {
|
|
1162
1319
|
const cwd = ctx.cwd ?? process.cwd();
|
|
1163
1320
|
const repositoryRoot = await resolveRepositoryRootForDecision(cwd);
|
|
1164
|
-
const paths =
|
|
1321
|
+
const paths = basouPaths4(repositoryRoot);
|
|
1165
1322
|
await assertWorkspaceInitialized2(paths.root);
|
|
1166
1323
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
1167
1324
|
const occurredAt = now.toISOString();
|
|
@@ -1252,7 +1409,7 @@ async function runDecisionCapture(options, ctx = {}) {
|
|
|
1252
1409
|
async function doRunDecisionCapture(options, ctx) {
|
|
1253
1410
|
const cwd = ctx.cwd ?? process.cwd();
|
|
1254
1411
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "decision capture");
|
|
1255
|
-
const paths =
|
|
1412
|
+
const paths = basouPaths4(repositoryRoot);
|
|
1256
1413
|
await assertWorkspaceInitialized2(paths.root);
|
|
1257
1414
|
const raw = await readCaptureInput(options, ctx);
|
|
1258
1415
|
const { decisions, markerWithoutKind } = parseCaptureInput(raw);
|
|
@@ -1334,7 +1491,7 @@ async function doRunDecisionVoid(decisionId, options, ctx) {
|
|
|
1334
1491
|
}
|
|
1335
1492
|
const cwd = ctx.cwd ?? process.cwd();
|
|
1336
1493
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "decision void");
|
|
1337
|
-
const paths =
|
|
1494
|
+
const paths = basouPaths4(repositoryRoot);
|
|
1338
1495
|
await assertWorkspaceInitialized2(paths.root);
|
|
1339
1496
|
if (!await decisionExists(paths, decisionId)) {
|
|
1340
1497
|
throw new Error(
|
|
@@ -1428,7 +1585,7 @@ function buildDecisionVoidedEvent(input) {
|
|
|
1428
1585
|
id: input.eventId,
|
|
1429
1586
|
session_id: input.sessionId,
|
|
1430
1587
|
occurred_at: input.occurredAt,
|
|
1431
|
-
source:
|
|
1588
|
+
source: LOCAL_CLI_EVENT_SOURCE2,
|
|
1432
1589
|
type: "decision_voided",
|
|
1433
1590
|
decision_id: input.decisionId,
|
|
1434
1591
|
...input.reason !== void 0 ? { reason: input.reason } : {},
|
|
@@ -1460,7 +1617,7 @@ function printVoidResult(options, result) {
|
|
|
1460
1617
|
}
|
|
1461
1618
|
function parseReason(raw) {
|
|
1462
1619
|
if (raw.trim().length === 0) {
|
|
1463
|
-
throw new
|
|
1620
|
+
throw new InvalidArgumentError2("--reason must not be empty");
|
|
1464
1621
|
}
|
|
1465
1622
|
return raw;
|
|
1466
1623
|
}
|
|
@@ -1694,7 +1851,7 @@ function buildDecisionEvent(input) {
|
|
|
1694
1851
|
id: input.eventId,
|
|
1695
1852
|
session_id: input.sessionId,
|
|
1696
1853
|
occurred_at: input.occurredAt,
|
|
1697
|
-
source:
|
|
1854
|
+
source: LOCAL_CLI_EVENT_SOURCE2,
|
|
1698
1855
|
type: "decision_recorded",
|
|
1699
1856
|
decision_id: input.decisionId,
|
|
1700
1857
|
title: input.title,
|
|
@@ -1712,25 +1869,25 @@ function buildAdHocLabel(title) {
|
|
|
1712
1869
|
}
|
|
1713
1870
|
function parseTitle(raw) {
|
|
1714
1871
|
if (isBlank(raw)) {
|
|
1715
|
-
throw new
|
|
1872
|
+
throw new InvalidArgumentError2("Title must not be empty");
|
|
1716
1873
|
}
|
|
1717
1874
|
return raw;
|
|
1718
1875
|
}
|
|
1719
1876
|
function parseRationale(raw) {
|
|
1720
1877
|
if (isBlank(raw)) {
|
|
1721
|
-
throw new
|
|
1878
|
+
throw new InvalidArgumentError2("Rationale must not be empty");
|
|
1722
1879
|
}
|
|
1723
1880
|
return raw;
|
|
1724
1881
|
}
|
|
1725
1882
|
function parseRejectedReason(raw) {
|
|
1726
1883
|
if (isBlank(raw)) {
|
|
1727
|
-
throw new
|
|
1884
|
+
throw new InvalidArgumentError2("Rejected reason must not be empty");
|
|
1728
1885
|
}
|
|
1729
1886
|
return raw;
|
|
1730
1887
|
}
|
|
1731
1888
|
function collectAlternative(value, prev) {
|
|
1732
1889
|
if (isBlank(value)) {
|
|
1733
|
-
throw new
|
|
1890
|
+
throw new InvalidArgumentError2("Alternative must not be empty");
|
|
1734
1891
|
}
|
|
1735
1892
|
return prev.concat(value);
|
|
1736
1893
|
}
|
|
@@ -1739,16 +1896,16 @@ function isValidEventId(value) {
|
|
|
1739
1896
|
}
|
|
1740
1897
|
function collectLinkedEvent(value, prev) {
|
|
1741
1898
|
if (!isValidEventId(value)) {
|
|
1742
|
-
throw new
|
|
1899
|
+
throw new InvalidArgumentError2(`Linked event id must match evt_<ULID>, got '${value}'`);
|
|
1743
1900
|
}
|
|
1744
1901
|
return prev.concat(value);
|
|
1745
1902
|
}
|
|
1746
1903
|
function collectLinkedFile(value, prev) {
|
|
1747
1904
|
if (isBlank(value)) {
|
|
1748
|
-
throw new
|
|
1905
|
+
throw new InvalidArgumentError2("Linked file path must not be empty");
|
|
1749
1906
|
}
|
|
1750
1907
|
if (value.length > 4096) {
|
|
1751
|
-
throw new
|
|
1908
|
+
throw new InvalidArgumentError2("Linked file path exceeds 4096 chars");
|
|
1752
1909
|
}
|
|
1753
1910
|
return prev.concat(value);
|
|
1754
1911
|
}
|
|
@@ -1813,7 +1970,7 @@ async function assertWorkspaceInitialized2(basouRoot) {
|
|
|
1813
1970
|
// src/commands/decisions.ts
|
|
1814
1971
|
import {
|
|
1815
1972
|
assertBasouRootSafe as assertBasouRootSafe3,
|
|
1816
|
-
basouPaths as
|
|
1973
|
+
basouPaths as basouPaths5,
|
|
1817
1974
|
findErrorCode as findErrorCode3,
|
|
1818
1975
|
readMarkdownFile as readMarkdownFile2,
|
|
1819
1976
|
renderDecisions,
|
|
@@ -1839,7 +1996,7 @@ async function doRunDecisionsGenerate(options, ctx) {
|
|
|
1839
1996
|
void options;
|
|
1840
1997
|
const cwd = ctx.cwd ?? process.cwd();
|
|
1841
1998
|
const repositoryRoot = await resolveRepositoryRootForDecisions(cwd);
|
|
1842
|
-
const paths =
|
|
1999
|
+
const paths = basouPaths5(repositoryRoot);
|
|
1843
2000
|
await assertWorkspaceInitialized3(paths.root);
|
|
1844
2001
|
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1845
2002
|
const result = await renderDecisions({
|
|
@@ -1884,7 +2041,7 @@ import { join as join6 } from "path";
|
|
|
1884
2041
|
import {
|
|
1885
2042
|
acquireLock as acquireLock3,
|
|
1886
2043
|
assertBasouRootSafe as assertBasouRootSafe4,
|
|
1887
|
-
basouPaths as
|
|
2044
|
+
basouPaths as basouPaths6,
|
|
1888
2045
|
ChildProcessRunner,
|
|
1889
2046
|
appendChainedEvent as coreAppendChainedEvent,
|
|
1890
2047
|
EVENT_SCHEMA_VERSION as EVENT_SCHEMA_VERSION3,
|
|
@@ -1896,6 +2053,7 @@ import {
|
|
|
1896
2053
|
readManifest as readManifest3,
|
|
1897
2054
|
readYamlFile as readYamlFile3,
|
|
1898
2055
|
resolveRepositoryRoot as resolveRepositoryRoot4,
|
|
2056
|
+
SESSION_SCHEMA_VERSION,
|
|
1899
2057
|
SessionSchema,
|
|
1900
2058
|
sanitizeWorkingDirectory,
|
|
1901
2059
|
writeObservedDuration,
|
|
@@ -1918,7 +2076,7 @@ async function runExec(command, args, options, ctx = {}) {
|
|
|
1918
2076
|
const cwd = options.cwd ?? process.cwd();
|
|
1919
2077
|
const timeout_ms = options.timeout !== void 0 ? parseDuration(options.timeout) : void 0;
|
|
1920
2078
|
const repoRoot = await resolveRepositoryRootForExec(cwd);
|
|
1921
|
-
const paths =
|
|
2079
|
+
const paths = basouPaths6(repoRoot);
|
|
1922
2080
|
await assertBasouRootSafe4(paths.root);
|
|
1923
2081
|
const manifest = await readManifest3(paths);
|
|
1924
2082
|
const sessionId = prefixedUlid3("ses");
|
|
@@ -2131,7 +2289,7 @@ function normalizeGitSnapshotSkipMessage(error) {
|
|
|
2131
2289
|
function buildInitialSession(input) {
|
|
2132
2290
|
const cmdline = [input.command, ...input.args].join(" ");
|
|
2133
2291
|
return {
|
|
2134
|
-
schema_version:
|
|
2292
|
+
schema_version: SESSION_SCHEMA_VERSION,
|
|
2135
2293
|
session: {
|
|
2136
2294
|
id: input.id,
|
|
2137
2295
|
label: `basou exec ${cmdline} (${input.startedAt})`,
|
|
@@ -2216,12 +2374,11 @@ async function resolveRepositoryRootForExec(cwd) {
|
|
|
2216
2374
|
// src/commands/handoff.ts
|
|
2217
2375
|
import {
|
|
2218
2376
|
assertBasouRootSafe as assertBasouRootSafe5,
|
|
2219
|
-
basouPaths as
|
|
2377
|
+
basouPaths as basouPaths7,
|
|
2220
2378
|
findErrorCode as findErrorCode4,
|
|
2221
2379
|
readMarkdownFile as readMarkdownFile3,
|
|
2222
2380
|
renderHandoff,
|
|
2223
2381
|
renderWithMarkers as renderWithMarkers2,
|
|
2224
|
-
resolveRepositoryRoot as resolveRepositoryRoot5,
|
|
2225
2382
|
writeMarkdownFile as writeMarkdownFile2
|
|
2226
2383
|
} from "@basou/core";
|
|
2227
2384
|
function registerHandoffCommand(program) {
|
|
@@ -2242,7 +2399,7 @@ async function doRunHandoffGenerate(options, ctx) {
|
|
|
2242
2399
|
void options;
|
|
2243
2400
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2244
2401
|
const repositoryRoot = await resolveRepositoryRootForHandoff(cwd);
|
|
2245
|
-
const paths =
|
|
2402
|
+
const paths = basouPaths7(repositoryRoot);
|
|
2246
2403
|
await assertWorkspaceInitialized4(paths.root);
|
|
2247
2404
|
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2248
2405
|
const result = await renderHandoff({
|
|
@@ -2261,7 +2418,7 @@ async function doRunHandoffGenerate(options, ctx) {
|
|
|
2261
2418
|
}
|
|
2262
2419
|
async function resolveRepositoryRootForHandoff(cwd) {
|
|
2263
2420
|
try {
|
|
2264
|
-
return await
|
|
2421
|
+
return await resolveBasouRootForCommand(cwd, "handoff generate");
|
|
2265
2422
|
} catch (error) {
|
|
2266
2423
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
2267
2424
|
throw new Error(
|
|
@@ -2284,10 +2441,12 @@ async function assertWorkspaceInitialized4(basouRoot) {
|
|
|
2284
2441
|
}
|
|
2285
2442
|
|
|
2286
2443
|
// src/commands/hook.ts
|
|
2444
|
+
import { execFile } from "child_process";
|
|
2287
2445
|
import { open as open2, readFile as readFile3, realpath as realpath3, stat as stat4 } from "fs/promises";
|
|
2288
|
-
import { homedir as
|
|
2289
|
-
import { join as
|
|
2446
|
+
import { homedir as homedir8 } from "os";
|
|
2447
|
+
import { join as join10 } from "path";
|
|
2290
2448
|
import { fileURLToPath } from "url";
|
|
2449
|
+
import { promisify } from "util";
|
|
2291
2450
|
import {
|
|
2292
2451
|
buildSessionStartHookCommand,
|
|
2293
2452
|
buildStopHookCommand,
|
|
@@ -2295,12 +2454,20 @@ import {
|
|
|
2295
2454
|
evaluateStopHook,
|
|
2296
2455
|
findBasouSessionStartHook,
|
|
2297
2456
|
findBasouStopHookCommand,
|
|
2457
|
+
isProtocolUpdateDue,
|
|
2298
2458
|
ORIENTATION_END as ORIENTATION_END2,
|
|
2299
2459
|
ORIENTATION_START as ORIENTATION_START2,
|
|
2460
|
+
PROTOCOL_END,
|
|
2461
|
+
PROTOCOL_START,
|
|
2300
2462
|
parseMarkers as parseMarkers2,
|
|
2463
|
+
parseProtocolStamp,
|
|
2464
|
+
protocolSectionsFrom,
|
|
2465
|
+
protocolUpdateToken,
|
|
2301
2466
|
readMarkdownFile as readMarkdownFile6,
|
|
2302
2467
|
removeSessionStartHook,
|
|
2303
2468
|
removeStopHook,
|
|
2469
|
+
renderProtocolUpdate,
|
|
2470
|
+
transcriptStartedAt,
|
|
2304
2471
|
upsertSessionStartHook,
|
|
2305
2472
|
upsertStopHook
|
|
2306
2473
|
} from "@basou/core";
|
|
@@ -2555,32 +2722,109 @@ async function warnIfPositionNamesOtherWorkspaces(args) {
|
|
|
2555
2722
|
}
|
|
2556
2723
|
}
|
|
2557
2724
|
|
|
2725
|
+
// src/lib/protocols-config.ts
|
|
2726
|
+
import { homedir as homedir5 } from "os";
|
|
2727
|
+
import { isAbsolute as isAbsolute2, join as join7, resolve as resolve4 } from "path";
|
|
2728
|
+
import { readYamlFile as readYamlFile4 } from "@basou/core";
|
|
2729
|
+
var DEFAULT_PROTOCOLS_CONFIG_PATH = join7(homedir5(), ".basou", "protocols.yaml");
|
|
2730
|
+
var DEFAULT_TARGET_PATH = join7(homedir5(), ".claude", "CLAUDE.md");
|
|
2731
|
+
var ALLOWED_TOP_KEYS = /* @__PURE__ */ new Set(["version", "protocols"]);
|
|
2732
|
+
var ALLOWED_ENTRY_KEYS = /* @__PURE__ */ new Set(["source", "title"]);
|
|
2733
|
+
function expandTilde2(p) {
|
|
2734
|
+
if (p === "~") return homedir5();
|
|
2735
|
+
if (p.startsWith("~/")) return join7(homedir5(), p.slice(2));
|
|
2736
|
+
return p;
|
|
2737
|
+
}
|
|
2738
|
+
function isRecord2(value) {
|
|
2739
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2740
|
+
}
|
|
2741
|
+
async function loadProtocolsConfig(configPath = DEFAULT_PROTOCOLS_CONFIG_PATH) {
|
|
2742
|
+
let raw;
|
|
2743
|
+
try {
|
|
2744
|
+
raw = await readYamlFile4(configPath);
|
|
2745
|
+
} catch (error) {
|
|
2746
|
+
if (error instanceof Error && error.message === "YAML file not found") {
|
|
2747
|
+
throw new Error(
|
|
2748
|
+
"No protocols config at ~/.basou/protocols.yaml. Create one (a 'protocols:' list of source markdown paths) before running 'basou protocol sync'."
|
|
2749
|
+
);
|
|
2750
|
+
}
|
|
2751
|
+
if (error instanceof Error && error.message === "Failed to parse YAML content") {
|
|
2752
|
+
throw new Error("~/.basou/protocols.yaml is not valid YAML.");
|
|
2753
|
+
}
|
|
2754
|
+
throw error;
|
|
2755
|
+
}
|
|
2756
|
+
if (!isRecord2(raw) || !Array.isArray(raw.protocols)) {
|
|
2757
|
+
throw new Error("~/.basou/protocols.yaml must contain a 'protocols:' list.");
|
|
2758
|
+
}
|
|
2759
|
+
for (const key of Object.keys(raw)) {
|
|
2760
|
+
if (!ALLOWED_TOP_KEYS.has(key)) {
|
|
2761
|
+
throw new Error(
|
|
2762
|
+
`~/.basou/protocols.yaml has an unknown key '${key}' (allowed: version, protocols).`
|
|
2763
|
+
);
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2767
|
+
const result = [];
|
|
2768
|
+
for (const entry of raw.protocols) {
|
|
2769
|
+
if (!isRecord2(entry)) {
|
|
2770
|
+
throw new Error("Each protocol entry must be a mapping with a 'source' key.");
|
|
2771
|
+
}
|
|
2772
|
+
for (const key of Object.keys(entry)) {
|
|
2773
|
+
if (!ALLOWED_ENTRY_KEYS.has(key)) {
|
|
2774
|
+
throw new Error(`A protocol entry has an unknown key '${key}' (allowed: source, title).`);
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
if (typeof entry.source !== "string" || entry.source.trim().length === 0) {
|
|
2778
|
+
throw new Error("Each protocol entry needs a non-empty string 'source'.");
|
|
2779
|
+
}
|
|
2780
|
+
if (entry.title !== void 0 && (typeof entry.title !== "string" || entry.title.trim().length === 0)) {
|
|
2781
|
+
throw new Error("A protocol entry 'title' must be a non-empty string when present.");
|
|
2782
|
+
}
|
|
2783
|
+
const expanded = expandTilde2(entry.source.trim());
|
|
2784
|
+
if (!isAbsolute2(expanded)) {
|
|
2785
|
+
throw new Error("Protocol 'source' paths must be absolute (or start with '~').");
|
|
2786
|
+
}
|
|
2787
|
+
const abs = resolve4(expanded);
|
|
2788
|
+
if (seen.has(abs)) {
|
|
2789
|
+
throw new Error("Duplicate protocol source (each source path may appear only once).");
|
|
2790
|
+
}
|
|
2791
|
+
seen.add(abs);
|
|
2792
|
+
result.push(
|
|
2793
|
+
entry.title !== void 0 ? { source: abs, title: entry.title.trim() } : { source: abs }
|
|
2794
|
+
);
|
|
2795
|
+
}
|
|
2796
|
+
if (result.length === 0) {
|
|
2797
|
+
throw new Error("~/.basou/protocols.yaml has no protocols.");
|
|
2798
|
+
}
|
|
2799
|
+
return result;
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2558
2802
|
// src/commands/orient.ts
|
|
2559
2803
|
import {
|
|
2560
2804
|
assertBasouRootSafe as assertBasouRootSafe7,
|
|
2561
|
-
basouPaths as
|
|
2805
|
+
basouPaths as basouPaths9,
|
|
2562
2806
|
findErrorCode as findErrorCode6,
|
|
2563
2807
|
renderOrientation as renderOrientation2,
|
|
2564
2808
|
writeMarkdownFile as writeMarkdownFile4
|
|
2565
2809
|
} from "@basou/core";
|
|
2566
2810
|
|
|
2567
2811
|
// src/lib/hosts-config.ts
|
|
2568
|
-
import { homedir as
|
|
2569
|
-
import { isAbsolute as
|
|
2570
|
-
import { readYamlFile as
|
|
2571
|
-
var DEFAULT_HOSTS_CONFIG_PATH =
|
|
2572
|
-
function
|
|
2573
|
-
if (p === "~") return
|
|
2574
|
-
if (p.startsWith("~/")) return
|
|
2812
|
+
import { homedir as homedir6 } from "os";
|
|
2813
|
+
import { isAbsolute as isAbsolute3, join as join8, resolve as resolve5 } from "path";
|
|
2814
|
+
import { readYamlFile as readYamlFile5 } from "@basou/core";
|
|
2815
|
+
var DEFAULT_HOSTS_CONFIG_PATH = join8(homedir6(), ".basou", "hosts.yaml");
|
|
2816
|
+
function expandTilde3(p) {
|
|
2817
|
+
if (p === "~") return homedir6();
|
|
2818
|
+
if (p.startsWith("~/")) return join8(homedir6(), p.slice(2));
|
|
2575
2819
|
return p;
|
|
2576
2820
|
}
|
|
2577
|
-
function
|
|
2821
|
+
function isRecord3(value) {
|
|
2578
2822
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2579
2823
|
}
|
|
2580
2824
|
async function loadHostsConfig(configPath = DEFAULT_HOSTS_CONFIG_PATH) {
|
|
2581
2825
|
let raw;
|
|
2582
2826
|
try {
|
|
2583
|
-
raw = await
|
|
2827
|
+
raw = await readYamlFile5(configPath);
|
|
2584
2828
|
} catch (error) {
|
|
2585
2829
|
if (error instanceof Error && error.message === "YAML file not found") {
|
|
2586
2830
|
return null;
|
|
@@ -2590,25 +2834,25 @@ async function loadHostsConfig(configPath = DEFAULT_HOSTS_CONFIG_PATH) {
|
|
|
2590
2834
|
}
|
|
2591
2835
|
throw error;
|
|
2592
2836
|
}
|
|
2593
|
-
if (!
|
|
2837
|
+
if (!isRecord3(raw) || !Array.isArray(raw.hosts)) {
|
|
2594
2838
|
throw new Error("~/.basou/hosts.yaml must contain a 'hosts:' list.");
|
|
2595
2839
|
}
|
|
2596
2840
|
const seenPaths = /* @__PURE__ */ new Set();
|
|
2597
2841
|
const seenLabels = /* @__PURE__ */ new Set();
|
|
2598
2842
|
const result = [];
|
|
2599
2843
|
for (const entry of raw.hosts) {
|
|
2600
|
-
if (!
|
|
2844
|
+
if (!isRecord3(entry) || typeof entry.label !== "string" || entry.label.trim().length === 0) {
|
|
2601
2845
|
throw new Error("Each host needs a non-empty string 'label'.");
|
|
2602
2846
|
}
|
|
2603
2847
|
const label = entry.label.trim();
|
|
2604
2848
|
if (typeof entry.path !== "string" || entry.path.trim().length === 0) {
|
|
2605
2849
|
throw new Error("Each host needs a non-empty string 'path'.");
|
|
2606
2850
|
}
|
|
2607
|
-
const expanded =
|
|
2608
|
-
if (!
|
|
2851
|
+
const expanded = expandTilde3(entry.path.trim());
|
|
2852
|
+
if (!isAbsolute3(expanded)) {
|
|
2609
2853
|
throw new Error("Host paths must be absolute (or start with '~').");
|
|
2610
2854
|
}
|
|
2611
|
-
const abs =
|
|
2855
|
+
const abs = resolve5(expanded);
|
|
2612
2856
|
if (seenPaths.has(abs)) continue;
|
|
2613
2857
|
if (seenLabels.has(label)) {
|
|
2614
2858
|
throw new Error(`Duplicate host label '${label}'; each host needs a distinct label.`);
|
|
@@ -2633,13 +2877,13 @@ import {
|
|
|
2633
2877
|
// src/commands/import.ts
|
|
2634
2878
|
import { createReadStream } from "fs";
|
|
2635
2879
|
import { readdir, readFile as readFile2, rm, stat as stat3 } from "fs/promises";
|
|
2636
|
-
import { homedir as
|
|
2637
|
-
import { basename as basename4, dirname as dirname3, join as
|
|
2880
|
+
import { homedir as homedir7 } from "os";
|
|
2881
|
+
import { basename as basename4, dirname as dirname3, join as join9, resolve as resolve6 } from "path";
|
|
2638
2882
|
import { createInterface } from "readline";
|
|
2639
2883
|
import {
|
|
2640
2884
|
AGENT_INFRA_DIRS as AGENT_INFRA_DIRS2,
|
|
2641
2885
|
assertBasouRootSafe as assertBasouRootSafe6,
|
|
2642
|
-
basouPaths as
|
|
2886
|
+
basouPaths as basouPaths8,
|
|
2643
2887
|
CLAUDE_IMPORT_SOURCE,
|
|
2644
2888
|
CODEX_IMPORT_SOURCE,
|
|
2645
2889
|
classifyFilesBySourceRoot as classifyFilesBySourceRoot2,
|
|
@@ -2651,7 +2895,7 @@ import {
|
|
|
2651
2895
|
readManifest as readManifest4,
|
|
2652
2896
|
readSessionYaml as readSessionYaml2,
|
|
2653
2897
|
reimportPreservingId,
|
|
2654
|
-
resolveRepositoryRoot as
|
|
2898
|
+
resolveRepositoryRoot as resolveRepositoryRoot5,
|
|
2655
2899
|
SESSION_IMPORT_SCHEMA_VERSION,
|
|
2656
2900
|
SessionImportPayloadSchema
|
|
2657
2901
|
} from "@basou/core";
|
|
@@ -2705,10 +2949,10 @@ function resolveSourceRoots(args) {
|
|
|
2705
2949
|
const { projectFlags, manifest, repoRoot, cwd } = args;
|
|
2706
2950
|
let resolved;
|
|
2707
2951
|
if (projectFlags.length > 0) {
|
|
2708
|
-
resolved = projectFlags.map((p) =>
|
|
2952
|
+
resolved = projectFlags.map((p) => resolve6(cwd, p));
|
|
2709
2953
|
} else {
|
|
2710
2954
|
const roots = manifest.import?.source_roots;
|
|
2711
|
-
resolved = roots !== void 0 && roots.length > 0 ? roots.map((r) =>
|
|
2955
|
+
resolved = roots !== void 0 && roots.length > 0 ? roots.map((r) => resolve6(repoRoot, r)) : [repoRoot];
|
|
2712
2956
|
}
|
|
2713
2957
|
return [...new Set(resolved)];
|
|
2714
2958
|
}
|
|
@@ -2721,7 +2965,7 @@ async function doRunImportClaudeCode(options, ctx) {
|
|
|
2721
2965
|
repoRoot: repositoryRoot,
|
|
2722
2966
|
cwd: ctx.cwd ?? process.cwd()
|
|
2723
2967
|
});
|
|
2724
|
-
const projectsRoot = ctx.claudeProjectsDir ??
|
|
2968
|
+
const projectsRoot = ctx.claudeProjectsDir ?? join9(homedir7(), ".claude", "projects");
|
|
2725
2969
|
const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
|
|
2726
2970
|
const projectSet = new Set(projectPaths);
|
|
2727
2971
|
const candidates = files.map((file) => {
|
|
@@ -2760,7 +3004,7 @@ async function doRunImportCodex(options, ctx) {
|
|
|
2760
3004
|
repoRoot: repositoryRoot,
|
|
2761
3005
|
cwd: ctx.cwd ?? process.cwd()
|
|
2762
3006
|
});
|
|
2763
|
-
const sessionsRoot = ctx.codexSessionsDir ??
|
|
3007
|
+
const sessionsRoot = ctx.codexSessionsDir ?? join9(homedir7(), ".codex", "sessions");
|
|
2764
3008
|
const rollouts = await discoverCodexRollouts(sessionsRoot, projectPaths, options);
|
|
2765
3009
|
const candidates = rollouts.map(({ file, externalId }) => ({
|
|
2766
3010
|
externalId,
|
|
@@ -2798,7 +3042,7 @@ function assertSelector(options) {
|
|
|
2798
3042
|
async function resolveImportTarget(ctx) {
|
|
2799
3043
|
const cwd = ctx.cwd ?? process.cwd();
|
|
2800
3044
|
const repositoryRoot = await resolveRepositoryRootForImport(cwd);
|
|
2801
|
-
const paths =
|
|
3045
|
+
const paths = basouPaths8(repositoryRoot);
|
|
2802
3046
|
await assertWorkspaceInitialized5(paths.root);
|
|
2803
3047
|
const manifest = await readManifest4(paths);
|
|
2804
3048
|
return { repositoryRoot, paths, manifest };
|
|
@@ -2841,7 +3085,9 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
2841
3085
|
throw new Error("Invalid import payload", { cause: parsed.error });
|
|
2842
3086
|
}
|
|
2843
3087
|
if (parsed.data.schema_version !== SESSION_IMPORT_SCHEMA_VERSION) {
|
|
2844
|
-
throw new Error(
|
|
3088
|
+
throw new Error(
|
|
3089
|
+
`Unsupported import schema_version: ${parsed.data.schema_version} (expected ${SESSION_IMPORT_SCHEMA_VERSION}). The envelope's shape is published as @basou/core/schemas/session-import.schema.json; this version may move at a minor release, so a producer should read it from the installed @basou/core rather than pinning it.`
|
|
3090
|
+
);
|
|
2845
3091
|
}
|
|
2846
3092
|
return parsed.data;
|
|
2847
3093
|
};
|
|
@@ -2889,7 +3135,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
2889
3135
|
if (priors.length > 0 && options.force === true) {
|
|
2890
3136
|
if (options.dryRun !== true) {
|
|
2891
3137
|
for (const { sessionId } of priors) {
|
|
2892
|
-
await rm(
|
|
3138
|
+
await rm(join9(paths.sessions, sessionId), { recursive: true, force: true });
|
|
2893
3139
|
}
|
|
2894
3140
|
}
|
|
2895
3141
|
counts.replaced++;
|
|
@@ -3000,7 +3246,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
|
3000
3246
|
if (options.session !== void 0) {
|
|
3001
3247
|
const matches = [];
|
|
3002
3248
|
for (const projectPath of projectPaths) {
|
|
3003
|
-
const file =
|
|
3249
|
+
const file = join9(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
|
|
3004
3250
|
if (await pathExists(file)) matches.push(file);
|
|
3005
3251
|
}
|
|
3006
3252
|
if (matches.length === 0) {
|
|
@@ -3011,7 +3257,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
|
3011
3257
|
const files = [];
|
|
3012
3258
|
let anyDirFound = false;
|
|
3013
3259
|
for (const projectPath of projectPaths) {
|
|
3014
|
-
const transcriptDir =
|
|
3260
|
+
const transcriptDir = join9(projectsRoot, encodeProjectDir(projectPath));
|
|
3015
3261
|
let entries;
|
|
3016
3262
|
try {
|
|
3017
3263
|
entries = await readdir(transcriptDir);
|
|
@@ -3021,7 +3267,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
|
3021
3267
|
}
|
|
3022
3268
|
anyDirFound = true;
|
|
3023
3269
|
for (const name of entries) {
|
|
3024
|
-
if (name.endsWith(".jsonl")) files.push(
|
|
3270
|
+
if (name.endsWith(".jsonl")) files.push(join9(transcriptDir, name));
|
|
3025
3271
|
}
|
|
3026
3272
|
}
|
|
3027
3273
|
if (!anyDirFound) {
|
|
@@ -3078,7 +3324,7 @@ async function findRolloutFiles(sessionsRoot) {
|
|
|
3078
3324
|
throw new Error("Failed to read Codex sessions directory", { cause: error });
|
|
3079
3325
|
}
|
|
3080
3326
|
for (const entry of entries) {
|
|
3081
|
-
const full =
|
|
3327
|
+
const full = join9(dir, entry.name);
|
|
3082
3328
|
if (entry.isDirectory()) {
|
|
3083
3329
|
await walk(full, false);
|
|
3084
3330
|
} else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) {
|
|
@@ -3234,7 +3480,7 @@ function shortId2(id) {
|
|
|
3234
3480
|
}
|
|
3235
3481
|
async function resolveRepositoryRootForImport(cwd) {
|
|
3236
3482
|
try {
|
|
3237
|
-
return await
|
|
3483
|
+
return await resolveRepositoryRoot5(cwd);
|
|
3238
3484
|
} catch (error) {
|
|
3239
3485
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
3240
3486
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou import'.", {
|
|
@@ -3467,7 +3713,7 @@ async function warnIfPositionNamesOtherWorkspaces2(result, ctx) {
|
|
|
3467
3713
|
});
|
|
3468
3714
|
if (report !== null) {
|
|
3469
3715
|
console.error(
|
|
3470
|
-
positionForeignWorkspaceWarning(report,
|
|
3716
|
+
positionForeignWorkspaceWarning(report, basouPaths9(result.workspaceRoot).files.orientation)
|
|
3471
3717
|
);
|
|
3472
3718
|
}
|
|
3473
3719
|
}
|
|
@@ -3477,7 +3723,7 @@ async function renderOrientationForCwd(options, ctx) {
|
|
|
3477
3723
|
return renderOrientationForRoot(repositoryRoot, options, ctx, { write: true });
|
|
3478
3724
|
}
|
|
3479
3725
|
async function renderOrientationForRoot(repositoryRoot, options, ctx, behaviour) {
|
|
3480
|
-
const paths =
|
|
3726
|
+
const paths = basouPaths9(repositoryRoot);
|
|
3481
3727
|
await assertWorkspaceInitialized6(paths.root);
|
|
3482
3728
|
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
3483
3729
|
const probeCtx = { cwd: repositoryRoot };
|
|
@@ -3491,7 +3737,7 @@ async function renderOrientationForRoot(repositoryRoot, options, ctx, behaviour)
|
|
|
3491
3737
|
try {
|
|
3492
3738
|
const hosts = await loadHostsConfig(ctx.hostsConfigPath);
|
|
3493
3739
|
if (hosts !== null) {
|
|
3494
|
-
federatedRoots = hosts.map((h) => ({ paths:
|
|
3740
|
+
federatedRoots = hosts.map((h) => ({ paths: basouPaths9(h.path), host: h.label }));
|
|
3495
3741
|
}
|
|
3496
3742
|
} catch (error) {
|
|
3497
3743
|
console.error(
|
|
@@ -3535,6 +3781,9 @@ async function assertWorkspaceInitialized6(basouRoot) {
|
|
|
3535
3781
|
|
|
3536
3782
|
// src/commands/hook.ts
|
|
3537
3783
|
var MAX_TRANSCRIPT_BYTES = 8 * 1024 * 1024;
|
|
3784
|
+
var MAX_TRANSCRIPT_HEAD_BYTES = 256 * 1024;
|
|
3785
|
+
var TOKEN_SCAN_CHUNK_BYTES = 1024 * 1024;
|
|
3786
|
+
var execFileAsync = promisify(execFile);
|
|
3538
3787
|
function registerHookCommand(program) {
|
|
3539
3788
|
const hook = program.command("hook").description(
|
|
3540
3789
|
"Hook handlers for AI coding tools (Claude Code, Codex): read a hook payload on stdin, emit the tool's hook output on stdout"
|
|
@@ -3545,13 +3794,13 @@ function registerHookCommand(program) {
|
|
|
3545
3794
|
await runHookSessionStart();
|
|
3546
3795
|
});
|
|
3547
3796
|
hook.command("stop").description(
|
|
3548
|
-
"Stop-hook: when a substantive session recorded no decisions or next step, emit a non-blocking nudge to capture them. Reads the Stop hook JSON payload on stdin; never blocks and never fails the session."
|
|
3797
|
+
"Stop-hook: when a substantive session recorded no decisions or next step, emit a non-blocking nudge to capture them. Also hands a running session the standing protocols when they changed after it started \u2014 the copy it read at start is stale, and only this hook reaches a session still running. Reads the Stop hook JSON payload on stdin; never blocks and never fails the session."
|
|
3549
3798
|
).option(
|
|
3550
3799
|
"--min-edits <n>",
|
|
3551
3800
|
`Minimum file edits before nudging on edits alone (default ${DEFAULT_STOP_HOOK_MIN_EDITS})`
|
|
3552
3801
|
).option(
|
|
3553
3802
|
"--block",
|
|
3554
|
-
"Opt-in enforcement: hold the agent in-turn (decision:block) instead of a non-blocking
|
|
3803
|
+
"Opt-in enforcement: hold the agent in-turn (decision:block) instead of a non-blocking message"
|
|
3555
3804
|
).option(
|
|
3556
3805
|
"--require-review",
|
|
3557
3806
|
"Opt-in review gate: also remind when a session shipped substantive code (push / PR / merge) without recording a review"
|
|
@@ -3664,12 +3913,25 @@ gh pr create|merge) without recording a review ('basou review record'). This
|
|
|
3664
3913
|
gate is off by default; when on, its reminder is composed into the same
|
|
3665
3914
|
envelope as the capture reminder.
|
|
3666
3915
|
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3916
|
+
It also hands a RUNNING session the standing protocols when they changed after
|
|
3917
|
+
that session started. The protocol block in ~/.claude/CLAUDE.md is read at
|
|
3918
|
+
session start, so an update made mid-session never reaches the session it was
|
|
3919
|
+
meant to correct; this is the only channel that does. The complete current set
|
|
3920
|
+
is delivered, not a diff, so what it supersedes -- including a protocol that is
|
|
3921
|
+
no longer there -- is unambiguous. It reads the rendered block and nothing else,
|
|
3922
|
+
so an edit not yet published by 'basou protocol sync' cannot reach a session,
|
|
3923
|
+
and it delivers once per block state: a second update in the same session still
|
|
3924
|
+
lands, the same one twice does not. This part is always on, needs no flag, and
|
|
3925
|
+
says nothing at all unless the block actually changed.
|
|
3926
|
+
|
|
3927
|
+
By default every message here is non-blocking: Claude sees it and may act on it
|
|
3928
|
+
or stop. With --block (opt-in enforcement, 'basou hook install --block') it
|
|
3929
|
+
instead returns decision:block, holding the agent in-turn; the 'stop_hook_active'
|
|
3930
|
+
flag and Claude Code's own loop prevention bound it to a single turn. Note that
|
|
3931
|
+
this covers the protocol delivery too, which carries no action to take -- with
|
|
3932
|
+
--block the turn is held so the new text lands before more work is done on the
|
|
3933
|
+
old. Either way the hook fails open: a bad payload or unreadable transcript
|
|
3934
|
+
exits cleanly with no output.
|
|
3673
3935
|
`;
|
|
3674
3936
|
async function runHookStop(options, ctx = {}) {
|
|
3675
3937
|
try {
|
|
@@ -3707,7 +3969,12 @@ async function doRunHookStop(options, ctx) {
|
|
|
3707
3969
|
stopHookActive: false,
|
|
3708
3970
|
...options.minEdits !== void 0 ? { minEdits: options.minEdits } : {}
|
|
3709
3971
|
});
|
|
3972
|
+
const protocolUpdate = await evaluateProtocolUpdateGate({
|
|
3973
|
+
transcriptPath,
|
|
3974
|
+
target: ctx.protocolTargetPath ?? DEFAULT_TARGET_PATH
|
|
3975
|
+
});
|
|
3710
3976
|
const parts = [];
|
|
3977
|
+
if (protocolUpdate !== null) parts.push(protocolUpdate);
|
|
3711
3978
|
if (evaluation.kind === "nudge") parts.push(evaluation.additionalContext);
|
|
3712
3979
|
if (options.requireReview === true && evaluation.review.fires) {
|
|
3713
3980
|
parts.push(evaluation.review.additionalContext);
|
|
@@ -3723,6 +3990,56 @@ async function doRunHookStop(options, ctx) {
|
|
|
3723
3990
|
write(`${payloadJson}
|
|
3724
3991
|
`);
|
|
3725
3992
|
}
|
|
3993
|
+
async function evaluateProtocolUpdateGate(input) {
|
|
3994
|
+
try {
|
|
3995
|
+
const head = await readTranscriptHead(input.transcriptPath);
|
|
3996
|
+
const sessionStartedAt = transcriptStartedAt(parseTranscript(head));
|
|
3997
|
+
if (sessionStartedAt === void 0) return null;
|
|
3998
|
+
const touchedAt = await targetModifiedAt(input.target);
|
|
3999
|
+
if (touchedAt !== null && touchedAt <= Date.parse(sessionStartedAt)) return null;
|
|
4000
|
+
const existing = await readMarkdownFile6(input.target);
|
|
4001
|
+
if (existing === null) return null;
|
|
4002
|
+
const section = parseMarkers2(existing, { start: PROTOCOL_START, end: PROTOCOL_END });
|
|
4003
|
+
if (section.kind !== "ok") return null;
|
|
4004
|
+
const stamp = parseProtocolStamp(section.generated);
|
|
4005
|
+
if (stamp === null) return null;
|
|
4006
|
+
if (!isProtocolUpdateDue({ stamp, sessionStartedAt })) return null;
|
|
4007
|
+
const sections = protocolSectionsFrom(section.generated);
|
|
4008
|
+
if (sections === null || sections.trim().length === 0) return null;
|
|
4009
|
+
if (await transcriptCarries(input.transcriptPath, protocolUpdateToken(stamp.contentHash))) {
|
|
4010
|
+
return null;
|
|
4011
|
+
}
|
|
4012
|
+
return renderProtocolUpdate(sections, stamp);
|
|
4013
|
+
} catch {
|
|
4014
|
+
return null;
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
async function targetModifiedAt(target) {
|
|
4018
|
+
try {
|
|
4019
|
+
return (await stat4(target)).mtimeMs;
|
|
4020
|
+
} catch {
|
|
4021
|
+
return null;
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
async function transcriptCarries(path, token) {
|
|
4025
|
+
const handle = await open2(path, "r");
|
|
4026
|
+
try {
|
|
4027
|
+
const overlap = Math.max(token.length - 1, 0);
|
|
4028
|
+
const chunk = Buffer.alloc(TOKEN_SCAN_CHUNK_BYTES);
|
|
4029
|
+
let carry = "";
|
|
4030
|
+
let position = 0;
|
|
4031
|
+
for (; ; ) {
|
|
4032
|
+
const { bytesRead } = await handle.read(chunk, 0, TOKEN_SCAN_CHUNK_BYTES, position);
|
|
4033
|
+
if (bytesRead === 0) return false;
|
|
4034
|
+
position += bytesRead;
|
|
4035
|
+
const text = carry + chunk.subarray(0, bytesRead).toString("utf8");
|
|
4036
|
+
if (text.includes(token)) return true;
|
|
4037
|
+
carry = overlap > 0 ? text.slice(-overlap) : "";
|
|
4038
|
+
}
|
|
4039
|
+
} finally {
|
|
4040
|
+
await handle.close();
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
3726
4043
|
async function renderRegisteredWorkspacePosition(cwd, portfolioConfigPath = DEFAULT_PORTFOLIO_CONFIG_PATH) {
|
|
3727
4044
|
const root = await resolveBasouRootForCommand(cwd, "hook session-start", {
|
|
3728
4045
|
portfolioConfigPath
|
|
@@ -3822,11 +4139,25 @@ async function readTranscriptBounded(path, maxBytes = MAX_TRANSCRIPT_BYTES) {
|
|
|
3822
4139
|
await handle.close();
|
|
3823
4140
|
}
|
|
3824
4141
|
}
|
|
4142
|
+
async function readTranscriptHead(path, maxBytes = MAX_TRANSCRIPT_HEAD_BYTES) {
|
|
4143
|
+
const { size } = await stat4(path);
|
|
4144
|
+
if (size <= maxBytes) return readFile3(path, "utf8");
|
|
4145
|
+
const handle = await open2(path, "r");
|
|
4146
|
+
try {
|
|
4147
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
4148
|
+
const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
|
|
4149
|
+
const text = buffer.subarray(0, bytesRead).toString("utf8");
|
|
4150
|
+
const lastNewline = text.lastIndexOf("\n");
|
|
4151
|
+
return lastNewline >= 0 ? text.slice(0, lastNewline + 1) : text;
|
|
4152
|
+
} finally {
|
|
4153
|
+
await handle.close();
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
3825
4156
|
function parseMinEdits(raw) {
|
|
3826
4157
|
if (raw === void 0 || !/^\d+$/.test(raw)) return void 0;
|
|
3827
4158
|
return Number(raw);
|
|
3828
4159
|
}
|
|
3829
|
-
var DEFAULT_CLAUDE_SETTINGS_PATH =
|
|
4160
|
+
var DEFAULT_CLAUDE_SETTINGS_PATH = join10(homedir8(), ".claude", "settings.json");
|
|
3830
4161
|
function resolveCliEntry() {
|
|
3831
4162
|
return fileURLToPath(import.meta.url);
|
|
3832
4163
|
}
|
|
@@ -3984,13 +4315,97 @@ async function doRunHookStatus(options) {
|
|
|
3984
4315
|
review: / --require-review\b/.test(command)
|
|
3985
4316
|
});
|
|
3986
4317
|
console.log(`basou Stop hook: registered, ${mode}.`);
|
|
4318
|
+
await reportHookEntryBuild(command);
|
|
4319
|
+
}
|
|
4320
|
+
async function reportHookEntryBuild(command) {
|
|
4321
|
+
console.log(` this basou is: ${BASOU_VERSION_LINE}`);
|
|
4322
|
+
const entry = extractHookEntryPath(command);
|
|
4323
|
+
if (entry === void 0) {
|
|
4324
|
+
console.log(
|
|
4325
|
+
" runs: (registered by alias, not by path) \u2014 which build that resolves to depends on the hook's PATH, so this cannot tell you."
|
|
4326
|
+
);
|
|
4327
|
+
return;
|
|
4328
|
+
}
|
|
4329
|
+
try {
|
|
4330
|
+
const { stdout } = await execFileAsync(process.execPath, [entry, "--version"], {
|
|
4331
|
+
timeout: 1e4
|
|
4332
|
+
});
|
|
4333
|
+
const reported = stdout.trim();
|
|
4334
|
+
console.log(` the hook runs: ${entry}`);
|
|
4335
|
+
console.log(` that build is: ${reported}`);
|
|
4336
|
+
if (!reported.includes("(build ")) {
|
|
4337
|
+
console.log(
|
|
4338
|
+
" note: that build predates build stamping, so what it reports is its package.json rather than itself \u2014 it cannot tell you which build it is. Update it (rebuild a source checkout, or reinstall the package) to find out."
|
|
4339
|
+
);
|
|
4340
|
+
}
|
|
4341
|
+
} catch {
|
|
4342
|
+
console.log(` the hook runs: ${entry}`);
|
|
4343
|
+
console.log(
|
|
4344
|
+
" that build is: could not be executed \u2014 the hook's wrapper fails open, so it is silently doing nothing."
|
|
4345
|
+
);
|
|
4346
|
+
}
|
|
4347
|
+
}
|
|
4348
|
+
function tokenizeShellCommand(command) {
|
|
4349
|
+
const tokens = [];
|
|
4350
|
+
let current = "";
|
|
4351
|
+
let started = false;
|
|
4352
|
+
let quote;
|
|
4353
|
+
for (let i = 0; i < command.length; i++) {
|
|
4354
|
+
const ch = command[i];
|
|
4355
|
+
if (quote === "'") {
|
|
4356
|
+
if (ch === "'") quote = void 0;
|
|
4357
|
+
else current += ch;
|
|
4358
|
+
continue;
|
|
4359
|
+
}
|
|
4360
|
+
if (quote === '"') {
|
|
4361
|
+
if (ch === '"') quote = void 0;
|
|
4362
|
+
else if (ch === "\\" && i + 1 < command.length) current += command[++i];
|
|
4363
|
+
else current += ch;
|
|
4364
|
+
continue;
|
|
4365
|
+
}
|
|
4366
|
+
if (ch === "'" || ch === '"') {
|
|
4367
|
+
quote = ch;
|
|
4368
|
+
started = true;
|
|
4369
|
+
continue;
|
|
4370
|
+
}
|
|
4371
|
+
if (ch === "\\" && i + 1 < command.length) {
|
|
4372
|
+
current += command[++i];
|
|
4373
|
+
started = true;
|
|
4374
|
+
continue;
|
|
4375
|
+
}
|
|
4376
|
+
if (/\s/.test(ch)) {
|
|
4377
|
+
if (started) tokens.push(current);
|
|
4378
|
+
current = "";
|
|
4379
|
+
started = false;
|
|
4380
|
+
continue;
|
|
4381
|
+
}
|
|
4382
|
+
current += ch;
|
|
4383
|
+
started = true;
|
|
4384
|
+
}
|
|
4385
|
+
if (quote !== void 0) return void 0;
|
|
4386
|
+
if (started) tokens.push(current);
|
|
4387
|
+
return tokens;
|
|
4388
|
+
}
|
|
4389
|
+
function extractHookEntryPath(command) {
|
|
4390
|
+
const tokens = tokenizeShellCommand(command);
|
|
4391
|
+
if (tokens === void 0) return void 0;
|
|
4392
|
+
let index = 0;
|
|
4393
|
+
while (index < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[index])) index++;
|
|
4394
|
+
const interpreter = tokens[index];
|
|
4395
|
+
if (interpreter === void 0) return void 0;
|
|
4396
|
+
const base = interpreter.replace(/\\/g, "/").split("/").pop() ?? "";
|
|
4397
|
+
if (base !== "node" && base !== "node.exe") return void 0;
|
|
4398
|
+
index++;
|
|
4399
|
+
while (index < tokens.length && tokens[index].startsWith("-")) index++;
|
|
4400
|
+
const entry = tokens[index];
|
|
4401
|
+
return entry === void 0 || entry === "" ? void 0 : entry;
|
|
3987
4402
|
}
|
|
3988
4403
|
function describeHookMode(tiers) {
|
|
3989
4404
|
const enforcement = tiers.block ? "blocking (opt-in enforcement)" : "advisory (non-blocking)";
|
|
3990
4405
|
const gates = tiers.review ? "capture + review" : "capture";
|
|
3991
4406
|
return `${enforcement}, ${gates}`;
|
|
3992
4407
|
}
|
|
3993
|
-
var DEFAULT_CODEX_FACE_PATH =
|
|
4408
|
+
var DEFAULT_CODEX_FACE_PATH = join10(homedir8(), ".codex", "AGENTS.md");
|
|
3994
4409
|
var LEFTOVER_FACE_NOTE = (label) => `${label} still carries an orientation block rendered by an earlier basou (0.39 or before); every Codex session on this machine reads it. \`basou channel clear codex\` removes it.`;
|
|
3995
4410
|
async function faceHasLeftoverOrientationBlock(facePath) {
|
|
3996
4411
|
try {
|
|
@@ -4002,8 +4417,8 @@ async function faceHasLeftoverOrientationBlock(facePath) {
|
|
|
4002
4417
|
return false;
|
|
4003
4418
|
}
|
|
4004
4419
|
}
|
|
4005
|
-
var DEFAULT_CODEX_HOOKS_PATH =
|
|
4006
|
-
var DEFAULT_CODEX_CONFIG_PATH =
|
|
4420
|
+
var DEFAULT_CODEX_HOOKS_PATH = join10(homedir8(), ".codex", "hooks.json");
|
|
4421
|
+
var DEFAULT_CODEX_CONFIG_PATH = join10(homedir8(), ".codex", "config.toml");
|
|
4007
4422
|
async function readHooksFile(path) {
|
|
4008
4423
|
let raw;
|
|
4009
4424
|
try {
|
|
@@ -4140,6 +4555,7 @@ async function doRunCodexHookStatus(options) {
|
|
|
4140
4555
|
console.log(
|
|
4141
4556
|
`basou Codex SessionStart hook: registered in ${hooksPath} (matcher: ${matcher}); speaks only for workspaces registered in ~/.basou/portfolio.yaml.`
|
|
4142
4557
|
);
|
|
4558
|
+
await reportHookEntryBuild(location.command);
|
|
4143
4559
|
await reportCodexHookState(hooksPath, parsed, options);
|
|
4144
4560
|
}
|
|
4145
4561
|
async function codexHookTrustFor(hooksPath, location, configPath) {
|
|
@@ -4174,12 +4590,12 @@ async function codexHookTrustFor(hooksPath, location, configPath) {
|
|
|
4174
4590
|
}
|
|
4175
4591
|
|
|
4176
4592
|
// src/commands/init.ts
|
|
4177
|
-
import { basename as basename5, relative, resolve as
|
|
4593
|
+
import { basename as basename5, relative, resolve as resolve7 } from "path";
|
|
4178
4594
|
import {
|
|
4179
4595
|
appendBasouGitignore,
|
|
4180
4596
|
createManifest,
|
|
4181
4597
|
ensureBasouDirectory,
|
|
4182
|
-
resolveRepositoryRoot as
|
|
4598
|
+
resolveRepositoryRoot as resolveRepositoryRoot6,
|
|
4183
4599
|
writeManifest
|
|
4184
4600
|
} from "@basou/core";
|
|
4185
4601
|
function collectValue(value, previous) {
|
|
@@ -4219,7 +4635,7 @@ async function doRunInit(options, ctx) {
|
|
|
4219
4635
|
);
|
|
4220
4636
|
}
|
|
4221
4637
|
const sourceRoots = (options.sourceRoot ?? []).map((p) => {
|
|
4222
|
-
const rel = relative(repositoryRoot,
|
|
4638
|
+
const rel = relative(repositoryRoot, resolve7(cwd, p));
|
|
4223
4639
|
return rel === "" ? "." : rel;
|
|
4224
4640
|
});
|
|
4225
4641
|
const paths = await ensureBasouDirectory(repositoryRoot);
|
|
@@ -4249,7 +4665,7 @@ function renderGitignoreWarning(error, verbose) {
|
|
|
4249
4665
|
}
|
|
4250
4666
|
async function resolveRepositoryRootForInit(cwd) {
|
|
4251
4667
|
try {
|
|
4252
|
-
return await
|
|
4668
|
+
return await resolveRepositoryRoot6(cwd);
|
|
4253
4669
|
} catch (error) {
|
|
4254
4670
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
4255
4671
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou init'.", {
|
|
@@ -4265,14 +4681,15 @@ import {
|
|
|
4265
4681
|
acquireLock as acquireLock4,
|
|
4266
4682
|
appendEventToExistingSession as appendEventToExistingSession2,
|
|
4267
4683
|
assertBasouRootSafe as assertBasouRootSafe8,
|
|
4268
|
-
basouPaths as
|
|
4684
|
+
basouPaths as basouPaths10,
|
|
4269
4685
|
createAdHocSessionWithEvent as createAdHocSessionWithEvent2,
|
|
4270
4686
|
EVENT_SCHEMA_VERSION as EVENT_SCHEMA_VERSION4,
|
|
4271
4687
|
findErrorCode as findErrorCode7,
|
|
4688
|
+
LOCAL_CLI_EVENT_SOURCE as LOCAL_CLI_EVENT_SOURCE3,
|
|
4272
4689
|
readManifest as readManifest5,
|
|
4273
4690
|
resolveSessionId as resolveSessionId2
|
|
4274
4691
|
} from "@basou/core";
|
|
4275
|
-
import { InvalidArgumentError as
|
|
4692
|
+
import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
|
|
4276
4693
|
var NOTE_SUBCOMMAND_LOOKALIKES = /* @__PURE__ */ new Set([
|
|
4277
4694
|
"list",
|
|
4278
4695
|
"ls",
|
|
@@ -4319,7 +4736,7 @@ async function doRunNote(body, options, ctx) {
|
|
|
4319
4736
|
}
|
|
4320
4737
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4321
4738
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "note");
|
|
4322
|
-
const paths =
|
|
4739
|
+
const paths = basouPaths10(repositoryRoot);
|
|
4323
4740
|
await assertWorkspaceInitialized7(paths.root);
|
|
4324
4741
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
4325
4742
|
const occurredAt = now.toISOString();
|
|
@@ -4376,7 +4793,7 @@ function buildNoteEvent(input) {
|
|
|
4376
4793
|
id: input.eventId,
|
|
4377
4794
|
session_id: input.sessionId,
|
|
4378
4795
|
occurred_at: input.occurredAt,
|
|
4379
|
-
source:
|
|
4796
|
+
source: LOCAL_CLI_EVENT_SOURCE3,
|
|
4380
4797
|
type: "note_added",
|
|
4381
4798
|
body: input.body,
|
|
4382
4799
|
// `basou note` is the resume-hint command; mark it so orientation surfaces
|
|
@@ -4385,13 +4802,13 @@ function buildNoteEvent(input) {
|
|
|
4385
4802
|
};
|
|
4386
4803
|
}
|
|
4387
4804
|
function buildAdHocLabel2(body) {
|
|
4388
|
-
const
|
|
4389
|
-
const truncated =
|
|
4805
|
+
const oneLine3 = body.replace(/\s+/g, " ").trim();
|
|
4806
|
+
const truncated = oneLine3.length > LABEL_BODY_MAX ? `${oneLine3.slice(0, LABEL_TRUNCATE_HEAD2)}...` : oneLine3;
|
|
4390
4807
|
return `Ad-hoc note: ${truncated}`;
|
|
4391
4808
|
}
|
|
4392
4809
|
function parseBody(raw) {
|
|
4393
4810
|
if (raw.trim().length === 0) {
|
|
4394
|
-
throw new
|
|
4811
|
+
throw new InvalidArgumentError3("Note body must not be empty");
|
|
4395
4812
|
}
|
|
4396
4813
|
return raw;
|
|
4397
4814
|
}
|
|
@@ -4428,7 +4845,7 @@ async function assertWorkspaceInitialized7(basouRoot) {
|
|
|
4428
4845
|
|
|
4429
4846
|
// src/commands/portfolio.ts
|
|
4430
4847
|
import { existsSync, statSync } from "fs";
|
|
4431
|
-
import { join as
|
|
4848
|
+
import { join as join11 } from "path";
|
|
4432
4849
|
function registerPortfolioCommand(program) {
|
|
4433
4850
|
program.command("portfolio").description(
|
|
4434
4851
|
"List the workspaces you orient across (read-only): every planning master registered in ~/.basou/portfolio.yaml, with its path and whether it exists / is initialized. The headless text/JSON counterpart to the `basou view --portfolio` GUI \u2014 for discovering where a sibling project lives without opening a browser"
|
|
@@ -4474,7 +4891,7 @@ async function runPortfolioList(options, ctx = {}) {
|
|
|
4474
4891
|
async function doRunPortfolioList(options, ctx) {
|
|
4475
4892
|
const configPath = ctx.configPath ?? DEFAULT_PORTFOLIO_CONFIG_PATH;
|
|
4476
4893
|
const pathExists2 = ctx.pathExists ?? ((p) => existsSync(p));
|
|
4477
|
-
const isInitialized = ctx.isInitialized ?? ((p) => isDirectory(
|
|
4894
|
+
const isInitialized = ctx.isInitialized ?? ((p) => isDirectory(join11(p, ".basou")));
|
|
4478
4895
|
const workspaces = await loadPortfolioConfig(configPath);
|
|
4479
4896
|
const result = {
|
|
4480
4897
|
configPath,
|
|
@@ -4539,10 +4956,10 @@ import {
|
|
|
4539
4956
|
writeFileSync,
|
|
4540
4957
|
writeSync
|
|
4541
4958
|
} from "fs";
|
|
4542
|
-
import { basename as basename6, dirname as dirname4, isAbsolute as
|
|
4959
|
+
import { basename as basename6, dirname as dirname4, isAbsolute as isAbsolute4, join as join12, relative as relative2, resolve as resolve8 } from "path";
|
|
4543
4960
|
import {
|
|
4544
4961
|
appendBasouGitignore as appendBasouGitignore2,
|
|
4545
|
-
basouPaths as
|
|
4962
|
+
basouPaths as basouPaths11,
|
|
4546
4963
|
classifyRetrofit,
|
|
4547
4964
|
createManifest as createManifest2,
|
|
4548
4965
|
ensureBasouDirectory as ensureBasouDirectory2,
|
|
@@ -4564,7 +4981,7 @@ import {
|
|
|
4564
4981
|
renderAnchorStarter,
|
|
4565
4982
|
renderViewPresetBlock,
|
|
4566
4983
|
renderWithMarkers as renderWithMarkers4,
|
|
4567
|
-
resolveRepositoryRoot as
|
|
4984
|
+
resolveRepositoryRoot as resolveRepositoryRoot7,
|
|
4568
4985
|
safeSimpleGit,
|
|
4569
4986
|
seedMarkers,
|
|
4570
4987
|
summarizePresetPlan,
|
|
@@ -4705,7 +5122,7 @@ function preservedUnknownLines(fields) {
|
|
|
4705
5122
|
async function doRunProjectCheck(options, ctx) {
|
|
4706
5123
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4707
5124
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project check");
|
|
4708
|
-
const paths =
|
|
5125
|
+
const paths = basouPaths11(repositoryRoot);
|
|
4709
5126
|
const manifest = await readManifest6(paths);
|
|
4710
5127
|
const roster = summarizeRosterDrift({
|
|
4711
5128
|
...manifest.repos !== void 0 ? { repos: manifest.repos } : {},
|
|
@@ -4840,7 +5257,7 @@ async function runProjectSync(options, ctx = {}) {
|
|
|
4840
5257
|
async function doRunProjectSync(options, ctx) {
|
|
4841
5258
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4842
5259
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project sync");
|
|
4843
|
-
const paths =
|
|
5260
|
+
const paths = basouPaths11(repositoryRoot);
|
|
4844
5261
|
const manifest = await readManifest6(paths);
|
|
4845
5262
|
const hasRoster = manifest.repos !== void 0 && manifest.repos.length > 0;
|
|
4846
5263
|
const reconcile = reconcileSourceRoots({
|
|
@@ -4914,19 +5331,19 @@ async function runProjectAdopt(options, ctx = {}) {
|
|
|
4914
5331
|
}
|
|
4915
5332
|
}
|
|
4916
5333
|
function classifySourceRoot(repositoryRoot, declaredPath) {
|
|
4917
|
-
const absolute =
|
|
5334
|
+
const absolute = resolve8(repositoryRoot, declaredPath);
|
|
4918
5335
|
let real;
|
|
4919
5336
|
try {
|
|
4920
5337
|
real = realpathSync(absolute);
|
|
4921
5338
|
} catch {
|
|
4922
5339
|
return { path: declaredPath, kind: "unresolved" };
|
|
4923
5340
|
}
|
|
4924
|
-
return { path: declaredPath, kind: existsSync2(
|
|
5341
|
+
return { path: declaredPath, kind: existsSync2(join12(real, ".git")) ? "repo" : "non-repo" };
|
|
4925
5342
|
}
|
|
4926
5343
|
async function doRunProjectAdopt(options, ctx) {
|
|
4927
5344
|
const cwd = ctx.cwd ?? process.cwd();
|
|
4928
5345
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project adopt");
|
|
4929
|
-
const paths =
|
|
5346
|
+
const paths = basouPaths11(repositoryRoot);
|
|
4930
5347
|
const manifest = await readManifest6(paths);
|
|
4931
5348
|
const alreadyDeclared = manifest.repos !== void 0 && manifest.repos.length > 0;
|
|
4932
5349
|
const candidates = effectiveSourceRoots(manifest).map(
|
|
@@ -5021,11 +5438,11 @@ async function gatherRepoWiring(repositoryRoot, entry) {
|
|
|
5021
5438
|
};
|
|
5022
5439
|
let real;
|
|
5023
5440
|
try {
|
|
5024
|
-
real = realpathSync(
|
|
5441
|
+
real = realpathSync(resolve8(repositoryRoot, entry.path));
|
|
5025
5442
|
} catch {
|
|
5026
5443
|
return { ...base, reachable: false, instructionFiles: [] };
|
|
5027
5444
|
}
|
|
5028
|
-
if (!existsSync2(
|
|
5445
|
+
if (!existsSync2(join12(real, ".git"))) {
|
|
5029
5446
|
return { ...base, reachable: false, instructionFiles: [] };
|
|
5030
5447
|
}
|
|
5031
5448
|
try {
|
|
@@ -5033,7 +5450,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
|
|
|
5033
5450
|
for (const name of INSTRUCTION_FILES) {
|
|
5034
5451
|
let present = true;
|
|
5035
5452
|
try {
|
|
5036
|
-
lstatSync(
|
|
5453
|
+
lstatSync(join12(real, name));
|
|
5037
5454
|
} catch {
|
|
5038
5455
|
present = false;
|
|
5039
5456
|
}
|
|
@@ -5048,7 +5465,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
|
|
|
5048
5465
|
async function doRunProjectWiring(options, ctx) {
|
|
5049
5466
|
const cwd = ctx.cwd ?? process.cwd();
|
|
5050
5467
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project wiring");
|
|
5051
|
-
const paths =
|
|
5468
|
+
const paths = basouPaths11(repositoryRoot);
|
|
5052
5469
|
const manifest = await readManifest6(paths);
|
|
5053
5470
|
const roster = manifest.repos ?? [];
|
|
5054
5471
|
const facts = [];
|
|
@@ -5140,14 +5557,14 @@ function gatherRepoGitignore(repositoryRoot, entry) {
|
|
|
5140
5557
|
};
|
|
5141
5558
|
let real;
|
|
5142
5559
|
try {
|
|
5143
|
-
real = realpathSync(
|
|
5560
|
+
real = realpathSync(resolve8(repositoryRoot, entry.path));
|
|
5144
5561
|
} catch {
|
|
5145
5562
|
return { ...base, reachable: false, currentLines: [] };
|
|
5146
5563
|
}
|
|
5147
|
-
if (!existsSync2(
|
|
5564
|
+
if (!existsSync2(join12(real, ".git"))) {
|
|
5148
5565
|
return { ...base, reachable: false, currentLines: [] };
|
|
5149
5566
|
}
|
|
5150
|
-
return { ...base, reachable: true, currentLines: readGitignoreLines(
|
|
5567
|
+
return { ...base, reachable: true, currentLines: readGitignoreLines(join12(real, ".gitignore")) };
|
|
5151
5568
|
}
|
|
5152
5569
|
function hasErrorCode(error) {
|
|
5153
5570
|
return error instanceof Error && typeof error.code === "string";
|
|
@@ -5161,7 +5578,7 @@ function readGitignoreLines(file) {
|
|
|
5161
5578
|
}
|
|
5162
5579
|
}
|
|
5163
5580
|
function applyGitignorePlan(repositoryRoot, plan) {
|
|
5164
|
-
const file =
|
|
5581
|
+
const file = join12(realpathSync(resolve8(repositoryRoot, plan.path)), ".gitignore");
|
|
5165
5582
|
let existing = "";
|
|
5166
5583
|
try {
|
|
5167
5584
|
existing = readFileSync(file, "utf8");
|
|
@@ -5181,7 +5598,7 @@ function applyGitignorePlan(repositoryRoot, plan) {
|
|
|
5181
5598
|
async function doRunProjectGitignore(options, ctx) {
|
|
5182
5599
|
const cwd = ctx.cwd ?? process.cwd();
|
|
5183
5600
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project gitignore");
|
|
5184
|
-
const paths =
|
|
5601
|
+
const paths = basouPaths11(repositoryRoot);
|
|
5185
5602
|
const manifest = await readManifest6(paths);
|
|
5186
5603
|
const roster = manifest.repos ?? [];
|
|
5187
5604
|
const facts = roster.map((entry) => gatherRepoGitignore(repositoryRoot, entry));
|
|
@@ -5296,12 +5713,12 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
5296
5713
|
const base = { path: entry.path, ...isSelf ? { self: true } : {} };
|
|
5297
5714
|
let real;
|
|
5298
5715
|
try {
|
|
5299
|
-
real = realpathSync(
|
|
5716
|
+
real = realpathSync(resolve8(repositoryRoot, entry.path));
|
|
5300
5717
|
} catch {
|
|
5301
5718
|
return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
|
|
5302
5719
|
}
|
|
5303
5720
|
if (real === anchorReal) {
|
|
5304
|
-
const anchorCanonical =
|
|
5721
|
+
const anchorCanonical = join12(real, CANONICAL_FILE);
|
|
5305
5722
|
const anchorState = anchorCanonicalState(anchorCanonical);
|
|
5306
5723
|
if (anchorState === "absent") {
|
|
5307
5724
|
return { ...base, isAnchor: true, reachable: true, canonicalPresent: false, files: [] };
|
|
@@ -5321,7 +5738,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
5321
5738
|
anchorCanonical,
|
|
5322
5739
|
"self"
|
|
5323
5740
|
).map((spec) => {
|
|
5324
|
-
const { state, actualTarget } = inspectSymlink(
|
|
5741
|
+
const { state, actualTarget } = inspectSymlink(join12(real, spec.name), spec.target);
|
|
5325
5742
|
return {
|
|
5326
5743
|
name: spec.name,
|
|
5327
5744
|
expectedTarget: spec.target,
|
|
@@ -5338,16 +5755,16 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
5338
5755
|
files: anchorFiles
|
|
5339
5756
|
};
|
|
5340
5757
|
}
|
|
5341
|
-
if (!existsSync2(
|
|
5758
|
+
if (!existsSync2(join12(real, ".git"))) {
|
|
5342
5759
|
return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
|
|
5343
5760
|
}
|
|
5344
|
-
const canonicalFile = isSelf ?
|
|
5761
|
+
const canonicalFile = isSelf ? join12(real, CANONICAL_FILE) : join12(anchorReal, "agents", basename6(real), CANONICAL_FILE);
|
|
5345
5762
|
if (!existsSync2(canonicalFile)) {
|
|
5346
5763
|
return { ...base, isAnchor: false, reachable: true, canonicalPresent: false, files: [] };
|
|
5347
5764
|
}
|
|
5348
5765
|
const files = expectedSymlinkTargets(real, canonicalFile, mode).map(
|
|
5349
5766
|
(spec) => {
|
|
5350
|
-
const { state, actualTarget } = inspectSymlink(
|
|
5767
|
+
const { state, actualTarget } = inspectSymlink(join12(real, spec.name), spec.target);
|
|
5351
5768
|
return {
|
|
5352
5769
|
name: spec.name,
|
|
5353
5770
|
expectedTarget: spec.target,
|
|
@@ -5368,7 +5785,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
5368
5785
|
function applySymlinkPlan(repositoryRoot, plan) {
|
|
5369
5786
|
let real;
|
|
5370
5787
|
try {
|
|
5371
|
-
real = realpathSync(
|
|
5788
|
+
real = realpathSync(resolve8(repositoryRoot, plan.path));
|
|
5372
5789
|
} catch (error) {
|
|
5373
5790
|
const message = failureReason(error);
|
|
5374
5791
|
return { created: [], failed: plan.toCreate.map((c) => ({ file: c.name, message })) };
|
|
@@ -5376,7 +5793,7 @@ function applySymlinkPlan(repositoryRoot, plan) {
|
|
|
5376
5793
|
const created = [];
|
|
5377
5794
|
const failed = [];
|
|
5378
5795
|
for (const { name, target } of plan.toCreate) {
|
|
5379
|
-
const filePath =
|
|
5796
|
+
const filePath = join12(real, name);
|
|
5380
5797
|
try {
|
|
5381
5798
|
mkdirSync(dirname4(filePath), { recursive: true });
|
|
5382
5799
|
symlinkSync(target, filePath);
|
|
@@ -5394,9 +5811,9 @@ function viewCanonicalCollision(repositoryRoot, roster, viewName) {
|
|
|
5394
5811
|
for (const entry of roster) {
|
|
5395
5812
|
let name;
|
|
5396
5813
|
try {
|
|
5397
|
-
name = basename6(realpathSync(
|
|
5814
|
+
name = basename6(realpathSync(resolve8(repositoryRoot, entry.path)));
|
|
5398
5815
|
} catch {
|
|
5399
|
-
name = basename6(
|
|
5816
|
+
name = basename6(resolve8(repositoryRoot, entry.path));
|
|
5400
5817
|
}
|
|
5401
5818
|
if (name === viewName) return entry.path;
|
|
5402
5819
|
}
|
|
@@ -5410,7 +5827,7 @@ function gatherViewSymlinks(repositoryRoot, anchorReal, roster, viewDir) {
|
|
|
5410
5827
|
if (!existsSync2(canonicalFile)) return { kind: "missing-canonical", viewName };
|
|
5411
5828
|
const files = expectedSymlinkTargets(viewDir, canonicalFile, "hub").map(
|
|
5412
5829
|
(spec) => {
|
|
5413
|
-
const { state, actualTarget } = inspectSymlink(
|
|
5830
|
+
const { state, actualTarget } = inspectSymlink(join12(viewDir, spec.name), spec.target);
|
|
5414
5831
|
return {
|
|
5415
5832
|
name: spec.name,
|
|
5416
5833
|
expectedTarget: spec.target,
|
|
@@ -5426,7 +5843,7 @@ function applyViewSymlinks(viewDir, files) {
|
|
|
5426
5843
|
const failed = [];
|
|
5427
5844
|
for (const f of files) {
|
|
5428
5845
|
if (f.state !== "missing") continue;
|
|
5429
|
-
const filePath =
|
|
5846
|
+
const filePath = join12(viewDir, f.name);
|
|
5430
5847
|
try {
|
|
5431
5848
|
mkdirSync(dirname4(filePath), { recursive: true });
|
|
5432
5849
|
symlinkSync(f.expectedTarget, filePath);
|
|
@@ -5440,7 +5857,7 @@ function applyViewSymlinks(viewDir, files) {
|
|
|
5440
5857
|
async function doRunProjectSymlinks(options, ctx) {
|
|
5441
5858
|
const cwd = ctx.cwd ?? process.cwd();
|
|
5442
5859
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project symlinks");
|
|
5443
|
-
const paths =
|
|
5860
|
+
const paths = basouPaths11(repositoryRoot);
|
|
5444
5861
|
const manifest = await readManifest6(paths);
|
|
5445
5862
|
const roster = manifest.repos ?? [];
|
|
5446
5863
|
const anchorReal = realpathSync(repositoryRoot);
|
|
@@ -5643,12 +6060,12 @@ async function runProjectWorkspace(options, ctx = {}) {
|
|
|
5643
6060
|
}
|
|
5644
6061
|
}
|
|
5645
6062
|
function resolveViewDir(repositoryRoot, viewPath) {
|
|
5646
|
-
const abs =
|
|
6063
|
+
const abs = resolve8(repositoryRoot, viewPath);
|
|
5647
6064
|
try {
|
|
5648
6065
|
return realpathSync(abs);
|
|
5649
6066
|
} catch {
|
|
5650
6067
|
try {
|
|
5651
|
-
return
|
|
6068
|
+
return join12(realpathSync(dirname4(abs)), basename6(abs));
|
|
5652
6069
|
} catch {
|
|
5653
6070
|
return abs;
|
|
5654
6071
|
}
|
|
@@ -5657,7 +6074,7 @@ function resolveViewDir(repositoryRoot, viewPath) {
|
|
|
5657
6074
|
function gatherViewRepo(repositoryRoot, viewDir, entry) {
|
|
5658
6075
|
let repoReal;
|
|
5659
6076
|
try {
|
|
5660
|
-
repoReal = realpathSync(
|
|
6077
|
+
repoReal = realpathSync(resolve8(repositoryRoot, entry.path));
|
|
5661
6078
|
} catch {
|
|
5662
6079
|
return { path: entry.path, reachable: false };
|
|
5663
6080
|
}
|
|
@@ -5666,7 +6083,7 @@ function gatherViewRepo(repositoryRoot, viewDir, entry) {
|
|
|
5666
6083
|
return { path: entry.path, reachable: false };
|
|
5667
6084
|
}
|
|
5668
6085
|
const linkName = basename6(repoReal);
|
|
5669
|
-
const { state, actualTarget } = inspectSymlink(
|
|
6086
|
+
const { state, actualTarget } = inspectSymlink(join12(viewDir, linkName), expectedTarget);
|
|
5670
6087
|
return {
|
|
5671
6088
|
path: entry.path,
|
|
5672
6089
|
reachable: true,
|
|
@@ -5680,7 +6097,7 @@ function applyViewPlan(viewDir, toCreate) {
|
|
|
5680
6097
|
const created = [];
|
|
5681
6098
|
const failed = [];
|
|
5682
6099
|
for (const { name, target } of toCreate) {
|
|
5683
|
-
const filePath =
|
|
6100
|
+
const filePath = join12(viewDir, name);
|
|
5684
6101
|
try {
|
|
5685
6102
|
mkdirSync(dirname4(filePath), { recursive: true });
|
|
5686
6103
|
symlinkSync(target, filePath);
|
|
@@ -5695,7 +6112,7 @@ var TOP_LEVEL_INSTRUCTION_FILES_LOWER = new Set(
|
|
|
5695
6112
|
INSTRUCTION_FILES.filter((f) => !f.includes("/")).map((f) => f.toLowerCase())
|
|
5696
6113
|
);
|
|
5697
6114
|
function classifyViewLink(viewDir, name, rosterRealpaths) {
|
|
5698
|
-
const filePath =
|
|
6115
|
+
const filePath = join12(viewDir, name);
|
|
5699
6116
|
let isLink;
|
|
5700
6117
|
try {
|
|
5701
6118
|
isLink = lstatSync(filePath).isSymbolicLink();
|
|
@@ -5709,12 +6126,12 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
|
|
|
5709
6126
|
} catch {
|
|
5710
6127
|
return null;
|
|
5711
6128
|
}
|
|
5712
|
-
const resolved =
|
|
6129
|
+
const resolved = isAbsolute4(target) ? target : resolve8(viewDir, target);
|
|
5713
6130
|
try {
|
|
5714
6131
|
if (rosterRealpaths.has(realpathSync(resolved))) return null;
|
|
5715
6132
|
} catch {
|
|
5716
6133
|
}
|
|
5717
|
-
if (
|
|
6134
|
+
if (isAbsolute4(target)) return { target, kind: "absolute" };
|
|
5718
6135
|
let isDir = false;
|
|
5719
6136
|
try {
|
|
5720
6137
|
isDir = statSync2(resolved).isDirectory();
|
|
@@ -5724,7 +6141,7 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
|
|
|
5724
6141
|
if (!isDir) {
|
|
5725
6142
|
return { target, kind: existsSync2(resolved) ? "non-repo" : "broken" };
|
|
5726
6143
|
}
|
|
5727
|
-
return { target, kind: existsSync2(
|
|
6144
|
+
return { target, kind: existsSync2(join12(resolved, ".git")) ? "repo" : "non-repo" };
|
|
5728
6145
|
}
|
|
5729
6146
|
function gatherExistingViewLinks(viewDir, rosterRealpaths) {
|
|
5730
6147
|
let names;
|
|
@@ -5749,7 +6166,7 @@ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
|
|
|
5749
6166
|
const pruned = [];
|
|
5750
6167
|
const failed = [];
|
|
5751
6168
|
for (const { name } of toPrune) {
|
|
5752
|
-
const filePath =
|
|
6169
|
+
const filePath = join12(viewDir, name);
|
|
5753
6170
|
const c = classifyViewLink(viewDir, name, rosterRealpaths);
|
|
5754
6171
|
if (c === null || c.kind !== "repo") {
|
|
5755
6172
|
failed.push({
|
|
@@ -5770,7 +6187,7 @@ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
|
|
|
5770
6187
|
async function doRunProjectWorkspace(options, ctx) {
|
|
5771
6188
|
const cwd = ctx.cwd ?? process.cwd();
|
|
5772
6189
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project workspace");
|
|
5773
|
-
const paths =
|
|
6190
|
+
const paths = basouPaths11(repositoryRoot);
|
|
5774
6191
|
const manifest = await readManifest6(paths);
|
|
5775
6192
|
const viewPath = manifest.workspace.view;
|
|
5776
6193
|
const roster = manifest.repos ?? [];
|
|
@@ -5795,11 +6212,11 @@ async function doRunProjectWorkspace(options, ctx) {
|
|
|
5795
6212
|
} else {
|
|
5796
6213
|
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
5797
6214
|
const facts = roster.map((entry) => gatherViewRepo(repositoryRoot, viewDir, entry));
|
|
5798
|
-
const rosterNames = roster.map((entry) => basename6(
|
|
6215
|
+
const rosterNames = roster.map((entry) => basename6(resolve8(repositoryRoot, entry.path)));
|
|
5799
6216
|
const rosterRealpaths = /* @__PURE__ */ new Set();
|
|
5800
6217
|
for (const entry of roster) {
|
|
5801
6218
|
try {
|
|
5802
|
-
rosterRealpaths.add(realpathSync(
|
|
6219
|
+
rosterRealpaths.add(realpathSync(resolve8(repositoryRoot, entry.path)));
|
|
5803
6220
|
} catch {
|
|
5804
6221
|
}
|
|
5805
6222
|
}
|
|
@@ -5962,10 +6379,10 @@ async function runProjectPreset(options, ctx = {}) {
|
|
|
5962
6379
|
}
|
|
5963
6380
|
}
|
|
5964
6381
|
function canonicalFileFor(anchorReal, canonicalName) {
|
|
5965
|
-
return
|
|
6382
|
+
return join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
5966
6383
|
}
|
|
5967
6384
|
function canonicalLabelFor(canonicalName) {
|
|
5968
|
-
return
|
|
6385
|
+
return join12("agents", canonicalName, CANONICAL_FILE);
|
|
5969
6386
|
}
|
|
5970
6387
|
async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
|
|
5971
6388
|
const declared = {
|
|
@@ -5979,14 +6396,14 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
|
|
|
5979
6396
|
}
|
|
5980
6397
|
let real;
|
|
5981
6398
|
try {
|
|
5982
|
-
real = realpathSync(
|
|
6399
|
+
real = realpathSync(resolve8(repositoryRoot, entry.path));
|
|
5983
6400
|
} catch {
|
|
5984
6401
|
return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
|
|
5985
6402
|
}
|
|
5986
6403
|
if (real === anchorReal) {
|
|
5987
6404
|
return { ...declared, isAnchor: true, reachable: true, canonicalPresent: false };
|
|
5988
6405
|
}
|
|
5989
|
-
if (!existsSync2(
|
|
6406
|
+
if (!existsSync2(join12(real, ".git"))) {
|
|
5990
6407
|
return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
|
|
5991
6408
|
}
|
|
5992
6409
|
const canonicalName = basename6(real);
|
|
@@ -6035,7 +6452,7 @@ function viewPresetReposFor(repositoryRoot, roster) {
|
|
|
6035
6452
|
anchorReal = void 0;
|
|
6036
6453
|
}
|
|
6037
6454
|
return roster.map((entry) => {
|
|
6038
|
-
const abs =
|
|
6455
|
+
const abs = resolve8(repositoryRoot, entry.path);
|
|
6039
6456
|
let real;
|
|
6040
6457
|
try {
|
|
6041
6458
|
real = realpathSync(abs);
|
|
@@ -6130,7 +6547,7 @@ function presetFailureReason(error) {
|
|
|
6130
6547
|
async function doRunProjectPreset(options, ctx) {
|
|
6131
6548
|
const cwd = ctx.cwd ?? process.cwd();
|
|
6132
6549
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project preset");
|
|
6133
|
-
const paths =
|
|
6550
|
+
const paths = basouPaths11(repositoryRoot);
|
|
6134
6551
|
const manifest = await readManifest6(paths);
|
|
6135
6552
|
const roster = manifest.repos ?? [];
|
|
6136
6553
|
const anchorReal = realpathSync(repositoryRoot);
|
|
@@ -6359,7 +6776,7 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
|
|
|
6359
6776
|
};
|
|
6360
6777
|
let real;
|
|
6361
6778
|
try {
|
|
6362
|
-
real = realpathSync(
|
|
6779
|
+
real = realpathSync(resolve8(repositoryRoot, target));
|
|
6363
6780
|
} catch {
|
|
6364
6781
|
return empty;
|
|
6365
6782
|
}
|
|
@@ -6368,24 +6785,24 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
|
|
|
6368
6785
|
const instructionFiles = [];
|
|
6369
6786
|
for (const name of INSTRUCTION_FILES) {
|
|
6370
6787
|
try {
|
|
6371
|
-
lstatSync(
|
|
6788
|
+
lstatSync(join12(real, name));
|
|
6372
6789
|
instructionFiles.push(name);
|
|
6373
6790
|
} catch {
|
|
6374
6791
|
}
|
|
6375
6792
|
}
|
|
6376
6793
|
let ignored;
|
|
6377
6794
|
try {
|
|
6378
|
-
ignored = new Set(readGitignoreLines(
|
|
6795
|
+
ignored = new Set(readGitignoreLines(join12(real, ".gitignore")).map((l) => l.trim()));
|
|
6379
6796
|
} catch {
|
|
6380
6797
|
ignored = /* @__PURE__ */ new Set();
|
|
6381
6798
|
}
|
|
6382
6799
|
const gitignorePatterns = INSTRUCTION_FILES.filter((p) => ignored.has(p) || ignored.has(`/${p}`));
|
|
6383
|
-
const canonical2 = existsSync2(
|
|
6800
|
+
const canonical2 = existsSync2(join12(anchorReal, "agents", canonicalName, CANONICAL_FILE));
|
|
6384
6801
|
let viewLink = false;
|
|
6385
6802
|
const viewPath = manifest.workspace.view;
|
|
6386
6803
|
if (viewPath !== void 0) {
|
|
6387
6804
|
try {
|
|
6388
|
-
lstatSync(
|
|
6805
|
+
lstatSync(join12(resolveViewDir(repositoryRoot, viewPath), canonicalName));
|
|
6389
6806
|
viewLink = true;
|
|
6390
6807
|
} catch {
|
|
6391
6808
|
}
|
|
@@ -6399,27 +6816,27 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
|
|
|
6399
6816
|
};
|
|
6400
6817
|
}
|
|
6401
6818
|
function teardownExpectedTargets(repoReal, anchorReal, canonicalName) {
|
|
6402
|
-
const canonicalFile =
|
|
6819
|
+
const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
6403
6820
|
return expectedSymlinkTargets(repoReal, canonicalFile);
|
|
6404
6821
|
}
|
|
6405
6822
|
function viewLinkPointsAt(viewDir, name, repoReal) {
|
|
6406
|
-
const filePath =
|
|
6823
|
+
const filePath = join12(viewDir, name);
|
|
6407
6824
|
try {
|
|
6408
6825
|
if (!lstatSync(filePath).isSymbolicLink()) return false;
|
|
6409
6826
|
const target = readlinkSync(filePath);
|
|
6410
|
-
if (
|
|
6411
|
-
return realpathSync(
|
|
6827
|
+
if (isAbsolute4(target)) return false;
|
|
6828
|
+
return realpathSync(resolve8(viewDir, target)) === repoReal;
|
|
6412
6829
|
} catch {
|
|
6413
6830
|
return false;
|
|
6414
6831
|
}
|
|
6415
6832
|
}
|
|
6416
6833
|
function viewLinkPointsAtPath(viewDir, name, expectedRepoPath) {
|
|
6417
|
-
const filePath =
|
|
6834
|
+
const filePath = join12(viewDir, name);
|
|
6418
6835
|
try {
|
|
6419
6836
|
if (!lstatSync(filePath).isSymbolicLink()) return false;
|
|
6420
6837
|
const target = readlinkSync(filePath);
|
|
6421
|
-
if (
|
|
6422
|
-
return
|
|
6838
|
+
if (isAbsolute4(target)) return false;
|
|
6839
|
+
return resolve8(viewDir, target) === expectedRepoPath;
|
|
6423
6840
|
} catch {
|
|
6424
6841
|
return false;
|
|
6425
6842
|
}
|
|
@@ -6428,19 +6845,19 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
6428
6845
|
const anchorReal = realpathSync(repositoryRoot);
|
|
6429
6846
|
let repoReal;
|
|
6430
6847
|
try {
|
|
6431
|
-
repoReal = realpathSync(
|
|
6848
|
+
repoReal = realpathSync(resolve8(repositoryRoot, target));
|
|
6432
6849
|
} catch {
|
|
6433
6850
|
repoReal = void 0;
|
|
6434
6851
|
}
|
|
6435
6852
|
const isAnchor = repoReal !== void 0 && repoReal === anchorReal;
|
|
6436
|
-
const targetAbs =
|
|
6853
|
+
const targetAbs = resolve8(repositoryRoot, target);
|
|
6437
6854
|
const canonicalName = basename6(repoReal ?? targetAbs);
|
|
6438
6855
|
const roster = manifest.repos ?? [];
|
|
6439
6856
|
const declaredEntry = roster.find((r) => {
|
|
6440
6857
|
try {
|
|
6441
|
-
return realpathSync(
|
|
6858
|
+
return realpathSync(resolve8(repositoryRoot, r.path)) === (repoReal ?? "\0");
|
|
6442
6859
|
} catch {
|
|
6443
|
-
return
|
|
6860
|
+
return resolve8(repositoryRoot, r.path) === targetAbs;
|
|
6444
6861
|
}
|
|
6445
6862
|
});
|
|
6446
6863
|
const inRoster = declaredEntry !== void 0;
|
|
@@ -6449,7 +6866,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
6449
6866
|
const canonicalShared = roster.some((r) => {
|
|
6450
6867
|
let rReal = null;
|
|
6451
6868
|
try {
|
|
6452
|
-
rReal = realpathSync(
|
|
6869
|
+
rReal = realpathSync(resolve8(repositoryRoot, r.path));
|
|
6453
6870
|
} catch {
|
|
6454
6871
|
rReal = null;
|
|
6455
6872
|
}
|
|
@@ -6457,15 +6874,15 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
6457
6874
|
if (repoReal !== void 0 && rReal === repoReal) return false;
|
|
6458
6875
|
return basename6(rReal).toLowerCase() === cnFold;
|
|
6459
6876
|
}
|
|
6460
|
-
if (
|
|
6461
|
-
return basename6(
|
|
6877
|
+
if (resolve8(repositoryRoot, r.path) === targetAbs) return false;
|
|
6878
|
+
return basename6(resolve8(repositoryRoot, r.path)).toLowerCase() === cnFold;
|
|
6462
6879
|
});
|
|
6463
6880
|
const collisionNote = "shared with another repo of the same basename, so it cannot be removed (check manually)";
|
|
6464
6881
|
const items = [];
|
|
6465
6882
|
if (!isAnchor) {
|
|
6466
6883
|
if (repoReal !== void 0) {
|
|
6467
6884
|
for (const spec of teardownExpectedTargets(repoReal, anchorReal, canonicalName)) {
|
|
6468
|
-
const { state, actualTarget } = inspectSymlink(
|
|
6885
|
+
const { state, actualTarget } = inspectSymlink(join12(repoReal, spec.name), spec.target);
|
|
6469
6886
|
if (isSelf) {
|
|
6470
6887
|
if (state !== "missing")
|
|
6471
6888
|
items.push({
|
|
@@ -6502,7 +6919,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
6502
6919
|
}
|
|
6503
6920
|
let ignored;
|
|
6504
6921
|
try {
|
|
6505
|
-
ignored = new Set(readGitignoreLines(
|
|
6922
|
+
ignored = new Set(readGitignoreLines(join12(repoReal, ".gitignore")).map((l) => l.trim()));
|
|
6506
6923
|
for (const p of INSTRUCTION_FILES) {
|
|
6507
6924
|
if (ignored.has(p) || ignored.has(`/${p}`)) {
|
|
6508
6925
|
items.push({
|
|
@@ -6525,7 +6942,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
6525
6942
|
const viewPath = manifest.workspace.view;
|
|
6526
6943
|
if (viewPath !== void 0) {
|
|
6527
6944
|
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
6528
|
-
const linkPath =
|
|
6945
|
+
const linkPath = join12(viewDir, canonicalName);
|
|
6529
6946
|
let isLink = false;
|
|
6530
6947
|
try {
|
|
6531
6948
|
isLink = lstatSync(linkPath).isSymbolicLink();
|
|
@@ -6551,8 +6968,8 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
6551
6968
|
else items.push({ kind: "view-symlink", label: canonicalName, state: "removable" });
|
|
6552
6969
|
}
|
|
6553
6970
|
}
|
|
6554
|
-
const canonicalFile =
|
|
6555
|
-
const canonicalLabel =
|
|
6971
|
+
const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
6972
|
+
const canonicalLabel = join12("agents", canonicalName, CANONICAL_FILE);
|
|
6556
6973
|
let canonicalIsLink = false;
|
|
6557
6974
|
try {
|
|
6558
6975
|
canonicalIsLink = lstatSync(canonicalFile).isSymbolicLink();
|
|
@@ -6639,7 +7056,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
6639
7056
|
const changed = (label) => failed.push({ label, message: "the state changed since the scan (re-run)" });
|
|
6640
7057
|
let currentRepoReal = null;
|
|
6641
7058
|
try {
|
|
6642
|
-
currentRepoReal = realpathSync(
|
|
7059
|
+
currentRepoReal = realpathSync(resolve8(repositoryRoot, plan.target));
|
|
6643
7060
|
} catch {
|
|
6644
7061
|
currentRepoReal = null;
|
|
6645
7062
|
}
|
|
@@ -6656,12 +7073,12 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
6656
7073
|
);
|
|
6657
7074
|
for (const item of removable.filter((i) => i.kind === "instruction-symlink")) {
|
|
6658
7075
|
const expected = expectedByName.get(item.label);
|
|
6659
|
-
if (repoReal === null || expected === void 0 || inspectSymlink(
|
|
7076
|
+
if (repoReal === null || expected === void 0 || inspectSymlink(join12(repoReal, item.label), expected).state !== "correct") {
|
|
6660
7077
|
changed(item.label);
|
|
6661
7078
|
continue;
|
|
6662
7079
|
}
|
|
6663
7080
|
try {
|
|
6664
|
-
unlinkSync(
|
|
7081
|
+
unlinkSync(join12(repoReal, item.label));
|
|
6665
7082
|
removed.push(item.label);
|
|
6666
7083
|
} catch (error) {
|
|
6667
7084
|
failed.push({ label: item.label, message: failureReason(error) });
|
|
@@ -6674,13 +7091,13 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
6674
7091
|
continue;
|
|
6675
7092
|
}
|
|
6676
7093
|
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
6677
|
-
const owned = repoReal !== null ? viewLinkPointsAt(viewDir, item.label, repoReal) : viewLinkPointsAtPath(viewDir, item.label,
|
|
7094
|
+
const owned = repoReal !== null ? viewLinkPointsAt(viewDir, item.label, repoReal) : viewLinkPointsAtPath(viewDir, item.label, resolve8(repositoryRoot, plan.target));
|
|
6678
7095
|
if (!owned) {
|
|
6679
7096
|
changed(item.label);
|
|
6680
7097
|
continue;
|
|
6681
7098
|
}
|
|
6682
7099
|
try {
|
|
6683
|
-
unlinkSync(
|
|
7100
|
+
unlinkSync(join12(viewDir, item.label));
|
|
6684
7101
|
removed.push(`view/${item.label}`);
|
|
6685
7102
|
} catch (error) {
|
|
6686
7103
|
failed.push({ label: `view/${item.label}`, message: failureReason(error) });
|
|
@@ -6688,7 +7105,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
6688
7105
|
}
|
|
6689
7106
|
const NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
|
|
6690
7107
|
for (const item of removable.filter((i) => i.kind === "canonical-block")) {
|
|
6691
|
-
const canonicalFile =
|
|
7108
|
+
const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
6692
7109
|
try {
|
|
6693
7110
|
if (lstatSync(canonicalFile).isSymbolicLink()) {
|
|
6694
7111
|
changed(item.label);
|
|
@@ -6789,7 +7206,7 @@ function renderProjectTeardown(result) {
|
|
|
6789
7206
|
async function doRunProjectTeardown(target, options, ctx = {}) {
|
|
6790
7207
|
const cwd = ctx.cwd ?? process.cwd();
|
|
6791
7208
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project teardown");
|
|
6792
|
-
const paths =
|
|
7209
|
+
const paths = basouPaths11(repositoryRoot);
|
|
6793
7210
|
const manifest = await readManifest6(paths);
|
|
6794
7211
|
const plan = gatherRepoTeardown(repositoryRoot, manifest, target);
|
|
6795
7212
|
const willApply = options.apply === true && !plan.isAnchor && plan.removableCount > 0;
|
|
@@ -6831,12 +7248,12 @@ function buildArchivedManifest(manifest, plan, updatedAt) {
|
|
|
6831
7248
|
async function doRunProjectArchive(target, options, ctx) {
|
|
6832
7249
|
const cwd = ctx.cwd ?? process.cwd();
|
|
6833
7250
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project archive");
|
|
6834
|
-
const paths =
|
|
7251
|
+
const paths = basouPaths11(repositoryRoot);
|
|
6835
7252
|
const manifest = await readManifest6(paths);
|
|
6836
7253
|
const roster = manifest.repos ?? [];
|
|
6837
7254
|
let targetIsAnchor = false;
|
|
6838
7255
|
try {
|
|
6839
|
-
targetIsAnchor = realpathSync(
|
|
7256
|
+
targetIsAnchor = realpathSync(resolve8(repositoryRoot, target)) === realpathSync(repositoryRoot);
|
|
6840
7257
|
} catch {
|
|
6841
7258
|
targetIsAnchor = false;
|
|
6842
7259
|
}
|
|
@@ -6962,12 +7379,12 @@ function gatherRenameWiring(repositoryRoot, manifest, oldBasename) {
|
|
|
6962
7379
|
} catch {
|
|
6963
7380
|
return { canonicalDirOld: false, viewLinkOld: false };
|
|
6964
7381
|
}
|
|
6965
|
-
const canonicalDirOld = existsSync2(
|
|
7382
|
+
const canonicalDirOld = existsSync2(join12(anchorReal, "agents", oldBasename));
|
|
6966
7383
|
let viewLinkOld = false;
|
|
6967
7384
|
const viewPath = manifest.workspace.view;
|
|
6968
7385
|
if (viewPath !== void 0) {
|
|
6969
7386
|
try {
|
|
6970
|
-
lstatSync(
|
|
7387
|
+
lstatSync(join12(resolveViewDir(repositoryRoot, viewPath), oldBasename));
|
|
6971
7388
|
viewLinkOld = true;
|
|
6972
7389
|
} catch {
|
|
6973
7390
|
}
|
|
@@ -6988,12 +7405,12 @@ function buildRenamedManifest(manifest, plan, updatedAt) {
|
|
|
6988
7405
|
async function doRunProjectRename(oldPath, newPath, options, ctx) {
|
|
6989
7406
|
const cwd = ctx.cwd ?? process.cwd();
|
|
6990
7407
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project rename");
|
|
6991
|
-
const paths =
|
|
7408
|
+
const paths = basouPaths11(repositoryRoot);
|
|
6992
7409
|
const manifest = await readManifest6(paths);
|
|
6993
7410
|
const roster = manifest.repos ?? [];
|
|
6994
7411
|
let oldIsAnchor = false;
|
|
6995
7412
|
try {
|
|
6996
|
-
oldIsAnchor = realpathSync(
|
|
7413
|
+
oldIsAnchor = realpathSync(resolve8(repositoryRoot, oldPath)) === realpathSync(repositoryRoot);
|
|
6997
7414
|
} catch {
|
|
6998
7415
|
oldIsAnchor = false;
|
|
6999
7416
|
}
|
|
@@ -7112,7 +7529,7 @@ async function runProjectNew(repos, options, ctx = {}) {
|
|
|
7112
7529
|
}
|
|
7113
7530
|
async function resolveRepositoryRootForNew(cwd) {
|
|
7114
7531
|
try {
|
|
7115
|
-
return await
|
|
7532
|
+
return await resolveRepositoryRoot7(cwd);
|
|
7116
7533
|
} catch (error) {
|
|
7117
7534
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
7118
7535
|
throw new Error(
|
|
@@ -7139,7 +7556,7 @@ async function doRunProjectNew(repos, options, ctx) {
|
|
|
7139
7556
|
const viewStem = productName ?? workspaceName;
|
|
7140
7557
|
const viewOverridesProjectName = productName !== void 0 && typeof options.view === "string";
|
|
7141
7558
|
const declared = repos.map((p) => {
|
|
7142
|
-
const abs =
|
|
7559
|
+
const abs = resolve8(cwd, p);
|
|
7143
7560
|
let real;
|
|
7144
7561
|
try {
|
|
7145
7562
|
real = realpathSync(abs);
|
|
@@ -7164,7 +7581,7 @@ async function doRunProjectNew(repos, options, ctx) {
|
|
|
7164
7581
|
const roster = rosterPaths.map((path) => ({ path }));
|
|
7165
7582
|
const viewPath = options.view === false ? null : options.view ?? `../${viewStem}-workspace`;
|
|
7166
7583
|
const sourceRoots = [...rosterPaths, ...viewPath !== null ? [viewPath] : []];
|
|
7167
|
-
const paths =
|
|
7584
|
+
const paths = basouPaths11(repositoryRoot);
|
|
7168
7585
|
const existed = existsSync2(paths.files.manifest);
|
|
7169
7586
|
const manifest = createManifest2({
|
|
7170
7587
|
workspaceName,
|
|
@@ -7275,7 +7692,7 @@ async function runProjectDerive(options, ctx = {}) {
|
|
|
7275
7692
|
async function doRunProjectDerive(options, ctx) {
|
|
7276
7693
|
const cwd = ctx.cwd ?? process.cwd();
|
|
7277
7694
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project derive");
|
|
7278
|
-
const paths =
|
|
7695
|
+
const paths = basouPaths11(repositoryRoot);
|
|
7279
7696
|
const manifest = await readManifest6(paths);
|
|
7280
7697
|
if (manifest.repos === void 0 || manifest.repos.length === 0) {
|
|
7281
7698
|
console.log(
|
|
@@ -7316,7 +7733,7 @@ async function doRunProjectDerive(options, ctx) {
|
|
|
7316
7733
|
async function doRunProjectSeedAnchor(options, ctx) {
|
|
7317
7734
|
const cwd = ctx.cwd ?? process.cwd();
|
|
7318
7735
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project derive");
|
|
7319
|
-
const paths =
|
|
7736
|
+
const paths = basouPaths11(repositoryRoot);
|
|
7320
7737
|
const manifest = await readManifest6(paths);
|
|
7321
7738
|
const roster = manifest.repos ?? [];
|
|
7322
7739
|
console.log("# Anchor instruction-file seed (the planning master's own AGENTS.md)");
|
|
@@ -7325,7 +7742,7 @@ async function doRunProjectSeedAnchor(options, ctx) {
|
|
|
7325
7742
|
console.log("\u2139\uFE0F No repo roster declared \u2014 nothing to seed.");
|
|
7326
7743
|
return;
|
|
7327
7744
|
}
|
|
7328
|
-
const anchorDoc =
|
|
7745
|
+
const anchorDoc = join12(repositoryRoot, CANONICAL_FILE);
|
|
7329
7746
|
if (pathPresent(anchorDoc)) {
|
|
7330
7747
|
console.log(
|
|
7331
7748
|
`\u2705 The anchor's own \`${CANONICAL_FILE}\` already exists \u2014 hand-maintained, left untouched.`
|
|
@@ -7387,7 +7804,7 @@ function regularFileSpokes(repoReal) {
|
|
|
7387
7804
|
const out = [];
|
|
7388
7805
|
for (const spoke of ["CLAUDE.md", ".github/copilot-instructions.md"]) {
|
|
7389
7806
|
try {
|
|
7390
|
-
const st = lstatSync(
|
|
7807
|
+
const st = lstatSync(join12(repoReal, spoke));
|
|
7391
7808
|
if (!st.isSymbolicLink() && st.isFile()) out.push(spoke);
|
|
7392
7809
|
} catch {
|
|
7393
7810
|
}
|
|
@@ -7402,9 +7819,9 @@ function pathPresent(p) {
|
|
|
7402
7819
|
return false;
|
|
7403
7820
|
}
|
|
7404
7821
|
}
|
|
7405
|
-
function gatherRetrofit(repositoryRoot, anchorReal, roster,
|
|
7822
|
+
function gatherRetrofit(repositoryRoot, anchorReal, roster, argAbs, argReal, viewCanonicalName) {
|
|
7406
7823
|
const declaredEntry = roster.find((entry) => {
|
|
7407
|
-
const entryAbs =
|
|
7824
|
+
const entryAbs = resolve8(repositoryRoot, entry.path);
|
|
7408
7825
|
if (argReal !== void 0) {
|
|
7409
7826
|
try {
|
|
7410
7827
|
if (realpathSync(entryAbs) === argReal) return true;
|
|
@@ -7433,8 +7850,8 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
|
|
|
7433
7850
|
};
|
|
7434
7851
|
}
|
|
7435
7852
|
const isAnchor = argReal === anchorReal;
|
|
7436
|
-
const reachable = existsSync2(
|
|
7437
|
-
const canonicalFile =
|
|
7853
|
+
const reachable = existsSync2(join12(argReal, ".git"));
|
|
7854
|
+
const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
7438
7855
|
return {
|
|
7439
7856
|
path,
|
|
7440
7857
|
declared,
|
|
@@ -7443,13 +7860,13 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
|
|
|
7443
7860
|
reachable,
|
|
7444
7861
|
canonicalName,
|
|
7445
7862
|
...viewCanonicalName !== void 0 ? { viewCanonicalName } : {},
|
|
7446
|
-
agentsState: inspectAgentsState(
|
|
7863
|
+
agentsState: inspectAgentsState(join12(argReal, CANONICAL_FILE)),
|
|
7447
7864
|
canonicalExists: pathPresent(canonicalFile),
|
|
7448
7865
|
regularSpokes: regularFileSpokes(argReal)
|
|
7449
7866
|
};
|
|
7450
7867
|
}
|
|
7451
7868
|
function relocateAgentsFile(repoReal, canonicalFile) {
|
|
7452
|
-
const agentsFile =
|
|
7869
|
+
const agentsFile = join12(repoReal, CANONICAL_FILE);
|
|
7453
7870
|
try {
|
|
7454
7871
|
mkdirSync(dirname4(canonicalFile), { recursive: true });
|
|
7455
7872
|
} catch (error) {
|
|
@@ -7510,7 +7927,7 @@ async function applyViewRetrofit(anchorReal, outcome) {
|
|
|
7510
7927
|
async function doRunProjectRetrofit(repo, options, ctx) {
|
|
7511
7928
|
const cwd = ctx.cwd ?? process.cwd();
|
|
7512
7929
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "project retrofit");
|
|
7513
|
-
const paths =
|
|
7930
|
+
const paths = basouPaths11(repositoryRoot);
|
|
7514
7931
|
const manifest = await readManifest6(paths);
|
|
7515
7932
|
const roster = manifest.repos ?? [];
|
|
7516
7933
|
const anchorReal = realpathSync(repositoryRoot);
|
|
@@ -7544,7 +7961,7 @@ async function doRunProjectRetrofit(repo, options, ctx) {
|
|
|
7544
7961
|
}
|
|
7545
7962
|
return result2;
|
|
7546
7963
|
}
|
|
7547
|
-
const argAbs =
|
|
7964
|
+
const argAbs = resolve8(repositoryRoot, repo);
|
|
7548
7965
|
let argReal;
|
|
7549
7966
|
try {
|
|
7550
7967
|
argReal = realpathSync(argAbs);
|
|
@@ -7556,7 +7973,6 @@ async function doRunProjectRetrofit(repo, options, ctx) {
|
|
|
7556
7973
|
repositoryRoot,
|
|
7557
7974
|
anchorReal,
|
|
7558
7975
|
roster,
|
|
7559
|
-
repo,
|
|
7560
7976
|
argAbs,
|
|
7561
7977
|
argReal,
|
|
7562
7978
|
viewCanonicalName
|
|
@@ -7566,7 +7982,7 @@ async function doRunProjectRetrofit(repo, options, ctx) {
|
|
|
7566
7982
|
let failure;
|
|
7567
7983
|
let partial = false;
|
|
7568
7984
|
if (options.apply === true && plan.action === "relocate" && argReal !== void 0) {
|
|
7569
|
-
const canonicalFile =
|
|
7985
|
+
const canonicalFile = join12(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
|
|
7570
7986
|
const res = relocateAgentsFile(argReal, canonicalFile);
|
|
7571
7987
|
if (res.ok) {
|
|
7572
7988
|
applied = true;
|
|
@@ -7766,88 +8182,19 @@ function renderProjectRetrofit(result) {
|
|
|
7766
8182
|
}
|
|
7767
8183
|
|
|
7768
8184
|
// src/commands/protocol.ts
|
|
7769
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
7770
|
-
import {
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
if (p.startsWith("~/")) return join12(homedir8(), p.slice(2));
|
|
7783
|
-
return p;
|
|
7784
|
-
}
|
|
7785
|
-
function isRecord3(value) {
|
|
7786
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7787
|
-
}
|
|
7788
|
-
async function loadProtocolsConfig(configPath = DEFAULT_PROTOCOLS_CONFIG_PATH) {
|
|
7789
|
-
let raw;
|
|
7790
|
-
try {
|
|
7791
|
-
raw = await readYamlFile5(configPath);
|
|
7792
|
-
} catch (error) {
|
|
7793
|
-
if (error instanceof Error && error.message === "YAML file not found") {
|
|
7794
|
-
throw new Error(
|
|
7795
|
-
"No protocols config at ~/.basou/protocols.yaml. Create one (a 'protocols:' list of source markdown paths) before running 'basou protocol sync'."
|
|
7796
|
-
);
|
|
7797
|
-
}
|
|
7798
|
-
if (error instanceof Error && error.message === "Failed to parse YAML content") {
|
|
7799
|
-
throw new Error("~/.basou/protocols.yaml is not valid YAML.");
|
|
7800
|
-
}
|
|
7801
|
-
throw error;
|
|
7802
|
-
}
|
|
7803
|
-
if (!isRecord3(raw) || !Array.isArray(raw.protocols)) {
|
|
7804
|
-
throw new Error("~/.basou/protocols.yaml must contain a 'protocols:' list.");
|
|
7805
|
-
}
|
|
7806
|
-
for (const key of Object.keys(raw)) {
|
|
7807
|
-
if (!ALLOWED_TOP_KEYS.has(key)) {
|
|
7808
|
-
throw new Error(
|
|
7809
|
-
`~/.basou/protocols.yaml has an unknown key '${key}' (allowed: version, protocols).`
|
|
7810
|
-
);
|
|
7811
|
-
}
|
|
7812
|
-
}
|
|
7813
|
-
const seen = /* @__PURE__ */ new Set();
|
|
7814
|
-
const result = [];
|
|
7815
|
-
for (const entry of raw.protocols) {
|
|
7816
|
-
if (!isRecord3(entry)) {
|
|
7817
|
-
throw new Error("Each protocol entry must be a mapping with a 'source' key.");
|
|
7818
|
-
}
|
|
7819
|
-
for (const key of Object.keys(entry)) {
|
|
7820
|
-
if (!ALLOWED_ENTRY_KEYS.has(key)) {
|
|
7821
|
-
throw new Error(`A protocol entry has an unknown key '${key}' (allowed: source, title).`);
|
|
7822
|
-
}
|
|
7823
|
-
}
|
|
7824
|
-
if (typeof entry.source !== "string" || entry.source.trim().length === 0) {
|
|
7825
|
-
throw new Error("Each protocol entry needs a non-empty string 'source'.");
|
|
7826
|
-
}
|
|
7827
|
-
if (entry.title !== void 0 && (typeof entry.title !== "string" || entry.title.trim().length === 0)) {
|
|
7828
|
-
throw new Error("A protocol entry 'title' must be a non-empty string when present.");
|
|
7829
|
-
}
|
|
7830
|
-
const expanded = expandTilde3(entry.source.trim());
|
|
7831
|
-
if (!isAbsolute4(expanded)) {
|
|
7832
|
-
throw new Error("Protocol 'source' paths must be absolute (or start with '~').");
|
|
7833
|
-
}
|
|
7834
|
-
const abs = resolve8(expanded);
|
|
7835
|
-
if (seen.has(abs)) {
|
|
7836
|
-
throw new Error("Duplicate protocol source (each source path may appear only once).");
|
|
7837
|
-
}
|
|
7838
|
-
seen.add(abs);
|
|
7839
|
-
result.push(
|
|
7840
|
-
entry.title !== void 0 ? { source: abs, title: entry.title.trim() } : { source: abs }
|
|
7841
|
-
);
|
|
7842
|
-
}
|
|
7843
|
-
if (result.length === 0) {
|
|
7844
|
-
throw new Error("~/.basou/protocols.yaml has no protocols.");
|
|
7845
|
-
}
|
|
7846
|
-
return result;
|
|
7847
|
-
}
|
|
7848
|
-
|
|
7849
|
-
// src/commands/protocol.ts
|
|
7850
|
-
var PROTOCOL_MARKERS = { start: PROTOCOL_START, end: PROTOCOL_END };
|
|
8185
|
+
import { readFile as readFile4, stat as stat5 } from "fs/promises";
|
|
8186
|
+
import {
|
|
8187
|
+
carryForwardProtocolStamp,
|
|
8188
|
+
PROTOCOL_END as PROTOCOL_END2,
|
|
8189
|
+
PROTOCOL_START as PROTOCOL_START2,
|
|
8190
|
+
parseMarkers as parseMarkers4,
|
|
8191
|
+
parseProtocolStamp as parseProtocolStamp2,
|
|
8192
|
+
protocolBlockHash,
|
|
8193
|
+
readMarkdownFile as readMarkdownFile8,
|
|
8194
|
+
renderProtocolStamp,
|
|
8195
|
+
unstampedProtocolSectionsFrom
|
|
8196
|
+
} from "@basou/core";
|
|
8197
|
+
var PROTOCOL_MARKERS = { start: PROTOCOL_START2, end: PROTOCOL_END2 };
|
|
7851
8198
|
var MANAGED_NOTE = "<!-- Managed by basou: 'basou protocol sync' regenerates everything between the BASOU:PROTOCOLS markers from ~/.basou/protocols.yaml. Manual edits inside the block are overwritten; edit the source files instead. -->";
|
|
7852
8199
|
function registerProtocolCommand(program) {
|
|
7853
8200
|
const protocol = program.command("protocol").description("Manage the basou-managed standing-protocol block in the global CLAUDE.md");
|
|
@@ -7907,24 +8254,54 @@ async function readProtocolSources(entries) {
|
|
|
7907
8254
|
}
|
|
7908
8255
|
return out;
|
|
7909
8256
|
}
|
|
7910
|
-
function
|
|
7911
|
-
|
|
8257
|
+
function buildSections(sources) {
|
|
8258
|
+
return sources.map(({ entry, content }) => {
|
|
7912
8259
|
const body = content.replace(/\s+$/, "");
|
|
7913
8260
|
return entry.title !== void 0 ? `## ${entry.title}
|
|
7914
8261
|
|
|
7915
8262
|
${body}` : body;
|
|
7916
|
-
});
|
|
8263
|
+
}).join("\n\n");
|
|
8264
|
+
}
|
|
8265
|
+
function buildBlock(sections, stamp) {
|
|
7917
8266
|
return `${MANAGED_NOTE}
|
|
8267
|
+
${renderProtocolStamp(stamp)}
|
|
7918
8268
|
|
|
7919
|
-
${sections
|
|
8269
|
+
${sections}
|
|
7920
8270
|
`;
|
|
7921
8271
|
}
|
|
8272
|
+
async function readPreviousStamp(target) {
|
|
8273
|
+
const existing = await readMarkdownFile8(target);
|
|
8274
|
+
if (existing === null) return null;
|
|
8275
|
+
const section = parseMarkers4(existing, PROTOCOL_MARKERS);
|
|
8276
|
+
if (section.kind !== "ok") return null;
|
|
8277
|
+
const stamp = parseProtocolStamp2(section.generated);
|
|
8278
|
+
if (stamp !== null) return stamp;
|
|
8279
|
+
const writtenAt = await lastWrittenAt(target);
|
|
8280
|
+
if (writtenAt === null) return null;
|
|
8281
|
+
return {
|
|
8282
|
+
changedAt: writtenAt,
|
|
8283
|
+
contentHash: protocolBlockHash(unstampedProtocolSectionsFrom(section.generated))
|
|
8284
|
+
};
|
|
8285
|
+
}
|
|
8286
|
+
async function lastWrittenAt(target) {
|
|
8287
|
+
try {
|
|
8288
|
+
return new Date((await stat5(target)).mtimeMs).toISOString();
|
|
8289
|
+
} catch {
|
|
8290
|
+
return null;
|
|
8291
|
+
}
|
|
8292
|
+
}
|
|
7922
8293
|
async function doRunProtocolSync(options, ctx = {}) {
|
|
7923
8294
|
const configPath = options.config ?? DEFAULT_PROTOCOLS_CONFIG_PATH;
|
|
7924
8295
|
const target = options.target ?? DEFAULT_TARGET_PATH;
|
|
7925
8296
|
const entries = await loadProtocolsConfig(configPath);
|
|
7926
8297
|
const sources = await readProtocolSources(entries);
|
|
7927
|
-
const
|
|
8298
|
+
const sections = buildSections(sources);
|
|
8299
|
+
const stamp = carryForwardProtocolStamp({
|
|
8300
|
+
sections,
|
|
8301
|
+
previous: await readPreviousStamp(target),
|
|
8302
|
+
now: (/* @__PURE__ */ new Date()).toISOString()
|
|
8303
|
+
});
|
|
8304
|
+
const block = buildBlock(sections, stamp);
|
|
7928
8305
|
const foreign = await findForeignWorkspaceNames({
|
|
7929
8306
|
text: block,
|
|
7930
8307
|
configPath: ctx.portfolioConfigPath
|
|
@@ -7987,14 +8364,14 @@ async function doRunProtocolUnsync(options) {
|
|
|
7987
8364
|
// src/commands/refresh.ts
|
|
7988
8365
|
import {
|
|
7989
8366
|
assertBasouRootSafe as assertBasouRootSafe9,
|
|
7990
|
-
basouPaths as
|
|
8367
|
+
basouPaths as basouPaths12,
|
|
7991
8368
|
findErrorCode as findErrorCode9,
|
|
7992
8369
|
readManifest as readManifest7
|
|
7993
8370
|
} from "@basou/core";
|
|
7994
|
-
import { InvalidArgumentError as
|
|
8371
|
+
import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
|
|
7995
8372
|
|
|
7996
8373
|
// src/commands/refresh-watch.ts
|
|
7997
|
-
import { readdir as readdir2, stat as
|
|
8374
|
+
import { readdir as readdir2, stat as stat6 } from "fs/promises";
|
|
7998
8375
|
import { homedir as homedir9 } from "os";
|
|
7999
8376
|
import { join as join13 } from "path";
|
|
8000
8377
|
import { findErrorCode as findErrorCode8 } from "@basou/core";
|
|
@@ -8023,7 +8400,7 @@ async function scanSourceLogs(roots) {
|
|
|
8023
8400
|
await walk(full);
|
|
8024
8401
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
8025
8402
|
try {
|
|
8026
|
-
const info = await
|
|
8403
|
+
const info = await stat6(full);
|
|
8027
8404
|
out.set(full, { mtimeMs: info.mtimeMs, size: info.size });
|
|
8028
8405
|
} catch (error) {
|
|
8029
8406
|
if (findErrorCode8(error, "ENOENT")) continue;
|
|
@@ -8118,7 +8495,7 @@ function collectPath2(value, previous) {
|
|
|
8118
8495
|
function parseInterval(value) {
|
|
8119
8496
|
const seconds = Number(value);
|
|
8120
8497
|
if (!Number.isInteger(seconds) || seconds < MIN_WATCH_INTERVAL_SEC || seconds > MAX_WATCH_INTERVAL_SEC) {
|
|
8121
|
-
throw new
|
|
8498
|
+
throw new InvalidArgumentError4(
|
|
8122
8499
|
`--interval must be an integer between ${MIN_WATCH_INTERVAL_SEC} and ${MAX_WATCH_INTERVAL_SEC} (seconds).`
|
|
8123
8500
|
);
|
|
8124
8501
|
}
|
|
@@ -8229,7 +8606,7 @@ async function doRunRefreshWatch(options, ctx) {
|
|
|
8229
8606
|
if (options.force === true) throw new Error("--watch cannot be combined with --force.");
|
|
8230
8607
|
const cwd = ctx.cwd ?? process.cwd();
|
|
8231
8608
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "refresh");
|
|
8232
|
-
const paths =
|
|
8609
|
+
const paths = basouPaths12(repositoryRoot);
|
|
8233
8610
|
await assertWorkspaceInitialized8(paths.root);
|
|
8234
8611
|
const intervalMs = (options.interval ?? DEFAULT_WATCH_INTERVAL_SEC) * 1e3;
|
|
8235
8612
|
const controller = new AbortController();
|
|
@@ -8257,7 +8634,7 @@ async function doRunRefreshWatch(options, ctx) {
|
|
|
8257
8634
|
async function computeRefresh(options, ctx) {
|
|
8258
8635
|
const cwd = ctx.cwd ?? process.cwd();
|
|
8259
8636
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "refresh");
|
|
8260
|
-
const paths =
|
|
8637
|
+
const paths = basouPaths12(repositoryRoot);
|
|
8261
8638
|
await assertWorkspaceInitialized8(paths.root);
|
|
8262
8639
|
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
8263
8640
|
const result = await refreshAll({
|
|
@@ -8366,10 +8743,10 @@ async function assertWorkspaceInitialized8(basouRoot) {
|
|
|
8366
8743
|
import { isAbsolute as isAbsolute5, resolve as resolve9 } from "path";
|
|
8367
8744
|
import {
|
|
8368
8745
|
assertBasouRootSafe as assertBasouRootSafe10,
|
|
8369
|
-
basouPaths as
|
|
8746
|
+
basouPaths as basouPaths13,
|
|
8370
8747
|
findErrorCode as findErrorCode10,
|
|
8371
8748
|
renderReport,
|
|
8372
|
-
resolveRepositoryRoot as
|
|
8749
|
+
resolveRepositoryRoot as resolveRepositoryRoot8,
|
|
8373
8750
|
writeMarkdownFile as writeMarkdownFile6
|
|
8374
8751
|
} from "@basou/core";
|
|
8375
8752
|
function registerReportCommand(program) {
|
|
@@ -8391,7 +8768,7 @@ async function runReportGenerate(options, ctx = {}) {
|
|
|
8391
8768
|
async function doRunReportGenerate(options, ctx) {
|
|
8392
8769
|
const cwd = ctx.cwd ?? process.cwd();
|
|
8393
8770
|
const repositoryRoot = await resolveRepositoryRootForReport(cwd);
|
|
8394
|
-
const paths =
|
|
8771
|
+
const paths = basouPaths13(repositoryRoot);
|
|
8395
8772
|
await assertWorkspaceInitialized9(paths.root);
|
|
8396
8773
|
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
8397
8774
|
const result = await renderReport({
|
|
@@ -8418,7 +8795,7 @@ async function doRunReportGenerate(options, ctx) {
|
|
|
8418
8795
|
}
|
|
8419
8796
|
async function resolveRepositoryRootForReport(cwd) {
|
|
8420
8797
|
try {
|
|
8421
|
-
return await
|
|
8798
|
+
return await resolveRepositoryRoot8(cwd);
|
|
8422
8799
|
} catch (error) {
|
|
8423
8800
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
8424
8801
|
throw new Error(
|
|
@@ -8446,7 +8823,7 @@ import { homedir as homedir10 } from "os";
|
|
|
8446
8823
|
import { resolve as resolve10 } from "path";
|
|
8447
8824
|
import {
|
|
8448
8825
|
assertBasouRootSafe as assertBasouRootSafe11,
|
|
8449
|
-
basouPaths as
|
|
8826
|
+
basouPaths as basouPaths14,
|
|
8450
8827
|
buildReviewRecordedEvent,
|
|
8451
8828
|
buildReviewRecordLabel,
|
|
8452
8829
|
createAdHocSessionWithEvent as createAdHocSessionWithEvent3,
|
|
@@ -8528,7 +8905,7 @@ async function runReviewRecord(options, ctx = {}) {
|
|
|
8528
8905
|
async function doRunReviewRecord(options, ctx) {
|
|
8529
8906
|
const cwd = ctx.cwd ?? process.cwd();
|
|
8530
8907
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "review record");
|
|
8531
|
-
const paths =
|
|
8908
|
+
const paths = basouPaths14(repositoryRoot);
|
|
8532
8909
|
await assertWorkspaceInitialized10(paths.root);
|
|
8533
8910
|
const raw = await readReviewInput(options, ctx);
|
|
8534
8911
|
const review = parseReviewRecordInput(raw);
|
|
@@ -8675,17 +9052,17 @@ async function assertWorkspaceInitialized10(basouRoot) {
|
|
|
8675
9052
|
|
|
8676
9053
|
// src/commands/review-gaps.ts
|
|
8677
9054
|
import {
|
|
8678
|
-
basouPaths as
|
|
9055
|
+
basouPaths as basouPaths15,
|
|
8679
9056
|
findReviewGaps
|
|
8680
9057
|
} from "@basou/core";
|
|
8681
|
-
import { InvalidArgumentError as
|
|
9058
|
+
import { InvalidArgumentError as InvalidArgumentError5 } from "commander";
|
|
8682
9059
|
function collectRepo(value, previous) {
|
|
8683
9060
|
return [...previous, value];
|
|
8684
9061
|
}
|
|
8685
9062
|
function parseWindow(value) {
|
|
8686
9063
|
const hours = Number(value);
|
|
8687
9064
|
if (!Number.isInteger(hours) || hours <= 0) {
|
|
8688
|
-
throw new
|
|
9065
|
+
throw new InvalidArgumentError5("--window must be a positive integer (hours).");
|
|
8689
9066
|
}
|
|
8690
9067
|
return hours;
|
|
8691
9068
|
}
|
|
@@ -8716,7 +9093,7 @@ async function runReviewGaps(options, ctx = {}) {
|
|
|
8716
9093
|
async function doRunReviewGaps(options, ctx) {
|
|
8717
9094
|
const cwd = ctx.cwd ?? process.cwd();
|
|
8718
9095
|
const repositoryRoot = await resolveBasouRootForCommand(cwd, "review-gaps");
|
|
8719
|
-
const paths =
|
|
9096
|
+
const paths = basouPaths15(repositoryRoot);
|
|
8720
9097
|
const nowIso = (ctx.nowProvider?.() ?? /* @__PURE__ */ new Date()).toISOString();
|
|
8721
9098
|
const summary = await findReviewGaps({
|
|
8722
9099
|
paths,
|
|
@@ -8733,7 +9110,7 @@ async function doRunReviewGaps(options, ctx) {
|
|
|
8733
9110
|
}
|
|
8734
9111
|
return summary;
|
|
8735
9112
|
}
|
|
8736
|
-
function
|
|
9113
|
+
function relAge2(iso, now) {
|
|
8737
9114
|
if (iso === null) return "(unknown)";
|
|
8738
9115
|
const ms = now.getTime() - Date.parse(iso);
|
|
8739
9116
|
if (!Number.isFinite(ms) || ms < 0) return "just now";
|
|
@@ -8746,11 +9123,11 @@ function relAge(iso, now) {
|
|
|
8746
9123
|
function flatten(value) {
|
|
8747
9124
|
return value.replace(/\s+/gu, " ").trim();
|
|
8748
9125
|
}
|
|
8749
|
-
var
|
|
9126
|
+
var GRAPHEMES2 = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
8750
9127
|
function graphemes(value) {
|
|
8751
|
-
return [...
|
|
9128
|
+
return [...GRAPHEMES2.segment(value)].map((s) => s.segment);
|
|
8752
9129
|
}
|
|
8753
|
-
function
|
|
9130
|
+
function oneLine2(value, max) {
|
|
8754
9131
|
const flat = graphemes(flatten(value));
|
|
8755
9132
|
return flat.length > max ? `${flat.slice(0, max - 1).join("")}\u2026` : flat.join("");
|
|
8756
9133
|
}
|
|
@@ -8763,7 +9140,7 @@ var SELF_REPORTS_SHOWN = 3;
|
|
|
8763
9140
|
function selfReportSuffix(u, stillCounted) {
|
|
8764
9141
|
if (u.selfReports.length === 0) return "";
|
|
8765
9142
|
const parts = u.selfReports.slice(0, SELF_REPORTS_SHOWN).map(
|
|
8766
|
-
(r) => `${
|
|
9143
|
+
(r) => `${oneLine2(r.reviewer, 40)}${claimedCommits(r.commits)}${r.recordedAfterCommit ? " (recorded after the commit)" : ""}`
|
|
8767
9144
|
);
|
|
8768
9145
|
const rest = u.selfReports.length - parts.length;
|
|
8769
9146
|
if (rest > 0) parts.push(`+${rest} more`);
|
|
@@ -8776,7 +9153,7 @@ function unobservedOutcomeSuffix(u) {
|
|
|
8776
9153
|
return ` \xB7 ${scope}exited with no recorded status \u2014 landing assumed, not observed`;
|
|
8777
9154
|
}
|
|
8778
9155
|
function unitLine(u, now) {
|
|
8779
|
-
const when =
|
|
9156
|
+
const when = relAge2(u.lastCommitAt, now);
|
|
8780
9157
|
const head = `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"})`;
|
|
8781
9158
|
if (u.verdict === "near_unbound") {
|
|
8782
9159
|
const ids = u.reviews.map((r) => r.sessionId.slice(0, 14)).join(", ");
|
|
@@ -8785,7 +9162,7 @@ function unitLine(u, now) {
|
|
|
8785
9162
|
return `${head} \u2014 no bound cross-model review${selfReportSuffix(u, true)}${unobservedOutcomeSuffix(u)}`;
|
|
8786
9163
|
}
|
|
8787
9164
|
function candidateLine(u, now) {
|
|
8788
|
-
const when =
|
|
9165
|
+
const when = relAge2(u.lastCommitAt, now);
|
|
8789
9166
|
const cite = u.reviews.map((r) => `${r.sessionId.slice(0, 14)}${r.examinedDiff ? "(diff)" : ""}`).join(", ");
|
|
8790
9167
|
return `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"}) \u2014 review trace: ${cite}${selfReportSuffix(u, false)}${unobservedOutcomeSuffix(u)}`;
|
|
8791
9168
|
}
|
|
@@ -8821,7 +9198,7 @@ function renderReviewGaps(summary) {
|
|
|
8821
9198
|
);
|
|
8822
9199
|
for (const u of summary.unknowns) {
|
|
8823
9200
|
lines.push(
|
|
8824
|
-
`- ${
|
|
9201
|
+
`- ${relAge2(u.lastCommitAt, now)} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"}) [${u.sessionId}]`
|
|
8825
9202
|
);
|
|
8826
9203
|
}
|
|
8827
9204
|
lines.push("");
|
|
@@ -8834,7 +9211,7 @@ function renderReviewGaps(summary) {
|
|
|
8834
9211
|
}
|
|
8835
9212
|
lines.push("");
|
|
8836
9213
|
lines.push(
|
|
8837
|
-
`Note: read-only advisory. Only captured commits are in scope (newest captured commit: ${summary.newestCommitAt === null ? "none" :
|
|
9214
|
+
`Note: read-only advisory. Only captured commits are in scope (newest captured commit: ${summary.newestCommitAt === null ? "none" : relAge2(summary.newestCommitAt, now)}). It never auto-judges that a review "happened", and temporal proximity alone is not a pass. It does not enforce.`
|
|
8838
9215
|
);
|
|
8839
9216
|
if (summary.gaps.some((u) => u.selfReports.length > 0)) {
|
|
8840
9217
|
lines.push(
|
|
@@ -8884,7 +9261,7 @@ import { join as join14 } from "path";
|
|
|
8884
9261
|
import {
|
|
8885
9262
|
acquireLock as acquireLock5,
|
|
8886
9263
|
assertBasouRootSafe as assertBasouRootSafe12,
|
|
8887
|
-
basouPaths as
|
|
9264
|
+
basouPaths as basouPaths16,
|
|
8888
9265
|
ChildProcessRunner as ChildProcessRunner2,
|
|
8889
9266
|
claudeCodeAdapterMetadata,
|
|
8890
9267
|
codexAdapterMetadata,
|
|
@@ -8900,7 +9277,8 @@ import {
|
|
|
8900
9277
|
readYamlFile as readYamlFile6,
|
|
8901
9278
|
resolveClaudeCodeCommand,
|
|
8902
9279
|
resolveCodexCommand,
|
|
8903
|
-
resolveRepositoryRoot as
|
|
9280
|
+
resolveRepositoryRoot as resolveRepositoryRoot9,
|
|
9281
|
+
SESSION_SCHEMA_VERSION as SESSION_SCHEMA_VERSION2,
|
|
8904
9282
|
SessionSchema as SessionSchema2,
|
|
8905
9283
|
sanitizeRelatedFiles,
|
|
8906
9284
|
sanitizeWorkingDirectory as sanitizeWorkingDirectory2,
|
|
@@ -8955,7 +9333,7 @@ async function runTrackedTool(args, options, ctx, adapter) {
|
|
|
8955
9333
|
const childArgs = adapter.transformArgs ? adapter.transformArgs(args) : args;
|
|
8956
9334
|
const cwd = options.cwd ?? process.cwd();
|
|
8957
9335
|
const repoRoot = await resolveRepositoryRootForRun(cwd);
|
|
8958
|
-
const paths =
|
|
9336
|
+
const paths = basouPaths16(repoRoot);
|
|
8959
9337
|
await assertBasouRootSafe12(paths.root);
|
|
8960
9338
|
const manifest = await readManifest9(paths);
|
|
8961
9339
|
const sessionId = prefixedUlid4("ses");
|
|
@@ -9239,7 +9617,7 @@ function normalizeFileChangedSkipMessage(error) {
|
|
|
9239
9617
|
function buildInitialSession2(input) {
|
|
9240
9618
|
const cmdline = [input.command, ...input.args].join(" ");
|
|
9241
9619
|
return {
|
|
9242
|
-
schema_version:
|
|
9620
|
+
schema_version: SESSION_SCHEMA_VERSION2,
|
|
9243
9621
|
session: {
|
|
9244
9622
|
id: input.id,
|
|
9245
9623
|
label: `basou run ${cmdline} (${input.startedAt})`,
|
|
@@ -9310,7 +9688,7 @@ async function finalizeSessionAsFailed2(paths, sessionDir, sessionId, appendEven
|
|
|
9310
9688
|
}
|
|
9311
9689
|
async function resolveRepositoryRootForRun(cwd) {
|
|
9312
9690
|
try {
|
|
9313
|
-
return await
|
|
9691
|
+
return await resolveRepositoryRoot9(cwd);
|
|
9314
9692
|
} catch (error) {
|
|
9315
9693
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
9316
9694
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou run'.", {
|
|
@@ -9344,11 +9722,12 @@ import {
|
|
|
9344
9722
|
acquireLock as acquireLock6,
|
|
9345
9723
|
appendEventToExistingSession as appendEventToExistingSession3,
|
|
9346
9724
|
assertBasouRootSafe as assertBasouRootSafe13,
|
|
9347
|
-
basouPaths as
|
|
9725
|
+
basouPaths as basouPaths17,
|
|
9348
9726
|
EVENT_SCHEMA_VERSION as EVENT_SCHEMA_VERSION6,
|
|
9349
9727
|
enumerateSessionDirs as enumerateSessionDirs2,
|
|
9350
9728
|
findErrorCode as findErrorCode12,
|
|
9351
9729
|
importSessionFromJson as importSessionFromJson2,
|
|
9730
|
+
LOCAL_CLI_EVENT_SOURCE as LOCAL_CLI_EVENT_SOURCE4,
|
|
9352
9731
|
loadSessionEntries as loadSessionEntries2,
|
|
9353
9732
|
readAllEvents,
|
|
9354
9733
|
readManifest as readManifest10,
|
|
@@ -9363,7 +9742,7 @@ import {
|
|
|
9363
9742
|
SessionStatusSchema,
|
|
9364
9743
|
sessionWorkStatsFromEvents
|
|
9365
9744
|
} from "@basou/core";
|
|
9366
|
-
import { InvalidArgumentError as
|
|
9745
|
+
import { InvalidArgumentError as InvalidArgumentError6 } from "commander";
|
|
9367
9746
|
|
|
9368
9747
|
// src/lib/format-duration.ts
|
|
9369
9748
|
import { formatDurationMs } from "@basou/core";
|
|
@@ -9412,7 +9791,7 @@ async function runSessionList(options, ctx = {}) {
|
|
|
9412
9791
|
async function doRunSessionList(options, ctx) {
|
|
9413
9792
|
const cwd = ctx.cwd ?? process.cwd();
|
|
9414
9793
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "list");
|
|
9415
|
-
const paths =
|
|
9794
|
+
const paths = basouPaths17(repositoryRoot);
|
|
9416
9795
|
await assertWorkspaceInitialized11(paths.root);
|
|
9417
9796
|
const now = /* @__PURE__ */ new Date();
|
|
9418
9797
|
const records = (await loadSessionEntries2(paths, {
|
|
@@ -9464,7 +9843,7 @@ async function runSessionShow(idInput, options, ctx = {}) {
|
|
|
9464
9843
|
async function doRunSessionShow(idInput, options, ctx) {
|
|
9465
9844
|
const cwd = ctx.cwd ?? process.cwd();
|
|
9466
9845
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "show");
|
|
9467
|
-
const paths =
|
|
9846
|
+
const paths = basouPaths17(repositoryRoot);
|
|
9468
9847
|
await assertWorkspaceInitialized11(paths.root);
|
|
9469
9848
|
const sessionId = await resolveSessionId3(paths, idInput);
|
|
9470
9849
|
const sessionDir = join15(paths.sessions, sessionId);
|
|
@@ -9776,7 +10155,7 @@ async function runSessionImport(options, ctx = {}) {
|
|
|
9776
10155
|
async function doRunSessionImport(options, ctx) {
|
|
9777
10156
|
const cwd = ctx.cwd ?? process.cwd();
|
|
9778
10157
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "import");
|
|
9779
|
-
const paths =
|
|
10158
|
+
const paths = basouPaths17(repositoryRoot);
|
|
9780
10159
|
await assertWorkspaceInitialized11(paths.root);
|
|
9781
10160
|
const manifest = await readManifest10(paths);
|
|
9782
10161
|
const rawBody = await readInputFile(options.from);
|
|
@@ -9786,7 +10165,9 @@ async function doRunSessionImport(options, ctx) {
|
|
|
9786
10165
|
throw new Error("Invalid import payload", { cause: parsed.error });
|
|
9787
10166
|
}
|
|
9788
10167
|
if (parsed.data.schema_version !== SESSION_IMPORT_SCHEMA_VERSION2) {
|
|
9789
|
-
throw new Error(
|
|
10168
|
+
throw new Error(
|
|
10169
|
+
`Unsupported import schema_version: ${parsed.data.schema_version} (expected ${SESSION_IMPORT_SCHEMA_VERSION2}). The envelope's shape is published as @basou/core/schemas/session-import.schema.json; this version may move at a minor release, so a producer should read it from the installed @basou/core rather than pinning it.`
|
|
10170
|
+
);
|
|
9790
10171
|
}
|
|
9791
10172
|
const importOptions2 = { dryRun: options.dryRun === true };
|
|
9792
10173
|
if (options.label !== void 0) importOptions2.labelOverride = options.label;
|
|
@@ -9825,19 +10206,19 @@ function parseJsonStrict(body) {
|
|
|
9825
10206
|
}
|
|
9826
10207
|
function parseImportFormat(raw) {
|
|
9827
10208
|
if (raw !== "json") {
|
|
9828
|
-
throw new
|
|
10209
|
+
throw new InvalidArgumentError6(`Unsupported format: ${raw}. Valid values: json`);
|
|
9829
10210
|
}
|
|
9830
10211
|
return "json";
|
|
9831
10212
|
}
|
|
9832
10213
|
function parseLabelOverride(raw) {
|
|
9833
10214
|
if (raw.length === 0) {
|
|
9834
|
-
throw new
|
|
10215
|
+
throw new InvalidArgumentError6("Label must not be empty");
|
|
9835
10216
|
}
|
|
9836
10217
|
return raw;
|
|
9837
10218
|
}
|
|
9838
10219
|
function parseTaskIdOverride(raw) {
|
|
9839
10220
|
if (raw.length === 0) {
|
|
9840
|
-
throw new
|
|
10221
|
+
throw new InvalidArgumentError6("Task id is empty");
|
|
9841
10222
|
}
|
|
9842
10223
|
return raw;
|
|
9843
10224
|
}
|
|
@@ -9890,7 +10271,7 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
|
|
|
9890
10271
|
}
|
|
9891
10272
|
const cwd = ctx.cwd ?? process.cwd();
|
|
9892
10273
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "note");
|
|
9893
|
-
const paths =
|
|
10274
|
+
const paths = basouPaths17(repositoryRoot);
|
|
9894
10275
|
await assertWorkspaceInitialized11(paths.root);
|
|
9895
10276
|
const sessionId = await resolveSessionId3(paths, sessionIdInput);
|
|
9896
10277
|
const body = hasBody ? options.body : await readNoteFile(options.fromFile);
|
|
@@ -9910,7 +10291,7 @@ async function doRunSessionNote(sessionIdInput, options, ctx) {
|
|
|
9910
10291
|
id: eventId,
|
|
9911
10292
|
session_id: sesId,
|
|
9912
10293
|
occurred_at: occurredAt,
|
|
9913
|
-
source:
|
|
10294
|
+
source: LOCAL_CLI_EVENT_SOURCE4,
|
|
9914
10295
|
type: "note_added",
|
|
9915
10296
|
body
|
|
9916
10297
|
})
|
|
@@ -9935,7 +10316,7 @@ async function readNoteFile(path) {
|
|
|
9935
10316
|
}
|
|
9936
10317
|
function parseNoteBodyOption(raw) {
|
|
9937
10318
|
if (raw.length === 0) {
|
|
9938
|
-
throw new
|
|
10319
|
+
throw new InvalidArgumentError6("--body must not be empty");
|
|
9939
10320
|
}
|
|
9940
10321
|
return raw;
|
|
9941
10322
|
}
|
|
@@ -9972,7 +10353,7 @@ async function doRunSessionRechain(options, ctx) {
|
|
|
9972
10353
|
}
|
|
9973
10354
|
const cwd = ctx.cwd ?? process.cwd();
|
|
9974
10355
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "rechain");
|
|
9975
|
-
const paths =
|
|
10356
|
+
const paths = basouPaths17(repositoryRoot);
|
|
9976
10357
|
await assertWorkspaceInitialized11(paths.root);
|
|
9977
10358
|
const sessionIds = options.session !== void 0 ? [await resolveSessionId3(paths, options.session)] : await enumerateSessionDirs2(paths);
|
|
9978
10359
|
const dryRun = options.dryRun === true;
|
|
@@ -10027,10 +10408,10 @@ function renderRechainRow(row, dryRun) {
|
|
|
10027
10408
|
// src/commands/stats.ts
|
|
10028
10409
|
import {
|
|
10029
10410
|
assertBasouRootSafe as assertBasouRootSafe14,
|
|
10030
|
-
basouPaths as
|
|
10411
|
+
basouPaths as basouPaths18,
|
|
10031
10412
|
computeWorkStats,
|
|
10032
10413
|
findErrorCode as findErrorCode13,
|
|
10033
|
-
resolveRepositoryRoot as
|
|
10414
|
+
resolveRepositoryRoot as resolveRepositoryRoot10
|
|
10034
10415
|
} from "@basou/core";
|
|
10035
10416
|
function registerStatsCommand(program) {
|
|
10036
10417
|
program.command("stats").description("Report how much the AI worked (output volume + time proxies) across sessions").option("--by-source", "Break the totals down by session source kind").option("--by-day", "Break billable time and volume down by calendar day").option("--json", "Output the full stats as JSON").option("-v, --verbose", "Show error causes").action(async (options) => {
|
|
@@ -10048,7 +10429,7 @@ async function runStats(options, ctx = {}) {
|
|
|
10048
10429
|
async function doRunStats(options, ctx) {
|
|
10049
10430
|
const cwd = ctx.cwd ?? process.cwd();
|
|
10050
10431
|
const repositoryRoot = await resolveRepositoryRootForStats(cwd);
|
|
10051
|
-
const paths =
|
|
10432
|
+
const paths = basouPaths18(repositoryRoot);
|
|
10052
10433
|
await assertWorkspaceInitialized12(paths.root);
|
|
10053
10434
|
const now = ctx.nowProvider?.() ?? /* @__PURE__ */ new Date();
|
|
10054
10435
|
const result = await computeWorkStats({
|
|
@@ -10137,7 +10518,7 @@ function formatInt(n) {
|
|
|
10137
10518
|
}
|
|
10138
10519
|
async function resolveRepositoryRootForStats(cwd) {
|
|
10139
10520
|
try {
|
|
10140
|
-
return await
|
|
10521
|
+
return await resolveRepositoryRoot10(cwd);
|
|
10141
10522
|
} catch (error) {
|
|
10142
10523
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
10143
10524
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou stats'.", {
|
|
@@ -10161,11 +10542,11 @@ async function assertWorkspaceInitialized12(basouRoot) {
|
|
|
10161
10542
|
// src/commands/status.ts
|
|
10162
10543
|
import {
|
|
10163
10544
|
assertBasouRootSafe as assertBasouRootSafe15,
|
|
10164
|
-
basouPaths as
|
|
10545
|
+
basouPaths as basouPaths19,
|
|
10165
10546
|
buildStatusSnapshot,
|
|
10166
10547
|
findErrorCode as findErrorCode14,
|
|
10167
10548
|
readManifest as readManifest11,
|
|
10168
|
-
resolveRepositoryRoot as
|
|
10549
|
+
resolveRepositoryRoot as resolveRepositoryRoot11,
|
|
10169
10550
|
writeStatus
|
|
10170
10551
|
} from "@basou/core";
|
|
10171
10552
|
function registerStatusCommand(program) {
|
|
@@ -10184,7 +10565,7 @@ async function runStatus(options, ctx = {}) {
|
|
|
10184
10565
|
async function doRunStatus(options, ctx) {
|
|
10185
10566
|
const cwd = ctx.cwd ?? process.cwd();
|
|
10186
10567
|
const repositoryRoot = await resolveRepositoryRootForStatus(cwd);
|
|
10187
|
-
const paths =
|
|
10568
|
+
const paths = basouPaths19(repositoryRoot);
|
|
10188
10569
|
try {
|
|
10189
10570
|
await assertBasouRootSafe15(paths.root);
|
|
10190
10571
|
} catch (error) {
|
|
@@ -10223,7 +10604,7 @@ function renderTextStatus(s) {
|
|
|
10223
10604
|
}
|
|
10224
10605
|
async function resolveRepositoryRootForStatus(cwd) {
|
|
10225
10606
|
try {
|
|
10226
|
-
return await
|
|
10607
|
+
return await resolveRepositoryRoot11(cwd);
|
|
10227
10608
|
} catch (error) {
|
|
10228
10609
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
10229
10610
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou status'.", {
|
|
@@ -10252,7 +10633,7 @@ import { join as join16 } from "path";
|
|
|
10252
10633
|
import {
|
|
10253
10634
|
archiveTask,
|
|
10254
10635
|
assertBasouRootSafe as assertBasouRootSafe16,
|
|
10255
|
-
basouPaths as
|
|
10636
|
+
basouPaths as basouPaths20,
|
|
10256
10637
|
createTaskWithEvent,
|
|
10257
10638
|
deleteTask,
|
|
10258
10639
|
editTask,
|
|
@@ -10274,7 +10655,7 @@ import {
|
|
|
10274
10655
|
TaskWriteAfterEventError,
|
|
10275
10656
|
updateTaskStatusWithEvent
|
|
10276
10657
|
} from "@basou/core";
|
|
10277
|
-
import { InvalidArgumentError as
|
|
10658
|
+
import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
|
|
10278
10659
|
var STATUS_VALUES3 = TaskStatusSchema.options;
|
|
10279
10660
|
function registerTaskCommand(program) {
|
|
10280
10661
|
const task = program.command("task").description("Manage Basou tasks (purpose units that span sessions)");
|
|
@@ -10353,7 +10734,7 @@ async function doRunTaskNew(options, ctx) {
|
|
|
10353
10734
|
}
|
|
10354
10735
|
const cwd = ctx.cwd ?? process.cwd();
|
|
10355
10736
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "new");
|
|
10356
|
-
const paths =
|
|
10737
|
+
const paths = basouPaths20(repositoryRoot);
|
|
10357
10738
|
await assertWorkspaceInitialized13(paths.root);
|
|
10358
10739
|
const description = options.description !== void 0 ? options.description : options.fromFile !== void 0 ? await readDescriptionFile(options.fromFile) : "";
|
|
10359
10740
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
@@ -10462,7 +10843,7 @@ async function runTaskList(options, ctx = {}) {
|
|
|
10462
10843
|
async function doRunTaskList(options, ctx) {
|
|
10463
10844
|
const cwd = ctx.cwd ?? process.cwd();
|
|
10464
10845
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "list");
|
|
10465
|
-
const paths =
|
|
10846
|
+
const paths = basouPaths20(repositoryRoot);
|
|
10466
10847
|
await assertWorkspaceInitialized13(paths.root);
|
|
10467
10848
|
const entries = await loadTaskEntries(paths, {
|
|
10468
10849
|
onSkip: (id, reason) => printTaskSkip(id, reason)
|
|
@@ -10566,7 +10947,7 @@ async function runTaskShow(idInput, options, ctx = {}) {
|
|
|
10566
10947
|
async function doRunTaskShow(idInput, options, ctx) {
|
|
10567
10948
|
const cwd = ctx.cwd ?? process.cwd();
|
|
10568
10949
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "show");
|
|
10569
|
-
const paths =
|
|
10950
|
+
const paths = basouPaths20(repositoryRoot);
|
|
10570
10951
|
await assertWorkspaceInitialized13(paths.root);
|
|
10571
10952
|
const taskId = await resolveTaskId2(paths, idInput, { includeArchived: true });
|
|
10572
10953
|
const { doc, archived } = await readTaskFileWithArchiveFallback(paths, taskId);
|
|
@@ -10710,7 +11091,7 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
|
|
|
10710
11091
|
const newStatus = parseTaskStatusPositional(newStatusInput);
|
|
10711
11092
|
const cwd = ctx.cwd ?? process.cwd();
|
|
10712
11093
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "status");
|
|
10713
|
-
const paths =
|
|
11094
|
+
const paths = basouPaths20(repositoryRoot);
|
|
10714
11095
|
await assertWorkspaceInitialized13(paths.root);
|
|
10715
11096
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
10716
11097
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
@@ -10787,7 +11168,7 @@ async function runTaskReconcile(options, ctx = {}) {
|
|
|
10787
11168
|
async function doRunTaskReconcile(options, ctx) {
|
|
10788
11169
|
const cwd = ctx.cwd ?? process.cwd();
|
|
10789
11170
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "reconcile");
|
|
10790
|
-
const paths =
|
|
11171
|
+
const paths = basouPaths20(repositoryRoot);
|
|
10791
11172
|
await assertWorkspaceInitialized13(paths.root);
|
|
10792
11173
|
const manifest = await readManifest12(paths);
|
|
10793
11174
|
const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
|
|
@@ -10967,7 +11348,7 @@ async function doRunTaskRefreshLinkage(taskIdInput, options, ctx) {
|
|
|
10967
11348
|
}
|
|
10968
11349
|
const cwd = ctx.cwd ?? process.cwd();
|
|
10969
11350
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "refresh-linkage");
|
|
10970
|
-
const paths =
|
|
11351
|
+
const paths = basouPaths20(repositoryRoot);
|
|
10971
11352
|
await assertWorkspaceInitialized13(paths.root);
|
|
10972
11353
|
const manifest = await readManifest12(paths);
|
|
10973
11354
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
@@ -11047,7 +11428,7 @@ async function doRunTaskEdit(taskIdInput, options, ctx) {
|
|
|
11047
11428
|
}
|
|
11048
11429
|
const cwd = ctx.cwd ?? process.cwd();
|
|
11049
11430
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "edit");
|
|
11050
|
-
const paths =
|
|
11431
|
+
const paths = basouPaths20(repositoryRoot);
|
|
11051
11432
|
await assertWorkspaceInitialized13(paths.root);
|
|
11052
11433
|
const manifest = await readManifest12(paths);
|
|
11053
11434
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
@@ -11103,7 +11484,7 @@ async function doRunTaskDelete(taskIdInput, options, ctx) {
|
|
|
11103
11484
|
}
|
|
11104
11485
|
const cwd = ctx.cwd ?? process.cwd();
|
|
11105
11486
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "delete");
|
|
11106
|
-
const paths =
|
|
11487
|
+
const paths = basouPaths20(repositoryRoot);
|
|
11107
11488
|
await assertWorkspaceInitialized13(paths.root);
|
|
11108
11489
|
const manifest = await readManifest12(paths);
|
|
11109
11490
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
@@ -11148,7 +11529,7 @@ async function doRunTaskArchive(taskIdInput, options, ctx) {
|
|
|
11148
11529
|
}
|
|
11149
11530
|
const cwd = ctx.cwd ?? process.cwd();
|
|
11150
11531
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "archive");
|
|
11151
|
-
const paths =
|
|
11532
|
+
const paths = basouPaths20(repositoryRoot);
|
|
11152
11533
|
await assertWorkspaceInitialized13(paths.root);
|
|
11153
11534
|
const manifest = await readManifest12(paths);
|
|
11154
11535
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
@@ -11203,20 +11584,20 @@ async function readSingleLineFromStdin() {
|
|
|
11203
11584
|
}
|
|
11204
11585
|
function parseTitle2(raw) {
|
|
11205
11586
|
if (raw.length === 0) {
|
|
11206
|
-
throw new
|
|
11587
|
+
throw new InvalidArgumentError7("Title must not be empty");
|
|
11207
11588
|
}
|
|
11208
11589
|
return raw;
|
|
11209
11590
|
}
|
|
11210
11591
|
function parseLabel(raw) {
|
|
11211
11592
|
if (raw.length === 0) {
|
|
11212
|
-
throw new
|
|
11593
|
+
throw new InvalidArgumentError7("Label must not be empty");
|
|
11213
11594
|
}
|
|
11214
11595
|
return raw;
|
|
11215
11596
|
}
|
|
11216
11597
|
function parseInitialTaskStatus(raw) {
|
|
11217
11598
|
const result = TaskStatusSchema.safeParse(raw);
|
|
11218
11599
|
if (!result.success) {
|
|
11219
|
-
throw new
|
|
11600
|
+
throw new InvalidArgumentError7(
|
|
11220
11601
|
`Initial task status must be one of: ${STATUS_VALUES3.join(", ")}`
|
|
11221
11602
|
);
|
|
11222
11603
|
}
|
|
@@ -11225,7 +11606,7 @@ function parseInitialTaskStatus(raw) {
|
|
|
11225
11606
|
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
11226
11607
|
function parseIsoTimestampOption(raw) {
|
|
11227
11608
|
if (!ISO_DATE_RE.test(raw) || Number.isNaN(Date.parse(raw))) {
|
|
11228
|
-
throw new
|
|
11609
|
+
throw new InvalidArgumentError7(
|
|
11229
11610
|
"Invalid --completed-at value; expected ISO-8601 timestamp like 2026-05-10T12:34:56+09:00"
|
|
11230
11611
|
);
|
|
11231
11612
|
}
|
|
@@ -11234,7 +11615,7 @@ function parseIsoTimestampOption(raw) {
|
|
|
11234
11615
|
function parseTaskStatusFilter(raw) {
|
|
11235
11616
|
const result = TaskStatusSchema.safeParse(raw);
|
|
11236
11617
|
if (!result.success) {
|
|
11237
|
-
throw new
|
|
11618
|
+
throw new InvalidArgumentError7(
|
|
11238
11619
|
`Invalid task status: ${raw}. Valid values: ${STATUS_VALUES3.join(", ")}`
|
|
11239
11620
|
);
|
|
11240
11621
|
}
|
|
@@ -11249,14 +11630,14 @@ function parseTaskStatusPositional(raw) {
|
|
|
11249
11630
|
}
|
|
11250
11631
|
function parseDescriptionOption(raw) {
|
|
11251
11632
|
if (raw.length === 0) {
|
|
11252
|
-
throw new
|
|
11633
|
+
throw new InvalidArgumentError7("Description must not be empty");
|
|
11253
11634
|
}
|
|
11254
11635
|
return raw;
|
|
11255
11636
|
}
|
|
11256
11637
|
function parsePositiveInt2(raw) {
|
|
11257
11638
|
const n = Number.parseInt(raw, 10);
|
|
11258
11639
|
if (!Number.isInteger(n) || n < 1 || raw.trim() !== String(n)) {
|
|
11259
|
-
throw new
|
|
11640
|
+
throw new InvalidArgumentError7(`Invalid number: ${raw}`);
|
|
11260
11641
|
}
|
|
11261
11642
|
return n;
|
|
11262
11643
|
}
|
|
@@ -11369,10 +11750,10 @@ function maxLen3(values, floor) {
|
|
|
11369
11750
|
// src/commands/verify.ts
|
|
11370
11751
|
import {
|
|
11371
11752
|
assertBasouRootSafe as assertBasouRootSafe17,
|
|
11372
|
-
basouPaths as
|
|
11753
|
+
basouPaths as basouPaths21,
|
|
11373
11754
|
enumerateSessionDirs as enumerateSessionDirs3,
|
|
11374
11755
|
findErrorCode as findErrorCode16,
|
|
11375
|
-
resolveRepositoryRoot as
|
|
11756
|
+
resolveRepositoryRoot as resolveRepositoryRoot12,
|
|
11376
11757
|
resolveSessionId as resolveSessionId5,
|
|
11377
11758
|
verifyEventsChain
|
|
11378
11759
|
} from "@basou/core";
|
|
@@ -11395,7 +11776,7 @@ async function doRunVerify(options, ctx) {
|
|
|
11395
11776
|
}
|
|
11396
11777
|
const cwd = ctx.cwd ?? process.cwd();
|
|
11397
11778
|
const repositoryRoot = await resolveRepositoryRootForVerify(cwd);
|
|
11398
|
-
const paths =
|
|
11779
|
+
const paths = basouPaths21(repositoryRoot);
|
|
11399
11780
|
await assertWorkspaceInitialized14(paths.root);
|
|
11400
11781
|
const sessionIds = options.session !== void 0 ? [await resolveSessionId5(paths, options.session)] : await enumerateSessionDirs3(paths);
|
|
11401
11782
|
const rows = [];
|
|
@@ -11443,7 +11824,7 @@ function renderVerdict(row) {
|
|
|
11443
11824
|
}
|
|
11444
11825
|
async function resolveRepositoryRootForVerify(cwd) {
|
|
11445
11826
|
try {
|
|
11446
|
-
return await
|
|
11827
|
+
return await resolveRepositoryRoot12(cwd);
|
|
11447
11828
|
} catch (error) {
|
|
11448
11829
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
11449
11830
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou verify'.", {
|
|
@@ -11470,20 +11851,20 @@ import { createHash as createHash2 } from "crypto";
|
|
|
11470
11851
|
import { basename as basename10, resolve as resolve13 } from "path";
|
|
11471
11852
|
import {
|
|
11472
11853
|
assertBasouRootSafe as assertBasouRootSafe18,
|
|
11473
|
-
basouPaths as
|
|
11854
|
+
basouPaths as basouPaths23,
|
|
11474
11855
|
findErrorCode as findErrorCode18,
|
|
11475
11856
|
readManifest as readManifest16,
|
|
11476
|
-
resolveRepositoryRoot as
|
|
11857
|
+
resolveRepositoryRoot as resolveRepositoryRoot14
|
|
11477
11858
|
} from "@basou/core";
|
|
11478
|
-
import { InvalidArgumentError as
|
|
11859
|
+
import { InvalidArgumentError as InvalidArgumentError8 } from "commander";
|
|
11479
11860
|
|
|
11480
11861
|
// src/lib/portfolio-coverage.ts
|
|
11481
11862
|
import { createReadStream as createReadStream2 } from "fs";
|
|
11482
|
-
import { readdir as readdir3, stat as
|
|
11863
|
+
import { readdir as readdir3, stat as stat7 } from "fs/promises";
|
|
11483
11864
|
import { homedir as homedir12 } from "os";
|
|
11484
11865
|
import { basename as basename8, dirname as dirname5, join as join17 } from "path";
|
|
11485
11866
|
import { createInterface as createInterface2 } from "readline";
|
|
11486
|
-
import { basouPaths as
|
|
11867
|
+
import { basouPaths as basouPaths22, readManifest as readManifest13, resolveRepositoryRoot as resolveRepositoryRoot13 } from "@basou/core";
|
|
11487
11868
|
function uncapturedTotal(result) {
|
|
11488
11869
|
return result.groups.reduce((sum, g) => sum + g.logs, 0) + result.unplaceable;
|
|
11489
11870
|
}
|
|
@@ -11519,14 +11900,14 @@ async function collectDeclaredRoots(workspaces) {
|
|
|
11519
11900
|
for (const ws of workspaces) {
|
|
11520
11901
|
let importRoot;
|
|
11521
11902
|
try {
|
|
11522
|
-
importRoot = await
|
|
11903
|
+
importRoot = await resolveRepositoryRoot13(ws.repoRoot);
|
|
11523
11904
|
} catch {
|
|
11524
11905
|
inertWorkspaces.push({ path: ws.repoRoot, reason: "not_a_git_repo" });
|
|
11525
11906
|
continue;
|
|
11526
11907
|
}
|
|
11527
11908
|
let resolved;
|
|
11528
11909
|
try {
|
|
11529
|
-
const manifest = await readManifest13(
|
|
11910
|
+
const manifest = await readManifest13(basouPaths22(importRoot));
|
|
11530
11911
|
resolved = resolveSourceRoots({
|
|
11531
11912
|
projectFlags: [],
|
|
11532
11913
|
manifest,
|
|
@@ -11626,7 +12007,7 @@ async function isDirEntry(parent, entry) {
|
|
|
11626
12007
|
if (entry.isDirectory()) return true;
|
|
11627
12008
|
if (!entry.isSymbolicLink()) return false;
|
|
11628
12009
|
try {
|
|
11629
|
-
return (await
|
|
12010
|
+
return (await stat7(join17(parent, entry.name))).isDirectory();
|
|
11630
12011
|
} catch {
|
|
11631
12012
|
return false;
|
|
11632
12013
|
}
|
|
@@ -11793,12 +12174,12 @@ function inertLines(result) {
|
|
|
11793
12174
|
}
|
|
11794
12175
|
|
|
11795
12176
|
// src/lib/portfolio-safety.ts
|
|
11796
|
-
import { execFile } from "child_process";
|
|
12177
|
+
import { execFile as execFile2 } from "child_process";
|
|
11797
12178
|
import { lstat as lstat2, realpath as realpath4 } from "fs/promises";
|
|
11798
12179
|
import { isAbsolute as isAbsolute7, join as join18, relative as relative4, resolve as resolve11 } from "path";
|
|
11799
|
-
import { promisify } from "util";
|
|
12180
|
+
import { promisify as promisify2 } from "util";
|
|
11800
12181
|
import { readManifest as readManifest14 } from "@basou/core";
|
|
11801
|
-
var
|
|
12182
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
11802
12183
|
function errorCode(error) {
|
|
11803
12184
|
return error instanceof Error ? error.code : void 0;
|
|
11804
12185
|
}
|
|
@@ -11830,7 +12211,7 @@ async function inspectRepo(repoPath) {
|
|
|
11830
12211
|
}
|
|
11831
12212
|
}
|
|
11832
12213
|
try {
|
|
11833
|
-
const { stdout } = await
|
|
12214
|
+
const { stdout } = await execFileAsync2("git", ["-C", repoPath, "ls-files", "-z"]);
|
|
11834
12215
|
const tracked = stdout.split("\0").some((f) => f.length > 0 && isBasouPath(f));
|
|
11835
12216
|
if (tracked) {
|
|
11836
12217
|
return {
|
|
@@ -13163,7 +13544,7 @@ var DEFAULT_PORT = 4319;
|
|
|
13163
13544
|
function parsePort(value) {
|
|
13164
13545
|
const port = Number.parseInt(value, 10);
|
|
13165
13546
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
13166
|
-
throw new
|
|
13547
|
+
throw new InvalidArgumentError8("Port must be an integer between 1 and 65535.");
|
|
13167
13548
|
}
|
|
13168
13549
|
return port;
|
|
13169
13550
|
}
|
|
@@ -13249,7 +13630,7 @@ async function doRunView(options, ctx) {
|
|
|
13249
13630
|
}
|
|
13250
13631
|
async function buildSingleDeps(ctx, cwd) {
|
|
13251
13632
|
const repositoryRoot = await resolveRepositoryRootForView(cwd);
|
|
13252
|
-
const paths =
|
|
13633
|
+
const paths = basouPaths23(repositoryRoot);
|
|
13253
13634
|
await assertWorkspaceInitialized15(paths.root);
|
|
13254
13635
|
const entry = await buildWorkspaceEntry(repositoryRoot, ctx);
|
|
13255
13636
|
return {
|
|
@@ -13283,7 +13664,7 @@ async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
|
|
|
13283
13664
|
};
|
|
13284
13665
|
}
|
|
13285
13666
|
async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
|
|
13286
|
-
const paths =
|
|
13667
|
+
const paths = basouPaths23(repoRoot);
|
|
13287
13668
|
const importCtx = {
|
|
13288
13669
|
cwd: repoRoot,
|
|
13289
13670
|
...ctx.claudeProjectsDir !== void 0 ? { claudeProjectsDir: ctx.claudeProjectsDir } : {},
|
|
@@ -13370,7 +13751,7 @@ function waitForShutdown(signal) {
|
|
|
13370
13751
|
}
|
|
13371
13752
|
async function resolveRepositoryRootForView(cwd) {
|
|
13372
13753
|
try {
|
|
13373
|
-
return await
|
|
13754
|
+
return await resolveRepositoryRoot14(cwd);
|
|
13374
13755
|
} catch (error) {
|
|
13375
13756
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
13376
13757
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou view'.", {
|
|
@@ -13392,12 +13773,30 @@ async function assertWorkspaceInitialized15(basouRoot) {
|
|
|
13392
13773
|
}
|
|
13393
13774
|
|
|
13394
13775
|
// src/program.ts
|
|
13776
|
+
function readBuildStamp() {
|
|
13777
|
+
if (false) return void 0;
|
|
13778
|
+
try {
|
|
13779
|
+
return JSON.parse('{"version":"0.46.0","commit":"dbc9c57","committedAt":"2026-09-19T15:43:35+09:00"}');
|
|
13780
|
+
} catch {
|
|
13781
|
+
return void 0;
|
|
13782
|
+
}
|
|
13783
|
+
}
|
|
13395
13784
|
var require2 = createRequire(import.meta.url);
|
|
13396
13785
|
var pkg = require2("../package.json");
|
|
13397
|
-
var
|
|
13786
|
+
var BASOU_BUILD = readBuildStamp();
|
|
13787
|
+
var BASOU_CLI_VERSION = BASOU_BUILD?.version ?? pkg.version;
|
|
13788
|
+
var BASOU_VERSION_LINE = buildVersionLine();
|
|
13789
|
+
function buildVersionLine() {
|
|
13790
|
+
if (BASOU_BUILD === void 0) return `${pkg.version} (source)`;
|
|
13791
|
+
const coreBuild = basouCore.BASOU_CORE_BUILD;
|
|
13792
|
+
const self = `${BASOU_BUILD.version} (build ${BASOU_BUILD.commit}, ${BASOU_BUILD.committedAt})`;
|
|
13793
|
+
if (coreBuild === void 0) return `${self}; core build unknown (not stamped)`;
|
|
13794
|
+
if (coreBuild.commit === BASOU_BUILD.commit) return self;
|
|
13795
|
+
return `${self}; core build ${coreBuild.commit}, ${coreBuild.committedAt}`;
|
|
13796
|
+
}
|
|
13398
13797
|
function buildProgram() {
|
|
13399
13798
|
const program = new Command();
|
|
13400
|
-
program.name("basou").description("A harness for steering AI coding agents").version(
|
|
13799
|
+
program.name("basou").description("A harness for steering AI coding agents").version(BASOU_VERSION_LINE).enablePositionalOptions();
|
|
13401
13800
|
registerInitCommand(program);
|
|
13402
13801
|
registerStatusCommand(program);
|
|
13403
13802
|
registerStatsCommand(program);
|
|
@@ -13426,7 +13825,9 @@ function buildProgram() {
|
|
|
13426
13825
|
return program;
|
|
13427
13826
|
}
|
|
13428
13827
|
export {
|
|
13828
|
+
BASOU_BUILD,
|
|
13429
13829
|
BASOU_CLI_VERSION,
|
|
13830
|
+
BASOU_VERSION_LINE,
|
|
13430
13831
|
buildProgram
|
|
13431
13832
|
};
|
|
13432
13833
|
//# sourceMappingURL=program.js.map
|