@basou/cli 0.37.0 → 0.39.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 +771 -327
- package/dist/index.js.map +1 -1
- package/dist/program.js +771 -327
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -632,10 +632,243 @@ function printNoApprovals(options) {
|
|
|
632
632
|
}
|
|
633
633
|
}
|
|
634
634
|
|
|
635
|
+
// src/lib/context-channel.ts
|
|
636
|
+
import { homedir } from "os";
|
|
637
|
+
import { join as join3 } from "path";
|
|
638
|
+
import {
|
|
639
|
+
ORIENTATION_END,
|
|
640
|
+
ORIENTATION_START,
|
|
641
|
+
parseMarkers,
|
|
642
|
+
readMarkdownFile,
|
|
643
|
+
removeMarkerSection
|
|
644
|
+
} from "@basou/core";
|
|
645
|
+
|
|
646
|
+
// src/lib/durable-write.ts
|
|
647
|
+
import { randomUUID } from "crypto";
|
|
648
|
+
import { lstat, open, rename, stat, unlink as unlink2 } from "fs/promises";
|
|
649
|
+
import { basename, dirname, join as join2 } from "path";
|
|
650
|
+
async function assertNotSymlink(targetPath) {
|
|
651
|
+
try {
|
|
652
|
+
const st = await lstat(targetPath);
|
|
653
|
+
if (st.isSymbolicLink()) {
|
|
654
|
+
throw new Error(
|
|
655
|
+
"Refusing to write through a symlink. Replace the symlinked target with a regular file (or remove it) and retry."
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
} catch (error) {
|
|
659
|
+
if (error instanceof Error && error.code === "ENOENT") return;
|
|
660
|
+
throw error;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
async function writeFileDurable(targetPath, content) {
|
|
664
|
+
const dir = dirname(targetPath);
|
|
665
|
+
const tmpPath = join2(dir, `.${basename(targetPath)}.tmp.${randomUUID()}`);
|
|
666
|
+
let mode = 420;
|
|
667
|
+
try {
|
|
668
|
+
mode = (await stat(targetPath)).mode & 511;
|
|
669
|
+
} catch (error) {
|
|
670
|
+
if (!(error instanceof Error && error.code === "ENOENT")) {
|
|
671
|
+
throw error;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
let handle;
|
|
675
|
+
try {
|
|
676
|
+
handle = await open(tmpPath, "wx", mode);
|
|
677
|
+
await handle.writeFile(content, "utf8");
|
|
678
|
+
await handle.chmod(mode);
|
|
679
|
+
await handle.sync();
|
|
680
|
+
await handle.close();
|
|
681
|
+
handle = void 0;
|
|
682
|
+
await rename(tmpPath, targetPath);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
if (handle) await handle.close().catch(() => void 0);
|
|
685
|
+
await unlink2(tmpPath).catch(() => void 0);
|
|
686
|
+
throw error;
|
|
687
|
+
}
|
|
688
|
+
try {
|
|
689
|
+
const dirHandle = await open(dir, "r");
|
|
690
|
+
try {
|
|
691
|
+
await dirHandle.sync();
|
|
692
|
+
} finally {
|
|
693
|
+
await dirHandle.close();
|
|
694
|
+
}
|
|
695
|
+
} catch {
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/lib/context-channel.ts
|
|
700
|
+
var CODEX_TARGET_PATH = join3(homedir(), ".codex", "AGENTS.md");
|
|
701
|
+
var ORIENTATION_MARKERS = { start: ORIENTATION_START, end: ORIENTATION_END };
|
|
702
|
+
var ORIENTATION_MANAGED_NOTE = "<!-- Managed by basou: 'basou refresh' regenerates everything between the BASOU:ORIENTATION markers with the workspace's current position. This block is transient \u2014 it changes every refresh; do not edit it. -->";
|
|
703
|
+
function buildTargetBody(existing, block, markers) {
|
|
704
|
+
const wrapped = `${markers.start}
|
|
705
|
+
${block}${markers.end}
|
|
706
|
+
`;
|
|
707
|
+
if (existing === null || existing === "") return wrapped;
|
|
708
|
+
const section = parseMarkers(existing, markers);
|
|
709
|
+
switch (section.kind) {
|
|
710
|
+
case "ok":
|
|
711
|
+
return `${section.before}${markers.start}
|
|
712
|
+
${block}${markers.end}${section.after}`;
|
|
713
|
+
case "no_markers": {
|
|
714
|
+
const sep = existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
715
|
+
return `${existing}${sep}${wrapped}`;
|
|
716
|
+
}
|
|
717
|
+
default:
|
|
718
|
+
throw new Error(
|
|
719
|
+
"The basou-managed markers in the target are malformed (a marker is missing, duplicated, or out of order). Fix or remove them, then retry."
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
async function backupOnce(target, existing) {
|
|
724
|
+
const bak = `${target}.basou-bak`;
|
|
725
|
+
const already = await readMarkdownFile(bak);
|
|
726
|
+
if (already !== null) return;
|
|
727
|
+
await writeFileDurable(bak, existing ?? "");
|
|
728
|
+
}
|
|
729
|
+
async function syncMarkerBlock(opts) {
|
|
730
|
+
const { target, markers, block } = opts;
|
|
731
|
+
await assertNotSymlink(target);
|
|
732
|
+
const existing = await readMarkdownFile(target);
|
|
733
|
+
const newBody = buildTargetBody(existing, block, markers);
|
|
734
|
+
if (newBody === existing) return { action: "unchanged" };
|
|
735
|
+
const hadBlock = existing !== null && parseMarkers(existing, markers).kind === "ok";
|
|
736
|
+
const action = hadBlock ? "updated" : "installed";
|
|
737
|
+
if (opts.dryRun === true) return { action };
|
|
738
|
+
const recheck = await readMarkdownFile(target);
|
|
739
|
+
if (recheck !== existing) {
|
|
740
|
+
throw new Error(
|
|
741
|
+
"The target changed during sync; aborting so a concurrent edit is not overwritten. Re-run the command."
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
await backupOnce(target, existing);
|
|
745
|
+
await writeFileDurable(target, newBody);
|
|
746
|
+
return { action };
|
|
747
|
+
}
|
|
748
|
+
function assertNoMarkerLine(body, markers) {
|
|
749
|
+
for (const line of body.split(/\r?\n/)) {
|
|
750
|
+
if (line === markers.start || line === markers.end) {
|
|
751
|
+
throw new Error(
|
|
752
|
+
"The content contains a basou marker line, which would corrupt the managed block. Remove that line from the source."
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
async function removeMarkerBlock(opts) {
|
|
758
|
+
const { target, markers, fileLabel } = opts;
|
|
759
|
+
await assertNotSymlink(target);
|
|
760
|
+
const existing = await readMarkdownFile(target);
|
|
761
|
+
if (existing === null) return { removed: false };
|
|
762
|
+
const newBody = removeMarkerSection(existing, fileLabel, markers);
|
|
763
|
+
if (newBody === existing) return { removed: false };
|
|
764
|
+
if (opts.dryRun === true) return { removed: true };
|
|
765
|
+
const recheck = await readMarkdownFile(target);
|
|
766
|
+
if (recheck !== existing) {
|
|
767
|
+
throw new Error(
|
|
768
|
+
"The target changed during unsync; aborting so a concurrent edit is not overwritten. Re-run the command."
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
if (opts.backup !== false) await backupOnce(target, existing);
|
|
772
|
+
await writeFileDurable(target, newBody);
|
|
773
|
+
return { removed: true };
|
|
774
|
+
}
|
|
775
|
+
async function syncOrientationChannel(opts) {
|
|
776
|
+
assertNoMarkerLine(opts.body, ORIENTATION_MARKERS);
|
|
777
|
+
const block = `${ORIENTATION_MANAGED_NOTE}
|
|
778
|
+
|
|
779
|
+
${opts.body.replace(/\s+$/, "")}
|
|
780
|
+
`;
|
|
781
|
+
return syncMarkerBlock({
|
|
782
|
+
target: opts.target ?? CODEX_TARGET_PATH,
|
|
783
|
+
markers: ORIENTATION_MARKERS,
|
|
784
|
+
block,
|
|
785
|
+
...opts.dryRun === true ? { dryRun: true } : {}
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
async function clearOrientationChannel(opts) {
|
|
789
|
+
return removeMarkerBlock({
|
|
790
|
+
target: opts.target ?? CODEX_TARGET_PATH,
|
|
791
|
+
markers: ORIENTATION_MARKERS,
|
|
792
|
+
// Name the file actually acted on, so an error under the test seam does not
|
|
793
|
+
// point at the locked path.
|
|
794
|
+
fileLabel: opts.target ?? "~/.codex/AGENTS.md",
|
|
795
|
+
// The block is being removed because it should not be on this machine;
|
|
796
|
+
// preserving it in `.basou-bak` would defeat the command.
|
|
797
|
+
backup: false,
|
|
798
|
+
...opts.dryRun === true ? { dryRun: true } : {}
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
async function renderOrientationToCodexChannel(opts) {
|
|
802
|
+
const body = await readMarkdownFile(opts.orientationPath);
|
|
803
|
+
if (body === null) return null;
|
|
804
|
+
const { action } = await syncOrientationChannel({
|
|
805
|
+
body,
|
|
806
|
+
...opts.channelPath !== void 0 ? { target: opts.channelPath } : {}
|
|
807
|
+
});
|
|
808
|
+
return {
|
|
809
|
+
action,
|
|
810
|
+
line: `codex channel: orientation ${action} in ${opts.channelPath ?? "~/.codex/AGENTS.md"}`
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// src/commands/channel.ts
|
|
815
|
+
function registerChannelCommand(program2) {
|
|
816
|
+
const channel = program2.command("channel").description(
|
|
817
|
+
"Manage the user-global context faces basou renders into \u2014 files every project's AI tool auto-loads (~/.codex/AGENTS.md)"
|
|
818
|
+
);
|
|
819
|
+
channel.command("clear").argument(
|
|
820
|
+
"<face>",
|
|
821
|
+
"the face to clear: `codex` (the basou:orientation block in ~/.codex/AGENTS.md)"
|
|
822
|
+
).description(
|
|
823
|
+
"Remove basou's block from a user-global context face, so no workspace's position is left in a file that another project's tool reads"
|
|
824
|
+
).option("--dry-run", "Report whether a block would be removed without writing").option("--json", "Output the result as JSON").option("--target <path>", "Override the target file (intended for tests)").option("-v, --verbose", "Show error causes").action(async (face, opts) => {
|
|
825
|
+
await runChannelClear(face, opts);
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
async function runChannelClear(face, options) {
|
|
829
|
+
if (face !== "codex") {
|
|
830
|
+
console.error(
|
|
831
|
+
`Unknown face '${face}'. Faces: codex (~/.codex/AGENTS.md). The basou:protocols block in ~/.claude/CLAUDE.md is removed with \`basou protocol unsync\`.`
|
|
832
|
+
);
|
|
833
|
+
process.exitCode = 1;
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
try {
|
|
837
|
+
await doRunChannelClear(face, options);
|
|
838
|
+
} catch (error) {
|
|
839
|
+
renderCliError(error, { verbose: isVerbose(options) });
|
|
840
|
+
process.exitCode = 1;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
async function doRunChannelClear(face, options) {
|
|
844
|
+
const isDry = options.dryRun === true;
|
|
845
|
+
const { removed } = await clearOrientationChannel({
|
|
846
|
+
...options.target !== void 0 ? { target: options.target } : {},
|
|
847
|
+
...isDry ? { dryRun: true } : {}
|
|
848
|
+
});
|
|
849
|
+
const target = options.target ?? CODEX_TARGET_PATH;
|
|
850
|
+
const label = options.target ?? "~/.codex/AGENTS.md";
|
|
851
|
+
const result = { face, target, removed, dry_run: isDry };
|
|
852
|
+
if (options.json === true) {
|
|
853
|
+
console.log(JSON.stringify(result));
|
|
854
|
+
return result;
|
|
855
|
+
}
|
|
856
|
+
if (!removed) {
|
|
857
|
+
console.log(`Nothing to clear: ${label} carries no basou:orientation block.`);
|
|
858
|
+
} else if (isDry) {
|
|
859
|
+
console.log(`[dry-run] Would remove the basou:orientation block from ${label}.`);
|
|
860
|
+
} else {
|
|
861
|
+
console.log(
|
|
862
|
+
`Removed the basou:orientation block from ${label}. Nothing basou wrote remains in that file; the next opted-in \`basou refresh\` renders it again.`
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
return result;
|
|
866
|
+
}
|
|
867
|
+
|
|
635
868
|
// src/commands/decision.ts
|
|
636
869
|
import { readFile } from "fs/promises";
|
|
637
|
-
import { homedir as
|
|
638
|
-
import { join as
|
|
870
|
+
import { homedir as homedir3 } from "os";
|
|
871
|
+
import { join as join5, resolve as resolve3 } from "path";
|
|
639
872
|
import {
|
|
640
873
|
AGENT_INFRA_DIRS,
|
|
641
874
|
acquireLock as acquireLock2,
|
|
@@ -657,18 +890,18 @@ import {
|
|
|
657
890
|
import { InvalidArgumentError } from "commander";
|
|
658
891
|
|
|
659
892
|
// src/lib/repo-root.ts
|
|
660
|
-
import { realpath, stat } from "fs/promises";
|
|
661
|
-
import { basename, resolve as resolve2 } from "path";
|
|
893
|
+
import { realpath, stat as stat2 } from "fs/promises";
|
|
894
|
+
import { basename as basename2, resolve as resolve2 } from "path";
|
|
662
895
|
import { basouPaths as basouPaths2, readManifest, resolveBasouRepositoryRoot } from "@basou/core";
|
|
663
896
|
|
|
664
897
|
// src/lib/portfolio-config.ts
|
|
665
|
-
import { homedir } from "os";
|
|
666
|
-
import { isAbsolute, join as
|
|
898
|
+
import { homedir as homedir2 } from "os";
|
|
899
|
+
import { isAbsolute, join as join4, resolve } from "path";
|
|
667
900
|
import { readYamlFile as readYamlFile2 } from "@basou/core";
|
|
668
|
-
var DEFAULT_PORTFOLIO_CONFIG_PATH =
|
|
901
|
+
var DEFAULT_PORTFOLIO_CONFIG_PATH = join4(homedir2(), ".basou", "portfolio.yaml");
|
|
669
902
|
function expandTilde(p) {
|
|
670
|
-
if (p === "~") return
|
|
671
|
-
if (p.startsWith("~/")) return
|
|
903
|
+
if (p === "~") return homedir2();
|
|
904
|
+
if (p.startsWith("~/")) return join4(homedir2(), p.slice(2));
|
|
672
905
|
return p;
|
|
673
906
|
}
|
|
674
907
|
function isRecord(value) {
|
|
@@ -750,7 +983,7 @@ async function resolveBasouRootForCommand(cwd, commandName, opts = {}) {
|
|
|
750
983
|
}
|
|
751
984
|
async function hasBasouStore(root) {
|
|
752
985
|
try {
|
|
753
|
-
return (await
|
|
986
|
+
return (await stat2(basouPaths2(root).root)).isDirectory();
|
|
754
987
|
} catch {
|
|
755
988
|
return false;
|
|
756
989
|
}
|
|
@@ -782,7 +1015,7 @@ async function resolveMemberToMaster(repoRoot, configPath) {
|
|
|
782
1015
|
} catch (error) {
|
|
783
1016
|
if (error instanceof Error && error.message !== "YAML file not found") {
|
|
784
1017
|
console.error(
|
|
785
|
-
`Skipping portfolio workspace '${ws.label ??
|
|
1018
|
+
`Skipping portfolio workspace '${ws.label ?? basename2(masterReal)}': could not read its manifest (${error.message}).`
|
|
786
1019
|
);
|
|
787
1020
|
}
|
|
788
1021
|
continue;
|
|
@@ -791,7 +1024,7 @@ async function resolveMemberToMaster(repoRoot, configPath) {
|
|
|
791
1024
|
for (const sr of sourceRoots) {
|
|
792
1025
|
const real = await realpathOrNull(resolve2(masterReal, sr));
|
|
793
1026
|
if (real !== null && real === memberReal) {
|
|
794
|
-
claimants.set(masterReal, { root: masterReal, label: ws.label ??
|
|
1027
|
+
claimants.set(masterReal, { root: masterReal, label: ws.label ?? basename2(masterReal) });
|
|
795
1028
|
break;
|
|
796
1029
|
}
|
|
797
1030
|
}
|
|
@@ -1044,7 +1277,7 @@ async function doRunDecisionCapture(options, ctx) {
|
|
|
1044
1277
|
"--file",
|
|
1045
1278
|
sanitizePath(resolve3(cwd, options.file), {
|
|
1046
1279
|
workingDirectory: repositoryRoot,
|
|
1047
|
-
homedir:
|
|
1280
|
+
homedir: homedir3()
|
|
1048
1281
|
})
|
|
1049
1282
|
] : [];
|
|
1050
1283
|
const adHoc = await createAdHocSessionWithEvent({
|
|
@@ -1181,7 +1414,7 @@ function isDecisionId(value) {
|
|
|
1181
1414
|
async function decisionExists(paths, decisionId) {
|
|
1182
1415
|
const entries = await loadSessionEntries(paths, { now: /* @__PURE__ */ new Date() });
|
|
1183
1416
|
for (const entry of entries) {
|
|
1184
|
-
const sessionDir =
|
|
1417
|
+
const sessionDir = join5(paths.sessions, entry.sessionId);
|
|
1185
1418
|
try {
|
|
1186
1419
|
for await (const ev of replayEvents2(sessionDir, {})) {
|
|
1187
1420
|
if (ev.type === "decision_recorded" && ev.decision_id === decisionId) return true;
|
|
@@ -1576,7 +1809,7 @@ import {
|
|
|
1576
1809
|
assertBasouRootSafe as assertBasouRootSafe3,
|
|
1577
1810
|
basouPaths as basouPaths4,
|
|
1578
1811
|
findErrorCode as findErrorCode3,
|
|
1579
|
-
readMarkdownFile,
|
|
1812
|
+
readMarkdownFile as readMarkdownFile2,
|
|
1580
1813
|
renderDecisions,
|
|
1581
1814
|
renderWithMarkers,
|
|
1582
1815
|
resolveRepositoryRoot as resolveRepositoryRoot3,
|
|
@@ -1609,7 +1842,7 @@ async function doRunDecisionsGenerate(options, ctx) {
|
|
|
1609
1842
|
onWarning: (w, sid) => printReplayWarning(w, sid),
|
|
1610
1843
|
onSessionSkip: (sid, reason) => printSessionSkip(sid, reason)
|
|
1611
1844
|
});
|
|
1612
|
-
const existing = await
|
|
1845
|
+
const existing = await readMarkdownFile2(paths.files.decisions);
|
|
1613
1846
|
const finalBody = renderWithMarkers(existing, result.body, "decisions.md");
|
|
1614
1847
|
await writeMarkdownFile(paths.files.decisions, finalBody);
|
|
1615
1848
|
console.log(`Generated .basou/decisions.md (decisions: ${result.decisionCount})`);
|
|
@@ -1640,8 +1873,8 @@ async function assertWorkspaceInitialized3(basouRoot) {
|
|
|
1640
1873
|
|
|
1641
1874
|
// src/commands/exec.ts
|
|
1642
1875
|
import { mkdir } from "fs/promises";
|
|
1643
|
-
import { homedir as
|
|
1644
|
-
import { join as
|
|
1876
|
+
import { homedir as homedir4 } from "os";
|
|
1877
|
+
import { join as join6 } from "path";
|
|
1645
1878
|
import {
|
|
1646
1879
|
acquireLock as acquireLock3,
|
|
1647
1880
|
assertBasouRootSafe as assertBasouRootSafe4,
|
|
@@ -1681,13 +1914,13 @@ async function runExec(command, args, options, ctx = {}) {
|
|
|
1681
1914
|
await assertBasouRootSafe4(paths.root);
|
|
1682
1915
|
const manifest = await readManifest3(paths);
|
|
1683
1916
|
const sessionId = prefixedUlid3("ses");
|
|
1684
|
-
const sessionDir =
|
|
1917
|
+
const sessionDir = join6(paths.sessions, sessionId);
|
|
1685
1918
|
await mkdir(sessionDir, { recursive: true });
|
|
1686
1919
|
const appendEvent = ctx.appendEvent ?? (async (_sessionDir, event) => {
|
|
1687
1920
|
await coreAppendChainedEvent(paths, sessionId, event);
|
|
1688
1921
|
});
|
|
1689
1922
|
const startedAt = now().toISOString();
|
|
1690
|
-
const sessionYamlPath =
|
|
1923
|
+
const sessionYamlPath = join6(sessionDir, "session.yaml");
|
|
1691
1924
|
const session = buildInitialSession({
|
|
1692
1925
|
id: sessionId,
|
|
1693
1926
|
command,
|
|
@@ -1892,7 +2125,7 @@ function buildInitialSession(input) {
|
|
|
1892
2125
|
source: { kind: "terminal", version: "0.1.0" },
|
|
1893
2126
|
started_at: input.startedAt,
|
|
1894
2127
|
status: "initialized",
|
|
1895
|
-
working_directory: sanitizeWorkingDirectory(input.cwd, { homedir:
|
|
2128
|
+
working_directory: sanitizeWorkingDirectory(input.cwd, { homedir: homedir4() }),
|
|
1896
2129
|
invocation: {
|
|
1897
2130
|
command: input.command,
|
|
1898
2131
|
args: [...input.args],
|
|
@@ -1968,7 +2201,7 @@ import {
|
|
|
1968
2201
|
assertBasouRootSafe as assertBasouRootSafe5,
|
|
1969
2202
|
basouPaths as basouPaths6,
|
|
1970
2203
|
findErrorCode as findErrorCode4,
|
|
1971
|
-
readMarkdownFile as
|
|
2204
|
+
readMarkdownFile as readMarkdownFile3,
|
|
1972
2205
|
renderHandoff,
|
|
1973
2206
|
renderWithMarkers as renderWithMarkers2,
|
|
1974
2207
|
resolveRepositoryRoot as resolveRepositoryRoot5,
|
|
@@ -2002,7 +2235,7 @@ async function doRunHandoffGenerate(options, ctx) {
|
|
|
2002
2235
|
onSessionSkip: (sid, reason) => printSessionSkip(sid, reason),
|
|
2003
2236
|
onTaskSkip: (taskId, reason) => printTaskSkip(taskId, reason)
|
|
2004
2237
|
});
|
|
2005
|
-
const existing = await
|
|
2238
|
+
const existing = await readMarkdownFile3(paths.files.handoff);
|
|
2006
2239
|
const finalBody = renderWithMarkers2(existing, result.body, "handoff.md");
|
|
2007
2240
|
await writeMarkdownFile2(paths.files.handoff, finalBody);
|
|
2008
2241
|
console.log(
|
|
@@ -2035,8 +2268,8 @@ async function assertWorkspaceInitialized4(basouRoot) {
|
|
|
2035
2268
|
|
|
2036
2269
|
// src/commands/hook.ts
|
|
2037
2270
|
import { open as open2, readFile as readFile2, stat as stat3 } from "fs/promises";
|
|
2038
|
-
import { homedir as
|
|
2039
|
-
import { join as
|
|
2271
|
+
import { homedir as homedir5 } from "os";
|
|
2272
|
+
import { join as join7 } from "path";
|
|
2040
2273
|
import { fileURLToPath } from "url";
|
|
2041
2274
|
import {
|
|
2042
2275
|
buildStopHookCommand,
|
|
@@ -2046,61 +2279,6 @@ import {
|
|
|
2046
2279
|
removeStopHook,
|
|
2047
2280
|
upsertStopHook
|
|
2048
2281
|
} from "@basou/core";
|
|
2049
|
-
|
|
2050
|
-
// src/lib/durable-write.ts
|
|
2051
|
-
import { randomUUID } from "crypto";
|
|
2052
|
-
import { lstat, open, rename, stat as stat2, unlink as unlink2 } from "fs/promises";
|
|
2053
|
-
import { basename as basename2, dirname, join as join5 } from "path";
|
|
2054
|
-
async function assertNotSymlink(targetPath) {
|
|
2055
|
-
try {
|
|
2056
|
-
const st = await lstat(targetPath);
|
|
2057
|
-
if (st.isSymbolicLink()) {
|
|
2058
|
-
throw new Error(
|
|
2059
|
-
"Refusing to write through a symlink. Replace the symlinked target with a regular file (or remove it) and retry."
|
|
2060
|
-
);
|
|
2061
|
-
}
|
|
2062
|
-
} catch (error) {
|
|
2063
|
-
if (error instanceof Error && error.code === "ENOENT") return;
|
|
2064
|
-
throw error;
|
|
2065
|
-
}
|
|
2066
|
-
}
|
|
2067
|
-
async function writeFileDurable(targetPath, content) {
|
|
2068
|
-
const dir = dirname(targetPath);
|
|
2069
|
-
const tmpPath = join5(dir, `.${basename2(targetPath)}.tmp.${randomUUID()}`);
|
|
2070
|
-
let mode = 420;
|
|
2071
|
-
try {
|
|
2072
|
-
mode = (await stat2(targetPath)).mode & 511;
|
|
2073
|
-
} catch (error) {
|
|
2074
|
-
if (!(error instanceof Error && error.code === "ENOENT")) {
|
|
2075
|
-
throw error;
|
|
2076
|
-
}
|
|
2077
|
-
}
|
|
2078
|
-
let handle;
|
|
2079
|
-
try {
|
|
2080
|
-
handle = await open(tmpPath, "wx", mode);
|
|
2081
|
-
await handle.writeFile(content, "utf8");
|
|
2082
|
-
await handle.chmod(mode);
|
|
2083
|
-
await handle.sync();
|
|
2084
|
-
await handle.close();
|
|
2085
|
-
handle = void 0;
|
|
2086
|
-
await rename(tmpPath, targetPath);
|
|
2087
|
-
} catch (error) {
|
|
2088
|
-
if (handle) await handle.close().catch(() => void 0);
|
|
2089
|
-
await unlink2(tmpPath).catch(() => void 0);
|
|
2090
|
-
throw error;
|
|
2091
|
-
}
|
|
2092
|
-
try {
|
|
2093
|
-
const dirHandle = await open(dir, "r");
|
|
2094
|
-
try {
|
|
2095
|
-
await dirHandle.sync();
|
|
2096
|
-
} finally {
|
|
2097
|
-
await dirHandle.close();
|
|
2098
|
-
}
|
|
2099
|
-
} catch {
|
|
2100
|
-
}
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
// src/commands/hook.ts
|
|
2104
2282
|
var MAX_TRANSCRIPT_BYTES = 8 * 1024 * 1024;
|
|
2105
2283
|
function registerHookCommand(program2) {
|
|
2106
2284
|
const hook = program2.command("hook").description(
|
|
@@ -2256,7 +2434,7 @@ function parseMinEdits(raw) {
|
|
|
2256
2434
|
if (raw === void 0 || !/^\d+$/.test(raw)) return void 0;
|
|
2257
2435
|
return Number(raw);
|
|
2258
2436
|
}
|
|
2259
|
-
var DEFAULT_CLAUDE_SETTINGS_PATH =
|
|
2437
|
+
var DEFAULT_CLAUDE_SETTINGS_PATH = join7(homedir5(), ".claude", "settings.json");
|
|
2260
2438
|
function resolveCliEntry() {
|
|
2261
2439
|
return fileURLToPath(import.meta.url);
|
|
2262
2440
|
}
|
|
@@ -2421,8 +2599,8 @@ function describeHookMode(tiers) {
|
|
|
2421
2599
|
// src/commands/import.ts
|
|
2422
2600
|
import { createReadStream } from "fs";
|
|
2423
2601
|
import { readdir, readFile as readFile3, rm, stat as stat4 } from "fs/promises";
|
|
2424
|
-
import { homedir as
|
|
2425
|
-
import { basename as basename3, dirname as dirname2, join as
|
|
2602
|
+
import { homedir as homedir6 } from "os";
|
|
2603
|
+
import { basename as basename3, dirname as dirname2, join as join8, resolve as resolve4 } from "path";
|
|
2426
2604
|
import { createInterface } from "readline";
|
|
2427
2605
|
import {
|
|
2428
2606
|
AGENT_INFRA_DIRS as AGENT_INFRA_DIRS2,
|
|
@@ -2508,7 +2686,7 @@ async function doRunImportClaudeCode(options, ctx) {
|
|
|
2508
2686
|
repoRoot: repositoryRoot,
|
|
2509
2687
|
cwd: ctx.cwd ?? process.cwd()
|
|
2510
2688
|
});
|
|
2511
|
-
const projectsRoot = ctx.claudeProjectsDir ??
|
|
2689
|
+
const projectsRoot = ctx.claudeProjectsDir ?? join8(homedir6(), ".claude", "projects");
|
|
2512
2690
|
const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
|
|
2513
2691
|
const projectSet = new Set(projectPaths);
|
|
2514
2692
|
const candidates = files.map((file) => {
|
|
@@ -2547,7 +2725,7 @@ async function doRunImportCodex(options, ctx) {
|
|
|
2547
2725
|
repoRoot: repositoryRoot,
|
|
2548
2726
|
cwd: ctx.cwd ?? process.cwd()
|
|
2549
2727
|
});
|
|
2550
|
-
const sessionsRoot = ctx.codexSessionsDir ??
|
|
2728
|
+
const sessionsRoot = ctx.codexSessionsDir ?? join8(homedir6(), ".codex", "sessions");
|
|
2551
2729
|
const rollouts = await discoverCodexRollouts(sessionsRoot, projectPaths, options);
|
|
2552
2730
|
const candidates = rollouts.map(({ file, externalId }) => ({
|
|
2553
2731
|
externalId,
|
|
@@ -2676,7 +2854,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
2676
2854
|
if (priors.length > 0 && options.force === true) {
|
|
2677
2855
|
if (options.dryRun !== true) {
|
|
2678
2856
|
for (const { sessionId } of priors) {
|
|
2679
|
-
await rm(
|
|
2857
|
+
await rm(join8(paths.sessions, sessionId), { recursive: true, force: true });
|
|
2680
2858
|
}
|
|
2681
2859
|
}
|
|
2682
2860
|
counts.replaced++;
|
|
@@ -2787,7 +2965,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
|
2787
2965
|
if (options.session !== void 0) {
|
|
2788
2966
|
const matches = [];
|
|
2789
2967
|
for (const projectPath of projectPaths) {
|
|
2790
|
-
const file =
|
|
2968
|
+
const file = join8(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
|
|
2791
2969
|
if (await pathExists(file)) matches.push(file);
|
|
2792
2970
|
}
|
|
2793
2971
|
if (matches.length === 0) {
|
|
@@ -2798,7 +2976,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
|
2798
2976
|
const files = [];
|
|
2799
2977
|
let anyDirFound = false;
|
|
2800
2978
|
for (const projectPath of projectPaths) {
|
|
2801
|
-
const transcriptDir =
|
|
2979
|
+
const transcriptDir = join8(projectsRoot, encodeProjectDir(projectPath));
|
|
2802
2980
|
let entries;
|
|
2803
2981
|
try {
|
|
2804
2982
|
entries = await readdir(transcriptDir);
|
|
@@ -2808,7 +2986,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
|
2808
2986
|
}
|
|
2809
2987
|
anyDirFound = true;
|
|
2810
2988
|
for (const name of entries) {
|
|
2811
|
-
if (name.endsWith(".jsonl")) files.push(
|
|
2989
|
+
if (name.endsWith(".jsonl")) files.push(join8(transcriptDir, name));
|
|
2812
2990
|
}
|
|
2813
2991
|
}
|
|
2814
2992
|
if (!anyDirFound) {
|
|
@@ -2865,7 +3043,7 @@ async function findRolloutFiles(sessionsRoot) {
|
|
|
2865
3043
|
throw new Error("Failed to read Codex sessions directory", { cause: error });
|
|
2866
3044
|
}
|
|
2867
3045
|
for (const entry of entries) {
|
|
2868
|
-
const full =
|
|
3046
|
+
const full = join8(dir, entry.name);
|
|
2869
3047
|
if (entry.isDirectory()) {
|
|
2870
3048
|
await walk(full, false);
|
|
2871
3049
|
} else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) {
|
|
@@ -3304,13 +3482,13 @@ import {
|
|
|
3304
3482
|
} from "@basou/core";
|
|
3305
3483
|
|
|
3306
3484
|
// src/lib/hosts-config.ts
|
|
3307
|
-
import { homedir as
|
|
3308
|
-
import { isAbsolute as isAbsolute2, join as
|
|
3485
|
+
import { homedir as homedir7 } from "os";
|
|
3486
|
+
import { isAbsolute as isAbsolute2, join as join9, resolve as resolve6 } from "path";
|
|
3309
3487
|
import { readYamlFile as readYamlFile4 } from "@basou/core";
|
|
3310
|
-
var DEFAULT_HOSTS_CONFIG_PATH =
|
|
3488
|
+
var DEFAULT_HOSTS_CONFIG_PATH = join9(homedir7(), ".basou", "hosts.yaml");
|
|
3311
3489
|
function expandTilde2(p) {
|
|
3312
|
-
if (p === "~") return
|
|
3313
|
-
if (p.startsWith("~/")) return
|
|
3490
|
+
if (p === "~") return homedir7();
|
|
3491
|
+
if (p.startsWith("~/")) return join9(homedir7(), p.slice(2));
|
|
3314
3492
|
return p;
|
|
3315
3493
|
}
|
|
3316
3494
|
function isRecord2(value) {
|
|
@@ -3361,7 +3539,7 @@ async function loadHostsConfig(configPath = DEFAULT_HOSTS_CONFIG_PATH) {
|
|
|
3361
3539
|
|
|
3362
3540
|
// src/lib/provenance-actions.ts
|
|
3363
3541
|
import {
|
|
3364
|
-
readMarkdownFile as
|
|
3542
|
+
readMarkdownFile as readMarkdownFile4,
|
|
3365
3543
|
renderDecisions as renderDecisions2,
|
|
3366
3544
|
renderHandoff as renderHandoff2,
|
|
3367
3545
|
renderOrientation,
|
|
@@ -3445,7 +3623,7 @@ function importCodex(options, ctx) {
|
|
|
3445
3623
|
}
|
|
3446
3624
|
async function regenerateHandoff(paths, nowIso, callbacks) {
|
|
3447
3625
|
const result = await renderHandoff2({ paths, nowIso, ...callbacks });
|
|
3448
|
-
const existing = await
|
|
3626
|
+
const existing = await readMarkdownFile4(paths.files.handoff);
|
|
3449
3627
|
await writeMarkdownFile3(
|
|
3450
3628
|
paths.files.handoff,
|
|
3451
3629
|
renderWithMarkers3(existing, result.body, "handoff.md")
|
|
@@ -3459,7 +3637,7 @@ async function regenerateHandoff(paths, nowIso, callbacks) {
|
|
|
3459
3637
|
}
|
|
3460
3638
|
async function regenerateDecisions(paths, nowIso, callbacks) {
|
|
3461
3639
|
const result = await renderDecisions2({ paths, nowIso, ...callbacks });
|
|
3462
|
-
const existing = await
|
|
3640
|
+
const existing = await readMarkdownFile4(paths.files.decisions);
|
|
3463
3641
|
await writeMarkdownFile3(
|
|
3464
3642
|
paths.files.decisions,
|
|
3465
3643
|
renderWithMarkers3(existing, result.body, "decisions.md")
|
|
@@ -3620,13 +3798,13 @@ async function assertWorkspaceInitialized7(basouRoot) {
|
|
|
3620
3798
|
|
|
3621
3799
|
// src/commands/portfolio.ts
|
|
3622
3800
|
import { existsSync, statSync } from "fs";
|
|
3623
|
-
import { join as
|
|
3801
|
+
import { join as join10 } from "path";
|
|
3624
3802
|
function registerPortfolioCommand(program2) {
|
|
3625
3803
|
program2.command("portfolio").description(
|
|
3626
3804
|
"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"
|
|
3627
3805
|
).argument("[action]", "optional literal `list` (the only, and default, action)").option("--json", "Output the result as JSON").option(
|
|
3628
3806
|
"--check",
|
|
3629
|
-
"moved: the redundancy/footprint
|
|
3807
|
+
"moved: the redundancy/footprint preflight and the capture-coverage report are `basou view --portfolio --check` (this prints that pointer and exits)"
|
|
3630
3808
|
).option("-v, --verbose", "Show error causes").action(async (action, opts) => {
|
|
3631
3809
|
await runPortfolioCommand(action, opts);
|
|
3632
3810
|
});
|
|
@@ -3641,7 +3819,7 @@ function isDirectory(path) {
|
|
|
3641
3819
|
async function runPortfolioCommand(action, options, ctx = {}) {
|
|
3642
3820
|
if (options.check === true) {
|
|
3643
3821
|
console.error(
|
|
3644
|
-
"`basou portfolio` is a read-only listing; it has no
|
|
3822
|
+
"`basou portfolio` is a read-only listing; it has no preflight.\nRun `basou view --portfolio --check` for the redundancy/footprint check and the capture-coverage report."
|
|
3645
3823
|
);
|
|
3646
3824
|
process.exitCode = 1;
|
|
3647
3825
|
return;
|
|
@@ -3666,7 +3844,7 @@ async function runPortfolioList(options, ctx = {}) {
|
|
|
3666
3844
|
async function doRunPortfolioList(options, ctx) {
|
|
3667
3845
|
const configPath = ctx.configPath ?? DEFAULT_PORTFOLIO_CONFIG_PATH;
|
|
3668
3846
|
const pathExists2 = ctx.pathExists ?? ((p) => existsSync(p));
|
|
3669
|
-
const isInitialized = ctx.isInitialized ?? ((p) => isDirectory(
|
|
3847
|
+
const isInitialized = ctx.isInitialized ?? ((p) => isDirectory(join10(p, ".basou")));
|
|
3670
3848
|
const workspaces = await loadPortfolioConfig(configPath);
|
|
3671
3849
|
const result = {
|
|
3672
3850
|
configPath,
|
|
@@ -3706,7 +3884,7 @@ function renderPortfolioList(result) {
|
|
|
3706
3884
|
}
|
|
3707
3885
|
lines.push("");
|
|
3708
3886
|
lines.push(
|
|
3709
|
-
"Note: read-only listing of ~/.basou/portfolio.yaml. Run `basou view --portfolio` for the cross-workspace GUI, or `basou view --portfolio --check` for the redundancy/footprint
|
|
3887
|
+
"Note: read-only listing of ~/.basou/portfolio.yaml. Run `basou view --portfolio` for the cross-workspace GUI, or `basou view --portfolio --check` for the redundancy/footprint preflight and the capture-coverage report (which session logs no registered workspace imports)."
|
|
3710
3888
|
);
|
|
3711
3889
|
return lines.join("\n");
|
|
3712
3890
|
}
|
|
@@ -3731,7 +3909,7 @@ import {
|
|
|
3731
3909
|
writeFileSync,
|
|
3732
3910
|
writeSync
|
|
3733
3911
|
} from "fs";
|
|
3734
|
-
import { basename as basename5, dirname as dirname3, isAbsolute as isAbsolute3, join as
|
|
3912
|
+
import { basename as basename5, dirname as dirname3, isAbsolute as isAbsolute3, join as join11, relative as relative2, resolve as resolve7 } from "path";
|
|
3735
3913
|
import {
|
|
3736
3914
|
appendBasouGitignore as appendBasouGitignore2,
|
|
3737
3915
|
basouPaths as basouPaths10,
|
|
@@ -3742,7 +3920,7 @@ import {
|
|
|
3742
3920
|
GENERATED_START,
|
|
3743
3921
|
instructionMode,
|
|
3744
3922
|
isGitNotFound,
|
|
3745
|
-
parseMarkers,
|
|
3923
|
+
parseMarkers as parseMarkers2,
|
|
3746
3924
|
pathBasename,
|
|
3747
3925
|
planArchive,
|
|
3748
3926
|
planGitignore,
|
|
@@ -3750,9 +3928,9 @@ import {
|
|
|
3750
3928
|
planRosterAdoption,
|
|
3751
3929
|
planWorkspaceView,
|
|
3752
3930
|
readManifest as readManifest6,
|
|
3753
|
-
readMarkdownFile as
|
|
3931
|
+
readMarkdownFile as readMarkdownFile5,
|
|
3754
3932
|
reconcileSourceRoots,
|
|
3755
|
-
removeMarkerSection,
|
|
3933
|
+
removeMarkerSection as removeMarkerSection2,
|
|
3756
3934
|
renderAnchorStarter,
|
|
3757
3935
|
renderViewPresetBlock,
|
|
3758
3936
|
renderWithMarkers as renderWithMarkers4,
|
|
@@ -4113,7 +4291,7 @@ function classifySourceRoot(repositoryRoot, declaredPath) {
|
|
|
4113
4291
|
} catch {
|
|
4114
4292
|
return { path: declaredPath, kind: "unresolved" };
|
|
4115
4293
|
}
|
|
4116
|
-
return { path: declaredPath, kind: existsSync2(
|
|
4294
|
+
return { path: declaredPath, kind: existsSync2(join11(real, ".git")) ? "repo" : "non-repo" };
|
|
4117
4295
|
}
|
|
4118
4296
|
async function doRunProjectAdopt(options, ctx) {
|
|
4119
4297
|
const cwd = ctx.cwd ?? process.cwd();
|
|
@@ -4217,7 +4395,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
|
|
|
4217
4395
|
} catch {
|
|
4218
4396
|
return { ...base, reachable: false, instructionFiles: [] };
|
|
4219
4397
|
}
|
|
4220
|
-
if (!existsSync2(
|
|
4398
|
+
if (!existsSync2(join11(real, ".git"))) {
|
|
4221
4399
|
return { ...base, reachable: false, instructionFiles: [] };
|
|
4222
4400
|
}
|
|
4223
4401
|
try {
|
|
@@ -4225,7 +4403,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
|
|
|
4225
4403
|
for (const name of INSTRUCTION_FILES) {
|
|
4226
4404
|
let present = true;
|
|
4227
4405
|
try {
|
|
4228
|
-
lstatSync(
|
|
4406
|
+
lstatSync(join11(real, name));
|
|
4229
4407
|
} catch {
|
|
4230
4408
|
present = false;
|
|
4231
4409
|
}
|
|
@@ -4336,10 +4514,10 @@ function gatherRepoGitignore(repositoryRoot, entry) {
|
|
|
4336
4514
|
} catch {
|
|
4337
4515
|
return { ...base, reachable: false, currentLines: [] };
|
|
4338
4516
|
}
|
|
4339
|
-
if (!existsSync2(
|
|
4517
|
+
if (!existsSync2(join11(real, ".git"))) {
|
|
4340
4518
|
return { ...base, reachable: false, currentLines: [] };
|
|
4341
4519
|
}
|
|
4342
|
-
return { ...base, reachable: true, currentLines: readGitignoreLines(
|
|
4520
|
+
return { ...base, reachable: true, currentLines: readGitignoreLines(join11(real, ".gitignore")) };
|
|
4343
4521
|
}
|
|
4344
4522
|
function hasErrorCode(error) {
|
|
4345
4523
|
return error instanceof Error && typeof error.code === "string";
|
|
@@ -4353,7 +4531,7 @@ function readGitignoreLines(file) {
|
|
|
4353
4531
|
}
|
|
4354
4532
|
}
|
|
4355
4533
|
function applyGitignorePlan(repositoryRoot, plan) {
|
|
4356
|
-
const file =
|
|
4534
|
+
const file = join11(realpathSync(resolve7(repositoryRoot, plan.path)), ".gitignore");
|
|
4357
4535
|
let existing = "";
|
|
4358
4536
|
try {
|
|
4359
4537
|
existing = readFileSync(file, "utf8");
|
|
@@ -4493,7 +4671,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
4493
4671
|
return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
|
|
4494
4672
|
}
|
|
4495
4673
|
if (real === anchorReal) {
|
|
4496
|
-
const anchorCanonical =
|
|
4674
|
+
const anchorCanonical = join11(real, CANONICAL_FILE);
|
|
4497
4675
|
const anchorState = anchorCanonicalState(anchorCanonical);
|
|
4498
4676
|
if (anchorState === "absent") {
|
|
4499
4677
|
return { ...base, isAnchor: true, reachable: true, canonicalPresent: false, files: [] };
|
|
@@ -4513,7 +4691,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
4513
4691
|
anchorCanonical,
|
|
4514
4692
|
"self"
|
|
4515
4693
|
).map((spec) => {
|
|
4516
|
-
const { state, actualTarget } = inspectSymlink(
|
|
4694
|
+
const { state, actualTarget } = inspectSymlink(join11(real, spec.name), spec.target);
|
|
4517
4695
|
return {
|
|
4518
4696
|
name: spec.name,
|
|
4519
4697
|
expectedTarget: spec.target,
|
|
@@ -4530,16 +4708,16 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
4530
4708
|
files: anchorFiles
|
|
4531
4709
|
};
|
|
4532
4710
|
}
|
|
4533
|
-
if (!existsSync2(
|
|
4711
|
+
if (!existsSync2(join11(real, ".git"))) {
|
|
4534
4712
|
return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
|
|
4535
4713
|
}
|
|
4536
|
-
const canonicalFile = isSelf ?
|
|
4714
|
+
const canonicalFile = isSelf ? join11(real, CANONICAL_FILE) : join11(anchorReal, "agents", basename5(real), CANONICAL_FILE);
|
|
4537
4715
|
if (!existsSync2(canonicalFile)) {
|
|
4538
4716
|
return { ...base, isAnchor: false, reachable: true, canonicalPresent: false, files: [] };
|
|
4539
4717
|
}
|
|
4540
4718
|
const files = expectedSymlinkTargets(real, canonicalFile, mode).map(
|
|
4541
4719
|
(spec) => {
|
|
4542
|
-
const { state, actualTarget } = inspectSymlink(
|
|
4720
|
+
const { state, actualTarget } = inspectSymlink(join11(real, spec.name), spec.target);
|
|
4543
4721
|
return {
|
|
4544
4722
|
name: spec.name,
|
|
4545
4723
|
expectedTarget: spec.target,
|
|
@@ -4568,7 +4746,7 @@ function applySymlinkPlan(repositoryRoot, plan) {
|
|
|
4568
4746
|
const created = [];
|
|
4569
4747
|
const failed = [];
|
|
4570
4748
|
for (const { name, target } of plan.toCreate) {
|
|
4571
|
-
const filePath =
|
|
4749
|
+
const filePath = join11(real, name);
|
|
4572
4750
|
try {
|
|
4573
4751
|
mkdirSync(dirname3(filePath), { recursive: true });
|
|
4574
4752
|
symlinkSync(target, filePath);
|
|
@@ -4602,7 +4780,7 @@ function gatherViewSymlinks(repositoryRoot, anchorReal, roster, viewDir) {
|
|
|
4602
4780
|
if (!existsSync2(canonicalFile)) return { kind: "missing-canonical", viewName };
|
|
4603
4781
|
const files = expectedSymlinkTargets(viewDir, canonicalFile, "hub").map(
|
|
4604
4782
|
(spec) => {
|
|
4605
|
-
const { state, actualTarget } = inspectSymlink(
|
|
4783
|
+
const { state, actualTarget } = inspectSymlink(join11(viewDir, spec.name), spec.target);
|
|
4606
4784
|
return {
|
|
4607
4785
|
name: spec.name,
|
|
4608
4786
|
expectedTarget: spec.target,
|
|
@@ -4618,7 +4796,7 @@ function applyViewSymlinks(viewDir, files) {
|
|
|
4618
4796
|
const failed = [];
|
|
4619
4797
|
for (const f of files) {
|
|
4620
4798
|
if (f.state !== "missing") continue;
|
|
4621
|
-
const filePath =
|
|
4799
|
+
const filePath = join11(viewDir, f.name);
|
|
4622
4800
|
try {
|
|
4623
4801
|
mkdirSync(dirname3(filePath), { recursive: true });
|
|
4624
4802
|
symlinkSync(f.expectedTarget, filePath);
|
|
@@ -4840,7 +5018,7 @@ function resolveViewDir(repositoryRoot, viewPath) {
|
|
|
4840
5018
|
return realpathSync(abs);
|
|
4841
5019
|
} catch {
|
|
4842
5020
|
try {
|
|
4843
|
-
return
|
|
5021
|
+
return join11(realpathSync(dirname3(abs)), basename5(abs));
|
|
4844
5022
|
} catch {
|
|
4845
5023
|
return abs;
|
|
4846
5024
|
}
|
|
@@ -4858,7 +5036,7 @@ function gatherViewRepo(repositoryRoot, viewDir, entry) {
|
|
|
4858
5036
|
return { path: entry.path, reachable: false };
|
|
4859
5037
|
}
|
|
4860
5038
|
const linkName = basename5(repoReal);
|
|
4861
|
-
const { state, actualTarget } = inspectSymlink(
|
|
5039
|
+
const { state, actualTarget } = inspectSymlink(join11(viewDir, linkName), expectedTarget);
|
|
4862
5040
|
return {
|
|
4863
5041
|
path: entry.path,
|
|
4864
5042
|
reachable: true,
|
|
@@ -4872,7 +5050,7 @@ function applyViewPlan(viewDir, toCreate) {
|
|
|
4872
5050
|
const created = [];
|
|
4873
5051
|
const failed = [];
|
|
4874
5052
|
for (const { name, target } of toCreate) {
|
|
4875
|
-
const filePath =
|
|
5053
|
+
const filePath = join11(viewDir, name);
|
|
4876
5054
|
try {
|
|
4877
5055
|
mkdirSync(dirname3(filePath), { recursive: true });
|
|
4878
5056
|
symlinkSync(target, filePath);
|
|
@@ -4887,7 +5065,7 @@ var TOP_LEVEL_INSTRUCTION_FILES_LOWER = new Set(
|
|
|
4887
5065
|
INSTRUCTION_FILES.filter((f) => !f.includes("/")).map((f) => f.toLowerCase())
|
|
4888
5066
|
);
|
|
4889
5067
|
function classifyViewLink(viewDir, name, rosterRealpaths) {
|
|
4890
|
-
const filePath =
|
|
5068
|
+
const filePath = join11(viewDir, name);
|
|
4891
5069
|
let isLink;
|
|
4892
5070
|
try {
|
|
4893
5071
|
isLink = lstatSync(filePath).isSymbolicLink();
|
|
@@ -4916,7 +5094,7 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
|
|
|
4916
5094
|
if (!isDir) {
|
|
4917
5095
|
return { target, kind: existsSync2(resolved) ? "non-repo" : "broken" };
|
|
4918
5096
|
}
|
|
4919
|
-
return { target, kind: existsSync2(
|
|
5097
|
+
return { target, kind: existsSync2(join11(resolved, ".git")) ? "repo" : "non-repo" };
|
|
4920
5098
|
}
|
|
4921
5099
|
function gatherExistingViewLinks(viewDir, rosterRealpaths) {
|
|
4922
5100
|
let names;
|
|
@@ -4941,7 +5119,7 @@ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
|
|
|
4941
5119
|
const pruned = [];
|
|
4942
5120
|
const failed = [];
|
|
4943
5121
|
for (const { name } of toPrune) {
|
|
4944
|
-
const filePath =
|
|
5122
|
+
const filePath = join11(viewDir, name);
|
|
4945
5123
|
const c = classifyViewLink(viewDir, name, rosterRealpaths);
|
|
4946
5124
|
if (c === null || c.kind !== "repo") {
|
|
4947
5125
|
failed.push({
|
|
@@ -5154,10 +5332,10 @@ async function runProjectPreset(options, ctx = {}) {
|
|
|
5154
5332
|
}
|
|
5155
5333
|
}
|
|
5156
5334
|
function canonicalFileFor(anchorReal, canonicalName) {
|
|
5157
|
-
return
|
|
5335
|
+
return join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
5158
5336
|
}
|
|
5159
5337
|
function canonicalLabelFor(canonicalName) {
|
|
5160
|
-
return
|
|
5338
|
+
return join11("agents", canonicalName, CANONICAL_FILE);
|
|
5161
5339
|
}
|
|
5162
5340
|
async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
|
|
5163
5341
|
const declared = {
|
|
@@ -5178,13 +5356,13 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
|
|
|
5178
5356
|
if (real === anchorReal) {
|
|
5179
5357
|
return { ...declared, isAnchor: true, reachable: true, canonicalPresent: false };
|
|
5180
5358
|
}
|
|
5181
|
-
if (!existsSync2(
|
|
5359
|
+
if (!existsSync2(join11(real, ".git"))) {
|
|
5182
5360
|
return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
|
|
5183
5361
|
}
|
|
5184
5362
|
const canonicalName = basename5(real);
|
|
5185
5363
|
let content;
|
|
5186
5364
|
try {
|
|
5187
|
-
content = await
|
|
5365
|
+
content = await readMarkdownFile5(canonicalFileFor(anchorReal, canonicalName));
|
|
5188
5366
|
} catch {
|
|
5189
5367
|
return {
|
|
5190
5368
|
...declared,
|
|
@@ -5204,7 +5382,7 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
|
|
|
5204
5382
|
canonicalPresent: false
|
|
5205
5383
|
};
|
|
5206
5384
|
}
|
|
5207
|
-
const section =
|
|
5385
|
+
const section = parseMarkers2(content);
|
|
5208
5386
|
return {
|
|
5209
5387
|
...declared,
|
|
5210
5388
|
isAnchor: false,
|
|
@@ -5267,7 +5445,7 @@ function gatherViewPreset(repositoryRoot, anchorReal, viewName, roster) {
|
|
|
5267
5445
|
}
|
|
5268
5446
|
return { kind: "unreadable", canonicalName: viewName, viewName };
|
|
5269
5447
|
}
|
|
5270
|
-
const section =
|
|
5448
|
+
const section = parseMarkers2(content);
|
|
5271
5449
|
if (section.kind === "ok") {
|
|
5272
5450
|
if (normalizeViewBlock(section.generated) === normalizeViewBlock(desiredBlock)) {
|
|
5273
5451
|
return { kind: "in-sync", canonicalName: viewName, viewName };
|
|
@@ -5293,7 +5471,7 @@ async function applyViewPreset(anchorReal, outcome) {
|
|
|
5293
5471
|
}
|
|
5294
5472
|
if (isLink) throw new Error(`Canonical is a symlink in ${label}`);
|
|
5295
5473
|
if (outcome.action === "create") mkdirSync(dirname3(file), { recursive: true });
|
|
5296
|
-
const existing = await
|
|
5474
|
+
const existing = await readMarkdownFile5(file);
|
|
5297
5475
|
await writeMarkdownFile5(file, renderWithMarkers4(existing, outcome.block, label));
|
|
5298
5476
|
}
|
|
5299
5477
|
async function applyPresetPlan(anchorReal, plan) {
|
|
@@ -5307,7 +5485,7 @@ async function applyPresetPlan(anchorReal, plan) {
|
|
|
5307
5485
|
}
|
|
5308
5486
|
if (isLink) throw new Error(`Canonical is a symlink in ${label}`);
|
|
5309
5487
|
if (plan.action === "create") mkdirSync(dirname3(file), { recursive: true });
|
|
5310
|
-
const existing = await
|
|
5488
|
+
const existing = await readMarkdownFile5(file);
|
|
5311
5489
|
await writeMarkdownFile5(file, renderWithMarkers4(existing, plan.desiredBlock, label));
|
|
5312
5490
|
}
|
|
5313
5491
|
function presetFailureReason(error) {
|
|
@@ -5560,24 +5738,24 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
|
|
|
5560
5738
|
const instructionFiles = [];
|
|
5561
5739
|
for (const name of INSTRUCTION_FILES) {
|
|
5562
5740
|
try {
|
|
5563
|
-
lstatSync(
|
|
5741
|
+
lstatSync(join11(real, name));
|
|
5564
5742
|
instructionFiles.push(name);
|
|
5565
5743
|
} catch {
|
|
5566
5744
|
}
|
|
5567
5745
|
}
|
|
5568
5746
|
let ignored;
|
|
5569
5747
|
try {
|
|
5570
|
-
ignored = new Set(readGitignoreLines(
|
|
5748
|
+
ignored = new Set(readGitignoreLines(join11(real, ".gitignore")).map((l) => l.trim()));
|
|
5571
5749
|
} catch {
|
|
5572
5750
|
ignored = /* @__PURE__ */ new Set();
|
|
5573
5751
|
}
|
|
5574
5752
|
const gitignorePatterns = INSTRUCTION_FILES.filter((p) => ignored.has(p) || ignored.has(`/${p}`));
|
|
5575
|
-
const canonical2 = existsSync2(
|
|
5753
|
+
const canonical2 = existsSync2(join11(anchorReal, "agents", canonicalName, CANONICAL_FILE));
|
|
5576
5754
|
let viewLink = false;
|
|
5577
5755
|
const viewPath = manifest.workspace.view;
|
|
5578
5756
|
if (viewPath !== void 0) {
|
|
5579
5757
|
try {
|
|
5580
|
-
lstatSync(
|
|
5758
|
+
lstatSync(join11(resolveViewDir(repositoryRoot, viewPath), canonicalName));
|
|
5581
5759
|
viewLink = true;
|
|
5582
5760
|
} catch {
|
|
5583
5761
|
}
|
|
@@ -5591,11 +5769,11 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
|
|
|
5591
5769
|
};
|
|
5592
5770
|
}
|
|
5593
5771
|
function teardownExpectedTargets(repoReal, anchorReal, canonicalName) {
|
|
5594
|
-
const canonicalFile =
|
|
5772
|
+
const canonicalFile = join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
5595
5773
|
return expectedSymlinkTargets(repoReal, canonicalFile);
|
|
5596
5774
|
}
|
|
5597
5775
|
function viewLinkPointsAt(viewDir, name, repoReal) {
|
|
5598
|
-
const filePath =
|
|
5776
|
+
const filePath = join11(viewDir, name);
|
|
5599
5777
|
try {
|
|
5600
5778
|
if (!lstatSync(filePath).isSymbolicLink()) return false;
|
|
5601
5779
|
const target = readlinkSync(filePath);
|
|
@@ -5606,7 +5784,7 @@ function viewLinkPointsAt(viewDir, name, repoReal) {
|
|
|
5606
5784
|
}
|
|
5607
5785
|
}
|
|
5608
5786
|
function viewLinkPointsAtPath(viewDir, name, expectedRepoPath) {
|
|
5609
|
-
const filePath =
|
|
5787
|
+
const filePath = join11(viewDir, name);
|
|
5610
5788
|
try {
|
|
5611
5789
|
if (!lstatSync(filePath).isSymbolicLink()) return false;
|
|
5612
5790
|
const target = readlinkSync(filePath);
|
|
@@ -5657,7 +5835,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
5657
5835
|
if (!isAnchor) {
|
|
5658
5836
|
if (repoReal !== void 0) {
|
|
5659
5837
|
for (const spec of teardownExpectedTargets(repoReal, anchorReal, canonicalName)) {
|
|
5660
|
-
const { state, actualTarget } = inspectSymlink(
|
|
5838
|
+
const { state, actualTarget } = inspectSymlink(join11(repoReal, spec.name), spec.target);
|
|
5661
5839
|
if (isSelf) {
|
|
5662
5840
|
if (state !== "missing")
|
|
5663
5841
|
items.push({
|
|
@@ -5694,7 +5872,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
5694
5872
|
}
|
|
5695
5873
|
let ignored;
|
|
5696
5874
|
try {
|
|
5697
|
-
ignored = new Set(readGitignoreLines(
|
|
5875
|
+
ignored = new Set(readGitignoreLines(join11(repoReal, ".gitignore")).map((l) => l.trim()));
|
|
5698
5876
|
for (const p of INSTRUCTION_FILES) {
|
|
5699
5877
|
if (ignored.has(p) || ignored.has(`/${p}`)) {
|
|
5700
5878
|
items.push({
|
|
@@ -5717,7 +5895,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
5717
5895
|
const viewPath = manifest.workspace.view;
|
|
5718
5896
|
if (viewPath !== void 0) {
|
|
5719
5897
|
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
5720
|
-
const linkPath =
|
|
5898
|
+
const linkPath = join11(viewDir, canonicalName);
|
|
5721
5899
|
let isLink = false;
|
|
5722
5900
|
try {
|
|
5723
5901
|
isLink = lstatSync(linkPath).isSymbolicLink();
|
|
@@ -5743,8 +5921,8 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
5743
5921
|
else items.push({ kind: "view-symlink", label: canonicalName, state: "removable" });
|
|
5744
5922
|
}
|
|
5745
5923
|
}
|
|
5746
|
-
const canonicalFile =
|
|
5747
|
-
const canonicalLabel =
|
|
5924
|
+
const canonicalFile = join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
5925
|
+
const canonicalLabel = join11("agents", canonicalName, CANONICAL_FILE);
|
|
5748
5926
|
let canonicalIsLink = false;
|
|
5749
5927
|
try {
|
|
5750
5928
|
canonicalIsLink = lstatSync(canonicalFile).isSymbolicLink();
|
|
@@ -5771,7 +5949,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
5771
5949
|
});
|
|
5772
5950
|
}
|
|
5773
5951
|
if (content !== void 0 && content !== "") {
|
|
5774
|
-
const section =
|
|
5952
|
+
const section = parseMarkers2(content);
|
|
5775
5953
|
if (section.kind === "ok" && canonicalShared) {
|
|
5776
5954
|
items.push({
|
|
5777
5955
|
kind: "canonical-block",
|
|
@@ -5787,7 +5965,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
5787
5965
|
note: "repo could not be resolved, so ownership cannot be verified (check manually)"
|
|
5788
5966
|
});
|
|
5789
5967
|
} else if (section.kind === "ok") {
|
|
5790
|
-
const emptyAfter =
|
|
5968
|
+
const emptyAfter = removeMarkerSection2(content, canonicalLabel).trim().length === 0;
|
|
5791
5969
|
items.push({
|
|
5792
5970
|
kind: "canonical-block",
|
|
5793
5971
|
label: canonicalLabel,
|
|
@@ -5848,12 +6026,12 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
5848
6026
|
);
|
|
5849
6027
|
for (const item of removable.filter((i) => i.kind === "instruction-symlink")) {
|
|
5850
6028
|
const expected = expectedByName.get(item.label);
|
|
5851
|
-
if (repoReal === null || expected === void 0 || inspectSymlink(
|
|
6029
|
+
if (repoReal === null || expected === void 0 || inspectSymlink(join11(repoReal, item.label), expected).state !== "correct") {
|
|
5852
6030
|
changed(item.label);
|
|
5853
6031
|
continue;
|
|
5854
6032
|
}
|
|
5855
6033
|
try {
|
|
5856
|
-
unlinkSync(
|
|
6034
|
+
unlinkSync(join11(repoReal, item.label));
|
|
5857
6035
|
removed.push(item.label);
|
|
5858
6036
|
} catch (error) {
|
|
5859
6037
|
failed.push({ label: item.label, message: failureReason(error) });
|
|
@@ -5872,7 +6050,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
5872
6050
|
continue;
|
|
5873
6051
|
}
|
|
5874
6052
|
try {
|
|
5875
|
-
unlinkSync(
|
|
6053
|
+
unlinkSync(join11(viewDir, item.label));
|
|
5876
6054
|
removed.push(`view/${item.label}`);
|
|
5877
6055
|
} catch (error) {
|
|
5878
6056
|
failed.push({ label: `view/${item.label}`, message: failureReason(error) });
|
|
@@ -5880,7 +6058,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
5880
6058
|
}
|
|
5881
6059
|
const NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
|
|
5882
6060
|
for (const item of removable.filter((i) => i.kind === "canonical-block")) {
|
|
5883
|
-
const canonicalFile =
|
|
6061
|
+
const canonicalFile = join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
5884
6062
|
try {
|
|
5885
6063
|
if (lstatSync(canonicalFile).isSymbolicLink()) {
|
|
5886
6064
|
changed(item.label);
|
|
@@ -5900,11 +6078,11 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
5900
6078
|
}
|
|
5901
6079
|
try {
|
|
5902
6080
|
const content = readFileSync(fd, "utf8");
|
|
5903
|
-
if (
|
|
6081
|
+
if (parseMarkers2(content).kind !== "ok") {
|
|
5904
6082
|
changed(item.label);
|
|
5905
6083
|
continue;
|
|
5906
6084
|
}
|
|
5907
|
-
const next = Buffer.from(
|
|
6085
|
+
const next = Buffer.from(removeMarkerSection2(content, item.label), "utf8");
|
|
5908
6086
|
ftruncateSync(fd, 0);
|
|
5909
6087
|
writeSync(fd, next, 0, next.length, 0);
|
|
5910
6088
|
removed.push(item.label);
|
|
@@ -6154,12 +6332,12 @@ function gatherRenameWiring(repositoryRoot, manifest, oldBasename) {
|
|
|
6154
6332
|
} catch {
|
|
6155
6333
|
return { canonicalDirOld: false, viewLinkOld: false };
|
|
6156
6334
|
}
|
|
6157
|
-
const canonicalDirOld = existsSync2(
|
|
6335
|
+
const canonicalDirOld = existsSync2(join11(anchorReal, "agents", oldBasename));
|
|
6158
6336
|
let viewLinkOld = false;
|
|
6159
6337
|
const viewPath = manifest.workspace.view;
|
|
6160
6338
|
if (viewPath !== void 0) {
|
|
6161
6339
|
try {
|
|
6162
|
-
lstatSync(
|
|
6340
|
+
lstatSync(join11(resolveViewDir(repositoryRoot, viewPath), oldBasename));
|
|
6163
6341
|
viewLinkOld = true;
|
|
6164
6342
|
} catch {
|
|
6165
6343
|
}
|
|
@@ -6517,7 +6695,7 @@ async function doRunProjectSeedAnchor(options, ctx) {
|
|
|
6517
6695
|
console.log("\u2139\uFE0F No repo roster declared \u2014 nothing to seed.");
|
|
6518
6696
|
return;
|
|
6519
6697
|
}
|
|
6520
|
-
const anchorDoc =
|
|
6698
|
+
const anchorDoc = join11(repositoryRoot, CANONICAL_FILE);
|
|
6521
6699
|
if (pathPresent(anchorDoc)) {
|
|
6522
6700
|
console.log(
|
|
6523
6701
|
`\u2705 The anchor's own \`${CANONICAL_FILE}\` already exists \u2014 hand-maintained, left untouched.`
|
|
@@ -6579,7 +6757,7 @@ function regularFileSpokes(repoReal) {
|
|
|
6579
6757
|
const out = [];
|
|
6580
6758
|
for (const spoke of ["CLAUDE.md", ".github/copilot-instructions.md"]) {
|
|
6581
6759
|
try {
|
|
6582
|
-
const st = lstatSync(
|
|
6760
|
+
const st = lstatSync(join11(repoReal, spoke));
|
|
6583
6761
|
if (!st.isSymbolicLink() && st.isFile()) out.push(spoke);
|
|
6584
6762
|
} catch {
|
|
6585
6763
|
}
|
|
@@ -6625,8 +6803,8 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
|
|
|
6625
6803
|
};
|
|
6626
6804
|
}
|
|
6627
6805
|
const isAnchor = argReal === anchorReal;
|
|
6628
|
-
const reachable = existsSync2(
|
|
6629
|
-
const canonicalFile =
|
|
6806
|
+
const reachable = existsSync2(join11(argReal, ".git"));
|
|
6807
|
+
const canonicalFile = join11(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
6630
6808
|
return {
|
|
6631
6809
|
path,
|
|
6632
6810
|
declared,
|
|
@@ -6635,13 +6813,13 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, arg
|
|
|
6635
6813
|
reachable,
|
|
6636
6814
|
canonicalName,
|
|
6637
6815
|
...viewCanonicalName !== void 0 ? { viewCanonicalName } : {},
|
|
6638
|
-
agentsState: inspectAgentsState(
|
|
6816
|
+
agentsState: inspectAgentsState(join11(argReal, CANONICAL_FILE)),
|
|
6639
6817
|
canonicalExists: pathPresent(canonicalFile),
|
|
6640
6818
|
regularSpokes: regularFileSpokes(argReal)
|
|
6641
6819
|
};
|
|
6642
6820
|
}
|
|
6643
6821
|
function relocateAgentsFile(repoReal, canonicalFile) {
|
|
6644
|
-
const agentsFile =
|
|
6822
|
+
const agentsFile = join11(repoReal, CANONICAL_FILE);
|
|
6645
6823
|
try {
|
|
6646
6824
|
mkdirSync(dirname3(canonicalFile), { recursive: true });
|
|
6647
6825
|
} catch (error) {
|
|
@@ -6675,7 +6853,7 @@ function gatherViewRetrofit(repositoryRoot, anchorReal, viewPath, roster) {
|
|
|
6675
6853
|
if (hasErrorCode(error) && error.code === "ENOENT") return { kind: "absent", viewName };
|
|
6676
6854
|
return { kind: "unreadable", viewName };
|
|
6677
6855
|
}
|
|
6678
|
-
const section =
|
|
6856
|
+
const section = parseMarkers2(content);
|
|
6679
6857
|
if (section.kind === "ok") return { kind: "already-marked", viewName };
|
|
6680
6858
|
if (section.kind === "no_markers") {
|
|
6681
6859
|
const block = renderViewPresetBlock({
|
|
@@ -6696,7 +6874,7 @@ async function applyViewRetrofit(anchorReal, outcome) {
|
|
|
6696
6874
|
isLink = false;
|
|
6697
6875
|
}
|
|
6698
6876
|
if (isLink) throw new Error(`Canonical is a symlink in ${label}`);
|
|
6699
|
-
const existing = await
|
|
6877
|
+
const existing = await readMarkdownFile5(file);
|
|
6700
6878
|
await writeMarkdownFile5(file, seedMarkers(existing, outcome.block, label));
|
|
6701
6879
|
}
|
|
6702
6880
|
async function doRunProjectRetrofit(repo, options, ctx) {
|
|
@@ -6758,7 +6936,7 @@ async function doRunProjectRetrofit(repo, options, ctx) {
|
|
|
6758
6936
|
let failure;
|
|
6759
6937
|
let partial = false;
|
|
6760
6938
|
if (options.apply === true && plan.action === "relocate" && argReal !== void 0) {
|
|
6761
|
-
const canonicalFile =
|
|
6939
|
+
const canonicalFile = join11(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
|
|
6762
6940
|
const res = relocateAgentsFile(argReal, canonicalFile);
|
|
6763
6941
|
if (res.ok) {
|
|
6764
6942
|
applied = true;
|
|
@@ -6961,118 +7139,6 @@ function renderProjectRetrofit(result) {
|
|
|
6961
7139
|
import { readFile as readFile4 } from "fs/promises";
|
|
6962
7140
|
import { PROTOCOL_END, PROTOCOL_START, parseMarkers as parseMarkers3, readMarkdownFile as readMarkdownFile6 } from "@basou/core";
|
|
6963
7141
|
|
|
6964
|
-
// src/lib/context-channel.ts
|
|
6965
|
-
import { homedir as homedir7 } from "os";
|
|
6966
|
-
import { join as join11 } from "path";
|
|
6967
|
-
import {
|
|
6968
|
-
ORIENTATION_END,
|
|
6969
|
-
ORIENTATION_START,
|
|
6970
|
-
parseMarkers as parseMarkers2,
|
|
6971
|
-
readMarkdownFile as readMarkdownFile5,
|
|
6972
|
-
removeMarkerSection as removeMarkerSection2
|
|
6973
|
-
} from "@basou/core";
|
|
6974
|
-
var CODEX_TARGET_PATH = join11(homedir7(), ".codex", "AGENTS.md");
|
|
6975
|
-
var ORIENTATION_MARKERS = { start: ORIENTATION_START, end: ORIENTATION_END };
|
|
6976
|
-
var ORIENTATION_MANAGED_NOTE = "<!-- Managed by basou: 'basou refresh' regenerates everything between the BASOU:ORIENTATION markers with the workspace's current position. This block is transient \u2014 it changes every refresh; do not edit it. -->";
|
|
6977
|
-
function buildTargetBody(existing, block, markers) {
|
|
6978
|
-
const wrapped = `${markers.start}
|
|
6979
|
-
${block}${markers.end}
|
|
6980
|
-
`;
|
|
6981
|
-
if (existing === null || existing === "") return wrapped;
|
|
6982
|
-
const section = parseMarkers2(existing, markers);
|
|
6983
|
-
switch (section.kind) {
|
|
6984
|
-
case "ok":
|
|
6985
|
-
return `${section.before}${markers.start}
|
|
6986
|
-
${block}${markers.end}${section.after}`;
|
|
6987
|
-
case "no_markers": {
|
|
6988
|
-
const sep = existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
6989
|
-
return `${existing}${sep}${wrapped}`;
|
|
6990
|
-
}
|
|
6991
|
-
default:
|
|
6992
|
-
throw new Error(
|
|
6993
|
-
"The basou-managed markers in the target are malformed (a marker is missing, duplicated, or out of order). Fix or remove them, then retry."
|
|
6994
|
-
);
|
|
6995
|
-
}
|
|
6996
|
-
}
|
|
6997
|
-
async function backupOnce(target, existing) {
|
|
6998
|
-
if (existing === null) return;
|
|
6999
|
-
const bak = `${target}.basou-bak`;
|
|
7000
|
-
const already = await readMarkdownFile5(bak);
|
|
7001
|
-
if (already !== null) return;
|
|
7002
|
-
await writeFileDurable(bak, existing);
|
|
7003
|
-
}
|
|
7004
|
-
async function syncMarkerBlock(opts) {
|
|
7005
|
-
const { target, markers, block } = opts;
|
|
7006
|
-
await assertNotSymlink(target);
|
|
7007
|
-
const existing = await readMarkdownFile5(target);
|
|
7008
|
-
const newBody = buildTargetBody(existing, block, markers);
|
|
7009
|
-
if (newBody === existing) return { action: "unchanged" };
|
|
7010
|
-
const hadBlock = existing !== null && parseMarkers2(existing, markers).kind === "ok";
|
|
7011
|
-
const action = hadBlock ? "updated" : "installed";
|
|
7012
|
-
if (opts.dryRun === true) return { action };
|
|
7013
|
-
const recheck = await readMarkdownFile5(target);
|
|
7014
|
-
if (recheck !== existing) {
|
|
7015
|
-
throw new Error(
|
|
7016
|
-
"The target changed during sync; aborting so a concurrent edit is not overwritten. Re-run the command."
|
|
7017
|
-
);
|
|
7018
|
-
}
|
|
7019
|
-
await backupOnce(target, existing);
|
|
7020
|
-
await writeFileDurable(target, newBody);
|
|
7021
|
-
return { action };
|
|
7022
|
-
}
|
|
7023
|
-
function assertNoMarkerLine(body, markers) {
|
|
7024
|
-
for (const line of body.split(/\r?\n/)) {
|
|
7025
|
-
if (line === markers.start || line === markers.end) {
|
|
7026
|
-
throw new Error(
|
|
7027
|
-
"The content contains a basou marker line, which would corrupt the managed block. Remove that line from the source."
|
|
7028
|
-
);
|
|
7029
|
-
}
|
|
7030
|
-
}
|
|
7031
|
-
}
|
|
7032
|
-
async function removeMarkerBlock(opts) {
|
|
7033
|
-
const { target, markers, fileLabel } = opts;
|
|
7034
|
-
await assertNotSymlink(target);
|
|
7035
|
-
const existing = await readMarkdownFile5(target);
|
|
7036
|
-
if (existing === null) return { removed: false };
|
|
7037
|
-
const newBody = removeMarkerSection2(existing, fileLabel, markers);
|
|
7038
|
-
if (newBody === existing) return { removed: false };
|
|
7039
|
-
if (opts.dryRun === true) return { removed: true };
|
|
7040
|
-
const recheck = await readMarkdownFile5(target);
|
|
7041
|
-
if (recheck !== existing) {
|
|
7042
|
-
throw new Error(
|
|
7043
|
-
"The target changed during unsync; aborting so a concurrent edit is not overwritten. Re-run the command."
|
|
7044
|
-
);
|
|
7045
|
-
}
|
|
7046
|
-
await backupOnce(target, existing);
|
|
7047
|
-
await writeFileDurable(target, newBody);
|
|
7048
|
-
return { removed: true };
|
|
7049
|
-
}
|
|
7050
|
-
async function syncOrientationChannel(opts) {
|
|
7051
|
-
assertNoMarkerLine(opts.body, ORIENTATION_MARKERS);
|
|
7052
|
-
const block = `${ORIENTATION_MANAGED_NOTE}
|
|
7053
|
-
|
|
7054
|
-
${opts.body.replace(/\s+$/, "")}
|
|
7055
|
-
`;
|
|
7056
|
-
return syncMarkerBlock({
|
|
7057
|
-
target: opts.target ?? CODEX_TARGET_PATH,
|
|
7058
|
-
markers: ORIENTATION_MARKERS,
|
|
7059
|
-
block,
|
|
7060
|
-
...opts.dryRun === true ? { dryRun: true } : {}
|
|
7061
|
-
});
|
|
7062
|
-
}
|
|
7063
|
-
async function renderOrientationToCodexChannel(opts) {
|
|
7064
|
-
const body = await readMarkdownFile5(opts.orientationPath);
|
|
7065
|
-
if (body === null) return null;
|
|
7066
|
-
const { action } = await syncOrientationChannel({
|
|
7067
|
-
body,
|
|
7068
|
-
...opts.channelPath !== void 0 ? { target: opts.channelPath } : {}
|
|
7069
|
-
});
|
|
7070
|
-
return {
|
|
7071
|
-
action,
|
|
7072
|
-
line: `codex channel: orientation ${action} in ${opts.channelPath ?? "~/.codex/AGENTS.md"}`
|
|
7073
|
-
};
|
|
7074
|
-
}
|
|
7075
|
-
|
|
7076
7142
|
// src/lib/protocols-config.ts
|
|
7077
7143
|
import { homedir as homedir8 } from "os";
|
|
7078
7144
|
import { isAbsolute as isAbsolute4, join as join12, resolve as resolve8 } from "path";
|
|
@@ -7282,9 +7348,29 @@ async function doRunProtocolUnsync(options) {
|
|
|
7282
7348
|
}
|
|
7283
7349
|
|
|
7284
7350
|
// src/commands/refresh.ts
|
|
7285
|
-
import {
|
|
7351
|
+
import {
|
|
7352
|
+
assertBasouRootSafe as assertBasouRootSafe9,
|
|
7353
|
+
basouPaths as basouPaths11,
|
|
7354
|
+
findErrorCode as findErrorCode9,
|
|
7355
|
+
readManifest as readManifest7
|
|
7356
|
+
} from "@basou/core";
|
|
7286
7357
|
import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
|
|
7287
7358
|
|
|
7359
|
+
// src/lib/channel-policy.ts
|
|
7360
|
+
function decideCodexChannel(manifest) {
|
|
7361
|
+
if (manifest.policies?.confidential === true) return { write: false, reason: "confidential" };
|
|
7362
|
+
if (manifest.channels?.codex === true) return { write: true };
|
|
7363
|
+
return { write: false, reason: "not_enabled" };
|
|
7364
|
+
}
|
|
7365
|
+
function describeCodexChannelSkip(reason) {
|
|
7366
|
+
switch (reason) {
|
|
7367
|
+
case "confidential":
|
|
7368
|
+
return "codex channel: skipped (confidential workspace \u2014 nothing is written to the user-global ~/.codex/AGENTS.md)";
|
|
7369
|
+
case "not_enabled":
|
|
7370
|
+
return "codex channel: skipped (this workspace has not opted in; the user-global ~/.codex/AGENTS.md is left untouched \u2014 see docs/spec/schemas.md \xA74.2)";
|
|
7371
|
+
}
|
|
7372
|
+
}
|
|
7373
|
+
|
|
7288
7374
|
// src/commands/refresh-watch.ts
|
|
7289
7375
|
import { readdir as readdir2, stat as stat5 } from "fs/promises";
|
|
7290
7376
|
import { homedir as homedir9 } from "os";
|
|
@@ -7567,24 +7653,47 @@ async function computeRefresh(options, ctx) {
|
|
|
7567
7653
|
}
|
|
7568
7654
|
async function doRunRefresh(options, ctx) {
|
|
7569
7655
|
const { result, paths } = await computeRefresh(options, ctx);
|
|
7570
|
-
const
|
|
7656
|
+
const channel = options.dryRun === true ? { outcome: { status: "skipped", reason: "dry_run" }, line: null } : await syncCodexOrientationChannel(paths, ctx.codexChannelPath);
|
|
7657
|
+
const reported = { ...result, codexChannel: channel.outcome };
|
|
7571
7658
|
if (options.json === true) {
|
|
7572
|
-
console.log(JSON.stringify(
|
|
7659
|
+
console.log(JSON.stringify(reported));
|
|
7573
7660
|
} else {
|
|
7574
7661
|
printRefreshSummary(result);
|
|
7575
|
-
if (
|
|
7662
|
+
if (channel.line !== null) console.log(channel.line);
|
|
7576
7663
|
}
|
|
7577
|
-
return
|
|
7664
|
+
return reported;
|
|
7578
7665
|
}
|
|
7579
7666
|
async function syncCodexOrientationChannel(paths, channelPath) {
|
|
7667
|
+
let decision;
|
|
7668
|
+
try {
|
|
7669
|
+
decision = decideCodexChannel(await readManifest7(paths));
|
|
7670
|
+
} catch (error) {
|
|
7671
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
7672
|
+
return {
|
|
7673
|
+
outcome: { status: "skipped", reason: "error", detail },
|
|
7674
|
+
line: `codex channel: skipped (manifest could not be re-read, so the opt-in cannot be confirmed: ${detail})`
|
|
7675
|
+
};
|
|
7676
|
+
}
|
|
7677
|
+
if (!decision.write) {
|
|
7678
|
+
return {
|
|
7679
|
+
outcome: { status: "skipped", reason: decision.reason },
|
|
7680
|
+
line: describeCodexChannelSkip(decision.reason)
|
|
7681
|
+
};
|
|
7682
|
+
}
|
|
7580
7683
|
try {
|
|
7581
7684
|
const rendered = await renderOrientationToCodexChannel({
|
|
7582
7685
|
orientationPath: paths.files.orientation,
|
|
7583
7686
|
...channelPath !== void 0 ? { channelPath } : {}
|
|
7584
7687
|
});
|
|
7585
|
-
|
|
7688
|
+
if (rendered === null)
|
|
7689
|
+
return { outcome: { status: "skipped", reason: "no_orientation" }, line: null };
|
|
7690
|
+
return { outcome: { status: "written", action: rendered.action }, line: rendered.line };
|
|
7586
7691
|
} catch (error) {
|
|
7587
|
-
|
|
7692
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
7693
|
+
return {
|
|
7694
|
+
outcome: { status: "skipped", reason: "error", detail },
|
|
7695
|
+
line: `codex channel skipped: ${detail}`
|
|
7696
|
+
};
|
|
7588
7697
|
}
|
|
7589
7698
|
}
|
|
7590
7699
|
function describeImport(outcome) {
|
|
@@ -7737,7 +7846,7 @@ import {
|
|
|
7737
7846
|
findErrorCode as findErrorCode11,
|
|
7738
7847
|
findUnbindableRepos,
|
|
7739
7848
|
parseReviewRecordInput,
|
|
7740
|
-
readManifest as
|
|
7849
|
+
readManifest as readManifest8,
|
|
7741
7850
|
resolveRepoRoot,
|
|
7742
7851
|
sanitizePath as sanitizePath2
|
|
7743
7852
|
} from "@basou/core";
|
|
@@ -7824,7 +7933,7 @@ async function doRunReviewRecord(options, ctx) {
|
|
|
7824
7933
|
}
|
|
7825
7934
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
7826
7935
|
const occurredAt = now.toISOString();
|
|
7827
|
-
const manifest = await
|
|
7936
|
+
const manifest = await readManifest8(paths);
|
|
7828
7937
|
const invocationArgs = options.file !== void 0 ? [
|
|
7829
7938
|
"--file",
|
|
7830
7939
|
sanitizePath2(resolve10(cwd, options.file), {
|
|
@@ -8053,19 +8162,25 @@ function selfReportSuffix(u, stillCounted) {
|
|
|
8053
8162
|
if (rest > 0) parts.push(`+${rest} more`);
|
|
8054
8163
|
return ` \xB7 self-reported by ${parts.join("; ")} \u2014 ${stillCounted ? "unverified, still counted" : "unverified"}`;
|
|
8055
8164
|
}
|
|
8165
|
+
function unobservedOutcomeSuffix(u) {
|
|
8166
|
+
const n = u.commitsWithUnobservedOutcome;
|
|
8167
|
+
if (n === 0) return "";
|
|
8168
|
+
const scope = n === u.commitCount ? "" : `${n} of them `;
|
|
8169
|
+
return ` \xB7 ${scope}exited with no recorded status \u2014 landing assumed, not observed`;
|
|
8170
|
+
}
|
|
8056
8171
|
function unitLine(u, now) {
|
|
8057
8172
|
const when = relAge(u.lastCommitAt, now);
|
|
8058
8173
|
const head = `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"})`;
|
|
8059
8174
|
if (u.verdict === "near_unbound") {
|
|
8060
8175
|
const ids = u.reviews.map((r) => r.sessionId.slice(0, 14)).join(", ");
|
|
8061
|
-
return `${head} \u2014 a nearby review exists, but the diff / changed files were not examined [${ids}]${selfReportSuffix(u, true)}`;
|
|
8176
|
+
return `${head} \u2014 a nearby review exists, but the diff / changed files were not examined [${ids}]${selfReportSuffix(u, true)}${unobservedOutcomeSuffix(u)}`;
|
|
8062
8177
|
}
|
|
8063
|
-
return `${head} \u2014 no bound cross-model review${selfReportSuffix(u, true)}`;
|
|
8178
|
+
return `${head} \u2014 no bound cross-model review${selfReportSuffix(u, true)}${unobservedOutcomeSuffix(u)}`;
|
|
8064
8179
|
}
|
|
8065
8180
|
function candidateLine(u, now) {
|
|
8066
8181
|
const when = relAge(u.lastCommitAt, now);
|
|
8067
8182
|
const cite = u.reviews.map((r) => `${r.sessionId.slice(0, 14)}${r.examinedDiff ? "(diff)" : ""}`).join(", ");
|
|
8068
|
-
return `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"}) \u2014 review trace: ${cite}${selfReportSuffix(u, false)}`;
|
|
8183
|
+
return `- ${u.repo} ${when} (${u.commitCount} commit${u.commitCount === 1 ? "" : "s"}) \u2014 review trace: ${cite}${selfReportSuffix(u, false)}${unobservedOutcomeSuffix(u)}`;
|
|
8069
8184
|
}
|
|
8070
8185
|
function renderReviewGaps(summary) {
|
|
8071
8186
|
const now = new Date(summary.generatedAt);
|
|
@@ -8172,7 +8287,7 @@ import {
|
|
|
8172
8287
|
getSnapshot as getSnapshot2,
|
|
8173
8288
|
overwriteYamlFile as overwriteYamlFile2,
|
|
8174
8289
|
prefixedUlid as prefixedUlid4,
|
|
8175
|
-
readManifest as
|
|
8290
|
+
readManifest as readManifest9,
|
|
8176
8291
|
readYamlFile as readYamlFile6,
|
|
8177
8292
|
resolveClaudeCodeCommand,
|
|
8178
8293
|
resolveCodexCommand,
|
|
@@ -8232,7 +8347,7 @@ async function runTrackedTool(args, options, ctx, adapter) {
|
|
|
8232
8347
|
const repoRoot = await resolveRepositoryRootForRun(cwd);
|
|
8233
8348
|
const paths = basouPaths15(repoRoot);
|
|
8234
8349
|
await assertBasouRootSafe12(paths.root);
|
|
8235
|
-
const manifest = await
|
|
8350
|
+
const manifest = await readManifest9(paths);
|
|
8236
8351
|
const sessionId = prefixedUlid4("ses");
|
|
8237
8352
|
const sessionDir = join14(paths.sessions, sessionId);
|
|
8238
8353
|
await mkdir2(sessionDir, { recursive: true });
|
|
@@ -8590,6 +8705,8 @@ async function syncCodexOrientationChannelPreSpawn(cwd, ctx) {
|
|
|
8590
8705
|
try {
|
|
8591
8706
|
const root = await resolveBasouRootForCommand(cwd, "run");
|
|
8592
8707
|
const paths = basouPaths15(root);
|
|
8708
|
+
const decision = decideCodexChannel(await readManifest9(paths));
|
|
8709
|
+
if (!decision.write) return describeCodexChannelSkip(decision.reason);
|
|
8593
8710
|
const rendered = await renderOrientationToCodexChannel({
|
|
8594
8711
|
orientationPath: paths.files.orientation,
|
|
8595
8712
|
...ctx.codexChannelPath !== void 0 ? { channelPath: ctx.codexChannelPath } : {}
|
|
@@ -8613,7 +8730,7 @@ import {
|
|
|
8613
8730
|
importSessionFromJson as importSessionFromJson2,
|
|
8614
8731
|
loadSessionEntries as loadSessionEntries2,
|
|
8615
8732
|
readAllEvents,
|
|
8616
|
-
readManifest as
|
|
8733
|
+
readManifest as readManifest10,
|
|
8617
8734
|
readYamlFile as readYamlFile7,
|
|
8618
8735
|
rechainSessionInPlace,
|
|
8619
8736
|
resolveSessionId as resolveSessionId3,
|
|
@@ -8880,8 +8997,9 @@ function eventVariantSummary(ev) {
|
|
|
8880
8997
|
switch (ev.type) {
|
|
8881
8998
|
case "command_executed": {
|
|
8882
8999
|
const argsPart = ev.args.length > 0 ? ` ${ev.args.join(" ")}` : "";
|
|
8883
|
-
const
|
|
8884
|
-
|
|
9000
|
+
const executorPart = ev.command ?? "(executor unrecorded)";
|
|
9001
|
+
const exitPart = ev.exit_code === null ? "exit=unknown" : `exit=${ev.exit_code}`;
|
|
9002
|
+
return `${executorPart}${argsPart} (${exitPart}, ${ev.duration_ms}ms)`;
|
|
8885
9003
|
}
|
|
8886
9004
|
case "git_snapshot":
|
|
8887
9005
|
return `branch=${ev.branch} dirty=${ev.dirty}`;
|
|
@@ -9024,7 +9142,7 @@ async function doRunSessionImport(options, ctx) {
|
|
|
9024
9142
|
const repositoryRoot = await resolveRepositoryRootForSession(cwd, "import");
|
|
9025
9143
|
const paths = basouPaths16(repositoryRoot);
|
|
9026
9144
|
await assertWorkspaceInitialized11(paths.root);
|
|
9027
|
-
const manifest = await
|
|
9145
|
+
const manifest = await readManifest10(paths);
|
|
9028
9146
|
const rawBody = await readInputFile(options.from);
|
|
9029
9147
|
const json = parseJsonStrict(rawBody);
|
|
9030
9148
|
const parsed = SessionImportPayloadSchema2.safeParse(json);
|
|
@@ -9406,7 +9524,7 @@ import {
|
|
|
9406
9524
|
basouPaths as basouPaths18,
|
|
9407
9525
|
buildStatusSnapshot,
|
|
9408
9526
|
findErrorCode as findErrorCode14,
|
|
9409
|
-
readManifest as
|
|
9527
|
+
readManifest as readManifest11,
|
|
9410
9528
|
resolveRepositoryRoot as resolveRepositoryRoot12,
|
|
9411
9529
|
writeStatus
|
|
9412
9530
|
} from "@basou/core";
|
|
@@ -9437,7 +9555,7 @@ async function doRunStatus(options, ctx) {
|
|
|
9437
9555
|
}
|
|
9438
9556
|
let manifest;
|
|
9439
9557
|
try {
|
|
9440
|
-
manifest = await
|
|
9558
|
+
manifest = await readManifest11(paths);
|
|
9441
9559
|
} catch (error) {
|
|
9442
9560
|
if (findErrorCode14(error, "ENOENT")) {
|
|
9443
9561
|
throw new Error("Workspace not initialized. Run 'basou init' first.");
|
|
@@ -9503,7 +9621,7 @@ import {
|
|
|
9503
9621
|
loadSessionEntries as loadSessionEntries3,
|
|
9504
9622
|
loadTaskEntries,
|
|
9505
9623
|
prefixedUlid as prefixedUlid5,
|
|
9506
|
-
readManifest as
|
|
9624
|
+
readManifest as readManifest12,
|
|
9507
9625
|
readTaskFile,
|
|
9508
9626
|
readTaskFileWithArchiveFallback,
|
|
9509
9627
|
reconcileAllTasks,
|
|
@@ -9630,7 +9748,7 @@ async function doRunTaskNew(options, ctx) {
|
|
|
9630
9748
|
});
|
|
9631
9749
|
return;
|
|
9632
9750
|
}
|
|
9633
|
-
const manifest = await
|
|
9751
|
+
const manifest = await readManifest12(paths);
|
|
9634
9752
|
const result = await createTaskWithEvent({
|
|
9635
9753
|
mode: "ad-hoc",
|
|
9636
9754
|
paths,
|
|
@@ -9978,7 +10096,7 @@ async function doRunTaskStatus(taskIdInput, newStatusInput, options, ctx) {
|
|
|
9978
10096
|
});
|
|
9979
10097
|
return;
|
|
9980
10098
|
}
|
|
9981
|
-
const manifest = await
|
|
10099
|
+
const manifest = await readManifest12(paths);
|
|
9982
10100
|
const result = await updateTaskStatusWithEvent({
|
|
9983
10101
|
mode: "ad-hoc",
|
|
9984
10102
|
paths,
|
|
@@ -10031,7 +10149,7 @@ async function doRunTaskReconcile(options, ctx) {
|
|
|
10031
10149
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "reconcile");
|
|
10032
10150
|
const paths = basouPaths19(repositoryRoot);
|
|
10033
10151
|
await assertWorkspaceInitialized13(paths.root);
|
|
10034
|
-
const manifest = await
|
|
10152
|
+
const manifest = await readManifest12(paths);
|
|
10035
10153
|
const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
|
|
10036
10154
|
const write = options.write === true;
|
|
10037
10155
|
const verbose = isVerbose(options);
|
|
@@ -10211,7 +10329,7 @@ async function doRunTaskRefreshLinkage(taskIdInput, options, ctx) {
|
|
|
10211
10329
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "refresh-linkage");
|
|
10212
10330
|
const paths = basouPaths19(repositoryRoot);
|
|
10213
10331
|
await assertWorkspaceInitialized13(paths.root);
|
|
10214
|
-
const manifest = await
|
|
10332
|
+
const manifest = await readManifest12(paths);
|
|
10215
10333
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
10216
10334
|
const nowProvider = ctx.nowProvider ?? (() => /* @__PURE__ */ new Date());
|
|
10217
10335
|
const write = options.write === true;
|
|
@@ -10291,7 +10409,7 @@ async function doRunTaskEdit(taskIdInput, options, ctx) {
|
|
|
10291
10409
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "edit");
|
|
10292
10410
|
const paths = basouPaths19(repositoryRoot);
|
|
10293
10411
|
await assertWorkspaceInitialized13(paths.root);
|
|
10294
|
-
const manifest = await
|
|
10412
|
+
const manifest = await readManifest12(paths);
|
|
10295
10413
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
10296
10414
|
const now = ctx.nowProvider !== void 0 ? ctx.nowProvider() : /* @__PURE__ */ new Date();
|
|
10297
10415
|
const occurredAt = now.toISOString();
|
|
@@ -10347,7 +10465,7 @@ async function doRunTaskDelete(taskIdInput, options, ctx) {
|
|
|
10347
10465
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "delete");
|
|
10348
10466
|
const paths = basouPaths19(repositoryRoot);
|
|
10349
10467
|
await assertWorkspaceInitialized13(paths.root);
|
|
10350
|
-
const manifest = await
|
|
10468
|
+
const manifest = await readManifest12(paths);
|
|
10351
10469
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
10352
10470
|
if (options.yes !== true) {
|
|
10353
10471
|
await confirmDestructiveAction("delete", taskId);
|
|
@@ -10392,7 +10510,7 @@ async function doRunTaskArchive(taskIdInput, options, ctx) {
|
|
|
10392
10510
|
const repositoryRoot = await resolveRepositoryRootForTask(cwd, "archive");
|
|
10393
10511
|
const paths = basouPaths19(repositoryRoot);
|
|
10394
10512
|
await assertWorkspaceInitialized13(paths.root);
|
|
10395
|
-
const manifest = await
|
|
10513
|
+
const manifest = await readManifest12(paths);
|
|
10396
10514
|
const taskId = await resolveTaskId2(paths, taskIdInput);
|
|
10397
10515
|
if (options.yes !== true) {
|
|
10398
10516
|
await confirmDestructiveAction("archive", taskId);
|
|
@@ -10434,8 +10552,8 @@ async function confirmDestructiveAction(action, taskId) {
|
|
|
10434
10552
|
}
|
|
10435
10553
|
}
|
|
10436
10554
|
async function readSingleLineFromStdin() {
|
|
10437
|
-
const { createInterface:
|
|
10438
|
-
const rl =
|
|
10555
|
+
const { createInterface: createInterface3 } = await import("readline/promises");
|
|
10556
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
10439
10557
|
try {
|
|
10440
10558
|
const line = await rl.question("");
|
|
10441
10559
|
return line;
|
|
@@ -10709,22 +10827,334 @@ async function assertWorkspaceInitialized14(basouRoot) {
|
|
|
10709
10827
|
// src/commands/view.ts
|
|
10710
10828
|
import { spawn } from "child_process";
|
|
10711
10829
|
import { createHash } from "crypto";
|
|
10712
|
-
import { basename as
|
|
10830
|
+
import { basename as basename9, resolve as resolve13 } from "path";
|
|
10713
10831
|
import {
|
|
10714
10832
|
assertBasouRootSafe as assertBasouRootSafe18,
|
|
10715
|
-
basouPaths as
|
|
10833
|
+
basouPaths as basouPaths22,
|
|
10716
10834
|
findErrorCode as findErrorCode18,
|
|
10717
|
-
readManifest as
|
|
10718
|
-
resolveRepositoryRoot as
|
|
10835
|
+
readManifest as readManifest16,
|
|
10836
|
+
resolveRepositoryRoot as resolveRepositoryRoot15
|
|
10719
10837
|
} from "@basou/core";
|
|
10720
10838
|
import { InvalidArgumentError as InvalidArgumentError7 } from "commander";
|
|
10721
10839
|
|
|
10840
|
+
// src/lib/portfolio-coverage.ts
|
|
10841
|
+
import { createReadStream as createReadStream2 } from "fs";
|
|
10842
|
+
import { readdir as readdir3, stat as stat6 } from "fs/promises";
|
|
10843
|
+
import { homedir as homedir12 } from "os";
|
|
10844
|
+
import { basename as basename7, dirname as dirname4, join as join17 } from "path";
|
|
10845
|
+
import { createInterface as createInterface2 } from "readline";
|
|
10846
|
+
import { basouPaths as basouPaths21, readManifest as readManifest13, resolveRepositoryRoot as resolveRepositoryRoot14 } from "@basou/core";
|
|
10847
|
+
function uncapturedTotal(result) {
|
|
10848
|
+
return result.groups.reduce((sum, g) => sum + g.logs, 0) + result.unplaceable;
|
|
10849
|
+
}
|
|
10850
|
+
async function checkPortfolioCoverage(workspaces, ctx = {}) {
|
|
10851
|
+
const { roots, inertWorkspaces } = await collectDeclaredRoots(workspaces);
|
|
10852
|
+
const listedDirs = new Set([...roots].map((root) => encodeProjectDir(root)));
|
|
10853
|
+
const claudeProjectsDir = ctx.claudeProjectsDir ?? join17(homedir12(), ".claude", "projects");
|
|
10854
|
+
const codexSessionsDir = ctx.codexSessionsDir ?? join17(homedir12(), ".codex", "sessions");
|
|
10855
|
+
const tally = new Tally(roots);
|
|
10856
|
+
const absentTrees = [];
|
|
10857
|
+
const claudeFiles = await listClaudeTranscripts(claudeProjectsDir);
|
|
10858
|
+
if (claudeFiles === void 0) absentTrees.push(claudeProjectsDir);
|
|
10859
|
+
else {
|
|
10860
|
+
for (const file of claudeFiles) {
|
|
10861
|
+
await tally.add(
|
|
10862
|
+
file,
|
|
10863
|
+
"claude-code",
|
|
10864
|
+
claudeTranscriptCwd,
|
|
10865
|
+
listedDirs.has(basename7(dirname4(file)))
|
|
10866
|
+
);
|
|
10867
|
+
}
|
|
10868
|
+
}
|
|
10869
|
+
const codexFiles = await listCodexRollouts(codexSessionsDir);
|
|
10870
|
+
if (codexFiles === void 0) absentTrees.push(codexSessionsDir);
|
|
10871
|
+
else {
|
|
10872
|
+
for (const file of codexFiles) await tally.add(file, "codex", codexRolloutCwd, true);
|
|
10873
|
+
}
|
|
10874
|
+
return tally.finish(absentTrees, inertWorkspaces);
|
|
10875
|
+
}
|
|
10876
|
+
async function collectDeclaredRoots(workspaces) {
|
|
10877
|
+
const roots = /* @__PURE__ */ new Set();
|
|
10878
|
+
const inertWorkspaces = [];
|
|
10879
|
+
for (const ws of workspaces) {
|
|
10880
|
+
let importRoot;
|
|
10881
|
+
try {
|
|
10882
|
+
importRoot = await resolveRepositoryRoot14(ws.repoRoot);
|
|
10883
|
+
} catch {
|
|
10884
|
+
inertWorkspaces.push({ path: ws.repoRoot, reason: "not_a_git_repo" });
|
|
10885
|
+
continue;
|
|
10886
|
+
}
|
|
10887
|
+
let resolved;
|
|
10888
|
+
try {
|
|
10889
|
+
const manifest = await readManifest13(basouPaths21(importRoot));
|
|
10890
|
+
resolved = resolveSourceRoots({
|
|
10891
|
+
projectFlags: [],
|
|
10892
|
+
manifest,
|
|
10893
|
+
repoRoot: importRoot,
|
|
10894
|
+
cwd: importRoot
|
|
10895
|
+
});
|
|
10896
|
+
} catch (error) {
|
|
10897
|
+
const absent = error instanceof Error && error.message === "YAML file not found";
|
|
10898
|
+
inertWorkspaces.push({
|
|
10899
|
+
path: ws.repoRoot,
|
|
10900
|
+
reason: absent ? "no_store" : "unreadable_store"
|
|
10901
|
+
});
|
|
10902
|
+
continue;
|
|
10903
|
+
}
|
|
10904
|
+
for (const root of resolved) roots.add(root);
|
|
10905
|
+
}
|
|
10906
|
+
return { roots, inertWorkspaces };
|
|
10907
|
+
}
|
|
10908
|
+
var Tally = class {
|
|
10909
|
+
constructor(declaredRoots) {
|
|
10910
|
+
this.declaredRoots = declaredRoots;
|
|
10911
|
+
}
|
|
10912
|
+
declaredRoots;
|
|
10913
|
+
groups = /* @__PURE__ */ new Map();
|
|
10914
|
+
kinds = /* @__PURE__ */ new Map();
|
|
10915
|
+
attributed = 0;
|
|
10916
|
+
scanned = 0;
|
|
10917
|
+
unplaceable = 0;
|
|
10918
|
+
unreadable = 0;
|
|
10919
|
+
/**
|
|
10920
|
+
* Record one source log. `listedByImport` is whether the importer would even
|
|
10921
|
+
* read this file (the Claude directory guard); a cwd match on a file import
|
|
10922
|
+
* never lists is not capture.
|
|
10923
|
+
*/
|
|
10924
|
+
async add(file, source, readCwd, listedByImport) {
|
|
10925
|
+
this.scanned++;
|
|
10926
|
+
const cwd = await readCwd(file);
|
|
10927
|
+
if (cwd === null) {
|
|
10928
|
+
this.unreadable++;
|
|
10929
|
+
return;
|
|
10930
|
+
}
|
|
10931
|
+
if (cwd === void 0) {
|
|
10932
|
+
this.unplaceable++;
|
|
10933
|
+
return;
|
|
10934
|
+
}
|
|
10935
|
+
const declared = this.declaredRoots.has(cwd);
|
|
10936
|
+
if (declared && listedByImport) {
|
|
10937
|
+
this.attributed++;
|
|
10938
|
+
return;
|
|
10939
|
+
}
|
|
10940
|
+
const kind = declared ? "dir_not_listed" : enclosingRoot(cwd, this.declaredRoots) !== void 0 ? "below_declared_root" : "no_declared_root";
|
|
10941
|
+
this.kinds.set(cwd, kind);
|
|
10942
|
+
const group = this.groups.get(cwd);
|
|
10943
|
+
if (group === void 0) this.groups.set(cwd, { logs: 1, sources: /* @__PURE__ */ new Set([source]) });
|
|
10944
|
+
else {
|
|
10945
|
+
group.logs++;
|
|
10946
|
+
group.sources.add(source);
|
|
10947
|
+
}
|
|
10948
|
+
}
|
|
10949
|
+
finish(absentTrees, inertWorkspaces) {
|
|
10950
|
+
const groups = [];
|
|
10951
|
+
for (const [cwd, { logs, sources }] of this.groups) {
|
|
10952
|
+
const kind = this.kinds.get(cwd) ?? "no_declared_root";
|
|
10953
|
+
const declaredRoot = kind === "below_declared_root" ? enclosingRoot(cwd, this.declaredRoots) : void 0;
|
|
10954
|
+
groups.push({
|
|
10955
|
+
cwd,
|
|
10956
|
+
logs,
|
|
10957
|
+
sources: [...sources].sort(),
|
|
10958
|
+
kind,
|
|
10959
|
+
...declaredRoot !== void 0 ? { declaredRoot } : {}
|
|
10960
|
+
});
|
|
10961
|
+
}
|
|
10962
|
+
const rank = (k) => k === "dir_not_listed" ? 0 : k === "below_declared_root" ? 1 : 2;
|
|
10963
|
+
groups.sort((a, b) => {
|
|
10964
|
+
if (a.kind !== b.kind) return rank(a.kind) - rank(b.kind);
|
|
10965
|
+
if (a.logs !== b.logs) return b.logs - a.logs;
|
|
10966
|
+
return a.cwd < b.cwd ? -1 : a.cwd > b.cwd ? 1 : 0;
|
|
10967
|
+
});
|
|
10968
|
+
return {
|
|
10969
|
+
logsScanned: this.scanned,
|
|
10970
|
+
attributed: this.attributed,
|
|
10971
|
+
groups,
|
|
10972
|
+
unplaceable: this.unplaceable,
|
|
10973
|
+
unreadable: this.unreadable,
|
|
10974
|
+
absentTrees,
|
|
10975
|
+
inertWorkspaces
|
|
10976
|
+
};
|
|
10977
|
+
}
|
|
10978
|
+
};
|
|
10979
|
+
function enclosingRoot(cwd, roots) {
|
|
10980
|
+
for (const root of roots) {
|
|
10981
|
+
if (cwd.startsWith(root.endsWith("/") ? root : `${root}/`)) return root;
|
|
10982
|
+
}
|
|
10983
|
+
return void 0;
|
|
10984
|
+
}
|
|
10985
|
+
async function isDirEntry(parent, entry) {
|
|
10986
|
+
if (entry.isDirectory()) return true;
|
|
10987
|
+
if (!entry.isSymbolicLink()) return false;
|
|
10988
|
+
try {
|
|
10989
|
+
return (await stat6(join17(parent, entry.name))).isDirectory();
|
|
10990
|
+
} catch {
|
|
10991
|
+
return false;
|
|
10992
|
+
}
|
|
10993
|
+
}
|
|
10994
|
+
async function listClaudeTranscripts(projectsRoot) {
|
|
10995
|
+
let entries;
|
|
10996
|
+
try {
|
|
10997
|
+
entries = await readdir3(projectsRoot, { withFileTypes: true });
|
|
10998
|
+
} catch {
|
|
10999
|
+
return void 0;
|
|
11000
|
+
}
|
|
11001
|
+
const files = [];
|
|
11002
|
+
for (const entry of entries) {
|
|
11003
|
+
if (!await isDirEntry(projectsRoot, entry)) continue;
|
|
11004
|
+
const full = join17(projectsRoot, entry.name);
|
|
11005
|
+
let names;
|
|
11006
|
+
try {
|
|
11007
|
+
names = await readdir3(full);
|
|
11008
|
+
} catch {
|
|
11009
|
+
continue;
|
|
11010
|
+
}
|
|
11011
|
+
for (const name of names) {
|
|
11012
|
+
if (name.endsWith(".jsonl")) files.push(join17(full, name));
|
|
11013
|
+
}
|
|
11014
|
+
}
|
|
11015
|
+
return files.sort();
|
|
11016
|
+
}
|
|
11017
|
+
async function listCodexRollouts(sessionsRoot) {
|
|
11018
|
+
const found = [];
|
|
11019
|
+
const walk = async (dir) => {
|
|
11020
|
+
let entries;
|
|
11021
|
+
try {
|
|
11022
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
11023
|
+
} catch {
|
|
11024
|
+
return;
|
|
11025
|
+
}
|
|
11026
|
+
for (const entry of entries) {
|
|
11027
|
+
const full = join17(dir, entry.name);
|
|
11028
|
+
if (entry.isDirectory()) await walk(full);
|
|
11029
|
+
else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) {
|
|
11030
|
+
found.push(full);
|
|
11031
|
+
}
|
|
11032
|
+
}
|
|
11033
|
+
};
|
|
11034
|
+
try {
|
|
11035
|
+
await readdir3(sessionsRoot);
|
|
11036
|
+
} catch {
|
|
11037
|
+
return void 0;
|
|
11038
|
+
}
|
|
11039
|
+
await walk(sessionsRoot);
|
|
11040
|
+
return found.sort();
|
|
11041
|
+
}
|
|
11042
|
+
async function claudeTranscriptCwd(file) {
|
|
11043
|
+
const stream = createReadStream2(file, { encoding: "utf8" });
|
|
11044
|
+
const lines = createInterface2({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
|
|
11045
|
+
try {
|
|
11046
|
+
for await (const line of lines) {
|
|
11047
|
+
if (line.length === 0) continue;
|
|
11048
|
+
let record;
|
|
11049
|
+
try {
|
|
11050
|
+
record = JSON.parse(line);
|
|
11051
|
+
} catch {
|
|
11052
|
+
continue;
|
|
11053
|
+
}
|
|
11054
|
+
if (typeof record !== "object" || record === null || Array.isArray(record)) continue;
|
|
11055
|
+
const cwd = record.cwd;
|
|
11056
|
+
if (typeof cwd === "string" && cwd.length > 0) return cwd;
|
|
11057
|
+
}
|
|
11058
|
+
return void 0;
|
|
11059
|
+
} catch {
|
|
11060
|
+
return null;
|
|
11061
|
+
} finally {
|
|
11062
|
+
lines.close();
|
|
11063
|
+
stream.destroy();
|
|
11064
|
+
}
|
|
11065
|
+
}
|
|
11066
|
+
async function codexRolloutCwd(file) {
|
|
11067
|
+
const meta = await readRolloutMeta(file);
|
|
11068
|
+
return meta === void 0 ? void 0 : meta.cwd;
|
|
11069
|
+
}
|
|
11070
|
+
var GROUP_LIST_CAP = 10;
|
|
11071
|
+
function groupNote(group) {
|
|
11072
|
+
if (group.kind === "below_declared_root") {
|
|
11073
|
+
return ` \u2014 inside declared root ${group.declaredRoot}, and the recorded cwd must EQUAL a source root`;
|
|
11074
|
+
}
|
|
11075
|
+
if (group.kind === "dir_not_listed") {
|
|
11076
|
+
return " \u2014 this cwd IS a declared root, but the transcript's per-project directory is not one import lists";
|
|
11077
|
+
}
|
|
11078
|
+
return "";
|
|
11079
|
+
}
|
|
11080
|
+
function formatCoverageReport(result) {
|
|
11081
|
+
const lines = [];
|
|
11082
|
+
if (result.logsScanned === 0) {
|
|
11083
|
+
const where = result.absentTrees.length > 0 ? ` (no source logs found: ${result.absentTrees.join(", ")})` : "";
|
|
11084
|
+
lines.push(
|
|
11085
|
+
`Capture coverage: nothing to check \u2014 no native session logs on this machine${where}.`
|
|
11086
|
+
);
|
|
11087
|
+
return [...lines, ...inertLines(result)];
|
|
11088
|
+
}
|
|
11089
|
+
const total = uncapturedTotal(result);
|
|
11090
|
+
const caveats = [];
|
|
11091
|
+
if (result.unreadable > 0) caveats.push(`${result.unreadable} unreadable, verdict unknown`);
|
|
11092
|
+
if (result.absentTrees.length > 0) caveats.push(`not scanned: ${result.absentTrees.join(", ")}`);
|
|
11093
|
+
const caveat = caveats.length > 0 ? ` (${caveats.join("; ")})` : "";
|
|
11094
|
+
if (total === 0 && result.unreadable === 0) {
|
|
11095
|
+
lines.push(
|
|
11096
|
+
`Capture coverage: OK. ${result.logsScanned} source log(s) scanned, all imported by a registered workspace${caveat}.`
|
|
11097
|
+
);
|
|
11098
|
+
return [...lines, ...inertLines(result)];
|
|
11099
|
+
}
|
|
11100
|
+
const pct = Math.round(total / result.logsScanned * 100);
|
|
11101
|
+
lines.push(
|
|
11102
|
+
`Capture coverage: ${total} of ${result.logsScanned} source log(s) (${pct}%) are imported by no registered workspace${caveat}:`
|
|
11103
|
+
);
|
|
11104
|
+
for (const g of result.groups.slice(0, GROUP_LIST_CAP)) {
|
|
11105
|
+
const via = g.sources.join("+");
|
|
11106
|
+
lines.push(` ${String(g.logs).padStart(4)} ${g.cwd} (${via})${groupNote(g)}`);
|
|
11107
|
+
}
|
|
11108
|
+
const rest = result.groups.length - GROUP_LIST_CAP;
|
|
11109
|
+
if (rest > 0) {
|
|
11110
|
+
const restLogs = result.groups.slice(GROUP_LIST_CAP).reduce((sum, g) => sum + g.logs, 0);
|
|
11111
|
+
lines.push(
|
|
11112
|
+
` \u2026 +${rest} more working director${rest === 1 ? "y" : "ies"} (${restLogs} log(s))`
|
|
11113
|
+
);
|
|
11114
|
+
}
|
|
11115
|
+
if (result.unplaceable > 0) {
|
|
11116
|
+
lines.push(
|
|
11117
|
+
` ${String(result.unplaceable).padStart(4)} (no directory to name: the log records no cwd import can use, so import drops it)`
|
|
11118
|
+
);
|
|
11119
|
+
}
|
|
11120
|
+
if (result.groups.some((g) => g.kind === "below_declared_root")) {
|
|
11121
|
+
lines.push(
|
|
11122
|
+
"A cwd inside a declared root is dropped by the exact-match rule: the enclosing declaration does not cover it. Declaring that subdirectory itself as a further import.source_roots entry captures it."
|
|
11123
|
+
);
|
|
11124
|
+
}
|
|
11125
|
+
if (result.groups.some((g) => g.kind === "dir_not_listed")) {
|
|
11126
|
+
lines.push(
|
|
11127
|
+
"A transcript whose cwd IS declared but whose per-project directory no declared root encodes to is never listed by the importer. Check that the workspace is registered by the same path spelling the sessions ran in."
|
|
11128
|
+
);
|
|
11129
|
+
}
|
|
11130
|
+
lines.push(
|
|
11131
|
+
"Register the project in a workspace's import.source_roots to start capturing it (registering a path in ~/.basou/portfolio.yaml alone imports nothing \u2014 it only adds the workspace to this view). A scratch directory, a temp path, or a GUI tool's own working directory has no repo to declare and is expected to stay here."
|
|
11132
|
+
);
|
|
11133
|
+
return [...lines, ...inertLines(result)];
|
|
11134
|
+
}
|
|
11135
|
+
function inertLines(result) {
|
|
11136
|
+
if (result.inertWorkspaces.length === 0) return [];
|
|
11137
|
+
const detail = {
|
|
11138
|
+
not_a_git_repo: "not a git repository",
|
|
11139
|
+
no_store: "no .basou store (never initialized)",
|
|
11140
|
+
unreadable_store: "the .basou manifest is unreadable"
|
|
11141
|
+
};
|
|
11142
|
+
const n = result.inertWorkspaces.length;
|
|
11143
|
+
const lines = [
|
|
11144
|
+
`Capture coverage: ${n} registered entr${n === 1 ? "y" : "ies"} import cannot run in, so ${n === 1 ? "it declares" : "they declare"} no source roots:`
|
|
11145
|
+
];
|
|
11146
|
+
for (const ws of result.inertWorkspaces) {
|
|
11147
|
+
lines.push(` ${ws.path} \u2014 ${detail[ws.reason]}`);
|
|
11148
|
+
}
|
|
11149
|
+
return lines;
|
|
11150
|
+
}
|
|
11151
|
+
|
|
10722
11152
|
// src/lib/portfolio-safety.ts
|
|
10723
11153
|
import { execFile } from "child_process";
|
|
10724
11154
|
import { lstat as lstat2, realpath as realpath2 } from "fs/promises";
|
|
10725
|
-
import { isAbsolute as isAbsolute7, join as
|
|
11155
|
+
import { isAbsolute as isAbsolute7, join as join18, relative as relative4, resolve as resolve11 } from "path";
|
|
10726
11156
|
import { promisify } from "util";
|
|
10727
|
-
import { readManifest as
|
|
11157
|
+
import { readManifest as readManifest14 } from "@basou/core";
|
|
10728
11158
|
var execFileAsync = promisify(execFile);
|
|
10729
11159
|
function errorCode(error) {
|
|
10730
11160
|
return error instanceof Error ? error.code : void 0;
|
|
@@ -10746,7 +11176,7 @@ function isBasouPath(p) {
|
|
|
10746
11176
|
async function inspectRepo(repoPath) {
|
|
10747
11177
|
let hasEntry = false;
|
|
10748
11178
|
try {
|
|
10749
|
-
await lstat2(
|
|
11179
|
+
await lstat2(join18(repoPath, ".basou"));
|
|
10750
11180
|
hasEntry = true;
|
|
10751
11181
|
} catch (error) {
|
|
10752
11182
|
if (errorCode(error) !== "ENOENT") {
|
|
@@ -10780,7 +11210,7 @@ async function checkPortfolioSafety(workspaces) {
|
|
|
10780
11210
|
let viewPath;
|
|
10781
11211
|
let isMaster = false;
|
|
10782
11212
|
try {
|
|
10783
|
-
const manifest = await
|
|
11213
|
+
const manifest = await readManifest14(ws.paths);
|
|
10784
11214
|
sourceRoots = manifest.import?.source_roots ?? [];
|
|
10785
11215
|
viewPath = manifest.workspace.view;
|
|
10786
11216
|
isMaster = true;
|
|
@@ -10900,7 +11330,7 @@ function formatSafetyReport(result) {
|
|
|
10900
11330
|
|
|
10901
11331
|
// src/lib/view-server.ts
|
|
10902
11332
|
import { createServer } from "http";
|
|
10903
|
-
import { basename as
|
|
11333
|
+
import { basename as basename8, join as join19, resolve as resolve12 } from "path";
|
|
10904
11334
|
import {
|
|
10905
11335
|
computeWorkStats as computeWorkStats2,
|
|
10906
11336
|
enumerateApprovals as enumerateApprovals2,
|
|
@@ -10910,7 +11340,7 @@ import {
|
|
|
10910
11340
|
loadSessionEntries as loadSessionEntries4,
|
|
10911
11341
|
loadTaskEntries as loadTaskEntries2,
|
|
10912
11342
|
readAllEvents as readAllEvents2,
|
|
10913
|
-
readManifest as
|
|
11343
|
+
readManifest as readManifest15,
|
|
10914
11344
|
readMarkdownFile as readMarkdownFile7,
|
|
10915
11345
|
readSessionYaml as readSessionYaml3,
|
|
10916
11346
|
readTaskFile as readTaskFile2,
|
|
@@ -11501,6 +11931,7 @@ var VIEW_HTML = `<!doctype html>
|
|
|
11501
11931
|
function eventSummary(ev) {
|
|
11502
11932
|
if (ev.type === 'command_executed') {
|
|
11503
11933
|
var cmd = (ev.args && ev.args.length) ? ev.args.join(' ') : ev.command;
|
|
11934
|
+
if (cmd === null || cmd === undefined) cmd = '(command unrecorded)';
|
|
11504
11935
|
var ex = (ev.exit_code === null || ev.exit_code === undefined) ? '' : ' (exit ' + ev.exit_code + ')';
|
|
11505
11936
|
return cmd + ex;
|
|
11506
11937
|
}
|
|
@@ -11865,7 +12296,7 @@ async function captureStaleness(ws, nowIso) {
|
|
|
11865
12296
|
async function overview(ws, nowProvider, resolveRemoteUrl) {
|
|
11866
12297
|
let manifest;
|
|
11867
12298
|
try {
|
|
11868
|
-
manifest = await
|
|
12299
|
+
manifest = await readManifest15(ws.paths);
|
|
11869
12300
|
} catch (error) {
|
|
11870
12301
|
if (findErrorCode17(error, "ENOENT")) {
|
|
11871
12302
|
return { initialized: false, repoRoot: ws.repoRoot };
|
|
@@ -11905,7 +12336,7 @@ async function rosterRepos(repoRoot, manifest, resolveRemoteUrl) {
|
|
|
11905
12336
|
const remote = await resolveRemoteUrl(abs);
|
|
11906
12337
|
const url = remote !== void 0 ? toBrowserUrl(remote) : null;
|
|
11907
12338
|
return {
|
|
11908
|
-
name:
|
|
12339
|
+
name: basename8(abs),
|
|
11909
12340
|
path: repo.path,
|
|
11910
12341
|
...url !== null ? { url } : {},
|
|
11911
12342
|
...repo.visibility !== void 0 ? { visibility: repo.visibility } : {}
|
|
@@ -11940,7 +12371,7 @@ async function sessionDetail(ws, sessionId) {
|
|
|
11940
12371
|
throw error;
|
|
11941
12372
|
}
|
|
11942
12373
|
try {
|
|
11943
|
-
const events = await readAllEvents2(
|
|
12374
|
+
const events = await readAllEvents2(join19(ws.paths.sessions, sessionId));
|
|
11944
12375
|
return { session, events };
|
|
11945
12376
|
} catch {
|
|
11946
12377
|
return { session, events: [], degraded: true };
|
|
@@ -12095,7 +12526,10 @@ function registerViewCommand(program2) {
|
|
|
12095
12526
|
"--workspace <path>",
|
|
12096
12527
|
"Workspace repo path to include (repeatable; implies portfolio mode; resolved against the cwd)",
|
|
12097
12528
|
collectPath3
|
|
12098
|
-
).option(
|
|
12529
|
+
).option(
|
|
12530
|
+
"--check",
|
|
12531
|
+
"Run the read-only preflight and exit (no server): the portfolio safety check, plus \u2014 in portfolio mode \u2014 a capture-coverage report of session logs no registered workspace imports"
|
|
12532
|
+
).option("--skip-safety-check", "Skip the portfolio safety preflight on start (not recommended)").option("-v, --verbose", "Show error causes").action(async (options) => {
|
|
12099
12533
|
await runView(options);
|
|
12100
12534
|
});
|
|
12101
12535
|
}
|
|
@@ -12111,10 +12545,19 @@ async function doRunView(options, ctx) {
|
|
|
12111
12545
|
const cwd = ctx.cwd ?? process.cwd();
|
|
12112
12546
|
const workspaceFlags = options.workspace ?? [];
|
|
12113
12547
|
const isPortfolio = workspaceFlags.length > 0 || options.portfolio === true;
|
|
12548
|
+
const isWholeRegistry = options.portfolio === true && workspaceFlags.length === 0;
|
|
12114
12549
|
const deps = isPortfolio ? await buildPortfolioDeps(workspaceFlags, ctx, cwd) : await buildSingleDeps(ctx, cwd);
|
|
12115
12550
|
if (options.check === true) {
|
|
12116
12551
|
const result = await checkPortfolioSafety(deps.workspaces);
|
|
12117
12552
|
for (const line of formatSafetyReport(result)) console.log(line);
|
|
12553
|
+
if (isWholeRegistry) {
|
|
12554
|
+
const coverage = await checkPortfolioCoverage(deps.workspaces, {
|
|
12555
|
+
...ctx.claudeProjectsDir !== void 0 ? { claudeProjectsDir: ctx.claudeProjectsDir } : {},
|
|
12556
|
+
...ctx.codexSessionsDir !== void 0 ? { codexSessionsDir: ctx.codexSessionsDir } : {}
|
|
12557
|
+
});
|
|
12558
|
+
console.log("");
|
|
12559
|
+
for (const line of formatCoverageReport(coverage)) console.log(line);
|
|
12560
|
+
}
|
|
12118
12561
|
if (result.findings.length > 0) process.exitCode = 1;
|
|
12119
12562
|
return;
|
|
12120
12563
|
}
|
|
@@ -12154,7 +12597,7 @@ async function doRunView(options, ctx) {
|
|
|
12154
12597
|
}
|
|
12155
12598
|
async function buildSingleDeps(ctx, cwd) {
|
|
12156
12599
|
const repositoryRoot = await resolveRepositoryRootForView(cwd);
|
|
12157
|
-
const paths =
|
|
12600
|
+
const paths = basouPaths22(repositoryRoot);
|
|
12158
12601
|
await assertWorkspaceInitialized15(paths.root);
|
|
12159
12602
|
const entry = await buildWorkspaceEntry(repositoryRoot, ctx);
|
|
12160
12603
|
return {
|
|
@@ -12188,14 +12631,14 @@ async function buildPortfolioDeps(workspaceFlags, ctx, cwd) {
|
|
|
12188
12631
|
};
|
|
12189
12632
|
}
|
|
12190
12633
|
async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
|
|
12191
|
-
const paths =
|
|
12634
|
+
const paths = basouPaths22(repoRoot);
|
|
12192
12635
|
const importCtx = {
|
|
12193
12636
|
cwd: repoRoot,
|
|
12194
12637
|
...ctx.claudeProjectsDir !== void 0 ? { claudeProjectsDir: ctx.claudeProjectsDir } : {},
|
|
12195
12638
|
...ctx.codexSessionsDir !== void 0 ? { codexSessionsDir: ctx.codexSessionsDir } : {}
|
|
12196
12639
|
};
|
|
12197
12640
|
try {
|
|
12198
|
-
const manifest = await
|
|
12641
|
+
const manifest = await readManifest16(paths);
|
|
12199
12642
|
return {
|
|
12200
12643
|
key: manifest.workspace.id,
|
|
12201
12644
|
label: labelOverride ?? manifest.workspace.name,
|
|
@@ -12208,7 +12651,7 @@ async function buildWorkspaceEntry(repoRoot, ctx, labelOverride) {
|
|
|
12208
12651
|
const notFound = error instanceof Error && error.message === "YAML file not found";
|
|
12209
12652
|
return {
|
|
12210
12653
|
key: `ws-${createHash("sha1").update(repoRoot).digest("hex").slice(0, 12)}`,
|
|
12211
|
-
label: labelOverride ??
|
|
12654
|
+
label: labelOverride ?? basename9(repoRoot),
|
|
12212
12655
|
paths,
|
|
12213
12656
|
repoRoot,
|
|
12214
12657
|
importCtx,
|
|
@@ -12275,7 +12718,7 @@ function waitForShutdown(signal) {
|
|
|
12275
12718
|
}
|
|
12276
12719
|
async function resolveRepositoryRootForView(cwd) {
|
|
12277
12720
|
try {
|
|
12278
|
-
return await
|
|
12721
|
+
return await resolveRepositoryRoot15(cwd);
|
|
12279
12722
|
} catch (error) {
|
|
12280
12723
|
if (error instanceof Error && error.message === "Not a git repository") {
|
|
12281
12724
|
throw new Error("Not a git repository. Run 'git init' first, then re-run 'basou view'.", {
|
|
@@ -12326,6 +12769,7 @@ function buildProgram() {
|
|
|
12326
12769
|
registerReviewGapsCommand(program2);
|
|
12327
12770
|
registerProjectCommand(program2);
|
|
12328
12771
|
registerProtocolCommand(program2);
|
|
12772
|
+
registerChannelCommand(program2);
|
|
12329
12773
|
registerHookCommand(program2);
|
|
12330
12774
|
return program2;
|
|
12331
12775
|
}
|