@basou/cli 0.45.0 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +348 -218
- package/dist/index.js.map +1 -1
- package/dist/program.js +348 -218
- package/dist/program.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2443,8 +2443,8 @@ async function assertWorkspaceInitialized4(basouRoot) {
|
|
|
2443
2443
|
// src/commands/hook.ts
|
|
2444
2444
|
import { execFile } from "child_process";
|
|
2445
2445
|
import { open as open2, readFile as readFile3, realpath as realpath3, stat as stat4 } from "fs/promises";
|
|
2446
|
-
import { homedir as
|
|
2447
|
-
import { join as
|
|
2446
|
+
import { homedir as homedir8 } from "os";
|
|
2447
|
+
import { join as join10 } from "path";
|
|
2448
2448
|
import { fileURLToPath } from "url";
|
|
2449
2449
|
import { promisify } from "util";
|
|
2450
2450
|
import {
|
|
@@ -2454,12 +2454,20 @@ import {
|
|
|
2454
2454
|
evaluateStopHook,
|
|
2455
2455
|
findBasouSessionStartHook,
|
|
2456
2456
|
findBasouStopHookCommand,
|
|
2457
|
+
isProtocolUpdateDue,
|
|
2457
2458
|
ORIENTATION_END as ORIENTATION_END2,
|
|
2458
2459
|
ORIENTATION_START as ORIENTATION_START2,
|
|
2460
|
+
PROTOCOL_END,
|
|
2461
|
+
PROTOCOL_START,
|
|
2459
2462
|
parseMarkers as parseMarkers2,
|
|
2463
|
+
parseProtocolStamp,
|
|
2464
|
+
protocolSectionsFrom,
|
|
2465
|
+
protocolUpdateToken,
|
|
2460
2466
|
readMarkdownFile as readMarkdownFile6,
|
|
2461
2467
|
removeSessionStartHook,
|
|
2462
2468
|
removeStopHook,
|
|
2469
|
+
renderProtocolUpdate,
|
|
2470
|
+
transcriptStartedAt,
|
|
2463
2471
|
upsertSessionStartHook,
|
|
2464
2472
|
upsertStopHook
|
|
2465
2473
|
} from "@basou/core";
|
|
@@ -2714,6 +2722,83 @@ async function warnIfPositionNamesOtherWorkspaces(args) {
|
|
|
2714
2722
|
}
|
|
2715
2723
|
}
|
|
2716
2724
|
|
|
2725
|
+
// src/lib/protocols-config.ts
|
|
2726
|
+
import { homedir as homedir5 } from "os";
|
|
2727
|
+
import { isAbsolute as isAbsolute2, join as join7, resolve as resolve4 } from "path";
|
|
2728
|
+
import { readYamlFile as readYamlFile4 } from "@basou/core";
|
|
2729
|
+
var DEFAULT_PROTOCOLS_CONFIG_PATH = join7(homedir5(), ".basou", "protocols.yaml");
|
|
2730
|
+
var DEFAULT_TARGET_PATH = join7(homedir5(), ".claude", "CLAUDE.md");
|
|
2731
|
+
var ALLOWED_TOP_KEYS = /* @__PURE__ */ new Set(["version", "protocols"]);
|
|
2732
|
+
var ALLOWED_ENTRY_KEYS = /* @__PURE__ */ new Set(["source", "title"]);
|
|
2733
|
+
function expandTilde2(p) {
|
|
2734
|
+
if (p === "~") return homedir5();
|
|
2735
|
+
if (p.startsWith("~/")) return join7(homedir5(), p.slice(2));
|
|
2736
|
+
return p;
|
|
2737
|
+
}
|
|
2738
|
+
function isRecord2(value) {
|
|
2739
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2740
|
+
}
|
|
2741
|
+
async function loadProtocolsConfig(configPath = DEFAULT_PROTOCOLS_CONFIG_PATH) {
|
|
2742
|
+
let raw;
|
|
2743
|
+
try {
|
|
2744
|
+
raw = await readYamlFile4(configPath);
|
|
2745
|
+
} catch (error) {
|
|
2746
|
+
if (error instanceof Error && error.message === "YAML file not found") {
|
|
2747
|
+
throw new Error(
|
|
2748
|
+
"No protocols config at ~/.basou/protocols.yaml. Create one (a 'protocols:' list of source markdown paths) before running 'basou protocol sync'."
|
|
2749
|
+
);
|
|
2750
|
+
}
|
|
2751
|
+
if (error instanceof Error && error.message === "Failed to parse YAML content") {
|
|
2752
|
+
throw new Error("~/.basou/protocols.yaml is not valid YAML.");
|
|
2753
|
+
}
|
|
2754
|
+
throw error;
|
|
2755
|
+
}
|
|
2756
|
+
if (!isRecord2(raw) || !Array.isArray(raw.protocols)) {
|
|
2757
|
+
throw new Error("~/.basou/protocols.yaml must contain a 'protocols:' list.");
|
|
2758
|
+
}
|
|
2759
|
+
for (const key of Object.keys(raw)) {
|
|
2760
|
+
if (!ALLOWED_TOP_KEYS.has(key)) {
|
|
2761
|
+
throw new Error(
|
|
2762
|
+
`~/.basou/protocols.yaml has an unknown key '${key}' (allowed: version, protocols).`
|
|
2763
|
+
);
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2767
|
+
const result = [];
|
|
2768
|
+
for (const entry of raw.protocols) {
|
|
2769
|
+
if (!isRecord2(entry)) {
|
|
2770
|
+
throw new Error("Each protocol entry must be a mapping with a 'source' key.");
|
|
2771
|
+
}
|
|
2772
|
+
for (const key of Object.keys(entry)) {
|
|
2773
|
+
if (!ALLOWED_ENTRY_KEYS.has(key)) {
|
|
2774
|
+
throw new Error(`A protocol entry has an unknown key '${key}' (allowed: source, title).`);
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
if (typeof entry.source !== "string" || entry.source.trim().length === 0) {
|
|
2778
|
+
throw new Error("Each protocol entry needs a non-empty string 'source'.");
|
|
2779
|
+
}
|
|
2780
|
+
if (entry.title !== void 0 && (typeof entry.title !== "string" || entry.title.trim().length === 0)) {
|
|
2781
|
+
throw new Error("A protocol entry 'title' must be a non-empty string when present.");
|
|
2782
|
+
}
|
|
2783
|
+
const expanded = expandTilde2(entry.source.trim());
|
|
2784
|
+
if (!isAbsolute2(expanded)) {
|
|
2785
|
+
throw new Error("Protocol 'source' paths must be absolute (or start with '~').");
|
|
2786
|
+
}
|
|
2787
|
+
const abs = resolve4(expanded);
|
|
2788
|
+
if (seen.has(abs)) {
|
|
2789
|
+
throw new Error("Duplicate protocol source (each source path may appear only once).");
|
|
2790
|
+
}
|
|
2791
|
+
seen.add(abs);
|
|
2792
|
+
result.push(
|
|
2793
|
+
entry.title !== void 0 ? { source: abs, title: entry.title.trim() } : { source: abs }
|
|
2794
|
+
);
|
|
2795
|
+
}
|
|
2796
|
+
if (result.length === 0) {
|
|
2797
|
+
throw new Error("~/.basou/protocols.yaml has no protocols.");
|
|
2798
|
+
}
|
|
2799
|
+
return result;
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2717
2802
|
// src/commands/orient.ts
|
|
2718
2803
|
import {
|
|
2719
2804
|
assertBasouRootSafe as assertBasouRootSafe7,
|
|
@@ -2724,22 +2809,22 @@ import {
|
|
|
2724
2809
|
} from "@basou/core";
|
|
2725
2810
|
|
|
2726
2811
|
// src/lib/hosts-config.ts
|
|
2727
|
-
import { homedir as
|
|
2728
|
-
import { isAbsolute as
|
|
2729
|
-
import { readYamlFile as
|
|
2730
|
-
var DEFAULT_HOSTS_CONFIG_PATH =
|
|
2731
|
-
function
|
|
2732
|
-
if (p === "~") return
|
|
2733
|
-
if (p.startsWith("~/")) return
|
|
2812
|
+
import { homedir as homedir6 } from "os";
|
|
2813
|
+
import { isAbsolute as isAbsolute3, join as join8, resolve as resolve5 } from "path";
|
|
2814
|
+
import { readYamlFile as readYamlFile5 } from "@basou/core";
|
|
2815
|
+
var DEFAULT_HOSTS_CONFIG_PATH = join8(homedir6(), ".basou", "hosts.yaml");
|
|
2816
|
+
function expandTilde3(p) {
|
|
2817
|
+
if (p === "~") return homedir6();
|
|
2818
|
+
if (p.startsWith("~/")) return join8(homedir6(), p.slice(2));
|
|
2734
2819
|
return p;
|
|
2735
2820
|
}
|
|
2736
|
-
function
|
|
2821
|
+
function isRecord3(value) {
|
|
2737
2822
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2738
2823
|
}
|
|
2739
2824
|
async function loadHostsConfig(configPath = DEFAULT_HOSTS_CONFIG_PATH) {
|
|
2740
2825
|
let raw;
|
|
2741
2826
|
try {
|
|
2742
|
-
raw = await
|
|
2827
|
+
raw = await readYamlFile5(configPath);
|
|
2743
2828
|
} catch (error) {
|
|
2744
2829
|
if (error instanceof Error && error.message === "YAML file not found") {
|
|
2745
2830
|
return null;
|
|
@@ -2749,25 +2834,25 @@ async function loadHostsConfig(configPath = DEFAULT_HOSTS_CONFIG_PATH) {
|
|
|
2749
2834
|
}
|
|
2750
2835
|
throw error;
|
|
2751
2836
|
}
|
|
2752
|
-
if (!
|
|
2837
|
+
if (!isRecord3(raw) || !Array.isArray(raw.hosts)) {
|
|
2753
2838
|
throw new Error("~/.basou/hosts.yaml must contain a 'hosts:' list.");
|
|
2754
2839
|
}
|
|
2755
2840
|
const seenPaths = /* @__PURE__ */ new Set();
|
|
2756
2841
|
const seenLabels = /* @__PURE__ */ new Set();
|
|
2757
2842
|
const result = [];
|
|
2758
2843
|
for (const entry of raw.hosts) {
|
|
2759
|
-
if (!
|
|
2844
|
+
if (!isRecord3(entry) || typeof entry.label !== "string" || entry.label.trim().length === 0) {
|
|
2760
2845
|
throw new Error("Each host needs a non-empty string 'label'.");
|
|
2761
2846
|
}
|
|
2762
2847
|
const label = entry.label.trim();
|
|
2763
2848
|
if (typeof entry.path !== "string" || entry.path.trim().length === 0) {
|
|
2764
2849
|
throw new Error("Each host needs a non-empty string 'path'.");
|
|
2765
2850
|
}
|
|
2766
|
-
const expanded =
|
|
2767
|
-
if (!
|
|
2851
|
+
const expanded = expandTilde3(entry.path.trim());
|
|
2852
|
+
if (!isAbsolute3(expanded)) {
|
|
2768
2853
|
throw new Error("Host paths must be absolute (or start with '~').");
|
|
2769
2854
|
}
|
|
2770
|
-
const abs =
|
|
2855
|
+
const abs = resolve5(expanded);
|
|
2771
2856
|
if (seenPaths.has(abs)) continue;
|
|
2772
2857
|
if (seenLabels.has(label)) {
|
|
2773
2858
|
throw new Error(`Duplicate host label '${label}'; each host needs a distinct label.`);
|
|
@@ -2792,8 +2877,8 @@ import {
|
|
|
2792
2877
|
// src/commands/import.ts
|
|
2793
2878
|
import { createReadStream } from "fs";
|
|
2794
2879
|
import { readdir, readFile as readFile2, rm, stat as stat3 } from "fs/promises";
|
|
2795
|
-
import { homedir as
|
|
2796
|
-
import { basename as basename4, dirname as dirname3, join as
|
|
2880
|
+
import { homedir as homedir7 } from "os";
|
|
2881
|
+
import { basename as basename4, dirname as dirname3, join as join9, resolve as resolve6 } from "path";
|
|
2797
2882
|
import { createInterface } from "readline";
|
|
2798
2883
|
import {
|
|
2799
2884
|
AGENT_INFRA_DIRS as AGENT_INFRA_DIRS2,
|
|
@@ -2864,10 +2949,10 @@ function resolveSourceRoots(args) {
|
|
|
2864
2949
|
const { projectFlags, manifest, repoRoot, cwd } = args;
|
|
2865
2950
|
let resolved;
|
|
2866
2951
|
if (projectFlags.length > 0) {
|
|
2867
|
-
resolved = projectFlags.map((p) =>
|
|
2952
|
+
resolved = projectFlags.map((p) => resolve6(cwd, p));
|
|
2868
2953
|
} else {
|
|
2869
2954
|
const roots = manifest.import?.source_roots;
|
|
2870
|
-
resolved = roots !== void 0 && roots.length > 0 ? roots.map((r) =>
|
|
2955
|
+
resolved = roots !== void 0 && roots.length > 0 ? roots.map((r) => resolve6(repoRoot, r)) : [repoRoot];
|
|
2871
2956
|
}
|
|
2872
2957
|
return [...new Set(resolved)];
|
|
2873
2958
|
}
|
|
@@ -2880,7 +2965,7 @@ async function doRunImportClaudeCode(options, ctx) {
|
|
|
2880
2965
|
repoRoot: repositoryRoot,
|
|
2881
2966
|
cwd: ctx.cwd ?? process.cwd()
|
|
2882
2967
|
});
|
|
2883
|
-
const projectsRoot = ctx.claudeProjectsDir ??
|
|
2968
|
+
const projectsRoot = ctx.claudeProjectsDir ?? join9(homedir7(), ".claude", "projects");
|
|
2884
2969
|
const files = await selectTranscriptFiles(projectsRoot, projectPaths, options);
|
|
2885
2970
|
const projectSet = new Set(projectPaths);
|
|
2886
2971
|
const candidates = files.map((file) => {
|
|
@@ -2919,7 +3004,7 @@ async function doRunImportCodex(options, ctx) {
|
|
|
2919
3004
|
repoRoot: repositoryRoot,
|
|
2920
3005
|
cwd: ctx.cwd ?? process.cwd()
|
|
2921
3006
|
});
|
|
2922
|
-
const sessionsRoot = ctx.codexSessionsDir ??
|
|
3007
|
+
const sessionsRoot = ctx.codexSessionsDir ?? join9(homedir7(), ".codex", "sessions");
|
|
2923
3008
|
const rollouts = await discoverCodexRollouts(sessionsRoot, projectPaths, options);
|
|
2924
3009
|
const candidates = rollouts.map(({ file, externalId }) => ({
|
|
2925
3010
|
externalId,
|
|
@@ -3050,7 +3135,7 @@ async function importDerivedSessions(paths, manifest, options, sourceKind, candi
|
|
|
3050
3135
|
if (priors.length > 0 && options.force === true) {
|
|
3051
3136
|
if (options.dryRun !== true) {
|
|
3052
3137
|
for (const { sessionId } of priors) {
|
|
3053
|
-
await rm(
|
|
3138
|
+
await rm(join9(paths.sessions, sessionId), { recursive: true, force: true });
|
|
3054
3139
|
}
|
|
3055
3140
|
}
|
|
3056
3141
|
counts.replaced++;
|
|
@@ -3161,7 +3246,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
|
3161
3246
|
if (options.session !== void 0) {
|
|
3162
3247
|
const matches = [];
|
|
3163
3248
|
for (const projectPath of projectPaths) {
|
|
3164
|
-
const file =
|
|
3249
|
+
const file = join9(projectsRoot, encodeProjectDir(projectPath), `${options.session}.jsonl`);
|
|
3165
3250
|
if (await pathExists(file)) matches.push(file);
|
|
3166
3251
|
}
|
|
3167
3252
|
if (matches.length === 0) {
|
|
@@ -3172,7 +3257,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
|
3172
3257
|
const files = [];
|
|
3173
3258
|
let anyDirFound = false;
|
|
3174
3259
|
for (const projectPath of projectPaths) {
|
|
3175
|
-
const transcriptDir =
|
|
3260
|
+
const transcriptDir = join9(projectsRoot, encodeProjectDir(projectPath));
|
|
3176
3261
|
let entries;
|
|
3177
3262
|
try {
|
|
3178
3263
|
entries = await readdir(transcriptDir);
|
|
@@ -3182,7 +3267,7 @@ async function selectTranscriptFiles(projectsRoot, projectPaths, options) {
|
|
|
3182
3267
|
}
|
|
3183
3268
|
anyDirFound = true;
|
|
3184
3269
|
for (const name of entries) {
|
|
3185
|
-
if (name.endsWith(".jsonl")) files.push(
|
|
3270
|
+
if (name.endsWith(".jsonl")) files.push(join9(transcriptDir, name));
|
|
3186
3271
|
}
|
|
3187
3272
|
}
|
|
3188
3273
|
if (!anyDirFound) {
|
|
@@ -3239,7 +3324,7 @@ async function findRolloutFiles(sessionsRoot) {
|
|
|
3239
3324
|
throw new Error("Failed to read Codex sessions directory", { cause: error });
|
|
3240
3325
|
}
|
|
3241
3326
|
for (const entry of entries) {
|
|
3242
|
-
const full =
|
|
3327
|
+
const full = join9(dir, entry.name);
|
|
3243
3328
|
if (entry.isDirectory()) {
|
|
3244
3329
|
await walk(full, false);
|
|
3245
3330
|
} else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) {
|
|
@@ -3696,6 +3781,8 @@ async function assertWorkspaceInitialized6(basouRoot) {
|
|
|
3696
3781
|
|
|
3697
3782
|
// src/commands/hook.ts
|
|
3698
3783
|
var MAX_TRANSCRIPT_BYTES = 8 * 1024 * 1024;
|
|
3784
|
+
var MAX_TRANSCRIPT_HEAD_BYTES = 256 * 1024;
|
|
3785
|
+
var TOKEN_SCAN_CHUNK_BYTES = 1024 * 1024;
|
|
3699
3786
|
var execFileAsync = promisify(execFile);
|
|
3700
3787
|
function registerHookCommand(program2) {
|
|
3701
3788
|
const hook = program2.command("hook").description(
|
|
@@ -3707,13 +3794,13 @@ function registerHookCommand(program2) {
|
|
|
3707
3794
|
await runHookSessionStart();
|
|
3708
3795
|
});
|
|
3709
3796
|
hook.command("stop").description(
|
|
3710
|
-
"Stop-hook: when a substantive session recorded no decisions or next step, emit a non-blocking nudge to capture them. Reads the Stop hook JSON payload on stdin; never blocks and never fails the session."
|
|
3797
|
+
"Stop-hook: when a substantive session recorded no decisions or next step, emit a non-blocking nudge to capture them. Also hands a running session the standing protocols when they changed after it started \u2014 the copy it read at start is stale, and only this hook reaches a session still running. Reads the Stop hook JSON payload on stdin; never blocks and never fails the session."
|
|
3711
3798
|
).option(
|
|
3712
3799
|
"--min-edits <n>",
|
|
3713
3800
|
`Minimum file edits before nudging on edits alone (default ${DEFAULT_STOP_HOOK_MIN_EDITS})`
|
|
3714
3801
|
).option(
|
|
3715
3802
|
"--block",
|
|
3716
|
-
"Opt-in enforcement: hold the agent in-turn (decision:block) instead of a non-blocking
|
|
3803
|
+
"Opt-in enforcement: hold the agent in-turn (decision:block) instead of a non-blocking message"
|
|
3717
3804
|
).option(
|
|
3718
3805
|
"--require-review",
|
|
3719
3806
|
"Opt-in review gate: also remind when a session shipped substantive code (push / PR / merge) without recording a review"
|
|
@@ -3826,12 +3913,25 @@ gh pr create|merge) without recording a review ('basou review record'). This
|
|
|
3826
3913
|
gate is off by default; when on, its reminder is composed into the same
|
|
3827
3914
|
envelope as the capture reminder.
|
|
3828
3915
|
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3916
|
+
It also hands a RUNNING session the standing protocols when they changed after
|
|
3917
|
+
that session started. The protocol block in ~/.claude/CLAUDE.md is read at
|
|
3918
|
+
session start, so an update made mid-session never reaches the session it was
|
|
3919
|
+
meant to correct; this is the only channel that does. The complete current set
|
|
3920
|
+
is delivered, not a diff, so what it supersedes -- including a protocol that is
|
|
3921
|
+
no longer there -- is unambiguous. It reads the rendered block and nothing else,
|
|
3922
|
+
so an edit not yet published by 'basou protocol sync' cannot reach a session,
|
|
3923
|
+
and it delivers once per block state: a second update in the same session still
|
|
3924
|
+
lands, the same one twice does not. This part is always on, needs no flag, and
|
|
3925
|
+
says nothing at all unless the block actually changed.
|
|
3926
|
+
|
|
3927
|
+
By default every message here is non-blocking: Claude sees it and may act on it
|
|
3928
|
+
or stop. With --block (opt-in enforcement, 'basou hook install --block') it
|
|
3929
|
+
instead returns decision:block, holding the agent in-turn; the 'stop_hook_active'
|
|
3930
|
+
flag and Claude Code's own loop prevention bound it to a single turn. Note that
|
|
3931
|
+
this covers the protocol delivery too, which carries no action to take -- with
|
|
3932
|
+
--block the turn is held so the new text lands before more work is done on the
|
|
3933
|
+
old. Either way the hook fails open: a bad payload or unreadable transcript
|
|
3934
|
+
exits cleanly with no output.
|
|
3835
3935
|
`;
|
|
3836
3936
|
async function runHookStop(options, ctx = {}) {
|
|
3837
3937
|
try {
|
|
@@ -3869,7 +3969,12 @@ async function doRunHookStop(options, ctx) {
|
|
|
3869
3969
|
stopHookActive: false,
|
|
3870
3970
|
...options.minEdits !== void 0 ? { minEdits: options.minEdits } : {}
|
|
3871
3971
|
});
|
|
3972
|
+
const protocolUpdate = await evaluateProtocolUpdateGate({
|
|
3973
|
+
transcriptPath,
|
|
3974
|
+
target: ctx.protocolTargetPath ?? DEFAULT_TARGET_PATH
|
|
3975
|
+
});
|
|
3872
3976
|
const parts = [];
|
|
3977
|
+
if (protocolUpdate !== null) parts.push(protocolUpdate);
|
|
3873
3978
|
if (evaluation.kind === "nudge") parts.push(evaluation.additionalContext);
|
|
3874
3979
|
if (options.requireReview === true && evaluation.review.fires) {
|
|
3875
3980
|
parts.push(evaluation.review.additionalContext);
|
|
@@ -3885,6 +3990,56 @@ async function doRunHookStop(options, ctx) {
|
|
|
3885
3990
|
write(`${payloadJson}
|
|
3886
3991
|
`);
|
|
3887
3992
|
}
|
|
3993
|
+
async function evaluateProtocolUpdateGate(input) {
|
|
3994
|
+
try {
|
|
3995
|
+
const head = await readTranscriptHead(input.transcriptPath);
|
|
3996
|
+
const sessionStartedAt = transcriptStartedAt(parseTranscript(head));
|
|
3997
|
+
if (sessionStartedAt === void 0) return null;
|
|
3998
|
+
const touchedAt = await targetModifiedAt(input.target);
|
|
3999
|
+
if (touchedAt !== null && touchedAt <= Date.parse(sessionStartedAt)) return null;
|
|
4000
|
+
const existing = await readMarkdownFile6(input.target);
|
|
4001
|
+
if (existing === null) return null;
|
|
4002
|
+
const section = parseMarkers2(existing, { start: PROTOCOL_START, end: PROTOCOL_END });
|
|
4003
|
+
if (section.kind !== "ok") return null;
|
|
4004
|
+
const stamp = parseProtocolStamp(section.generated);
|
|
4005
|
+
if (stamp === null) return null;
|
|
4006
|
+
if (!isProtocolUpdateDue({ stamp, sessionStartedAt })) return null;
|
|
4007
|
+
const sections = protocolSectionsFrom(section.generated);
|
|
4008
|
+
if (sections === null || sections.trim().length === 0) return null;
|
|
4009
|
+
if (await transcriptCarries(input.transcriptPath, protocolUpdateToken(stamp.contentHash))) {
|
|
4010
|
+
return null;
|
|
4011
|
+
}
|
|
4012
|
+
return renderProtocolUpdate(sections, stamp);
|
|
4013
|
+
} catch {
|
|
4014
|
+
return null;
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
async function targetModifiedAt(target) {
|
|
4018
|
+
try {
|
|
4019
|
+
return (await stat4(target)).mtimeMs;
|
|
4020
|
+
} catch {
|
|
4021
|
+
return null;
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
async function transcriptCarries(path, token) {
|
|
4025
|
+
const handle = await open2(path, "r");
|
|
4026
|
+
try {
|
|
4027
|
+
const overlap = Math.max(token.length - 1, 0);
|
|
4028
|
+
const chunk = Buffer.alloc(TOKEN_SCAN_CHUNK_BYTES);
|
|
4029
|
+
let carry = "";
|
|
4030
|
+
let position = 0;
|
|
4031
|
+
for (; ; ) {
|
|
4032
|
+
const { bytesRead } = await handle.read(chunk, 0, TOKEN_SCAN_CHUNK_BYTES, position);
|
|
4033
|
+
if (bytesRead === 0) return false;
|
|
4034
|
+
position += bytesRead;
|
|
4035
|
+
const text = carry + chunk.subarray(0, bytesRead).toString("utf8");
|
|
4036
|
+
if (text.includes(token)) return true;
|
|
4037
|
+
carry = overlap > 0 ? text.slice(-overlap) : "";
|
|
4038
|
+
}
|
|
4039
|
+
} finally {
|
|
4040
|
+
await handle.close();
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
3888
4043
|
async function renderRegisteredWorkspacePosition(cwd, portfolioConfigPath = DEFAULT_PORTFOLIO_CONFIG_PATH) {
|
|
3889
4044
|
const root = await resolveBasouRootForCommand(cwd, "hook session-start", {
|
|
3890
4045
|
portfolioConfigPath
|
|
@@ -3984,11 +4139,25 @@ async function readTranscriptBounded(path, maxBytes = MAX_TRANSCRIPT_BYTES) {
|
|
|
3984
4139
|
await handle.close();
|
|
3985
4140
|
}
|
|
3986
4141
|
}
|
|
4142
|
+
async function readTranscriptHead(path, maxBytes = MAX_TRANSCRIPT_HEAD_BYTES) {
|
|
4143
|
+
const { size } = await stat4(path);
|
|
4144
|
+
if (size <= maxBytes) return readFile3(path, "utf8");
|
|
4145
|
+
const handle = await open2(path, "r");
|
|
4146
|
+
try {
|
|
4147
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
4148
|
+
const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
|
|
4149
|
+
const text = buffer.subarray(0, bytesRead).toString("utf8");
|
|
4150
|
+
const lastNewline = text.lastIndexOf("\n");
|
|
4151
|
+
return lastNewline >= 0 ? text.slice(0, lastNewline + 1) : text;
|
|
4152
|
+
} finally {
|
|
4153
|
+
await handle.close();
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
3987
4156
|
function parseMinEdits(raw) {
|
|
3988
4157
|
if (raw === void 0 || !/^\d+$/.test(raw)) return void 0;
|
|
3989
4158
|
return Number(raw);
|
|
3990
4159
|
}
|
|
3991
|
-
var DEFAULT_CLAUDE_SETTINGS_PATH =
|
|
4160
|
+
var DEFAULT_CLAUDE_SETTINGS_PATH = join10(homedir8(), ".claude", "settings.json");
|
|
3992
4161
|
function resolveCliEntry() {
|
|
3993
4162
|
return fileURLToPath(import.meta.url);
|
|
3994
4163
|
}
|
|
@@ -4236,7 +4405,7 @@ function describeHookMode(tiers) {
|
|
|
4236
4405
|
const gates = tiers.review ? "capture + review" : "capture";
|
|
4237
4406
|
return `${enforcement}, ${gates}`;
|
|
4238
4407
|
}
|
|
4239
|
-
var DEFAULT_CODEX_FACE_PATH =
|
|
4408
|
+
var DEFAULT_CODEX_FACE_PATH = join10(homedir8(), ".codex", "AGENTS.md");
|
|
4240
4409
|
var LEFTOVER_FACE_NOTE = (label) => `${label} still carries an orientation block rendered by an earlier basou (0.39 or before); every Codex session on this machine reads it. \`basou channel clear codex\` removes it.`;
|
|
4241
4410
|
async function faceHasLeftoverOrientationBlock(facePath) {
|
|
4242
4411
|
try {
|
|
@@ -4248,8 +4417,8 @@ async function faceHasLeftoverOrientationBlock(facePath) {
|
|
|
4248
4417
|
return false;
|
|
4249
4418
|
}
|
|
4250
4419
|
}
|
|
4251
|
-
var DEFAULT_CODEX_HOOKS_PATH =
|
|
4252
|
-
var DEFAULT_CODEX_CONFIG_PATH =
|
|
4420
|
+
var DEFAULT_CODEX_HOOKS_PATH = join10(homedir8(), ".codex", "hooks.json");
|
|
4421
|
+
var DEFAULT_CODEX_CONFIG_PATH = join10(homedir8(), ".codex", "config.toml");
|
|
4253
4422
|
async function readHooksFile(path) {
|
|
4254
4423
|
let raw;
|
|
4255
4424
|
try {
|
|
@@ -4421,7 +4590,7 @@ async function codexHookTrustFor(hooksPath, location, configPath) {
|
|
|
4421
4590
|
}
|
|
4422
4591
|
|
|
4423
4592
|
// src/commands/init.ts
|
|
4424
|
-
import { basename as basename5, relative, resolve as
|
|
4593
|
+
import { basename as basename5, relative, resolve as resolve7 } from "path";
|
|
4425
4594
|
import {
|
|
4426
4595
|
appendBasouGitignore,
|
|
4427
4596
|
createManifest,
|
|
@@ -4466,7 +4635,7 @@ async function doRunInit(options, ctx) {
|
|
|
4466
4635
|
);
|
|
4467
4636
|
}
|
|
4468
4637
|
const sourceRoots = (options.sourceRoot ?? []).map((p) => {
|
|
4469
|
-
const rel = relative(repositoryRoot,
|
|
4638
|
+
const rel = relative(repositoryRoot, resolve7(cwd, p));
|
|
4470
4639
|
return rel === "" ? "." : rel;
|
|
4471
4640
|
});
|
|
4472
4641
|
const paths = await ensureBasouDirectory(repositoryRoot);
|
|
@@ -4676,7 +4845,7 @@ async function assertWorkspaceInitialized7(basouRoot) {
|
|
|
4676
4845
|
|
|
4677
4846
|
// src/commands/portfolio.ts
|
|
4678
4847
|
import { existsSync, statSync } from "fs";
|
|
4679
|
-
import { join as
|
|
4848
|
+
import { join as join11 } from "path";
|
|
4680
4849
|
function registerPortfolioCommand(program2) {
|
|
4681
4850
|
program2.command("portfolio").description(
|
|
4682
4851
|
"List the workspaces you orient across (read-only): every planning master registered in ~/.basou/portfolio.yaml, with its path and whether it exists / is initialized. The headless text/JSON counterpart to the `basou view --portfolio` GUI \u2014 for discovering where a sibling project lives without opening a browser"
|
|
@@ -4722,7 +4891,7 @@ async function runPortfolioList(options, ctx = {}) {
|
|
|
4722
4891
|
async function doRunPortfolioList(options, ctx) {
|
|
4723
4892
|
const configPath = ctx.configPath ?? DEFAULT_PORTFOLIO_CONFIG_PATH;
|
|
4724
4893
|
const pathExists2 = ctx.pathExists ?? ((p) => existsSync(p));
|
|
4725
|
-
const isInitialized = ctx.isInitialized ?? ((p) => isDirectory(
|
|
4894
|
+
const isInitialized = ctx.isInitialized ?? ((p) => isDirectory(join11(p, ".basou")));
|
|
4726
4895
|
const workspaces = await loadPortfolioConfig(configPath);
|
|
4727
4896
|
const result = {
|
|
4728
4897
|
configPath,
|
|
@@ -4787,7 +4956,7 @@ import {
|
|
|
4787
4956
|
writeFileSync,
|
|
4788
4957
|
writeSync
|
|
4789
4958
|
} from "fs";
|
|
4790
|
-
import { basename as basename6, dirname as dirname4, isAbsolute as
|
|
4959
|
+
import { basename as basename6, dirname as dirname4, isAbsolute as isAbsolute4, join as join12, relative as relative2, resolve as resolve8 } from "path";
|
|
4791
4960
|
import {
|
|
4792
4961
|
appendBasouGitignore as appendBasouGitignore2,
|
|
4793
4962
|
basouPaths as basouPaths11,
|
|
@@ -5162,14 +5331,14 @@ async function runProjectAdopt(options, ctx = {}) {
|
|
|
5162
5331
|
}
|
|
5163
5332
|
}
|
|
5164
5333
|
function classifySourceRoot(repositoryRoot, declaredPath) {
|
|
5165
|
-
const absolute =
|
|
5334
|
+
const absolute = resolve8(repositoryRoot, declaredPath);
|
|
5166
5335
|
let real;
|
|
5167
5336
|
try {
|
|
5168
5337
|
real = realpathSync(absolute);
|
|
5169
5338
|
} catch {
|
|
5170
5339
|
return { path: declaredPath, kind: "unresolved" };
|
|
5171
5340
|
}
|
|
5172
|
-
return { path: declaredPath, kind: existsSync2(
|
|
5341
|
+
return { path: declaredPath, kind: existsSync2(join12(real, ".git")) ? "repo" : "non-repo" };
|
|
5173
5342
|
}
|
|
5174
5343
|
async function doRunProjectAdopt(options, ctx) {
|
|
5175
5344
|
const cwd = ctx.cwd ?? process.cwd();
|
|
@@ -5269,11 +5438,11 @@ async function gatherRepoWiring(repositoryRoot, entry) {
|
|
|
5269
5438
|
};
|
|
5270
5439
|
let real;
|
|
5271
5440
|
try {
|
|
5272
|
-
real = realpathSync(
|
|
5441
|
+
real = realpathSync(resolve8(repositoryRoot, entry.path));
|
|
5273
5442
|
} catch {
|
|
5274
5443
|
return { ...base, reachable: false, instructionFiles: [] };
|
|
5275
5444
|
}
|
|
5276
|
-
if (!existsSync2(
|
|
5445
|
+
if (!existsSync2(join12(real, ".git"))) {
|
|
5277
5446
|
return { ...base, reachable: false, instructionFiles: [] };
|
|
5278
5447
|
}
|
|
5279
5448
|
try {
|
|
@@ -5281,7 +5450,7 @@ async function gatherRepoWiring(repositoryRoot, entry) {
|
|
|
5281
5450
|
for (const name of INSTRUCTION_FILES) {
|
|
5282
5451
|
let present = true;
|
|
5283
5452
|
try {
|
|
5284
|
-
lstatSync(
|
|
5453
|
+
lstatSync(join12(real, name));
|
|
5285
5454
|
} catch {
|
|
5286
5455
|
present = false;
|
|
5287
5456
|
}
|
|
@@ -5388,14 +5557,14 @@ function gatherRepoGitignore(repositoryRoot, entry) {
|
|
|
5388
5557
|
};
|
|
5389
5558
|
let real;
|
|
5390
5559
|
try {
|
|
5391
|
-
real = realpathSync(
|
|
5560
|
+
real = realpathSync(resolve8(repositoryRoot, entry.path));
|
|
5392
5561
|
} catch {
|
|
5393
5562
|
return { ...base, reachable: false, currentLines: [] };
|
|
5394
5563
|
}
|
|
5395
|
-
if (!existsSync2(
|
|
5564
|
+
if (!existsSync2(join12(real, ".git"))) {
|
|
5396
5565
|
return { ...base, reachable: false, currentLines: [] };
|
|
5397
5566
|
}
|
|
5398
|
-
return { ...base, reachable: true, currentLines: readGitignoreLines(
|
|
5567
|
+
return { ...base, reachable: true, currentLines: readGitignoreLines(join12(real, ".gitignore")) };
|
|
5399
5568
|
}
|
|
5400
5569
|
function hasErrorCode(error) {
|
|
5401
5570
|
return error instanceof Error && typeof error.code === "string";
|
|
@@ -5409,7 +5578,7 @@ function readGitignoreLines(file) {
|
|
|
5409
5578
|
}
|
|
5410
5579
|
}
|
|
5411
5580
|
function applyGitignorePlan(repositoryRoot, plan) {
|
|
5412
|
-
const file =
|
|
5581
|
+
const file = join12(realpathSync(resolve8(repositoryRoot, plan.path)), ".gitignore");
|
|
5413
5582
|
let existing = "";
|
|
5414
5583
|
try {
|
|
5415
5584
|
existing = readFileSync(file, "utf8");
|
|
@@ -5544,12 +5713,12 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
5544
5713
|
const base = { path: entry.path, ...isSelf ? { self: true } : {} };
|
|
5545
5714
|
let real;
|
|
5546
5715
|
try {
|
|
5547
|
-
real = realpathSync(
|
|
5716
|
+
real = realpathSync(resolve8(repositoryRoot, entry.path));
|
|
5548
5717
|
} catch {
|
|
5549
5718
|
return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
|
|
5550
5719
|
}
|
|
5551
5720
|
if (real === anchorReal) {
|
|
5552
|
-
const anchorCanonical =
|
|
5721
|
+
const anchorCanonical = join12(real, CANONICAL_FILE);
|
|
5553
5722
|
const anchorState = anchorCanonicalState(anchorCanonical);
|
|
5554
5723
|
if (anchorState === "absent") {
|
|
5555
5724
|
return { ...base, isAnchor: true, reachable: true, canonicalPresent: false, files: [] };
|
|
@@ -5569,7 +5738,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
5569
5738
|
anchorCanonical,
|
|
5570
5739
|
"self"
|
|
5571
5740
|
).map((spec) => {
|
|
5572
|
-
const { state, actualTarget } = inspectSymlink(
|
|
5741
|
+
const { state, actualTarget } = inspectSymlink(join12(real, spec.name), spec.target);
|
|
5573
5742
|
return {
|
|
5574
5743
|
name: spec.name,
|
|
5575
5744
|
expectedTarget: spec.target,
|
|
@@ -5586,16 +5755,16 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
5586
5755
|
files: anchorFiles
|
|
5587
5756
|
};
|
|
5588
5757
|
}
|
|
5589
|
-
if (!existsSync2(
|
|
5758
|
+
if (!existsSync2(join12(real, ".git"))) {
|
|
5590
5759
|
return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
|
|
5591
5760
|
}
|
|
5592
|
-
const canonicalFile = isSelf ?
|
|
5761
|
+
const canonicalFile = isSelf ? join12(real, CANONICAL_FILE) : join12(anchorReal, "agents", basename6(real), CANONICAL_FILE);
|
|
5593
5762
|
if (!existsSync2(canonicalFile)) {
|
|
5594
5763
|
return { ...base, isAnchor: false, reachable: true, canonicalPresent: false, files: [] };
|
|
5595
5764
|
}
|
|
5596
5765
|
const files = expectedSymlinkTargets(real, canonicalFile, mode).map(
|
|
5597
5766
|
(spec) => {
|
|
5598
|
-
const { state, actualTarget } = inspectSymlink(
|
|
5767
|
+
const { state, actualTarget } = inspectSymlink(join12(real, spec.name), spec.target);
|
|
5599
5768
|
return {
|
|
5600
5769
|
name: spec.name,
|
|
5601
5770
|
expectedTarget: spec.target,
|
|
@@ -5616,7 +5785,7 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
|
|
|
5616
5785
|
function applySymlinkPlan(repositoryRoot, plan) {
|
|
5617
5786
|
let real;
|
|
5618
5787
|
try {
|
|
5619
|
-
real = realpathSync(
|
|
5788
|
+
real = realpathSync(resolve8(repositoryRoot, plan.path));
|
|
5620
5789
|
} catch (error) {
|
|
5621
5790
|
const message = failureReason(error);
|
|
5622
5791
|
return { created: [], failed: plan.toCreate.map((c) => ({ file: c.name, message })) };
|
|
@@ -5624,7 +5793,7 @@ function applySymlinkPlan(repositoryRoot, plan) {
|
|
|
5624
5793
|
const created = [];
|
|
5625
5794
|
const failed = [];
|
|
5626
5795
|
for (const { name, target } of plan.toCreate) {
|
|
5627
|
-
const filePath =
|
|
5796
|
+
const filePath = join12(real, name);
|
|
5628
5797
|
try {
|
|
5629
5798
|
mkdirSync(dirname4(filePath), { recursive: true });
|
|
5630
5799
|
symlinkSync(target, filePath);
|
|
@@ -5642,9 +5811,9 @@ function viewCanonicalCollision(repositoryRoot, roster, viewName) {
|
|
|
5642
5811
|
for (const entry of roster) {
|
|
5643
5812
|
let name;
|
|
5644
5813
|
try {
|
|
5645
|
-
name = basename6(realpathSync(
|
|
5814
|
+
name = basename6(realpathSync(resolve8(repositoryRoot, entry.path)));
|
|
5646
5815
|
} catch {
|
|
5647
|
-
name = basename6(
|
|
5816
|
+
name = basename6(resolve8(repositoryRoot, entry.path));
|
|
5648
5817
|
}
|
|
5649
5818
|
if (name === viewName) return entry.path;
|
|
5650
5819
|
}
|
|
@@ -5658,7 +5827,7 @@ function gatherViewSymlinks(repositoryRoot, anchorReal, roster, viewDir) {
|
|
|
5658
5827
|
if (!existsSync2(canonicalFile)) return { kind: "missing-canonical", viewName };
|
|
5659
5828
|
const files = expectedSymlinkTargets(viewDir, canonicalFile, "hub").map(
|
|
5660
5829
|
(spec) => {
|
|
5661
|
-
const { state, actualTarget } = inspectSymlink(
|
|
5830
|
+
const { state, actualTarget } = inspectSymlink(join12(viewDir, spec.name), spec.target);
|
|
5662
5831
|
return {
|
|
5663
5832
|
name: spec.name,
|
|
5664
5833
|
expectedTarget: spec.target,
|
|
@@ -5674,7 +5843,7 @@ function applyViewSymlinks(viewDir, files) {
|
|
|
5674
5843
|
const failed = [];
|
|
5675
5844
|
for (const f of files) {
|
|
5676
5845
|
if (f.state !== "missing") continue;
|
|
5677
|
-
const filePath =
|
|
5846
|
+
const filePath = join12(viewDir, f.name);
|
|
5678
5847
|
try {
|
|
5679
5848
|
mkdirSync(dirname4(filePath), { recursive: true });
|
|
5680
5849
|
symlinkSync(f.expectedTarget, filePath);
|
|
@@ -5891,12 +6060,12 @@ async function runProjectWorkspace(options, ctx = {}) {
|
|
|
5891
6060
|
}
|
|
5892
6061
|
}
|
|
5893
6062
|
function resolveViewDir(repositoryRoot, viewPath) {
|
|
5894
|
-
const abs =
|
|
6063
|
+
const abs = resolve8(repositoryRoot, viewPath);
|
|
5895
6064
|
try {
|
|
5896
6065
|
return realpathSync(abs);
|
|
5897
6066
|
} catch {
|
|
5898
6067
|
try {
|
|
5899
|
-
return
|
|
6068
|
+
return join12(realpathSync(dirname4(abs)), basename6(abs));
|
|
5900
6069
|
} catch {
|
|
5901
6070
|
return abs;
|
|
5902
6071
|
}
|
|
@@ -5905,7 +6074,7 @@ function resolveViewDir(repositoryRoot, viewPath) {
|
|
|
5905
6074
|
function gatherViewRepo(repositoryRoot, viewDir, entry) {
|
|
5906
6075
|
let repoReal;
|
|
5907
6076
|
try {
|
|
5908
|
-
repoReal = realpathSync(
|
|
6077
|
+
repoReal = realpathSync(resolve8(repositoryRoot, entry.path));
|
|
5909
6078
|
} catch {
|
|
5910
6079
|
return { path: entry.path, reachable: false };
|
|
5911
6080
|
}
|
|
@@ -5914,7 +6083,7 @@ function gatherViewRepo(repositoryRoot, viewDir, entry) {
|
|
|
5914
6083
|
return { path: entry.path, reachable: false };
|
|
5915
6084
|
}
|
|
5916
6085
|
const linkName = basename6(repoReal);
|
|
5917
|
-
const { state, actualTarget } = inspectSymlink(
|
|
6086
|
+
const { state, actualTarget } = inspectSymlink(join12(viewDir, linkName), expectedTarget);
|
|
5918
6087
|
return {
|
|
5919
6088
|
path: entry.path,
|
|
5920
6089
|
reachable: true,
|
|
@@ -5928,7 +6097,7 @@ function applyViewPlan(viewDir, toCreate) {
|
|
|
5928
6097
|
const created = [];
|
|
5929
6098
|
const failed = [];
|
|
5930
6099
|
for (const { name, target } of toCreate) {
|
|
5931
|
-
const filePath =
|
|
6100
|
+
const filePath = join12(viewDir, name);
|
|
5932
6101
|
try {
|
|
5933
6102
|
mkdirSync(dirname4(filePath), { recursive: true });
|
|
5934
6103
|
symlinkSync(target, filePath);
|
|
@@ -5943,7 +6112,7 @@ var TOP_LEVEL_INSTRUCTION_FILES_LOWER = new Set(
|
|
|
5943
6112
|
INSTRUCTION_FILES.filter((f) => !f.includes("/")).map((f) => f.toLowerCase())
|
|
5944
6113
|
);
|
|
5945
6114
|
function classifyViewLink(viewDir, name, rosterRealpaths) {
|
|
5946
|
-
const filePath =
|
|
6115
|
+
const filePath = join12(viewDir, name);
|
|
5947
6116
|
let isLink;
|
|
5948
6117
|
try {
|
|
5949
6118
|
isLink = lstatSync(filePath).isSymbolicLink();
|
|
@@ -5957,12 +6126,12 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
|
|
|
5957
6126
|
} catch {
|
|
5958
6127
|
return null;
|
|
5959
6128
|
}
|
|
5960
|
-
const resolved =
|
|
6129
|
+
const resolved = isAbsolute4(target) ? target : resolve8(viewDir, target);
|
|
5961
6130
|
try {
|
|
5962
6131
|
if (rosterRealpaths.has(realpathSync(resolved))) return null;
|
|
5963
6132
|
} catch {
|
|
5964
6133
|
}
|
|
5965
|
-
if (
|
|
6134
|
+
if (isAbsolute4(target)) return { target, kind: "absolute" };
|
|
5966
6135
|
let isDir = false;
|
|
5967
6136
|
try {
|
|
5968
6137
|
isDir = statSync2(resolved).isDirectory();
|
|
@@ -5972,7 +6141,7 @@ function classifyViewLink(viewDir, name, rosterRealpaths) {
|
|
|
5972
6141
|
if (!isDir) {
|
|
5973
6142
|
return { target, kind: existsSync2(resolved) ? "non-repo" : "broken" };
|
|
5974
6143
|
}
|
|
5975
|
-
return { target, kind: existsSync2(
|
|
6144
|
+
return { target, kind: existsSync2(join12(resolved, ".git")) ? "repo" : "non-repo" };
|
|
5976
6145
|
}
|
|
5977
6146
|
function gatherExistingViewLinks(viewDir, rosterRealpaths) {
|
|
5978
6147
|
let names;
|
|
@@ -5997,7 +6166,7 @@ function pruneViewLinks(viewDir, toPrune, rosterRealpaths) {
|
|
|
5997
6166
|
const pruned = [];
|
|
5998
6167
|
const failed = [];
|
|
5999
6168
|
for (const { name } of toPrune) {
|
|
6000
|
-
const filePath =
|
|
6169
|
+
const filePath = join12(viewDir, name);
|
|
6001
6170
|
const c = classifyViewLink(viewDir, name, rosterRealpaths);
|
|
6002
6171
|
if (c === null || c.kind !== "repo") {
|
|
6003
6172
|
failed.push({
|
|
@@ -6043,11 +6212,11 @@ async function doRunProjectWorkspace(options, ctx) {
|
|
|
6043
6212
|
} else {
|
|
6044
6213
|
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
6045
6214
|
const facts = roster.map((entry) => gatherViewRepo(repositoryRoot, viewDir, entry));
|
|
6046
|
-
const rosterNames = roster.map((entry) => basename6(
|
|
6215
|
+
const rosterNames = roster.map((entry) => basename6(resolve8(repositoryRoot, entry.path)));
|
|
6047
6216
|
const rosterRealpaths = /* @__PURE__ */ new Set();
|
|
6048
6217
|
for (const entry of roster) {
|
|
6049
6218
|
try {
|
|
6050
|
-
rosterRealpaths.add(realpathSync(
|
|
6219
|
+
rosterRealpaths.add(realpathSync(resolve8(repositoryRoot, entry.path)));
|
|
6051
6220
|
} catch {
|
|
6052
6221
|
}
|
|
6053
6222
|
}
|
|
@@ -6210,10 +6379,10 @@ async function runProjectPreset(options, ctx = {}) {
|
|
|
6210
6379
|
}
|
|
6211
6380
|
}
|
|
6212
6381
|
function canonicalFileFor(anchorReal, canonicalName) {
|
|
6213
|
-
return
|
|
6382
|
+
return join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
6214
6383
|
}
|
|
6215
6384
|
function canonicalLabelFor(canonicalName) {
|
|
6216
|
-
return
|
|
6385
|
+
return join12("agents", canonicalName, CANONICAL_FILE);
|
|
6217
6386
|
}
|
|
6218
6387
|
async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
|
|
6219
6388
|
const declared = {
|
|
@@ -6227,14 +6396,14 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
|
|
|
6227
6396
|
}
|
|
6228
6397
|
let real;
|
|
6229
6398
|
try {
|
|
6230
|
-
real = realpathSync(
|
|
6399
|
+
real = realpathSync(resolve8(repositoryRoot, entry.path));
|
|
6231
6400
|
} catch {
|
|
6232
6401
|
return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
|
|
6233
6402
|
}
|
|
6234
6403
|
if (real === anchorReal) {
|
|
6235
6404
|
return { ...declared, isAnchor: true, reachable: true, canonicalPresent: false };
|
|
6236
6405
|
}
|
|
6237
|
-
if (!existsSync2(
|
|
6406
|
+
if (!existsSync2(join12(real, ".git"))) {
|
|
6238
6407
|
return { ...declared, isAnchor: false, reachable: false, canonicalPresent: false };
|
|
6239
6408
|
}
|
|
6240
6409
|
const canonicalName = basename6(real);
|
|
@@ -6283,7 +6452,7 @@ function viewPresetReposFor(repositoryRoot, roster) {
|
|
|
6283
6452
|
anchorReal = void 0;
|
|
6284
6453
|
}
|
|
6285
6454
|
return roster.map((entry) => {
|
|
6286
|
-
const abs =
|
|
6455
|
+
const abs = resolve8(repositoryRoot, entry.path);
|
|
6287
6456
|
let real;
|
|
6288
6457
|
try {
|
|
6289
6458
|
real = realpathSync(abs);
|
|
@@ -6607,7 +6776,7 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
|
|
|
6607
6776
|
};
|
|
6608
6777
|
let real;
|
|
6609
6778
|
try {
|
|
6610
|
-
real = realpathSync(
|
|
6779
|
+
real = realpathSync(resolve8(repositoryRoot, target));
|
|
6611
6780
|
} catch {
|
|
6612
6781
|
return empty;
|
|
6613
6782
|
}
|
|
@@ -6616,24 +6785,24 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
|
|
|
6616
6785
|
const instructionFiles = [];
|
|
6617
6786
|
for (const name of INSTRUCTION_FILES) {
|
|
6618
6787
|
try {
|
|
6619
|
-
lstatSync(
|
|
6788
|
+
lstatSync(join12(real, name));
|
|
6620
6789
|
instructionFiles.push(name);
|
|
6621
6790
|
} catch {
|
|
6622
6791
|
}
|
|
6623
6792
|
}
|
|
6624
6793
|
let ignored;
|
|
6625
6794
|
try {
|
|
6626
|
-
ignored = new Set(readGitignoreLines(
|
|
6795
|
+
ignored = new Set(readGitignoreLines(join12(real, ".gitignore")).map((l) => l.trim()));
|
|
6627
6796
|
} catch {
|
|
6628
6797
|
ignored = /* @__PURE__ */ new Set();
|
|
6629
6798
|
}
|
|
6630
6799
|
const gitignorePatterns = INSTRUCTION_FILES.filter((p) => ignored.has(p) || ignored.has(`/${p}`));
|
|
6631
|
-
const canonical2 = existsSync2(
|
|
6800
|
+
const canonical2 = existsSync2(join12(anchorReal, "agents", canonicalName, CANONICAL_FILE));
|
|
6632
6801
|
let viewLink = false;
|
|
6633
6802
|
const viewPath = manifest.workspace.view;
|
|
6634
6803
|
if (viewPath !== void 0) {
|
|
6635
6804
|
try {
|
|
6636
|
-
lstatSync(
|
|
6805
|
+
lstatSync(join12(resolveViewDir(repositoryRoot, viewPath), canonicalName));
|
|
6637
6806
|
viewLink = true;
|
|
6638
6807
|
} catch {
|
|
6639
6808
|
}
|
|
@@ -6647,27 +6816,27 @@ function gatherArchiveTeardown(repositoryRoot, manifest, target) {
|
|
|
6647
6816
|
};
|
|
6648
6817
|
}
|
|
6649
6818
|
function teardownExpectedTargets(repoReal, anchorReal, canonicalName) {
|
|
6650
|
-
const canonicalFile =
|
|
6819
|
+
const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
6651
6820
|
return expectedSymlinkTargets(repoReal, canonicalFile);
|
|
6652
6821
|
}
|
|
6653
6822
|
function viewLinkPointsAt(viewDir, name, repoReal) {
|
|
6654
|
-
const filePath =
|
|
6823
|
+
const filePath = join12(viewDir, name);
|
|
6655
6824
|
try {
|
|
6656
6825
|
if (!lstatSync(filePath).isSymbolicLink()) return false;
|
|
6657
6826
|
const target = readlinkSync(filePath);
|
|
6658
|
-
if (
|
|
6659
|
-
return realpathSync(
|
|
6827
|
+
if (isAbsolute4(target)) return false;
|
|
6828
|
+
return realpathSync(resolve8(viewDir, target)) === repoReal;
|
|
6660
6829
|
} catch {
|
|
6661
6830
|
return false;
|
|
6662
6831
|
}
|
|
6663
6832
|
}
|
|
6664
6833
|
function viewLinkPointsAtPath(viewDir, name, expectedRepoPath) {
|
|
6665
|
-
const filePath =
|
|
6834
|
+
const filePath = join12(viewDir, name);
|
|
6666
6835
|
try {
|
|
6667
6836
|
if (!lstatSync(filePath).isSymbolicLink()) return false;
|
|
6668
6837
|
const target = readlinkSync(filePath);
|
|
6669
|
-
if (
|
|
6670
|
-
return
|
|
6838
|
+
if (isAbsolute4(target)) return false;
|
|
6839
|
+
return resolve8(viewDir, target) === expectedRepoPath;
|
|
6671
6840
|
} catch {
|
|
6672
6841
|
return false;
|
|
6673
6842
|
}
|
|
@@ -6676,19 +6845,19 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
6676
6845
|
const anchorReal = realpathSync(repositoryRoot);
|
|
6677
6846
|
let repoReal;
|
|
6678
6847
|
try {
|
|
6679
|
-
repoReal = realpathSync(
|
|
6848
|
+
repoReal = realpathSync(resolve8(repositoryRoot, target));
|
|
6680
6849
|
} catch {
|
|
6681
6850
|
repoReal = void 0;
|
|
6682
6851
|
}
|
|
6683
6852
|
const isAnchor = repoReal !== void 0 && repoReal === anchorReal;
|
|
6684
|
-
const targetAbs =
|
|
6853
|
+
const targetAbs = resolve8(repositoryRoot, target);
|
|
6685
6854
|
const canonicalName = basename6(repoReal ?? targetAbs);
|
|
6686
6855
|
const roster = manifest.repos ?? [];
|
|
6687
6856
|
const declaredEntry = roster.find((r) => {
|
|
6688
6857
|
try {
|
|
6689
|
-
return realpathSync(
|
|
6858
|
+
return realpathSync(resolve8(repositoryRoot, r.path)) === (repoReal ?? "\0");
|
|
6690
6859
|
} catch {
|
|
6691
|
-
return
|
|
6860
|
+
return resolve8(repositoryRoot, r.path) === targetAbs;
|
|
6692
6861
|
}
|
|
6693
6862
|
});
|
|
6694
6863
|
const inRoster = declaredEntry !== void 0;
|
|
@@ -6697,7 +6866,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
6697
6866
|
const canonicalShared = roster.some((r) => {
|
|
6698
6867
|
let rReal = null;
|
|
6699
6868
|
try {
|
|
6700
|
-
rReal = realpathSync(
|
|
6869
|
+
rReal = realpathSync(resolve8(repositoryRoot, r.path));
|
|
6701
6870
|
} catch {
|
|
6702
6871
|
rReal = null;
|
|
6703
6872
|
}
|
|
@@ -6705,15 +6874,15 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
6705
6874
|
if (repoReal !== void 0 && rReal === repoReal) return false;
|
|
6706
6875
|
return basename6(rReal).toLowerCase() === cnFold;
|
|
6707
6876
|
}
|
|
6708
|
-
if (
|
|
6709
|
-
return basename6(
|
|
6877
|
+
if (resolve8(repositoryRoot, r.path) === targetAbs) return false;
|
|
6878
|
+
return basename6(resolve8(repositoryRoot, r.path)).toLowerCase() === cnFold;
|
|
6710
6879
|
});
|
|
6711
6880
|
const collisionNote = "shared with another repo of the same basename, so it cannot be removed (check manually)";
|
|
6712
6881
|
const items = [];
|
|
6713
6882
|
if (!isAnchor) {
|
|
6714
6883
|
if (repoReal !== void 0) {
|
|
6715
6884
|
for (const spec of teardownExpectedTargets(repoReal, anchorReal, canonicalName)) {
|
|
6716
|
-
const { state, actualTarget } = inspectSymlink(
|
|
6885
|
+
const { state, actualTarget } = inspectSymlink(join12(repoReal, spec.name), spec.target);
|
|
6717
6886
|
if (isSelf) {
|
|
6718
6887
|
if (state !== "missing")
|
|
6719
6888
|
items.push({
|
|
@@ -6750,7 +6919,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
6750
6919
|
}
|
|
6751
6920
|
let ignored;
|
|
6752
6921
|
try {
|
|
6753
|
-
ignored = new Set(readGitignoreLines(
|
|
6922
|
+
ignored = new Set(readGitignoreLines(join12(repoReal, ".gitignore")).map((l) => l.trim()));
|
|
6754
6923
|
for (const p of INSTRUCTION_FILES) {
|
|
6755
6924
|
if (ignored.has(p) || ignored.has(`/${p}`)) {
|
|
6756
6925
|
items.push({
|
|
@@ -6773,7 +6942,7 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
6773
6942
|
const viewPath = manifest.workspace.view;
|
|
6774
6943
|
if (viewPath !== void 0) {
|
|
6775
6944
|
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
6776
|
-
const linkPath =
|
|
6945
|
+
const linkPath = join12(viewDir, canonicalName);
|
|
6777
6946
|
let isLink = false;
|
|
6778
6947
|
try {
|
|
6779
6948
|
isLink = lstatSync(linkPath).isSymbolicLink();
|
|
@@ -6799,8 +6968,8 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
|
|
|
6799
6968
|
else items.push({ kind: "view-symlink", label: canonicalName, state: "removable" });
|
|
6800
6969
|
}
|
|
6801
6970
|
}
|
|
6802
|
-
const canonicalFile =
|
|
6803
|
-
const canonicalLabel =
|
|
6971
|
+
const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
6972
|
+
const canonicalLabel = join12("agents", canonicalName, CANONICAL_FILE);
|
|
6804
6973
|
let canonicalIsLink = false;
|
|
6805
6974
|
try {
|
|
6806
6975
|
canonicalIsLink = lstatSync(canonicalFile).isSymbolicLink();
|
|
@@ -6887,7 +7056,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
6887
7056
|
const changed = (label) => failed.push({ label, message: "the state changed since the scan (re-run)" });
|
|
6888
7057
|
let currentRepoReal = null;
|
|
6889
7058
|
try {
|
|
6890
|
-
currentRepoReal = realpathSync(
|
|
7059
|
+
currentRepoReal = realpathSync(resolve8(repositoryRoot, plan.target));
|
|
6891
7060
|
} catch {
|
|
6892
7061
|
currentRepoReal = null;
|
|
6893
7062
|
}
|
|
@@ -6904,12 +7073,12 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
6904
7073
|
);
|
|
6905
7074
|
for (const item of removable.filter((i) => i.kind === "instruction-symlink")) {
|
|
6906
7075
|
const expected = expectedByName.get(item.label);
|
|
6907
|
-
if (repoReal === null || expected === void 0 || inspectSymlink(
|
|
7076
|
+
if (repoReal === null || expected === void 0 || inspectSymlink(join12(repoReal, item.label), expected).state !== "correct") {
|
|
6908
7077
|
changed(item.label);
|
|
6909
7078
|
continue;
|
|
6910
7079
|
}
|
|
6911
7080
|
try {
|
|
6912
|
-
unlinkSync(
|
|
7081
|
+
unlinkSync(join12(repoReal, item.label));
|
|
6913
7082
|
removed.push(item.label);
|
|
6914
7083
|
} catch (error) {
|
|
6915
7084
|
failed.push({ label: item.label, message: failureReason(error) });
|
|
@@ -6922,13 +7091,13 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
6922
7091
|
continue;
|
|
6923
7092
|
}
|
|
6924
7093
|
const viewDir = resolveViewDir(repositoryRoot, viewPath);
|
|
6925
|
-
const owned = repoReal !== null ? viewLinkPointsAt(viewDir, item.label, repoReal) : viewLinkPointsAtPath(viewDir, item.label,
|
|
7094
|
+
const owned = repoReal !== null ? viewLinkPointsAt(viewDir, item.label, repoReal) : viewLinkPointsAtPath(viewDir, item.label, resolve8(repositoryRoot, plan.target));
|
|
6926
7095
|
if (!owned) {
|
|
6927
7096
|
changed(item.label);
|
|
6928
7097
|
continue;
|
|
6929
7098
|
}
|
|
6930
7099
|
try {
|
|
6931
|
-
unlinkSync(
|
|
7100
|
+
unlinkSync(join12(viewDir, item.label));
|
|
6932
7101
|
removed.push(`view/${item.label}`);
|
|
6933
7102
|
} catch (error) {
|
|
6934
7103
|
failed.push({ label: `view/${item.label}`, message: failureReason(error) });
|
|
@@ -6936,7 +7105,7 @@ function applyRepoTeardown(repositoryRoot, manifest, plan) {
|
|
|
6936
7105
|
}
|
|
6937
7106
|
const NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
|
|
6938
7107
|
for (const item of removable.filter((i) => i.kind === "canonical-block")) {
|
|
6939
|
-
const canonicalFile =
|
|
7108
|
+
const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
6940
7109
|
try {
|
|
6941
7110
|
if (lstatSync(canonicalFile).isSymbolicLink()) {
|
|
6942
7111
|
changed(item.label);
|
|
@@ -7084,7 +7253,7 @@ async function doRunProjectArchive(target, options, ctx) {
|
|
|
7084
7253
|
const roster = manifest.repos ?? [];
|
|
7085
7254
|
let targetIsAnchor = false;
|
|
7086
7255
|
try {
|
|
7087
|
-
targetIsAnchor = realpathSync(
|
|
7256
|
+
targetIsAnchor = realpathSync(resolve8(repositoryRoot, target)) === realpathSync(repositoryRoot);
|
|
7088
7257
|
} catch {
|
|
7089
7258
|
targetIsAnchor = false;
|
|
7090
7259
|
}
|
|
@@ -7210,12 +7379,12 @@ function gatherRenameWiring(repositoryRoot, manifest, oldBasename) {
|
|
|
7210
7379
|
} catch {
|
|
7211
7380
|
return { canonicalDirOld: false, viewLinkOld: false };
|
|
7212
7381
|
}
|
|
7213
|
-
const canonicalDirOld = existsSync2(
|
|
7382
|
+
const canonicalDirOld = existsSync2(join12(anchorReal, "agents", oldBasename));
|
|
7214
7383
|
let viewLinkOld = false;
|
|
7215
7384
|
const viewPath = manifest.workspace.view;
|
|
7216
7385
|
if (viewPath !== void 0) {
|
|
7217
7386
|
try {
|
|
7218
|
-
lstatSync(
|
|
7387
|
+
lstatSync(join12(resolveViewDir(repositoryRoot, viewPath), oldBasename));
|
|
7219
7388
|
viewLinkOld = true;
|
|
7220
7389
|
} catch {
|
|
7221
7390
|
}
|
|
@@ -7241,7 +7410,7 @@ async function doRunProjectRename(oldPath, newPath, options, ctx) {
|
|
|
7241
7410
|
const roster = manifest.repos ?? [];
|
|
7242
7411
|
let oldIsAnchor = false;
|
|
7243
7412
|
try {
|
|
7244
|
-
oldIsAnchor = realpathSync(
|
|
7413
|
+
oldIsAnchor = realpathSync(resolve8(repositoryRoot, oldPath)) === realpathSync(repositoryRoot);
|
|
7245
7414
|
} catch {
|
|
7246
7415
|
oldIsAnchor = false;
|
|
7247
7416
|
}
|
|
@@ -7387,7 +7556,7 @@ async function doRunProjectNew(repos, options, ctx) {
|
|
|
7387
7556
|
const viewStem = productName ?? workspaceName;
|
|
7388
7557
|
const viewOverridesProjectName = productName !== void 0 && typeof options.view === "string";
|
|
7389
7558
|
const declared = repos.map((p) => {
|
|
7390
|
-
const abs =
|
|
7559
|
+
const abs = resolve8(cwd, p);
|
|
7391
7560
|
let real;
|
|
7392
7561
|
try {
|
|
7393
7562
|
real = realpathSync(abs);
|
|
@@ -7573,7 +7742,7 @@ async function doRunProjectSeedAnchor(options, ctx) {
|
|
|
7573
7742
|
console.log("\u2139\uFE0F No repo roster declared \u2014 nothing to seed.");
|
|
7574
7743
|
return;
|
|
7575
7744
|
}
|
|
7576
|
-
const anchorDoc =
|
|
7745
|
+
const anchorDoc = join12(repositoryRoot, CANONICAL_FILE);
|
|
7577
7746
|
if (pathPresent(anchorDoc)) {
|
|
7578
7747
|
console.log(
|
|
7579
7748
|
`\u2705 The anchor's own \`${CANONICAL_FILE}\` already exists \u2014 hand-maintained, left untouched.`
|
|
@@ -7635,7 +7804,7 @@ function regularFileSpokes(repoReal) {
|
|
|
7635
7804
|
const out = [];
|
|
7636
7805
|
for (const spoke of ["CLAUDE.md", ".github/copilot-instructions.md"]) {
|
|
7637
7806
|
try {
|
|
7638
|
-
const st = lstatSync(
|
|
7807
|
+
const st = lstatSync(join12(repoReal, spoke));
|
|
7639
7808
|
if (!st.isSymbolicLink() && st.isFile()) out.push(spoke);
|
|
7640
7809
|
} catch {
|
|
7641
7810
|
}
|
|
@@ -7652,7 +7821,7 @@ function pathPresent(p) {
|
|
|
7652
7821
|
}
|
|
7653
7822
|
function gatherRetrofit(repositoryRoot, anchorReal, roster, argAbs, argReal, viewCanonicalName) {
|
|
7654
7823
|
const declaredEntry = roster.find((entry) => {
|
|
7655
|
-
const entryAbs =
|
|
7824
|
+
const entryAbs = resolve8(repositoryRoot, entry.path);
|
|
7656
7825
|
if (argReal !== void 0) {
|
|
7657
7826
|
try {
|
|
7658
7827
|
if (realpathSync(entryAbs) === argReal) return true;
|
|
@@ -7681,8 +7850,8 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argAbs, argReal, vie
|
|
|
7681
7850
|
};
|
|
7682
7851
|
}
|
|
7683
7852
|
const isAnchor = argReal === anchorReal;
|
|
7684
|
-
const reachable = existsSync2(
|
|
7685
|
-
const canonicalFile =
|
|
7853
|
+
const reachable = existsSync2(join12(argReal, ".git"));
|
|
7854
|
+
const canonicalFile = join12(anchorReal, "agents", canonicalName, CANONICAL_FILE);
|
|
7686
7855
|
return {
|
|
7687
7856
|
path,
|
|
7688
7857
|
declared,
|
|
@@ -7691,13 +7860,13 @@ function gatherRetrofit(repositoryRoot, anchorReal, roster, argAbs, argReal, vie
|
|
|
7691
7860
|
reachable,
|
|
7692
7861
|
canonicalName,
|
|
7693
7862
|
...viewCanonicalName !== void 0 ? { viewCanonicalName } : {},
|
|
7694
|
-
agentsState: inspectAgentsState(
|
|
7863
|
+
agentsState: inspectAgentsState(join12(argReal, CANONICAL_FILE)),
|
|
7695
7864
|
canonicalExists: pathPresent(canonicalFile),
|
|
7696
7865
|
regularSpokes: regularFileSpokes(argReal)
|
|
7697
7866
|
};
|
|
7698
7867
|
}
|
|
7699
7868
|
function relocateAgentsFile(repoReal, canonicalFile) {
|
|
7700
|
-
const agentsFile =
|
|
7869
|
+
const agentsFile = join12(repoReal, CANONICAL_FILE);
|
|
7701
7870
|
try {
|
|
7702
7871
|
mkdirSync(dirname4(canonicalFile), { recursive: true });
|
|
7703
7872
|
} catch (error) {
|
|
@@ -7792,7 +7961,7 @@ async function doRunProjectRetrofit(repo, options, ctx) {
|
|
|
7792
7961
|
}
|
|
7793
7962
|
return result2;
|
|
7794
7963
|
}
|
|
7795
|
-
const argAbs =
|
|
7964
|
+
const argAbs = resolve8(repositoryRoot, repo);
|
|
7796
7965
|
let argReal;
|
|
7797
7966
|
try {
|
|
7798
7967
|
argReal = realpathSync(argAbs);
|
|
@@ -7813,7 +7982,7 @@ async function doRunProjectRetrofit(repo, options, ctx) {
|
|
|
7813
7982
|
let failure;
|
|
7814
7983
|
let partial = false;
|
|
7815
7984
|
if (options.apply === true && plan.action === "relocate" && argReal !== void 0) {
|
|
7816
|
-
const canonicalFile =
|
|
7985
|
+
const canonicalFile = join12(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
|
|
7817
7986
|
const res = relocateAgentsFile(argReal, canonicalFile);
|
|
7818
7987
|
if (res.ok) {
|
|
7819
7988
|
applied = true;
|
|
@@ -8013,88 +8182,19 @@ function renderProjectRetrofit(result) {
|
|
|
8013
8182
|
}
|
|
8014
8183
|
|
|
8015
8184
|
// src/commands/protocol.ts
|
|
8016
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
8017
|
-
import {
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
if (p.startsWith("~/")) return join12(homedir8(), p.slice(2));
|
|
8030
|
-
return p;
|
|
8031
|
-
}
|
|
8032
|
-
function isRecord3(value) {
|
|
8033
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8034
|
-
}
|
|
8035
|
-
async function loadProtocolsConfig(configPath = DEFAULT_PROTOCOLS_CONFIG_PATH) {
|
|
8036
|
-
let raw;
|
|
8037
|
-
try {
|
|
8038
|
-
raw = await readYamlFile5(configPath);
|
|
8039
|
-
} catch (error) {
|
|
8040
|
-
if (error instanceof Error && error.message === "YAML file not found") {
|
|
8041
|
-
throw new Error(
|
|
8042
|
-
"No protocols config at ~/.basou/protocols.yaml. Create one (a 'protocols:' list of source markdown paths) before running 'basou protocol sync'."
|
|
8043
|
-
);
|
|
8044
|
-
}
|
|
8045
|
-
if (error instanceof Error && error.message === "Failed to parse YAML content") {
|
|
8046
|
-
throw new Error("~/.basou/protocols.yaml is not valid YAML.");
|
|
8047
|
-
}
|
|
8048
|
-
throw error;
|
|
8049
|
-
}
|
|
8050
|
-
if (!isRecord3(raw) || !Array.isArray(raw.protocols)) {
|
|
8051
|
-
throw new Error("~/.basou/protocols.yaml must contain a 'protocols:' list.");
|
|
8052
|
-
}
|
|
8053
|
-
for (const key of Object.keys(raw)) {
|
|
8054
|
-
if (!ALLOWED_TOP_KEYS.has(key)) {
|
|
8055
|
-
throw new Error(
|
|
8056
|
-
`~/.basou/protocols.yaml has an unknown key '${key}' (allowed: version, protocols).`
|
|
8057
|
-
);
|
|
8058
|
-
}
|
|
8059
|
-
}
|
|
8060
|
-
const seen = /* @__PURE__ */ new Set();
|
|
8061
|
-
const result = [];
|
|
8062
|
-
for (const entry of raw.protocols) {
|
|
8063
|
-
if (!isRecord3(entry)) {
|
|
8064
|
-
throw new Error("Each protocol entry must be a mapping with a 'source' key.");
|
|
8065
|
-
}
|
|
8066
|
-
for (const key of Object.keys(entry)) {
|
|
8067
|
-
if (!ALLOWED_ENTRY_KEYS.has(key)) {
|
|
8068
|
-
throw new Error(`A protocol entry has an unknown key '${key}' (allowed: source, title).`);
|
|
8069
|
-
}
|
|
8070
|
-
}
|
|
8071
|
-
if (typeof entry.source !== "string" || entry.source.trim().length === 0) {
|
|
8072
|
-
throw new Error("Each protocol entry needs a non-empty string 'source'.");
|
|
8073
|
-
}
|
|
8074
|
-
if (entry.title !== void 0 && (typeof entry.title !== "string" || entry.title.trim().length === 0)) {
|
|
8075
|
-
throw new Error("A protocol entry 'title' must be a non-empty string when present.");
|
|
8076
|
-
}
|
|
8077
|
-
const expanded = expandTilde3(entry.source.trim());
|
|
8078
|
-
if (!isAbsolute4(expanded)) {
|
|
8079
|
-
throw new Error("Protocol 'source' paths must be absolute (or start with '~').");
|
|
8080
|
-
}
|
|
8081
|
-
const abs = resolve8(expanded);
|
|
8082
|
-
if (seen.has(abs)) {
|
|
8083
|
-
throw new Error("Duplicate protocol source (each source path may appear only once).");
|
|
8084
|
-
}
|
|
8085
|
-
seen.add(abs);
|
|
8086
|
-
result.push(
|
|
8087
|
-
entry.title !== void 0 ? { source: abs, title: entry.title.trim() } : { source: abs }
|
|
8088
|
-
);
|
|
8089
|
-
}
|
|
8090
|
-
if (result.length === 0) {
|
|
8091
|
-
throw new Error("~/.basou/protocols.yaml has no protocols.");
|
|
8092
|
-
}
|
|
8093
|
-
return result;
|
|
8094
|
-
}
|
|
8095
|
-
|
|
8096
|
-
// src/commands/protocol.ts
|
|
8097
|
-
var PROTOCOL_MARKERS = { start: PROTOCOL_START, end: PROTOCOL_END };
|
|
8185
|
+
import { readFile as readFile4, stat as stat5 } from "fs/promises";
|
|
8186
|
+
import {
|
|
8187
|
+
carryForwardProtocolStamp,
|
|
8188
|
+
PROTOCOL_END as PROTOCOL_END2,
|
|
8189
|
+
PROTOCOL_START as PROTOCOL_START2,
|
|
8190
|
+
parseMarkers as parseMarkers4,
|
|
8191
|
+
parseProtocolStamp as parseProtocolStamp2,
|
|
8192
|
+
protocolBlockHash,
|
|
8193
|
+
readMarkdownFile as readMarkdownFile8,
|
|
8194
|
+
renderProtocolStamp,
|
|
8195
|
+
unstampedProtocolSectionsFrom
|
|
8196
|
+
} from "@basou/core";
|
|
8197
|
+
var PROTOCOL_MARKERS = { start: PROTOCOL_START2, end: PROTOCOL_END2 };
|
|
8098
8198
|
var MANAGED_NOTE = "<!-- Managed by basou: 'basou protocol sync' regenerates everything between the BASOU:PROTOCOLS markers from ~/.basou/protocols.yaml. Manual edits inside the block are overwritten; edit the source files instead. -->";
|
|
8099
8199
|
function registerProtocolCommand(program2) {
|
|
8100
8200
|
const protocol = program2.command("protocol").description("Manage the basou-managed standing-protocol block in the global CLAUDE.md");
|
|
@@ -8154,24 +8254,54 @@ async function readProtocolSources(entries) {
|
|
|
8154
8254
|
}
|
|
8155
8255
|
return out;
|
|
8156
8256
|
}
|
|
8157
|
-
function
|
|
8158
|
-
|
|
8257
|
+
function buildSections(sources) {
|
|
8258
|
+
return sources.map(({ entry, content }) => {
|
|
8159
8259
|
const body = content.replace(/\s+$/, "");
|
|
8160
8260
|
return entry.title !== void 0 ? `## ${entry.title}
|
|
8161
8261
|
|
|
8162
8262
|
${body}` : body;
|
|
8163
|
-
});
|
|
8263
|
+
}).join("\n\n");
|
|
8264
|
+
}
|
|
8265
|
+
function buildBlock(sections, stamp) {
|
|
8164
8266
|
return `${MANAGED_NOTE}
|
|
8267
|
+
${renderProtocolStamp(stamp)}
|
|
8165
8268
|
|
|
8166
|
-
${sections
|
|
8269
|
+
${sections}
|
|
8167
8270
|
`;
|
|
8168
8271
|
}
|
|
8272
|
+
async function readPreviousStamp(target) {
|
|
8273
|
+
const existing = await readMarkdownFile8(target);
|
|
8274
|
+
if (existing === null) return null;
|
|
8275
|
+
const section = parseMarkers4(existing, PROTOCOL_MARKERS);
|
|
8276
|
+
if (section.kind !== "ok") return null;
|
|
8277
|
+
const stamp = parseProtocolStamp2(section.generated);
|
|
8278
|
+
if (stamp !== null) return stamp;
|
|
8279
|
+
const writtenAt = await lastWrittenAt(target);
|
|
8280
|
+
if (writtenAt === null) return null;
|
|
8281
|
+
return {
|
|
8282
|
+
changedAt: writtenAt,
|
|
8283
|
+
contentHash: protocolBlockHash(unstampedProtocolSectionsFrom(section.generated))
|
|
8284
|
+
};
|
|
8285
|
+
}
|
|
8286
|
+
async function lastWrittenAt(target) {
|
|
8287
|
+
try {
|
|
8288
|
+
return new Date((await stat5(target)).mtimeMs).toISOString();
|
|
8289
|
+
} catch {
|
|
8290
|
+
return null;
|
|
8291
|
+
}
|
|
8292
|
+
}
|
|
8169
8293
|
async function doRunProtocolSync(options, ctx = {}) {
|
|
8170
8294
|
const configPath = options.config ?? DEFAULT_PROTOCOLS_CONFIG_PATH;
|
|
8171
8295
|
const target = options.target ?? DEFAULT_TARGET_PATH;
|
|
8172
8296
|
const entries = await loadProtocolsConfig(configPath);
|
|
8173
8297
|
const sources = await readProtocolSources(entries);
|
|
8174
|
-
const
|
|
8298
|
+
const sections = buildSections(sources);
|
|
8299
|
+
const stamp = carryForwardProtocolStamp({
|
|
8300
|
+
sections,
|
|
8301
|
+
previous: await readPreviousStamp(target),
|
|
8302
|
+
now: (/* @__PURE__ */ new Date()).toISOString()
|
|
8303
|
+
});
|
|
8304
|
+
const block = buildBlock(sections, stamp);
|
|
8175
8305
|
const foreign = await findForeignWorkspaceNames({
|
|
8176
8306
|
text: block,
|
|
8177
8307
|
configPath: ctx.portfolioConfigPath
|
|
@@ -8241,7 +8371,7 @@ import {
|
|
|
8241
8371
|
import { InvalidArgumentError as InvalidArgumentError4 } from "commander";
|
|
8242
8372
|
|
|
8243
8373
|
// src/commands/refresh-watch.ts
|
|
8244
|
-
import { readdir as readdir2, stat as
|
|
8374
|
+
import { readdir as readdir2, stat as stat6 } from "fs/promises";
|
|
8245
8375
|
import { homedir as homedir9 } from "os";
|
|
8246
8376
|
import { join as join13 } from "path";
|
|
8247
8377
|
import { findErrorCode as findErrorCode8 } from "@basou/core";
|
|
@@ -8270,7 +8400,7 @@ async function scanSourceLogs(roots) {
|
|
|
8270
8400
|
await walk(full);
|
|
8271
8401
|
} else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
8272
8402
|
try {
|
|
8273
|
-
const info = await
|
|
8403
|
+
const info = await stat6(full);
|
|
8274
8404
|
out.set(full, { mtimeMs: info.mtimeMs, size: info.size });
|
|
8275
8405
|
} catch (error) {
|
|
8276
8406
|
if (findErrorCode8(error, "ENOENT")) continue;
|
|
@@ -11730,7 +11860,7 @@ import { InvalidArgumentError as InvalidArgumentError8 } from "commander";
|
|
|
11730
11860
|
|
|
11731
11861
|
// src/lib/portfolio-coverage.ts
|
|
11732
11862
|
import { createReadStream as createReadStream2 } from "fs";
|
|
11733
|
-
import { readdir as readdir3, stat as
|
|
11863
|
+
import { readdir as readdir3, stat as stat7 } from "fs/promises";
|
|
11734
11864
|
import { homedir as homedir12 } from "os";
|
|
11735
11865
|
import { basename as basename8, dirname as dirname5, join as join17 } from "path";
|
|
11736
11866
|
import { createInterface as createInterface2 } from "readline";
|
|
@@ -11877,7 +12007,7 @@ async function isDirEntry(parent, entry) {
|
|
|
11877
12007
|
if (entry.isDirectory()) return true;
|
|
11878
12008
|
if (!entry.isSymbolicLink()) return false;
|
|
11879
12009
|
try {
|
|
11880
|
-
return (await
|
|
12010
|
+
return (await stat7(join17(parent, entry.name))).isDirectory();
|
|
11881
12011
|
} catch {
|
|
11882
12012
|
return false;
|
|
11883
12013
|
}
|
|
@@ -13646,7 +13776,7 @@ async function assertWorkspaceInitialized15(basouRoot) {
|
|
|
13646
13776
|
function readBuildStamp() {
|
|
13647
13777
|
if (false) return void 0;
|
|
13648
13778
|
try {
|
|
13649
|
-
return JSON.parse('{"version":"0.
|
|
13779
|
+
return JSON.parse('{"version":"0.46.0","commit":"dbc9c57","committedAt":"2026-09-19T15:43:35+09:00"}');
|
|
13650
13780
|
} catch {
|
|
13651
13781
|
return void 0;
|
|
13652
13782
|
}
|