@basou/cli 0.45.0 → 0.47.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 +400 -241
- package/dist/index.js.map +1 -1
- package/dist/program.js +400 -241
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/program.js
CHANGED
|
@@ -1250,7 +1250,8 @@ Input format (a JSON array; one object per decision):
|
|
|
1250
1250
|
"rationale": "Workspace protocol and a content-addressed store fit our layout.",
|
|
1251
1251
|
"alternatives": ["npm workspaces", "yarn"],
|
|
1252
1252
|
"rejected_reason": "npm hoisting caused phantom-dependency bugs",
|
|
1253
|
-
"linked_files": ["pnpm-workspace.yaml"]
|
|
1253
|
+
"linked_files": ["pnpm-workspace.yaml"],
|
|
1254
|
+
"kind": "decision"
|
|
1254
1255
|
},
|
|
1255
1256
|
{
|
|
1256
1257
|
"title": "Form-based admin editing is the next track (only 6/19 sections done)",
|
|
@@ -1259,10 +1260,14 @@ Input format (a JSON array; one object per decision):
|
|
|
1259
1260
|
}
|
|
1260
1261
|
]
|
|
1261
1262
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1263
|
+
"title" is required. "kind" names the vessel: "track" records a strategic,
|
|
1264
|
+
UNFINISHED direction (+ why) and orientation/handoff resurface it every session
|
|
1265
|
+
until you close it with 'basou decision void <id>', while "decision" records a
|
|
1266
|
+
point-in-time call (surfaced only as the latest). Every other field is optional.
|
|
1267
|
+
|
|
1268
|
+
DEPRECATED: omitting "kind" still writes the item, as a decision, but warns --
|
|
1269
|
+
and becomes an ERROR in the next release. It has no default because getting it
|
|
1270
|
+
wrong fails silently: a track filed as a decision simply never comes back.
|
|
1266
1271
|
All decisions are written into one ad-hoc session timestamped now, so
|
|
1267
1272
|
orientation surfaces them as the latest decisions. Run from a workspace-view
|
|
1268
1273
|
directory and it resolves to the planning repo, like 'basou orient' /
|
|
@@ -1270,7 +1275,7 @@ directory and it resolves to the planning repo, like 'basou orient' /
|
|
|
1270
1275
|
|
|
1271
1276
|
Example (heredoc on stdin):
|
|
1272
1277
|
basou decision capture <<'JSON'
|
|
1273
|
-
[{ "title": "Ship the capture command", "rationale": "Close the why-capture gap" }]
|
|
1278
|
+
[{ "title": "Ship the capture command", "rationale": "Close the why-capture gap", "kind": "decision" }]
|
|
1274
1279
|
JSON
|
|
1275
1280
|
`;
|
|
1276
1281
|
async function runDecisionRecord(options, ctx = {}) {
|
|
@@ -1306,12 +1311,11 @@ async function warnLinkedFilesOutsideRoots(input) {
|
|
|
1306
1311
|
} catch {
|
|
1307
1312
|
}
|
|
1308
1313
|
}
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
for (const index of markerWithoutKind) {
|
|
1314
|
+
function warnMissingKind(decisions, indices) {
|
|
1315
|
+
for (const index of indices) {
|
|
1312
1316
|
const title = (decisions[index]?.title ?? "").trim();
|
|
1313
1317
|
console.error(
|
|
1314
|
-
`basou: decision[${index}]
|
|
1318
|
+
`basou: decision[${index}] ("${title.slice(0, 40)}") declares no "kind" \u2014 it is recorded as a point-in-time decision and will NOT resurface in orient. Set "kind": "track" for an unfinished direction, or "kind": "decision" to say it is settled. Omitting it becomes an ERROR in the next release.`
|
|
1315
1319
|
);
|
|
1316
1320
|
}
|
|
1317
1321
|
}
|
|
@@ -1412,8 +1416,8 @@ async function doRunDecisionCapture(options, ctx) {
|
|
|
1412
1416
|
const paths = basouPaths4(repositoryRoot);
|
|
1413
1417
|
await assertWorkspaceInitialized2(paths.root);
|
|
1414
1418
|
const raw = await readCaptureInput(options, ctx);
|
|
1415
|
-
const { decisions,
|
|
1416
|
-
|
|
1419
|
+
const { decisions, missingKind } = parseCaptureInput(raw);
|
|
1420
|
+
warnMissingKind(decisions, missingKind);
|
|
1417
1421
|
await warnLinkedFilesOutsideRoots({
|
|
1418
1422
|
linkedFiles: decisions.flatMap((d) => d.linked_files ?? []),
|
|
1419
1423
|
cwd,
|
|
@@ -1675,13 +1679,13 @@ function parseCaptureInput(raw) {
|
|
|
1675
1679
|
throw new Error("Input array must contain at least one decision.");
|
|
1676
1680
|
}
|
|
1677
1681
|
const decisions = [];
|
|
1678
|
-
const
|
|
1682
|
+
const missingKind = [];
|
|
1679
1683
|
parsed.forEach((item, index) => {
|
|
1680
|
-
const
|
|
1684
|
+
const input = validateCaptureItem(item, index);
|
|
1681
1685
|
decisions.push(input);
|
|
1682
|
-
if (
|
|
1686
|
+
if (input.kind === void 0) missingKind.push(index);
|
|
1683
1687
|
});
|
|
1684
|
-
return { decisions,
|
|
1688
|
+
return { decisions, missingKind };
|
|
1685
1689
|
}
|
|
1686
1690
|
function validateCaptureItem(item, index) {
|
|
1687
1691
|
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
@@ -1703,7 +1707,7 @@ function validateCaptureItem(item, index) {
|
|
|
1703
1707
|
if (obj.kind !== "decision" && obj.kind !== "track") {
|
|
1704
1708
|
throw new Error(`decision[${index}].kind must be "decision" or "track", got '${obj.kind}'.`);
|
|
1705
1709
|
}
|
|
1706
|
-
|
|
1710
|
+
out.kind = obj.kind;
|
|
1707
1711
|
}
|
|
1708
1712
|
if (obj.rationale !== void 0) {
|
|
1709
1713
|
out.rationale = requireNonEmptyString(obj.rationale, index, "rationale");
|
|
@@ -1742,7 +1746,7 @@ function validateCaptureItem(item, index) {
|
|
|
1742
1746
|
}
|
|
1743
1747
|
});
|
|
1744
1748
|
}
|
|
1745
|
-
return
|
|
1749
|
+
return out;
|
|
1746
1750
|
}
|
|
1747
1751
|
function requireNonEmptyString(value, index, field) {
|
|
1748
1752
|
if (typeof value !== "string" || isBlank(value)) {
|
|
@@ -1793,8 +1797,13 @@ function captureItemToPayload(item) {
|
|
|
1793
1797
|
if (item.input.kind !== void 0) payload.kind = item.input.kind;
|
|
1794
1798
|
return payload;
|
|
1795
1799
|
}
|
|
1796
|
-
function
|
|
1797
|
-
|
|
1800
|
+
function previewKindMarker(kind) {
|
|
1801
|
+
if (kind === "track") return " [TRACK]";
|
|
1802
|
+
return kind === "decision" ? " [DECISION]" : " [NO KIND]";
|
|
1803
|
+
}
|
|
1804
|
+
function recordedKindMarker(kind) {
|
|
1805
|
+
if (kind === "track") return " [TRACK]";
|
|
1806
|
+
return kind === "decision" ? "" : " [NO KIND]";
|
|
1798
1807
|
}
|
|
1799
1808
|
function printCapturePreview(options, decisions) {
|
|
1800
1809
|
if (options.json === true) {
|
|
@@ -1805,7 +1814,7 @@ function printCapturePreview(options, decisions) {
|
|
|
1805
1814
|
`Would capture ${decisions.length} decision${decisions.length === 1 ? "" : "s"} (dry run; nothing written):`
|
|
1806
1815
|
);
|
|
1807
1816
|
for (const decision of decisions) {
|
|
1808
|
-
console.log(`- ${decision.title}${
|
|
1817
|
+
console.log(`- ${decision.title}${previewKindMarker(decision.kind)}`);
|
|
1809
1818
|
}
|
|
1810
1819
|
}
|
|
1811
1820
|
function printCaptureResult(options, result) {
|
|
@@ -1826,7 +1835,7 @@ function printCaptureResult(options, result) {
|
|
|
1826
1835
|
`Captured ${result.items.length} decision${result.items.length === 1 ? "" : "s"} in ad-hoc session ${sid}:`
|
|
1827
1836
|
);
|
|
1828
1837
|
for (const item of result.items) {
|
|
1829
|
-
console.log(`- ${item.decisionId}: ${item.input.title}${
|
|
1838
|
+
console.log(`- ${item.decisionId}: ${item.input.title}${recordedKindMarker(item.input.kind)}`);
|
|
1830
1839
|
}
|
|
1831
1840
|
}
|
|
1832
1841
|
function pickRichFields(options) {
|
|
@@ -2443,8 +2452,8 @@ async function assertWorkspaceInitialized4(basouRoot) {
|
|
|
2443
2452
|
// src/commands/hook.ts
|
|
2444
2453
|
import { execFile } from "child_process";
|
|
2445
2454
|
import { open as open2, readFile as readFile3, realpath as realpath3, stat as stat4 } from "fs/promises";
|
|
2446
|
-
import { homedir as
|
|
2447
|
-
import { join as
|
|
2455
|
+
import { homedir as homedir8 } from "os";
|
|
2456
|
+
import { join as join10 } from "path";
|
|
2448
2457
|
import { fileURLToPath } from "url";
|
|
2449
2458
|
import { promisify } from "util";
|
|
2450
2459
|
import {
|
|
@@ -2454,12 +2463,20 @@ import {
|
|
|
2454
2463
|
evaluateStopHook,
|
|
2455
2464
|
findBasouSessionStartHook,
|
|
2456
2465
|
findBasouStopHookCommand,
|
|
2466
|
+
isProtocolUpdateDue,
|
|
2457
2467
|
ORIENTATION_END as ORIENTATION_END2,
|
|
2458
2468
|
ORIENTATION_START as ORIENTATION_START2,
|
|
2469
|
+
PROTOCOL_END,
|
|
2470
|
+
PROTOCOL_START,
|
|
2459
2471
|
parseMarkers as parseMarkers2,
|
|
2472
|
+
parseProtocolStamp,
|
|
2473
|
+
protocolSectionsFrom,
|
|
2474
|
+
protocolUpdateToken,
|
|
2460
2475
|
readMarkdownFile as readMarkdownFile6,
|
|
2461
2476
|
removeSessionStartHook,
|
|
2462
2477
|
removeStopHook,
|
|
2478
|
+
renderProtocolUpdate,
|
|
2479
|
+
transcriptStartedAt,
|
|
2463
2480
|
upsertSessionStartHook,
|
|
2464
2481
|
upsertStopHook
|
|
2465
2482
|
} from "@basou/core";
|
|
@@ -2714,6 +2731,83 @@ async function warnIfPositionNamesOtherWorkspaces(args) {
|
|
|
2714
2731
|
}
|
|
2715
2732
|
}
|
|
2716
2733
|
|
|
2734
|
+
// src/lib/protocols-config.ts
|
|
2735
|
+
import { homedir as homedir5 } from "os";
|
|
2736
|
+
import { isAbsolute as isAbsolute2, join as join7, resolve as resolve4 } from "path";
|
|
2737
|
+
import { readYamlFile as readYamlFile4 } from "@basou/core";
|
|
2738
|
+
var DEFAULT_PROTOCOLS_CONFIG_PATH = join7(homedir5(), ".basou", "protocols.yaml");
|
|
2739
|
+
var DEFAULT_TARGET_PATH = join7(homedir5(), ".claude", "CLAUDE.md");
|
|
2740
|
+
var ALLOWED_TOP_KEYS = /* @__PURE__ */ new Set(["version", "protocols"]);
|
|
2741
|
+
var ALLOWED_ENTRY_KEYS = /* @__PURE__ */ new Set(["source", "title"]);
|
|
2742
|
+
function expandTilde2(p) {
|
|
2743
|
+
if (p === "~") return homedir5();
|
|
2744
|
+
if (p.startsWith("~/")) return join7(homedir5(), p.slice(2));
|
|
2745
|
+
return p;
|
|
2746
|
+
}
|
|
2747
|
+
function isRecord2(value) {
|
|
2748
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2749
|
+
}
|
|
2750
|
+
async function loadProtocolsConfig(configPath = DEFAULT_PROTOCOLS_CONFIG_PATH) {
|
|
2751
|
+
let raw;
|
|
2752
|
+
try {
|
|
2753
|
+
raw = await readYamlFile4(configPath);
|
|
2754
|
+
} catch (error) {
|
|
2755
|
+
if (error instanceof Error && error.message === "YAML file not found") {
|
|
2756
|
+
throw new Error(
|
|
2757
|
+
"No protocols config at ~/.basou/protocols.yaml. Create one (a 'protocols:' list of source markdown paths) before running 'basou protocol sync'."
|
|
2758
|
+
);
|
|
2759
|
+
}
|
|
2760
|
+
if (error instanceof Error && error.message === "Failed to parse YAML content") {
|
|
2761
|
+
throw new Error("~/.basou/protocols.yaml is not valid YAML.");
|
|
2762
|
+
}
|
|
2763
|
+
throw error;
|
|
2764
|
+
}
|
|
2765
|
+
if (!isRecord2(raw) || !Array.isArray(raw.protocols)) {
|
|
2766
|
+
throw new Error("~/.basou/protocols.yaml must contain a 'protocols:' list.");
|
|
2767
|
+
}
|
|
2768
|
+
for (const key of Object.keys(raw)) {
|
|
2769
|
+
if (!ALLOWED_TOP_KEYS.has(key)) {
|
|
2770
|
+
throw new Error(
|
|
2771
|
+
`~/.basou/protocols.yaml has an unknown key '${key}' (allowed: version, protocols).`
|
|
2772
|
+
);
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2776
|
+
const result = [];
|
|
2777
|
+
for (const entry of raw.protocols) {
|
|
2778
|
+
if (!isRecord2(entry)) {
|
|
2779
|
+
throw new Error("Each protocol entry must be a mapping with a 'source' key.");
|
|
2780
|
+
}
|
|
2781
|
+
for (const key of Object.keys(entry)) {
|
|
2782
|
+
if (!ALLOWED_ENTRY_KEYS.has(key)) {
|
|
2783
|
+
throw new Error(`A protocol entry has an unknown key '${key}' (allowed: source, title).`);
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
if (typeof entry.source !== "string" || entry.source.trim().length === 0) {
|
|
2787
|
+
throw new Error("Each protocol entry needs a non-empty string 'source'.");
|
|
2788
|
+
}
|
|
2789
|
+
if (entry.title !== void 0 && (typeof entry.title !== "string" || entry.title.trim().length === 0)) {
|
|
2790
|
+
throw new Error("A protocol entry 'title' must be a non-empty string when present.");
|
|
2791
|
+
}
|
|
2792
|
+
const expanded = expandTilde2(entry.source.trim());
|
|
2793
|
+
if (!isAbsolute2(expanded)) {
|
|
2794
|
+
throw new Error("Protocol 'source' paths must be absolute (or start with '~').");
|
|
2795
|
+
}
|
|
2796
|
+
const abs = resolve4(expanded);
|
|
2797
|
+
if (seen.has(abs)) {
|
|
2798
|
+
throw new Error("Duplicate protocol source (each source path may appear only once).");
|
|
2799
|
+
}
|
|
2800
|
+
seen.add(abs);
|
|
2801
|
+
result.push(
|
|
2802
|
+
entry.title !== void 0 ? { source: abs, title: entry.title.trim() } : { source: abs }
|
|
2803
|
+
);
|
|
2804
|
+
}
|
|
2805
|
+
if (result.length === 0) {
|
|
2806
|
+
throw new Error("~/.basou/protocols.yaml has no protocols.");
|
|
2807
|
+
}
|
|
2808
|
+
return result;
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2717
2811
|
// src/commands/orient.ts
|
|
2718
2812
|
import {
|
|
2719
2813
|
assertBasouRootSafe as assertBasouRootSafe7,
|
|
@@ -2724,22 +2818,22 @@ import {
|
|
|
2724
2818
|
} from "@basou/core";
|
|
2725
2819
|
|
|
2726
2820
|
// src/lib/hosts-config.ts
|
|
2727
|
-
import { homedir as
|
|
2728
|
-
import { isAbsolute as
|
|
2729
|
-
import { readYamlFile as
|
|
2730
|
-
var DEFAULT_HOSTS_CONFIG_PATH =
|
|
2731
|
-
function
|
|
2732
|
-
if (p === "~") return
|
|
2733
|
-
if (p.startsWith("~/")) return
|
|
2821
|
+
import { homedir as homedir6 } from "os";
|
|
2822
|
+
import { isAbsolute as isAbsolute3, join as join8, resolve as resolve5 } from "path";
|
|
2823
|
+
import { readYamlFile as readYamlFile5 } from "@basou/core";
|
|
2824
|
+
var DEFAULT_HOSTS_CONFIG_PATH = join8(homedir6(), ".basou", "hosts.yaml");
|
|
2825
|
+
function expandTilde3(p) {
|
|
2826
|
+
if (p === "~") return homedir6();
|
|
2827
|
+
if (p.startsWith("~/")) return join8(homedir6(), p.slice(2));
|
|
2734
2828
|
return p;
|
|
2735
2829
|
}
|
|
2736
|
-
function
|
|
2830
|
+
function isRecord3(value) {
|
|
2737
2831
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2738
2832
|
}
|
|
2739
2833
|
async function loadHostsConfig(configPath = DEFAULT_HOSTS_CONFIG_PATH) {
|
|
2740
2834
|
let raw;
|
|
2741
2835
|
try {
|
|
2742
|
-
raw = await
|
|
2836
|
+
raw = await readYamlFile5(configPath);
|
|
2743
2837
|
} catch (error) {
|
|
2744
2838
|
if (error instanceof Error && error.message === "YAML file not found") {
|
|
2745
2839
|
return null;
|
|
@@ -2749,25 +2843,25 @@ async function loadHostsConfig(configPath = DEFAULT_HOSTS_CONFIG_PATH) {
|
|
|
2749
2843
|
}
|
|
2750
2844
|
throw error;
|
|
2751
2845
|
}
|
|
2752
|
-
if (!
|
|
2846
|
+
if (!isRecord3(raw) || !Array.isArray(raw.hosts)) {
|
|
2753
2847
|
throw new Error("~/.basou/hosts.yaml must contain a 'hosts:' list.");
|
|
2754
2848
|
}
|
|
2755
2849
|
const seenPaths = /* @__PURE__ */ new Set();
|
|
2756
2850
|
const seenLabels = /* @__PURE__ */ new Set();
|
|
2757
2851
|
const result = [];
|
|
2758
2852
|
for (const entry of raw.hosts) {
|
|
2759
|
-
if (!
|
|
2853
|
+
if (!isRecord3(entry) || typeof entry.label !== "string" || entry.label.trim().length === 0) {
|
|
2760
2854
|
throw new Error("Each host needs a non-empty string 'label'.");
|
|
2761
2855
|
}
|
|
2762
2856
|
const label = entry.label.trim();
|
|
2763
2857
|
if (typeof entry.path !== "string" || entry.path.trim().length === 0) {
|
|
2764
2858
|
throw new Error("Each host needs a non-empty string 'path'.");
|
|
2765
2859
|
}
|
|
2766
|
-
const expanded =
|
|
2767
|
-
if (!
|
|
2860
|
+
const expanded = expandTilde3(entry.path.trim());
|
|
2861
|
+
if (!isAbsolute3(expanded)) {
|
|
2768
2862
|
throw new Error("Host paths must be absolute (or start with '~').");
|
|
2769
2863
|
}
|
|
2770
|
-
const abs =
|
|
2864
|
+
const abs = resolve5(expanded);
|
|
2771
2865
|
if (seenPaths.has(abs)) continue;
|
|
2772
2866
|
if (seenLabels.has(label)) {
|
|
2773
2867
|
throw new Error(`Duplicate host label '${label}'; each host needs a distinct label.`);
|
|
@@ -2792,8 +2886,8 @@ import {
|
|
|
2792
2886
|
// src/commands/import.ts
|
|
2793
2887
|
import { createReadStream } from "fs";
|
|
2794
2888
|
import { readdir, readFile as readFile2, rm, stat as stat3 } from "fs/promises";
|
|
2795
|
-
import { homedir as
|
|
2796
|
-
import { basename as basename4, dirname as dirname3, join as
|
|
2889
|
+
import { homedir as homedir7 } from "os";
|
|
2890
|
+
import { basename as basename4, dirname as dirname3, join as join9, resolve as resolve6 } from "path";
|
|
2797
2891
|
import { createInterface } from "readline";
|
|
2798
2892
|
import {
|
|
2799
2893
|
AGENT_INFRA_DIRS as AGENT_INFRA_DIRS2,
|
|
@@ -2864,10 +2958,10 @@ function resolveSourceRoots(args) {
|
|
|
2864
2958
|
const { projectFlags, manifest, repoRoot, cwd } = args;
|
|
2865
2959
|
let resolved;
|
|
2866
2960
|
if (projectFlags.length > 0) {
|
|
2867
|
-
resolved = projectFlags.map((p) =>
|
|
2961
|
+
resolved = projectFlags.map((p) => resolve6(cwd, p));
|
|
2868
2962
|
} else {
|
|
2869
2963
|
const roots = manifest.import?.source_roots;
|
|
2870
|
-
resolved = roots !== void 0 && roots.length > 0 ? roots.map((r) =>
|
|
2964
|
+
resolved = roots !== void 0 && roots.length > 0 ? roots.map((r) => resolve6(repoRoot, r)) : [repoRoot];
|
|
2871
2965
|
}
|
|
2872
2966
|
return [...new Set(resolved)];
|
|
2873
2967
|
}
|
|
@@ -2880,7 +2974,7 @@ async function doRunImportClaudeCode(options, ctx) {
|
|
|
2880
2974
|
repoRoot: repositoryRoot,
|
|
2881
2975
|
cwd: ctx.cwd ?? process.cwd()
|
|
2882
2976
|
});
|
|
2883
|
-
const projectsRoot = ctx.claudeProjectsDir ??
|
|
2977
|
+
const projectsRoot = ctx.claudeProjectsDir ?? join9(homedir7(), ".claude", "projects");
|
|
2884
2978
|
const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
|
|
2885
2979
|
const projectSet = new Set(projectPaths);
|
|
2886
2980
|
const candidates = files.map((file) => {
|
|
@@ -2919,7 +3013,7 @@ async function doRunImportCodex(options, ctx) {
|
|
|
2919
3013
|
repoRoot: repositoryRoot,
|
|
2920
3014
|
cwd: ctx.cwd ?? process.cwd()
|
|
2921
3015
|
});
|
|
2922
|
-
const sessionsRoot = ctx.codexSessionsDir ??
|
|
3016
|
+
const sessionsRoot = ctx.codexSessionsDir ?? join9(homedir7(), ".codex", "sessions");
|
|
2923
3017
|
const rollouts = await discoverCodexRollouts(sessionsRoot, projectPaths, options);
|
|
2924
3018
|
const candidates = rollouts.map(({ file, externalId }) => ({
|
|
2925
3019
|
externalId,
|
|
@@ -3050,7 +3144,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
3050
3144
|
if (priors.length > 0 && options.force === true) {
|
|
3051
3145
|
if (options.dryRun !== true) {
|
|
3052
3146
|
for (const { sessionId } of priors) {
|
|
3053
|
-
await rm(
|
|
3147
|
+
await rm(join9(paths.sessions, sessionId), { recursive: true, force: true });
|
|
3054
3148
|
}
|
|
3055
3149
|
}
|
|
3056
3150
|
counts.replaced++;
|
|
@@ -3161,7 +3255,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
|
3161
3255
|
if (options.session !== void 0) {
|
|
3162
3256
|
const matches = [];
|
|
3163
3257
|
for (const projectPath of projectPaths) {
|
|
3164
|
-
const file =
|
|
3258
|
+
const file = join9(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
|
|
3165
3259
|
if (await pathExists(file)) matches.push(file);
|
|
3166
3260
|
}
|
|
3167
3261
|
if (matches.length === 0) {
|
|
@@ -3172,7 +3266,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
|
3172
3266
|
const files = [];
|
|
3173
3267
|
let anyDirFound = false;
|
|
3174
3268
|
for (const projectPath of projectPaths) {
|
|
3175
|
-
const transcriptDir =
|
|
3269
|
+
const transcriptDir = join9(projectsRoot, encodeProjectDir(projectPath));
|
|
3176
3270
|
let entries;
|
|
3177
3271
|
try {
|
|
3178
3272
|
entries = await readdir(transcriptDir);
|
|
@@ -3182,7 +3276,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
|
3182
3276
|
}
|
|
3183
3277
|
anyDirFound = true;
|
|
3184
3278
|
for (const name of entries) {
|
|
3185
|
-
if (name.endsWith(".jsonl")) files.push(
|
|
3279
|
+
if (name.endsWith(".jsonl")) files.push(join9(transcriptDir, name));
|
|
3186
3280
|
}
|
|
3187
3281
|
}
|
|
3188
3282
|
if (!anyDirFound) {
|
|
@@ -3239,7 +3333,7 @@ async function findRolloutFiles(sessionsRoot) {
|
|
|
3239
3333
|
throw new Error("Failed to read Codex sessions directory", { cause: error });
|
|
3240
3334
|
}
|
|
3241
3335
|
for (const entry of entries) {
|
|
3242
|
-
const full =
|
|
3336
|
+
const full = join9(dir, entry.name);
|
|
3243
3337
|
if (entry.isDirectory()) {
|
|
3244
3338
|
await walk(full, false);
|
|
3245
3339
|
} else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) {
|
|
@@ -3696,6 +3790,8 @@ async function assertWorkspaceInitialized6(basouRoot) {
|
|
|
3696
3790
|
|
|
3697
3791
|
// src/commands/hook.ts
|
|
3698
3792
|
var MAX_TRANSCRIPT_BYTES = 8 * 1024 * 1024;
|
|
3793
|
+
var MAX_TRANSCRIPT_HEAD_BYTES = 256 * 1024;
|
|
3794
|
+
var TOKEN_SCAN_CHUNK_BYTES = 1024 * 1024;
|
|
3699
3795
|
var execFileAsync = promisify(execFile);
|
|
3700
3796
|
function registerHookCommand(program) {
|
|
3701
3797
|
const hook = program.command("hook").description(
|
|
@@ -3707,13 +3803,13 @@ function registerHookCommand(program) {
|
|
|
3707
3803
|
await runHookSessionStart();
|
|
3708
3804
|
});
|
|
3709
3805
|
hook.command("stop").description(
|
|
3710
|
-
"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."
|
|
3806
|
+
"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."
|
|
3711
3807
|
).option(
|
|
3712
3808
|
"--min-edits <n>",
|
|
3713
3809
|
`Minimum file edits before nudging on edits alone (default ${DEFAULT_STOP_HOOK_MIN_EDITS})`
|
|
3714
3810
|
).option(
|
|
3715
3811
|
"--block",
|
|
3716
|
-
"Opt-in enforcement: hold the agent in-turn (decision:block) instead of a non-blocking
|
|
3812
|
+
"Opt-in enforcement: hold the agent in-turn (decision:block) instead of a non-blocking message"
|
|
3717
3813
|
).option(
|
|
3718
3814
|
"--require-review",
|
|
3719
3815
|
"Opt-in review gate: also remind when a session shipped substantive code (push / PR / merge) without recording a review"
|
|
@@ -3826,12 +3922,25 @@ gh pr create|merge) without recording a review ('basou review record'). This
|
|
|
3826
3922
|
gate is off by default; when on, its reminder is composed into the same
|
|
3827
3923
|
envelope as the capture reminder.
|
|
3828
3924
|
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3925
|
+
It also hands a RUNNING session the standing protocols when they changed after
|
|
3926
|
+
that session started. The protocol block in ~/.claude/CLAUDE.md is read at
|
|
3927
|
+
session start, so an update made mid-session never reaches the session it was
|
|
3928
|
+
meant to correct; this is the only channel that does. The complete current set
|
|
3929
|
+
is delivered, not a diff, so what it supersedes -- including a protocol that is
|
|
3930
|
+
no longer there -- is unambiguous. It reads the rendered block and nothing else,
|
|
3931
|
+
so an edit not yet published by 'basou protocol sync' cannot reach a session,
|
|
3932
|
+
and it delivers once per block state: a second update in the same session still
|
|
3933
|
+
lands, the same one twice does not. This part is always on, needs no flag, and
|
|
3934
|
+
says nothing at all unless the block actually changed.
|
|
3935
|
+
|
|
3936
|
+
By default every message here is non-blocking: Claude sees it and may act on it
|
|
3937
|
+
or stop. With --block (opt-in enforcement, 'basou hook install --block') it
|
|
3938
|
+
instead returns decision:block, holding the agent in-turn; the 'stop_hook_active'
|
|
3939
|
+
flag and Claude Code's own loop prevention bound it to a single turn. Note that
|
|
3940
|
+
this covers the protocol delivery too, which carries no action to take -- with
|
|
3941
|
+
--block the turn is held so the new text lands before more work is done on the
|
|
3942
|
+
old. Either way the hook fails open: a bad payload or unreadable transcript
|
|
3943
|
+
exits cleanly with no output.
|
|
3835
3944
|
`;
|
|
3836
3945
|
async function runHookStop(options, ctx = {}) {
|
|
3837
3946
|
try {
|
|
@@ -3869,7 +3978,12 @@ async function doRunHookStop(options, ctx) {
|
|
|
3869
3978
|
stopHookActive: false,
|
|
3870
3979
|
...options.minEdits !== void 0 ? { minEdits: options.minEdits } : {}
|
|
3871
3980
|
});
|
|
3981
|
+
const protocolUpdate = await evaluateProtocolUpdateGate({
|
|
3982
|
+
transcriptPath,
|
|
3983
|
+
target: ctx.protocolTargetPath ?? DEFAULT_TARGET_PATH
|
|
3984
|
+
});
|
|
3872
3985
|
const parts = [];
|
|
3986
|
+
if (protocolUpdate !== null) parts.push(protocolUpdate);
|
|
3873
3987
|
if (evaluation.kind === "nudge") parts.push(evaluation.additionalContext);
|
|
3874
3988
|
if (options.requireReview === true && evaluation.review.fires) {
|
|
3875
3989
|
parts.push(evaluation.review.additionalContext);
|
|
@@ -3885,6 +3999,56 @@ async function doRunHookStop(options, ctx) {
|
|
|
3885
3999
|
write(`${payloadJson}
|
|
3886
4000
|
`);
|
|
3887
4001
|
}
|
|
4002
|
+
async function evaluateProtocolUpdateGate(input) {
|
|
4003
|
+
try {
|
|
4004
|
+
const head = await readTranscriptHead(input.transcriptPath);
|
|
4005
|
+
const sessionStartedAt = transcriptStartedAt(parseTranscript(head));
|
|
4006
|
+
if (sessionStartedAt === void 0) return null;
|
|
4007
|
+
const touchedAt = await targetModifiedAt(input.target);
|
|
4008
|
+
if (touchedAt !== null && touchedAt <= Date.parse(sessionStartedAt)) return null;
|
|
4009
|
+
const existing = await readMarkdownFile6(input.target);
|
|
4010
|
+
if (existing === null) return null;
|
|
4011
|
+
const section = parseMarkers2(existing, { start: PROTOCOL_START, end: PROTOCOL_END });
|
|
4012
|
+
if (section.kind !== "ok") return null;
|
|
4013
|
+
const stamp = parseProtocolStamp(section.generated);
|
|
4014
|
+
if (stamp === null) return null;
|
|
4015
|
+
if (!isProtocolUpdateDue({ stamp, sessionStartedAt })) return null;
|
|
4016
|
+
const sections = protocolSectionsFrom(section.generated);
|
|
4017
|
+
if (sections === null || sections.trim().length === 0) return null;
|
|
4018
|
+
if (await transcriptCarries(input.transcriptPath, protocolUpdateToken(stamp.contentHash))) {
|
|
4019
|
+
return null;
|
|
4020
|
+
}
|
|
4021
|
+
return renderProtocolUpdate(sections, stamp);
|
|
4022
|
+
} catch {
|
|
4023
|
+
return null;
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
async function targetModifiedAt(target) {
|
|
4027
|
+
try {
|
|
4028
|
+
return (await stat4(target)).mtimeMs;
|
|
4029
|
+
} catch {
|
|
4030
|
+
return null;
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
async function transcriptCarries(path, token) {
|
|
4034
|
+
const handle = await open2(path, "r");
|
|
4035
|
+
try {
|
|
4036
|
+
const overlap = Math.max(token.length - 1, 0);
|
|
4037
|
+
const chunk = Buffer.alloc(TOKEN_SCAN_CHUNK_BYTES);
|
|
4038
|
+
let carry = "";
|
|
4039
|
+
let position = 0;
|
|
4040
|
+
for (; ; ) {
|
|
4041
|
+
const { bytesRead } = await handle.read(chunk, 0, TOKEN_SCAN_CHUNK_BYTES, position);
|
|
4042
|
+
if (bytesRead === 0) return false;
|
|
4043
|
+
position += bytesRead;
|
|
4044
|
+
const text = carry + chunk.subarray(0, bytesRead).toString("utf8");
|
|
4045
|
+
if (text.includes(token)) return true;
|
|
4046
|
+
carry = overlap > 0 ? text.slice(-overlap) : "";
|
|
4047
|
+
}
|
|
4048
|
+
} finally {
|
|
4049
|
+
await handle.close();
|
|
4050
|
+
}
|
|
4051
|
+
}
|
|
3888
4052
|
async function renderRegisteredWorkspacePosition(cwd, portfolioConfigPath = DEFAULT_PORTFOLIO_CONFIG_PATH) {
|
|
3889
4053
|
const root = await resolveBasouRootForCommand(cwd, "hook session-start", {
|
|
3890
4054
|
portfolioConfigPath
|
|
@@ -3984,11 +4148,25 @@ async function readTranscriptBounded(path, maxBytes = MAX_TRANSCRIPT_BYTES) {
|
|
|
3984
4148
|
await handle.close();
|
|
3985
4149
|
}
|
|
3986
4150
|
}
|
|
4151
|
+
async function readTranscriptHead(path, maxBytes = MAX_TRANSCRIPT_HEAD_BYTES) {
|
|
4152
|
+
const { size } = await stat4(path);
|
|
4153
|
+
if (size <= maxBytes) return readFile3(path, "utf8");
|
|
4154
|
+
const handle = await open2(path, "r");
|
|
4155
|
+
try {
|
|
4156
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
4157
|
+
const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
|
|
4158
|
+
const text = buffer.subarray(0, bytesRead).toString("utf8");
|
|
4159
|
+
const lastNewline = text.lastIndexOf("\n");
|
|
4160
|
+
return lastNewline >= 0 ? text.slice(0, lastNewline + 1) : text;
|
|
4161
|
+
} finally {
|
|
4162
|
+
await handle.close();
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
3987
4165
|
function parseMinEdits(raw) {
|
|
3988
4166
|
if (raw === void 0 || !/^\d+$/.test(raw)) return void 0;
|
|
3989
4167
|
return Number(raw);
|
|
3990
4168
|
}
|
|
3991
|
-
var DEFAULT_CLAUDE_SETTINGS_PATH =
|
|
4169
|
+
var DEFAULT_CLAUDE_SETTINGS_PATH = join10(homedir8(), ".claude", "settings.json");
|
|
3992
4170
|
function resolveCliEntry() {
|
|
3993
4171
|
return fileURLToPath(import.meta.url);
|
|
3994
4172
|
}
|
|
@@ -4236,7 +4414,7 @@ function describeHookMode(tiers) {
|
|
|
4236
4414
|
const gates = tiers.review ? "capture + review" : "capture";
|
|
4237
4415
|
return `${enforcement}, ${gates}`;
|
|
4238
4416
|
}
|
|
4239
|
-
var DEFAULT_CODEX_FACE_PATH =
|
|
4417
|
+
var DEFAULT_CODEX_FACE_PATH = join10(homedir8(), ".codex", "AGENTS.md");
|
|
4240
4418
|
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.`;
|
|
4241
4419
|
async function faceHasLeftoverOrientationBlock(facePath) {
|
|
4242
4420
|
try {
|
|
@@ -4248,8 +4426,8 @@ async function faceHasLeftoverOrientationBlock(facePath) {
|
|
|
4248
4426
|
return false;
|
|
4249
4427
|
}
|
|
4250
4428
|
}
|
|
4251
|
-
var DEFAULT_CODEX_HOOKS_PATH =
|
|
4252
|
-
var DEFAULT_CODEX_CONFIG_PATH =
|
|
4429
|
+
var DEFAULT_CODEX_HOOKS_PATH = join10(homedir8(), ".codex", "hooks.json");
|
|
4430
|
+
var DEFAULT_CODEX_CONFIG_PATH = join10(homedir8(), ".codex", "config.toml");
|
|
4253
4431
|
async function readHooksFile(path) {
|
|
4254
4432
|
let raw;
|
|
4255
4433
|
try {
|
|
@@ -4421,7 +4599,7 @@ async function codexHookTrustFor(hooksPath, location, configPath) {
|
|
|
4421
4599
|
}
|
|
4422
4600
|
|
|
4423
4601
|
// src/commands/init.ts
|
|
4424
|
-
import { basename as basename5, relative, resolve as
|
|
4602
|
+
import { basename as basename5, relative, resolve as resolve7 } from "path";
|
|
4425
4603
|
import {
|
|
4426
4604
|
appendBasouGitignore,
|
|
4427
4605
|
createManifest,
|
|
@@ -4466,7 +4644,7 @@ async function doRunInit(options, ctx) {
|
|
|
4466
4644
|
);
|
|
4467
4645
|
}
|
|
4468
4646
|
const sourceRoots = (options.sourceRoot ?? []).map((p) => {
|
|
4469
|
-
const rel = relative(repositoryRoot,
|
|
4647
|
+
const rel = relative(repositoryRoot, resolve7(cwd, p));
|
|
4470
4648
|
return rel === "" ? "." : rel;
|
|
4471
4649
|
});
|
|
4472
4650
|
const paths = await ensureBasouDirectory(repositoryRoot);
|
|
@@ -4676,7 +4854,7 @@ async function assertWorkspaceInitialized7(basouRoot) {
|
|
|
4676
4854
|
|
|
4677
4855
|
// src/commands/portfolio.ts
|
|
4678
4856
|
import { existsSync, statSync } from "fs";
|
|
4679
|
-
import { join as
|
|
4857
|
+
import { join as join11 } from "path";
|
|
4680
4858
|
function registerPortfolioCommand(program) {
|
|
4681
4859
|
program.command("portfolio").description(
|
|
4682
4860
|
"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"
|
|
@@ -4722,7 +4900,7 @@ async function runPortfolioList(options, ctx = {}) {
|
|
|
4722
4900
|
async function doRunPortfolioList(options, ctx) {
|
|
4723
4901
|
const configPath = ctx.configPath ?? DEFAULT_PORTFOLIO_CONFIG_PATH;
|
|
4724
4902
|
const pathExists2 = ctx.pathExists ?? ((p) => existsSync(p));
|
|
4725
|
-
const isInitialized = ctx.isInitialized ?? ((p) => isDirectory(
|
|
4903
|
+
const isInitialized = ctx.isInitialized ?? ((p) => isDirectory(join11(p, ".basou")));
|
|
4726
4904
|
const workspaces = await loadPortfolioConfig(configPath);
|
|
4727
4905
|
const result = {
|
|
4728
4906
|
configPath,
|
|
@@ -4787,7 +4965,7 @@ import {
|
|
|
4787
4965
|
writeFileSync,
|
|
4788
4966
|
writeSync
|
|
4789
4967
|
} from "fs";
|
|
4790
|
-
import { basename as basename6, dirname as dirname4, isAbsolute as
|
|
4968
|
+
import { basename as basename6, dirname as dirname4, isAbsolute as isAbsolute4, join as join12, relative as relative2, resolve as resolve8 } from "path";
|
|
4791
4969
|
import {
|
|
4792
4970
|
appendBasouGitignore as appendBasouGitignore2,
|
|
4793
4971
|
basouPaths as basouPaths11,
|
|
@@ -5162,14 +5340,14 @@ async function runProjectAdopt(options, ctx = {}) {
|
|
|
5162
5340
|
}
|
|
5163
5341
|
}
|
|
5164
5342
|
function classifySourceRoot(repositoryRoot, declaredPath) {
|
|
5165
|
-
const absolute =
|
|
5343
|
+
const absolute = resolve8(repositoryRoot, declaredPath);
|
|
5166
5344
|
let real;
|
|
5167
5345
|
try {
|
|
5168
5346
|
real = realpathSync(absolute);
|
|
5169
5347
|
} catch {
|
|
5170
5348
|
return { path: declaredPath, kind: "unresolved" };
|
|
5171
5349
|
}
|
|
5172
|
-
return { path: declaredPath, kind: existsSync2(
|
|
5350
|
+
return { path: declaredPath, kind: existsSync2(join12(real, ".git")) ? "repo" : "non-repo" };
|
|
5173
5351
|
}
|
|
5174
5352
|
async function doRunProjectAdopt(options, ctx) {
|
|
5175
5353
|
const cwd = ctx.cwd ?? process.cwd();
|
|
@@ -5269,11 +5447,11 @@ async function gatherRepoWiring(repositoryRoot, entry) {
|
|
|
5269
5447
|
};
|
|
5270
5448
|
let real;
|
|
5271
5449
|
try {
|
|
5272
|
-
real = realpathSync(
|
|
5450
|
+
real = realpathSync(resolve8(repositoryRoot, entry.path));
|
|
5273
5451
|
} catch {
|
|
5274
5452
|
return { ...base, reachable: false, instructionFiles: [] };
|
|
5275
5453
|
}
|
|
5276
|
-
if (!existsSync2(
|
|
5454
|
+
if (!existsSync2(join12(real, ".git"))) {
|
|
5277
5455
|
return { ...base, reachable: false, instructionFiles: [] };
|
|
5278
5456
|
}
|
|
5279
5457
|
try {
|
|
@@ -5281,7 +5459,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
|
|
|
5281
5459
|
for (const name of INSTRUCTION_FILES) {
|
|
5282
5460
|
let present = true;
|
|
5283
5461
|
try {
|
|
5284
|
-
lstatSync(
|
|
5462
|
+
lstatSync(join12(real, name));
|
|
5285
5463
|
} catch {
|
|
5286
5464
|
present = false;
|
|
5287
5465
|
}
|
|
@@ -5388,14 +5566,14 @@ function gatherRepoGitignore(repositoryRoot, entry) {
|
|
|
5388
5566
|
};
|
|
5389
5567
|
let real;
|
|
5390
5568
|
try {
|
|
5391
|
-
real = realpathSync(
|
|
5569
|
+
real = realpathSync(resolve8(repositoryRoot, entry.path));
|
|
5392
5570
|
} catch {
|
|
5393
5571
|
return { ...base, reachable: false, currentLines: [] };
|
|
5394
5572
|
}
|
|
5395
|
-
if (!existsSync2(
|
|
5573
|
+
if (!existsSync2(join12(real, ".git"))) {
|
|
5396
5574
|
return { ...base, reachable: false, currentLines: [] };
|
|
5397
5575
|
}
|
|
5398
|
-
return { ...base, reachable: true, currentLines: readGitignoreLines(
|
|
5576
|
+
return { ...base, reachable: true, currentLines: readGitignoreLines(join12(real, ".gitignore")) };
|
|
5399
5577
|
}
|
|
5400
5578
|
function hasErrorCode(error) {
|
|
5401
5579
|
return error instanceof Error && typeof error.code === "string";
|
|
@@ -5409,7 +5587,7 @@ function readGitignoreLines(file) {
|
|
|
5409
5587
|
}
|
|
5410
5588
|
}
|
|
5411
5589
|
function applyGitignorePlan(repositoryRoot, plan) {
|
|
5412
|
-
const file =
|
|
5590
|
+
const file = join12(realpathSync(resolve8(repositoryRoot, plan.path)), ".gitignore");
|
|
5413
5591
|
let existing = "";
|
|
5414
5592
|
try {
|
|
5415
5593
|
existing = readFileSync(file, "utf8");
|
|
@@ -5544,12 +5722,12 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
5544
5722
|
const base = { path: entry.path, ...isSelf ? { self: true } : {} };
|
|
5545
5723
|
let real;
|
|
5546
5724
|
try {
|
|
5547
|
-
real = realpathSync(
|
|
5725
|
+
real = realpathSync(resolve8(repositoryRoot, entry.path));
|
|
5548
5726
|
} catch {
|
|
5549
5727
|
return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
|
|
5550
5728
|
}
|
|
5551
5729
|
if (real === anchorReal) {
|
|
5552
|
-
const anchorCanonical =
|
|
5730
|
+
const anchorCanonical = join12(real, CANONICAL_FILE);
|
|
5553
5731
|
const anchorState = anchorCanonicalState(anchorCanonical);
|
|
5554
5732
|
if (anchorState === "absent") {
|
|
5555
5733
|
return { ...base, isAnchor: true, reachable: true, canonicalPresent: false, files: [] };
|
|
@@ -5569,7 +5747,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
5569
5747
|
anchorCanonical,
|
|
5570
5748
|
"self"
|
|
5571
5749
|
).map((spec) => {
|
|
5572
|
-
const { state, actualTarget } = inspectSymlink(
|
|
5750
|
+
const { state, actualTarget } = inspectSymlink(join12(real, spec.name), spec.target);
|
|
5573
5751
|
return {
|
|
5574
5752
|
name: spec.name,
|
|
5575
5753
|
expectedTarget: spec.target,
|
|
@@ -5586,16 +5764,16 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
5586
5764
|
files: anchorFiles
|
|
5587
5765
|
};
|
|
5588
5766
|
}
|
|
5589
|
-
if (!existsSync2(
|
|
5767
|
+
if (!existsSync2(join12(real, ".git"))) {
|
|
5590
5768
|
return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
|
|
5591
5769
|
}
|
|
5592
|
-
const canonicalFile = isSelf ?
|
|
5770
|
+
const canonicalFile = isSelf ? join12(real, CANONICAL_FILE) : join12(anchorReal, "agents", basename6(real), CANONICAL_FILE);
|
|
5593
5771
|
if (!existsSync2(canonicalFile)) {
|
|
5594
5772
|
return { ...base, isAnchor: false, reachable: true, canonicalPresent: false, files: [] };
|
|
5595
5773
|
}
|
|
5596
5774
|
const files = expectedSymlinkTargets(real, canonicalFile, mode).map(
|
|
5597
5775
|
(spec) => {
|
|
5598
|
-
const { state, actualTarget } = inspectSymlink(
|
|
5776
|
+
const { state, actualTarget } = inspectSymlink(join12(real, spec.name), spec.target);
|
|
5599
5777
|
return {
|
|
5600
5778
|
name: spec.name,
|
|
5601
5779
|
expectedTarget: spec.target,
|
|
@@ -5616,7 +5794,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
5616
5794
|
function applySymlinkPlan(repositoryRoot, plan) {
|
|
5617
5795
|
let real;
|
|
5618
5796
|
try {
|
|
5619
|
-
real = realpathSync(
|
|
5797
|
+
real = realpathSync(resolve8(repositoryRoot, plan.path));
|
|
5620
5798
|
} catch (error) {
|
|
5621
5799
|
const message = failureReason(error);
|
|
5622
5800
|
return { created: [], failed: plan.toCreate.map((c) => ({ file: c.name, message })) };
|
|
@@ -5624,7 +5802,7 @@ function applySymlinkPlan(repositoryRoot, plan) {
|
|
|
5624
5802
|
const created = [];
|
|
5625
5803
|
const failed = [];
|
|
5626
5804
|
for (const { name, target } of plan.toCreate) {
|
|
5627
|
-
const filePath =
|
|
5805
|
+
const filePath = join12(real, name);
|
|
5628
5806
|
try {
|
|
5629
5807
|
mkdirSync(dirname4(filePath), { recursive: true });
|
|
5630
5808
|
symlinkSync(target, filePath);
|
|
@@ -5642,9 +5820,9 @@ function viewCanonicalCollision(repositoryRoot, roster, viewName) {
|
|
|
5642
5820
|
for (const entry of roster) {
|
|
5643
5821
|
let name;
|
|
5644
5822
|
try {
|
|
5645
|
-
name = basename6(realpathSync(
|
|
5823
|
+
name = basename6(realpathSync(resolve8(repositoryRoot, entry.path)));
|
|
5646
5824
|
} catch {
|
|
5647
|
-
name = basename6(
|
|
5825
|
+
name = basename6(resolve8(repositoryRoot, entry.path));
|
|
5648
5826
|
}
|
|
5649
5827
|
if (name === viewName) return entry.path;
|
|
5650
5828
|
}
|
|
@@ -5658,7 +5836,7 @@ function gatherViewSymlinks(repositoryRoot, anchorReal, roster, viewDir) {
|
|
|
5658
5836
|
if (!existsSync2(canonicalFile)) return { kind: "missing-canonical", viewName };
|
|
5659
5837
|
const files = expectedSymlinkTargets(viewDir, canonicalFile, "hub").map(
|
|
5660
5838
|
(spec) => {
|
|
5661
|
-
const { state, actualTarget } = inspectSymlink(
|
|
5839
|
+
const { state, actualTarget } = inspectSymlink(join12(viewDir, spec.name), spec.target);
|
|
5662
5840
|
return {
|
|
5663
5841
|
name: spec.name,
|
|
5664
5842
|
expectedTarget: spec.target,
|
|
@@ -5674,7 +5852,7 @@ function applyViewSymlinks(viewDir, files) {
|
|
|
5674
5852
|
const failed = [];
|
|
5675
5853
|
for (const f of files) {
|
|
5676
5854
|
if (f.state !== "missing") continue;
|
|
5677
|
-
const filePath =
|
|
5855
|
+
const filePath = join12(viewDir, f.name);
|
|
5678
5856
|
try {
|
|
5679
5857
|
mkdirSync(dirname4(filePath), { recursive: true });
|
|
5680
5858
|
symlinkSync(f.expectedTarget, filePath);
|
|
@@ -5891,12 +6069,12 @@ async function runProjectWorkspace(options, ctx = {}) {
|
|
|
5891
6069
|
}
|
|
5892
6070
|
}
|
|
5893
6071
|
function resolveViewDir(repositoryRoot, viewPath) {
|
|
5894
|
-
const abs =
|
|
6072
|
+
const abs = resolve8(repositoryRoot, viewPath);
|
|
5895
6073
|
try {
|
|
5896
6074
|
return realpathSync(abs);
|
|
5897
6075
|
} catch {
|
|
5898
6076
|
try {
|
|
5899
|
-
return
|
|
6077
|
+
return join12(realpathSync(dirname4(abs)), basename6(abs));
|
|
5900
6078
|
} catch {
|
|
5901
6079
|
return abs;
|
|
5902
6080
|
}
|
|
@@ -5905,7 +6083,7 @@ function resolveViewDir(repositoryRoot, viewPath) {
|
|
|
5905
6083
|
function gatherViewRepo(repositoryRoot, viewDir, entry) {
|
|
5906
6084
|
let repoReal;
|
|
5907
6085
|
try {
|
|
5908
|
-
repoReal = realpathSync(
|
|
6086
|
+
repoReal = realpathSync(resolve8(repositoryRoot, entry.path));
|
|
5909
6087
|
} catch {
|
|
5910
6088
|
return { path: entry.path, reachable: false };
|
|
5911
6089
|
}
|
|
@@ -5914,7 +6092,7 @@ function gatherViewRepo(repositoryRoot, viewDir, entry) {
|
|
|
5914
6092
|
return { path: entry.path, reachable: false };
|
|
5915
6093
|
}
|
|
5916
6094
|
const linkName = basename6(repoReal);
|
|
5917
|
-
const { state, actualTarget } = inspectSymlink(
|
|
6095
|
+
const { state, actualTarget } = inspectSymlink(join12(viewDir, linkName), expectedTarget);
|
|
5918
6096
|
return {
|
|
5919
6097
|
path: entry.path,
|
|
5920
6098
|
reachable: true,
|
|
@@ -5928,7 +6106,7 @@ function applyViewPlan(viewDir, toCreate) {
|
|
|
5928
6106
|
const created = [];
|
|
5929
6107
|
const failed = [];
|
|
5930
6108
|
for (const { name, target } of toCreate) {
|
|
5931
|
-
const filePath =
|
|
6109
|
+
const filePath = join12(viewDir, name);
|
|
5932
6110
|
try {
|
|
5933
6111
|
mkdirSync(dirname4(filePath), { recursive: true });
|
|
5934
6112
|
symlinkSync(target, filePath);
|
|
@@ -5943,7 +6121,7 @@ var TOP_LEVEL_INSTRUCTION_FILES_LOWER = new Set(
|
|
|
5943
6121
|
INSTRUCTION_FILES.filter((f) => !f.includes("/")).map((f) => f.toLowerCase())
|
|
5944
6122
|
);
|
|
5945
6123
|
function classifyViewLink(viewDir, name, rosterRealpaths) {
|
|
5946
|
-
const filePath =
|
|
6124
|
+
const filePath = join12(viewDir, name);
|
|
5947
6125
|
let isLink;
|
|
5948
6126
|
try {
|
|
5949
6127
|
isLink = lstatSync(filePath).isSymbolicLink();
|
|
@@ -5957,12 +6135,12 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
|
|
|
5957
6135
|
} catch {
|
|
5958
6136
|
return null;
|
|
5959
6137
|
}
|
|
5960
|
-
const resolved =
|
|
6138
|
+
const resolved = isAbsolute4(target) ? target : resolve8(viewDir, target);
|
|
5961
6139
|
try {
|
|
5962
6140
|
if (rosterRealpaths.has(realpathSync(resolved))) return null;
|
|
5963
6141
|
} catch {
|
|
5964
6142
|
}
|
|
5965
|
-
if (
|
|
6143
|
+
if (isAbsolute4(target)) return { target, kind: "absolute" };
|
|
5966
6144
|
let isDir = false;
|
|
5967
6145
|
try {
|
|
5968
6146
|
isDir = statSync2(resolved).isDirectory();
|
|
@@ -5972,7 +6150,7 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
|
|
|
5972
6150
|
if (!isDir) {
|
|
5973
6151
|
return { target, kind: existsSync2(resolved) ? "non-repo" : "broken" };
|
|
5974
6152
|
}
|
|
5975
|
-
return { target, kind: existsSync2(
|
|
6153
|
+
return { target, kind: existsSync2(join12(resolved, ".git")) ? "repo" : "non-repo" };
|
|
5976
6154
|
}
|
|
5977
6155
|
function gatherExistingViewLinks(viewDir, rosterRealpaths) {
|
|
5978
6156
|
let names;
|
|
@@ -5997,7 +6175,7 @@ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
|
|
|
5997
6175
|
const pruned = [];
|
|
5998
6176
|
const failed = [];
|
|
5999
6177
|
for (const { name } of toPrune) {
|
|
6000
|
-
const filePath =
|
|
6178
|
+
const filePath = join12(viewDir, name);
|
|
6001
6179
|
const c = classifyViewLink(viewDir, name, rosterRealpaths);
|
|
6002
6180
|
if (c === null || c.kind !== "repo") {
|
|
6003
6181
|
failed.push({
|
|
@@ -6043,11 +6221,11 @@ async function doRunProjectWorkspace(options, ctx) {
|
|
|
6043
6221
|
} else {
|
|
6044
6222
|
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
6045
6223
|
const facts = roster.map((entry) => gatherViewRepo(repositoryRoot, viewDir, entry));
|
|
6046
|
-
const rosterNames = roster.map((entry) => basename6(
|
|
6224
|
+
const rosterNames = roster.map((entry) => basename6(resolve8(repositoryRoot, entry.path)));
|
|
6047
6225
|
const rosterRealpaths = /* @__PURE__ */ new Set();
|
|
6048
6226
|
for (const entry of roster) {
|
|
6049
6227
|
try {
|
|
6050
|
-
rosterRealpaths.add(realpathSync(
|
|
6228
|
+
rosterRealpaths.add(realpathSync(resolve8(repositoryRoot, entry.path)));
|
|
6051
6229
|
} catch {
|
|
6052
6230
|
}
|
|
6053
6231
|
}
|
|
@@ -6210,10 +6388,10 @@ async function runProjectPreset(options, ctx = {}) {
|
|
|
6210
6388
|
}
|
|
6211
6389
|
}
|
|
6212
6390
|
function canonicalFileFor(anchorReal, canonicalName) {
|
|
6213
|
-
return
|
|
6391
|
+
return join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
6214
6392
|
}
|
|
6215
6393
|
function canonicalLabelFor(canonicalName) {
|
|
6216
|
-
return
|
|
6394
|
+
return join12("agents", canonicalName, CANONICAL_FILE);
|
|
6217
6395
|
}
|
|
6218
6396
|
async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
|
|
6219
6397
|
const declared = {
|
|
@@ -6227,14 +6405,14 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
|
|
|
6227
6405
|
}
|
|
6228
6406
|
let real;
|
|
6229
6407
|
try {
|
|
6230
|
-
real = realpathSync(
|
|
6408
|
+
real = realpathSync(resolve8(repositoryRoot, entry.path));
|
|
6231
6409
|
} catch {
|
|
6232
6410
|
return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
|
|
6233
6411
|
}
|
|
6234
6412
|
if (real === anchorReal) {
|
|
6235
6413
|
return { ...declared, isAnchor: true, reachable: true, canonicalPresent: false };
|
|
6236
6414
|
}
|
|
6237
|
-
if (!existsSync2(
|
|
6415
|
+
if (!existsSync2(join12(real, ".git"))) {
|
|
6238
6416
|
return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
|
|
6239
6417
|
}
|
|
6240
6418
|
const canonicalName = basename6(real);
|
|
@@ -6283,7 +6461,7 @@ function viewPresetReposFor(repositoryRoot, roster) {
|
|
|
6283
6461
|
anchorReal = void 0;
|
|
6284
6462
|
}
|
|
6285
6463
|
return roster.map((entry) => {
|
|
6286
|
-
const abs =
|
|
6464
|
+
const abs = resolve8(repositoryRoot, entry.path);
|
|
6287
6465
|
let real;
|
|
6288
6466
|
try {
|
|
6289
6467
|
real = realpathSync(abs);
|
|
@@ -6607,7 +6785,7 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
|
|
|
6607
6785
|
};
|
|
6608
6786
|
let real;
|
|
6609
6787
|
try {
|
|
6610
|
-
real = realpathSync(
|
|
6788
|
+
real = realpathSync(resolve8(repositoryRoot, target));
|
|
6611
6789
|
} catch {
|
|
6612
6790
|
return empty;
|
|
6613
6791
|
}
|
|
@@ -6616,24 +6794,24 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
|
|
|
6616
6794
|
const instructionFiles = [];
|
|
6617
6795
|
for (const name of INSTRUCTION_FILES) {
|
|
6618
6796
|
try {
|
|
6619
|
-
lstatSync(
|
|
6797
|
+
lstatSync(join12(real, name));
|
|
6620
6798
|
instructionFiles.push(name);
|
|
6621
6799
|
} catch {
|
|
6622
6800
|
}
|
|
6623
6801
|
}
|
|
6624
6802
|
let ignored;
|
|
6625
6803
|
try {
|
|
6626
|
-
ignored = new Set(readGitignoreLines(
|
|
6804
|
+
ignored = new Set(readGitignoreLines(join12(real, ".gitignore")).map((l) => l.trim()));
|
|
6627
6805
|
} catch {
|
|
6628
6806
|
ignored = /* @__PURE__ */ new Set();
|
|
6629
6807
|
}
|
|
6630
6808
|
const gitignorePatterns = INSTRUCTION_FILES.filter((p) => ignored.has(p) || ignored.has(`/${p}`));
|
|
6631
|
-
const canonical2 = existsSync2(
|
|
6809
|
+
const canonical2 = existsSync2(join12(anchorReal, "agents", canonicalName, CANONICAL_FILE));
|
|
6632
6810
|
let viewLink = false;
|
|
6633
6811
|
const viewPath = manifest.workspace.view;
|
|
6634
6812
|
if (viewPath !== void 0) {
|
|
6635
6813
|
try {
|
|
6636
|
-
lstatSync(
|
|
6814
|
+
lstatSync(join12(resolveViewDir(repositoryRoot, viewPath), canonicalName));
|
|
6637
6815
|
viewLink = true;
|
|
6638
6816
|
} catch {
|
|
6639
6817
|
}
|
|
@@ -6647,27 +6825,27 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
|
|
|
6647
6825
|
};
|
|
6648
6826
|
}
|
|
6649
6827
|
function teardownExpectedTargets(repoReal, anchorReal, canonicalName) {
|
|
6650
|
-
const canonicalFile =
|
|
6828
|
+
const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
6651
6829
|
return expectedSymlinkTargets(repoReal, canonicalFile);
|
|
6652
6830
|
}
|
|
6653
6831
|
function viewLinkPointsAt(viewDir, name, repoReal) {
|
|
6654
|
-
const filePath =
|
|
6832
|
+
const filePath = join12(viewDir, name);
|
|
6655
6833
|
try {
|
|
6656
6834
|
if (!lstatSync(filePath).isSymbolicLink()) return false;
|
|
6657
6835
|
const target = readlinkSync(filePath);
|
|
6658
|
-
if (
|
|
6659
|
-
return realpathSync(
|
|
6836
|
+
if (isAbsolute4(target)) return false;
|
|
6837
|
+
return realpathSync(resolve8(viewDir, target)) === repoReal;
|
|
6660
6838
|
} catch {
|
|
6661
6839
|
return false;
|
|
6662
6840
|
}
|
|
6663
6841
|
}
|
|
6664
6842
|
function viewLinkPointsAtPath(viewDir, name, expectedRepoPath) {
|
|
6665
|
-
const filePath =
|
|
6843
|
+
const filePath = join12(viewDir, name);
|
|
6666
6844
|
try {
|
|
6667
6845
|
if (!lstatSync(filePath).isSymbolicLink()) return false;
|
|
6668
6846
|
const target = readlinkSync(filePath);
|
|
6669
|
-
if (
|
|
6670
|
-
return
|
|
6847
|
+
if (isAbsolute4(target)) return false;
|
|
6848
|
+
return resolve8(viewDir, target) === expectedRepoPath;
|
|
6671
6849
|
} catch {
|
|
6672
6850
|
return false;
|
|
6673
6851
|
}
|
|
@@ -6676,19 +6854,19 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
6676
6854
|
const anchorReal = realpathSync(repositoryRoot);
|
|
6677
6855
|
let repoReal;
|
|
6678
6856
|
try {
|
|
6679
|
-
repoReal = realpathSync(
|
|
6857
|
+
repoReal = realpathSync(resolve8(repositoryRoot, target));
|
|
6680
6858
|
} catch {
|
|
6681
6859
|
repoReal = void 0;
|
|
6682
6860
|
}
|
|
6683
6861
|
const isAnchor = repoReal !== void 0 && repoReal === anchorReal;
|
|
6684
|
-
const targetAbs =
|
|
6862
|
+
const targetAbs = resolve8(repositoryRoot, target);
|
|
6685
6863
|
const canonicalName = basename6(repoReal ?? targetAbs);
|
|
6686
6864
|
const roster = manifest.repos ?? [];
|
|
6687
6865
|
const declaredEntry = roster.find((r) => {
|
|
6688
6866
|
try {
|
|
6689
|
-
return realpathSync(
|
|
6867
|
+
return realpathSync(resolve8(repositoryRoot, r.path)) === (repoReal ?? "\0");
|
|
6690
6868
|
} catch {
|
|
6691
|
-
return
|
|
6869
|
+
return resolve8(repositoryRoot, r.path) === targetAbs;
|
|
6692
6870
|
}
|
|
6693
6871
|
});
|
|
6694
6872
|
const inRoster = declaredEntry !== void 0;
|
|
@@ -6697,7 +6875,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
6697
6875
|
const canonicalShared = roster.some((r) => {
|
|
6698
6876
|
let rReal = null;
|
|
6699
6877
|
try {
|
|
6700
|
-
rReal = realpathSync(
|
|
6878
|
+
rReal = realpathSync(resolve8(repositoryRoot, r.path));
|
|
6701
6879
|
} catch {
|
|
6702
6880
|
rReal = null;
|
|
6703
6881
|
}
|
|
@@ -6705,15 +6883,15 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
6705
6883
|
if (repoReal !== void 0 && rReal === repoReal) return false;
|
|
6706
6884
|
return basename6(rReal).toLowerCase() === cnFold;
|
|
6707
6885
|
}
|
|
6708
|
-
if (
|
|
6709
|
-
return basename6(
|
|
6886
|
+
if (resolve8(repositoryRoot, r.path) === targetAbs) return false;
|
|
6887
|
+
return basename6(resolve8(repositoryRoot, r.path)).toLowerCase() === cnFold;
|
|
6710
6888
|
});
|
|
6711
6889
|
const collisionNote = "shared with another repo of the same basename, so it cannot be removed (check manually)";
|
|
6712
6890
|
const items = [];
|
|
6713
6891
|
if (!isAnchor) {
|
|
6714
6892
|
if (repoReal !== void 0) {
|
|
6715
6893
|
for (const spec of teardownExpectedTargets(repoReal, anchorReal, canonicalName)) {
|
|
6716
|
-
const { state, actualTarget } = inspectSymlink(
|
|
6894
|
+
const { state, actualTarget } = inspectSymlink(join12(repoReal, spec.name), spec.target);
|
|
6717
6895
|
if (isSelf) {
|
|
6718
6896
|
if (state !== "missing")
|
|
6719
6897
|
items.push({
|
|
@@ -6750,7 +6928,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
6750
6928
|
}
|
|
6751
6929
|
let ignored;
|
|
6752
6930
|
try {
|
|
6753
|
-
ignored = new Set(readGitignoreLines(
|
|
6931
|
+
ignored = new Set(readGitignoreLines(join12(repoReal, ".gitignore")).map((l) => l.trim()));
|
|
6754
6932
|
for (const p of INSTRUCTION_FILES) {
|
|
6755
6933
|
if (ignored.has(p) || ignored.has(`/${p}`)) {
|
|
6756
6934
|
items.push({
|
|
@@ -6773,7 +6951,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
6773
6951
|
const viewPath = manifest.workspace.view;
|
|
6774
6952
|
if (viewPath !== void 0) {
|
|
6775
6953
|
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
6776
|
-
const linkPath =
|
|
6954
|
+
const linkPath = join12(viewDir, canonicalName);
|
|
6777
6955
|
let isLink = false;
|
|
6778
6956
|
try {
|
|
6779
6957
|
isLink = lstatSync(linkPath).isSymbolicLink();
|
|
@@ -6799,8 +6977,8 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
6799
6977
|
else items.push({ kind: "view-symlink", label: canonicalName, state: "removable" });
|
|
6800
6978
|
}
|
|
6801
6979
|
}
|
|
6802
|
-
const canonicalFile =
|
|
6803
|
-
const canonicalLabel =
|
|
6980
|
+
const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
6981
|
+
const canonicalLabel = join12("agents", canonicalName, CANONICAL_FILE);
|
|
6804
6982
|
let canonicalIsLink = false;
|
|
6805
6983
|
try {
|
|
6806
6984
|
canonicalIsLink = lstatSync(canonicalFile).isSymbolicLink();
|
|
@@ -6887,7 +7065,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
6887
7065
|
const changed = (label) => failed.push({ label, message: "the state changed since the scan (re-run)" });
|
|
6888
7066
|
let currentRepoReal = null;
|
|
6889
7067
|
try {
|
|
6890
|
-
currentRepoReal = realpathSync(
|
|
7068
|
+
currentRepoReal = realpathSync(resolve8(repositoryRoot, plan.target));
|
|
6891
7069
|
} catch {
|
|
6892
7070
|
currentRepoReal = null;
|
|
6893
7071
|
}
|
|
@@ -6904,12 +7082,12 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
6904
7082
|
);
|
|
6905
7083
|
for (const item of removable.filter((i) => i.kind === "instruction-symlink")) {
|
|
6906
7084
|
const expected = expectedByName.get(item.label);
|
|
6907
|
-
if (repoReal === null || expected === void 0 || inspectSymlink(
|
|
7085
|
+
if (repoReal === null || expected === void 0 || inspectSymlink(join12(repoReal, item.label), expected).state !== "correct") {
|
|
6908
7086
|
changed(item.label);
|
|
6909
7087
|
continue;
|
|
6910
7088
|
}
|
|
6911
7089
|
try {
|
|
6912
|
-
unlinkSync(
|
|
7090
|
+
unlinkSync(join12(repoReal, item.label));
|
|
6913
7091
|
removed.push(item.label);
|
|
6914
7092
|
} catch (error) {
|
|
6915
7093
|
failed.push({ label: item.label, message: failureReason(error) });
|
|
@@ -6922,13 +7100,13 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
6922
7100
|
continue;
|
|
6923
7101
|
}
|
|
6924
7102
|
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
6925
|
-
const owned = repoReal !== null ? viewLinkPointsAt(viewDir, item.label, repoReal) : viewLinkPointsAtPath(viewDir, item.label,
|
|
7103
|
+
const owned = repoReal !== null ? viewLinkPointsAt(viewDir, item.label, repoReal) : viewLinkPointsAtPath(viewDir, item.label, resolve8(repositoryRoot, plan.target));
|
|
6926
7104
|
if (!owned) {
|
|
6927
7105
|
changed(item.label);
|
|
6928
7106
|
continue;
|
|
6929
7107
|
}
|
|
6930
7108
|
try {
|
|
6931
|
-
unlinkSync(
|
|
7109
|
+
unlinkSync(join12(viewDir, item.label));
|
|
6932
7110
|
removed.push(`view/${item.label}`);
|
|
6933
7111
|
} catch (error) {
|
|
6934
7112
|
failed.push({ label: `view/${item.label}`, message: failureReason(error) });
|
|
@@ -6936,7 +7114,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
6936
7114
|
}
|
|
6937
7115
|
const NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
|
|
6938
7116
|
for (const item of removable.filter((i) => i.kind === "canonical-block")) {
|
|
6939
|
-
const canonicalFile =
|
|
7117
|
+
const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
6940
7118
|
try {
|
|
6941
7119
|
if (lstatSync(canonicalFile).isSymbolicLink()) {
|
|
6942
7120
|
changed(item.label);
|
|
@@ -7084,7 +7262,7 @@ async function doRunProjectArchive(target, options, ctx) {
|
|
|
7084
7262
|
const roster = manifest.repos ?? [];
|
|
7085
7263
|
let targetIsAnchor = false;
|
|
7086
7264
|
try {
|
|
7087
|
-
targetIsAnchor = realpathSync(
|
|
7265
|
+
targetIsAnchor = realpathSync(resolve8(repositoryRoot, target)) === realpathSync(repositoryRoot);
|
|
7088
7266
|
} catch {
|
|
7089
7267
|
targetIsAnchor = false;
|
|
7090
7268
|
}
|
|
@@ -7210,12 +7388,12 @@ function gatherRenameWiring(repositoryRoot, manifest, oldBasename) {
|
|
|
7210
7388
|
} catch {
|
|
7211
7389
|
return { canonicalDirOld: false, viewLinkOld: false };
|
|
7212
7390
|
}
|
|
7213
|
-
const canonicalDirOld = existsSync2(
|
|
7391
|
+
const canonicalDirOld = existsSync2(join12(anchorReal, "agents", oldBasename));
|
|
7214
7392
|
let viewLinkOld = false;
|
|
7215
7393
|
const viewPath = manifest.workspace.view;
|
|
7216
7394
|
if (viewPath !== void 0) {
|
|
7217
7395
|
try {
|
|
7218
|
-
lstatSync(
|
|
7396
|
+
lstatSync(join12(resolveViewDir(repositoryRoot, viewPath), oldBasename));
|
|
7219
7397
|
viewLinkOld = true;
|
|
7220
7398
|
} catch {
|
|
7221
7399
|
}
|
|
@@ -7241,7 +7419,7 @@ async function doRunProjectRename(oldPath, newPath, options, ctx) {
|
|
|
7241
7419
|
const roster = manifest.repos ?? [];
|
|
7242
7420
|
let oldIsAnchor = false;
|
|
7243
7421
|
try {
|
|
7244
|
-
oldIsAnchor = realpathSync(
|
|
7422
|
+
oldIsAnchor = realpathSync(resolve8(repositoryRoot, oldPath)) === realpathSync(repositoryRoot);
|
|
7245
7423
|
} catch {
|
|
7246
7424
|
oldIsAnchor = false;
|
|
7247
7425
|
}
|
|
@@ -7387,7 +7565,7 @@ async function doRunProjectNew(repos, options, ctx) {
|
|
|
7387
7565
|
const viewStem = productName ?? workspaceName;
|
|
7388
7566
|
const viewOverridesProjectName = productName !== void 0 && typeof options.view === "string";
|
|
7389
7567
|
const declared = repos.map((p) => {
|
|
7390
|
-
const abs =
|
|
7568
|
+
const abs = resolve8(cwd, p);
|
|
7391
7569
|
let real;
|
|
7392
7570
|
try {
|
|
7393
7571
|
real = realpathSync(abs);
|
|
@@ -7573,7 +7751,7 @@ async function doRunProjectSeedAnchor(options, ctx) {
|
|
|
7573
7751
|
console.log("\u2139\uFE0F No repo roster declared \u2014 nothing to seed.");
|
|
7574
7752
|
return;
|
|
7575
7753
|
}
|
|
7576
|
-
const anchorDoc =
|
|
7754
|
+
const anchorDoc = join12(repositoryRoot, CANONICAL_FILE);
|
|
7577
7755
|
if (pathPresent(anchorDoc)) {
|
|
7578
7756
|
console.log(
|
|
7579
7757
|
`\u2705 The anchor's own \`${CANONICAL_FILE}\` already exists \u2014 hand-maintained, left untouched.`
|
|
@@ -7635,7 +7813,7 @@ function regularFileSpokes(repoReal) {
|
|
|
7635
7813
|
const out = [];
|
|
7636
7814
|
for (const spoke of ["CLAUDE.md", ".github/copilot-instructions.md"]) {
|
|
7637
7815
|
try {
|
|
7638
|
-
const st = lstatSync(
|
|
7816
|
+
const st = lstatSync(join12(repoReal, spoke));
|
|
7639
7817
|
if (!st.isSymbolicLink() && st.isFile()) out.push(spoke);
|
|
7640
7818
|
} catch {
|
|
7641
7819
|
}
|
|
@@ -7652,7 +7830,7 @@ function pathPresent(p) {
|
|
|
7652
7830
|
}
|
|
7653
7831
|
function gatherRetrofit(repositoryRoot, anchorReal, roster, argAbs, argReal, viewCanonicalName) {
|
|
7654
7832
|
const declaredEntry = roster.find((entry) => {
|
|
7655
|
-
const entryAbs =
|
|
7833
|
+
const entryAbs = resolve8(repositoryRoot, entry.path);
|
|
7656
7834
|
if (argReal !== void 0) {
|
|
7657
7835
|
try {
|
|
7658
7836
|
if (realpathSync(entryAbs) === argReal) return true;
|
|
@@ -7681,8 +7859,8 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argAbs, argReal, vie
|
|
|
7681
7859
|
};
|
|
7682
7860
|
}
|
|
7683
7861
|
const isAnchor = argReal === anchorReal;
|
|
7684
|
-
const reachable = existsSync2(
|
|
7685
|
-
const canonicalFile =
|
|
7862
|
+
const reachable = existsSync2(join12(argReal, ".git"));
|
|
7863
|
+
const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
7686
7864
|
return {
|
|
7687
7865
|
path,
|
|
7688
7866
|
declared,
|
|
@@ -7691,13 +7869,13 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argAbs, argReal, vie
|
|
|
7691
7869
|
reachable,
|
|
7692
7870
|
canonicalName,
|
|
7693
7871
|
...viewCanonicalName !== void 0 ? { viewCanonicalName } : {},
|
|
7694
|
-
agentsState: inspectAgentsState(
|
|
7872
|
+
agentsState: inspectAgentsState(join12(argReal, CANONICAL_FILE)),
|
|
7695
7873
|
canonicalExists: pathPresent(canonicalFile),
|
|
7696
7874
|
regularSpokes: regularFileSpokes(argReal)
|
|
7697
7875
|
};
|
|
7698
7876
|
}
|
|
7699
7877
|
function relocateAgentsFile(repoReal, canonicalFile) {
|
|
7700
|
-
const agentsFile =
|
|
7878
|
+
const agentsFile = join12(repoReal, CANONICAL_FILE);
|
|
7701
7879
|
try {
|
|
7702
7880
|
mkdirSync(dirname4(canonicalFile), { recursive: true });
|
|
7703
7881
|
} catch (error) {
|
|
@@ -7792,7 +7970,7 @@ async function doRunProjectRetrofit(repo, options, ctx) {
|
|
|
7792
7970
|
}
|
|
7793
7971
|
return result2;
|
|
7794
7972
|
}
|
|
7795
|
-
const argAbs =
|
|
7973
|
+
const argAbs = resolve8(repositoryRoot, repo);
|
|
7796
7974
|
let argReal;
|
|
7797
7975
|
try {
|
|
7798
7976
|
argReal = realpathSync(argAbs);
|
|
@@ -7813,7 +7991,7 @@ async function doRunProjectRetrofit(repo, options, ctx) {
|
|
|
7813
7991
|
let failure;
|
|
7814
7992
|
let partial = false;
|
|
7815
7993
|
if (options.apply === true && plan.action === "relocate" && argReal !== void 0) {
|
|
7816
|
-
const canonicalFile =
|
|
7994
|
+
const canonicalFile = join12(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
|
|
7817
7995
|
const res = relocateAgentsFile(argReal, canonicalFile);
|
|
7818
7996
|
if (res.ok) {
|
|
7819
7997
|
applied = true;
|
|
@@ -8013,88 +8191,19 @@ function renderProjectRetrofit(result) {
|
|
|
8013
8191
|
}
|
|
8014
8192
|
|
|
8015
8193
|
// src/commands/protocol.ts
|
|
8016
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
8017
|
-
import {
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
if (p.startsWith("~/")) return join12(homedir8(), p.slice(2));
|
|
8030
|
-
return p;
|
|
8031
|
-
}
|
|
8032
|
-
function isRecord3(value) {
|
|
8033
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8034
|
-
}
|
|
8035
|
-
async function loadProtocolsConfig(configPath = DEFAULT_PROTOCOLS_CONFIG_PATH) {
|
|
8036
|
-
let raw;
|
|
8037
|
-
try {
|
|
8038
|
-
raw = await readYamlFile5(configPath);
|
|
8039
|
-
} catch (error) {
|
|
8040
|
-
if (error instanceof Error && error.message === "YAML file not found") {
|
|
8041
|
-
throw new Error(
|
|
8042
|
-
"No protocols config at ~/.basou/protocols.yaml. Create one (a 'protocols:' list of source markdown paths) before running 'basou protocol sync'."
|
|
8043
|
-
);
|
|
8044
|
-
}
|
|
8045
|
-
if (error instanceof Error && error.message === "Failed to parse YAML content") {
|
|
8046
|
-
throw new Error("~/.basou/protocols.yaml is not valid YAML.");
|
|
8047
|
-
}
|
|
8048
|
-
throw error;
|
|
8049
|
-
}
|
|
8050
|
-
if (!isRecord3(raw) || !Array.isArray(raw.protocols)) {
|
|
8051
|
-
throw new Error("~/.basou/protocols.yaml must contain a 'protocols:' list.");
|
|
8052
|
-
}
|
|
8053
|
-
for (const key of Object.keys(raw)) {
|
|
8054
|
-
if (!ALLOWED_TOP_KEYS.has(key)) {
|
|
8055
|
-
throw new Error(
|
|
8056
|
-
`~/.basou/protocols.yaml has an unknown key '${key}' (allowed: version, protocols).`
|
|
8057
|
-
);
|
|
8058
|
-
}
|
|
8059
|
-
}
|
|
8060
|
-
const seen = /* @__PURE__ */ new Set();
|
|
8061
|
-
const result = [];
|
|
8062
|
-
for (const entry of raw.protocols) {
|
|
8063
|
-
if (!isRecord3(entry)) {
|
|
8064
|
-
throw new Error("Each protocol entry must be a mapping with a 'source' key.");
|
|
8065
|
-
}
|
|
8066
|
-
for (const key of Object.keys(entry)) {
|
|
8067
|
-
if (!ALLOWED_ENTRY_KEYS.has(key)) {
|
|
8068
|
-
throw new Error(`A protocol entry has an unknown key '${key}' (allowed: source, title).`);
|
|
8069
|
-
}
|
|
8070
|
-
}
|
|
8071
|
-
if (typeof entry.source !== "string" || entry.source.trim().length === 0) {
|
|
8072
|
-
throw new Error("Each protocol entry needs a non-empty string 'source'.");
|
|
8073
|
-
}
|
|
8074
|
-
if (entry.title !== void 0 && (typeof entry.title !== "string" || entry.title.trim().length === 0)) {
|
|
8075
|
-
throw new Error("A protocol entry 'title' must be a non-empty string when present.");
|
|
8076
|
-
}
|
|
8077
|
-
const expanded = expandTilde3(entry.source.trim());
|
|
8078
|
-
if (!isAbsolute4(expanded)) {
|
|
8079
|
-
throw new Error("Protocol 'source' paths must be absolute (or start with '~').");
|
|
8080
|
-
}
|
|
8081
|
-
const abs = resolve8(expanded);
|
|
8082
|
-
if (seen.has(abs)) {
|
|
8083
|
-
throw new Error("Duplicate protocol source (each source path may appear only once).");
|
|
8084
|
-
}
|
|
8085
|
-
seen.add(abs);
|
|
8086
|
-
result.push(
|
|
8087
|
-
entry.title !== void 0 ? { source: abs, title: entry.title.trim() } : { source: abs }
|
|
8088
|
-
);
|
|
8089
|
-
}
|
|
8090
|
-
if (result.length === 0) {
|
|
8091
|
-
throw new Error("~/.basou/protocols.yaml has no protocols.");
|
|
8092
|
-
}
|
|
8093
|
-
return result;
|
|
8094
|
-
}
|
|
8095
|
-
|
|
8096
|
-
// src/commands/protocol.ts
|
|
8097
|
-
var PROTOCOL_MARKERS = { start: PROTOCOL_START, end: PROTOCOL_END };
|
|
8194
|
+
import { readFile as readFile4, stat as stat5 } from "fs/promises";
|
|
8195
|
+
import {
|
|
8196
|
+
carryForwardProtocolStamp,
|
|
8197
|
+
PROTOCOL_END as PROTOCOL_END2,
|
|
8198
|
+
PROTOCOL_START as PROTOCOL_START2,
|
|
8199
|
+
parseMarkers as parseMarkers4,
|
|
8200
|
+
parseProtocolStamp as parseProtocolStamp2,
|
|
8201
|
+
protocolBlockHash,
|
|
8202
|
+
readMarkdownFile as readMarkdownFile8,
|
|
8203
|
+
renderProtocolStamp,
|
|
8204
|
+
unstampedProtocolSectionsFrom
|
|
8205
|
+
} from "@basou/core";
|
|
8206
|
+
var PROTOCOL_MARKERS = { start: PROTOCOL_START2, end: PROTOCOL_END2 };
|
|
8098
8207
|
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. -->";
|
|
8099
8208
|
function registerProtocolCommand(program) {
|
|
8100
8209
|
const protocol = program.command("protocol").description("Manage the basou-managed standing-protocol block in the global CLAUDE.md");
|
|
@@ -8154,24 +8263,54 @@ async function readProtocolSources(entries) {
|
|
|
8154
8263
|
}
|
|
8155
8264
|
return out;
|
|
8156
8265
|
}
|
|
8157
|
-
function
|
|
8158
|
-
|
|
8266
|
+
function buildSections(sources) {
|
|
8267
|
+
return sources.map(({ entry, content }) => {
|
|
8159
8268
|
const body = content.replace(/\s+$/, "");
|
|
8160
8269
|
return entry.title !== void 0 ? `## ${entry.title}
|
|
8161
8270
|
|
|
8162
8271
|
${body}` : body;
|
|
8163
|
-
});
|
|
8272
|
+
}).join("\n\n");
|
|
8273
|
+
}
|
|
8274
|
+
function buildBlock(sections, stamp) {
|
|
8164
8275
|
return `${MANAGED_NOTE}
|
|
8276
|
+
${renderProtocolStamp(stamp)}
|
|
8165
8277
|
|
|
8166
|
-
${sections
|
|
8278
|
+
${sections}
|
|
8167
8279
|
`;
|
|
8168
8280
|
}
|
|
8281
|
+
async function readPreviousStamp(target) {
|
|
8282
|
+
const existing = await readMarkdownFile8(target);
|
|
8283
|
+
if (existing === null) return null;
|
|
8284
|
+
const section = parseMarkers4(existing, PROTOCOL_MARKERS);
|
|
8285
|
+
if (section.kind !== "ok") return null;
|
|
8286
|
+
const stamp = parseProtocolStamp2(section.generated);
|
|
8287
|
+
if (stamp !== null) return stamp;
|
|
8288
|
+
const writtenAt = await lastWrittenAt(target);
|
|
8289
|
+
if (writtenAt === null) return null;
|
|
8290
|
+
return {
|
|
8291
|
+
changedAt: writtenAt,
|
|
8292
|
+
contentHash: protocolBlockHash(unstampedProtocolSectionsFrom(section.generated))
|
|
8293
|
+
};
|
|
8294
|
+
}
|
|
8295
|
+
async function lastWrittenAt(target) {
|
|
8296
|
+
try {
|
|
8297
|
+
return new Date((await stat5(target)).mtimeMs).toISOString();
|
|
8298
|
+
} catch {
|
|
8299
|
+
return null;
|
|
8300
|
+
}
|
|
8301
|
+
}
|
|
8169
8302
|
async function doRunProtocolSync(options, ctx = {}) {
|
|
8170
8303
|
const configPath = options.config ?? DEFAULT_PROTOCOLS_CONFIG_PATH;
|
|
8171
8304
|
const target = options.target ?? DEFAULT_TARGET_PATH;
|
|
8172
8305
|
const entries = await loadProtocolsConfig(configPath);
|
|
8173
8306
|
const sources = await readProtocolSources(entries);
|
|
8174
|
-
const
|
|
8307
|
+
const sections = buildSections(sources);
|
|
8308
|
+
const stamp = carryForwardProtocolStamp({
|
|
8309
|
+
sections,
|
|
8310
|
+
previous: await readPreviousStamp(target),
|
|
8311
|
+
now: (/* @__PURE__ */ new Date()).toISOString()
|
|
8312
|
+
});
|
|
8313
|
+
const block = buildBlock(sections, stamp);
|
|
8175
8314
|
const foreign = await findForeignWorkspaceNames({
|
|
8176
8315
|
text: block,
|
|
8177
8316
|
configPath: ctx.portfolioConfigPath
|
|
@@ -8241,7 +8380,7 @@ import {
|
|
|
8241
8380
|
import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
|
|
8242
8381
|
|
|
8243
8382
|
// src/commands/refresh-watch.ts
|
|
8244
|
-
import { readdir as readdir2, stat as
|
|
8383
|
+
import { readdir as readdir2, stat as stat6 } from "fs/promises";
|
|
8245
8384
|
import { homedir as homedir9 } from "os";
|
|
8246
8385
|
import { join as join13 } from "path";
|
|
8247
8386
|
import { findErrorCode as findErrorCode8 } from "@basou/core";
|
|
@@ -8270,7 +8409,7 @@ async function scanSourceLogs(roots) {
|
|
|
8270
8409
|
await walk(full);
|
|
8271
8410
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
8272
8411
|
try {
|
|
8273
|
-
const info = await
|
|
8412
|
+
const info = await stat6(full);
|
|
8274
8413
|
out.set(full, { mtimeMs: info.mtimeMs, size: info.size });
|
|
8275
8414
|
} catch (error) {
|
|
8276
8415
|
if (findErrorCode8(error, "ENOENT")) continue;
|
|
@@ -11730,7 +11869,7 @@ import { InvalidArgumentError as InvalidArgumentError8 } from "commander";
|
|
|
11730
11869
|
|
|
11731
11870
|
// src/lib/portfolio-coverage.ts
|
|
11732
11871
|
import { createReadStream as createReadStream2 } from "fs";
|
|
11733
|
-
import { readdir as readdir3, stat as
|
|
11872
|
+
import { readdir as readdir3, stat as stat7 } from "fs/promises";
|
|
11734
11873
|
import { homedir as homedir12 } from "os";
|
|
11735
11874
|
import { basename as basename8, dirname as dirname5, join as join17 } from "path";
|
|
11736
11875
|
import { createInterface as createInterface2 } from "readline";
|
|
@@ -11877,7 +12016,7 @@ async function isDirEntry(parent, entry) {
|
|
|
11877
12016
|
if (entry.isDirectory()) return true;
|
|
11878
12017
|
if (!entry.isSymbolicLink()) return false;
|
|
11879
12018
|
try {
|
|
11880
|
-
return (await
|
|
12019
|
+
return (await stat7(join17(parent, entry.name))).isDirectory();
|
|
11881
12020
|
} catch {
|
|
11882
12021
|
return false;
|
|
11883
12022
|
}
|
|
@@ -12575,6 +12714,18 @@ var VIEW_HTML = `<!doctype html>
|
|
|
12575
12714
|
return el('span', { class: 'badge ok', text: 'up to date' });
|
|
12576
12715
|
}
|
|
12577
12716
|
|
|
12717
|
+
// Three states hide behind a bare 'in-flight 0': nothing ever recorded,
|
|
12718
|
+
// everything finished, and a task store that cannot be read -- where the
|
|
12719
|
+
// count is not zero but UNKNOWN. Say which one it is, as orient does.
|
|
12720
|
+
// Kept as a named top-level function with no free variables so the test suite
|
|
12721
|
+
// can lift it out of this template and run it; inlined in the card it was
|
|
12722
|
+
// unreachable from any test.
|
|
12723
|
+
function taskFlightLabel(w) {
|
|
12724
|
+
if (w.unreadableTaskCount > 0) return 'in-flight unknown (' + w.unreadableTaskCount + ' unreadable)';
|
|
12725
|
+
if (w.anyTaskEverRecorded === false) return 'no tasks recorded';
|
|
12726
|
+
return 'in-flight ' + w.inFlightCount;
|
|
12727
|
+
}
|
|
12728
|
+
|
|
12578
12729
|
function portfolioCard(w, generatedAt) {
|
|
12579
12730
|
if (!w.initialized) {
|
|
12580
12731
|
return el('div', { class: 'card pcard muted' }, [
|
|
@@ -12590,6 +12741,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
12590
12741
|
}
|
|
12591
12742
|
var pend = w.pendingApprovals || [];
|
|
12592
12743
|
var pendText = 'pending ' + pend.length + (pend.length ? ' (' + highestRisk(pend) + ')' : '');
|
|
12744
|
+
var flightText = taskFlightLabel(w);
|
|
12593
12745
|
var now = w.latestSession ? ((w.latestSession.label || '(session)') + ' [' + w.latestSession.status + ']') : '(no live sessions)';
|
|
12594
12746
|
var dec = w.latestDecision ? w.latestDecision.title : '(no decisions yet)';
|
|
12595
12747
|
var newest = (w.freshness && w.freshness.newestStartedAt) ? w.freshness.newestStartedAt : null;
|
|
@@ -12602,7 +12754,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
12602
12754
|
]),
|
|
12603
12755
|
el('div', { class: 'f', text: 'now: ' + now }),
|
|
12604
12756
|
el('div', { class: 'f', text: 'latest: ' + dec }),
|
|
12605
|
-
el('div', { class: 'f', text:
|
|
12757
|
+
el('div', { class: 'f', text: flightText + ' | ' + pendText + ' | suspect ' + w.suspectCount }),
|
|
12606
12758
|
el('div', { class: 'f muted', text: 'sessions ' + w.sessionCount + ' | newest ' + relAge(newest, generatedAt) })
|
|
12607
12759
|
]);
|
|
12608
12760
|
}
|
|
@@ -13179,6 +13331,13 @@ async function portfolioCard(ws, nowIso) {
|
|
|
13179
13331
|
sessionCount: s.sessionCount,
|
|
13180
13332
|
suspectCount: s.suspects.length,
|
|
13181
13333
|
inFlightCount: s.inFlightTasks.length,
|
|
13334
|
+
// Three states hide behind `inFlightCount: 0`: nothing ever recorded,
|
|
13335
|
+
// everything finished, and a store whose task files cannot be read (where
|
|
13336
|
+
// the count is not 0 but UNKNOWN). `orient` branches all three ways; the
|
|
13337
|
+
// card carries the two extra facts so it can too, instead of reporting a
|
|
13338
|
+
// loader blind spot as an empty record.
|
|
13339
|
+
anyTaskEverRecorded: s.anyTaskEverRecorded,
|
|
13340
|
+
unreadableTaskCount: s.unreadableTaskCount,
|
|
13182
13341
|
pendingApprovals: s.pendingApprovals.map((a) => ({
|
|
13183
13342
|
risk: a.risk,
|
|
13184
13343
|
kind: a.kind,
|
|
@@ -13646,7 +13805,7 @@ async function assertWorkspaceInitialized15(basouRoot) {
|
|
|
13646
13805
|
function readBuildStamp() {
|
|
13647
13806
|
if (false) return void 0;
|
|
13648
13807
|
try {
|
|
13649
|
-
return JSON.parse('{"version":"0.
|
|
13808
|
+
return JSON.parse('{"version":"0.47.0","commit":"831e0cb","committedAt":"2026-09-20T22:28:13+09:00"}');
|
|
13650
13809
|
} catch {
|
|
13651
13810
|
return void 0;
|
|
13652
13811
|
}
|