@titan-design/active-work 0.3.0 → 0.4.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/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
NOTE_TITLE_MAX_LENGTH,
|
|
12
12
|
NextStepSchema,
|
|
13
13
|
NotFoundError,
|
|
14
|
+
NoteFrontmatterSchema,
|
|
14
15
|
NoteKindSchema,
|
|
15
16
|
SessionFrontmatterSchema,
|
|
16
17
|
SessionIdSchema,
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
WorktreeEntrySchema,
|
|
24
25
|
assembleBootstrap,
|
|
25
26
|
atomicWrite,
|
|
27
|
+
coerceDates,
|
|
26
28
|
color,
|
|
27
29
|
defineCommand,
|
|
28
30
|
deriveOpenLoops,
|
|
@@ -74,7 +76,7 @@ import {
|
|
|
74
76
|
writeFrontmatter,
|
|
75
77
|
writeNoteFile,
|
|
76
78
|
writeYaml
|
|
77
|
-
} from "./chunk-
|
|
79
|
+
} from "./chunk-BK25ASXA.js";
|
|
78
80
|
|
|
79
81
|
// src/cli.ts
|
|
80
82
|
import { Command, CommanderError } from "commander";
|
|
@@ -261,7 +263,7 @@ var new_default = defineCommand({
|
|
|
261
263
|
}
|
|
262
264
|
const rank = await computeNextRank(ctx.activeRoot);
|
|
263
265
|
const task_prefix = derivePrefix(args.slug);
|
|
264
|
-
const
|
|
266
|
+
const frontmatter2 = {
|
|
265
267
|
schema_version: 1,
|
|
266
268
|
title: args.title,
|
|
267
269
|
updated: today(),
|
|
@@ -281,7 +283,7 @@ Why: ...
|
|
|
281
283
|
`;
|
|
282
284
|
await writeFrontmatter(
|
|
283
285
|
path2.join(dir, "brief.md"),
|
|
284
|
-
|
|
286
|
+
frontmatter2,
|
|
285
287
|
briefBody,
|
|
286
288
|
BriefFrontmatterSchema
|
|
287
289
|
);
|
|
@@ -452,11 +454,11 @@ var set_default = defineCommand({
|
|
|
452
454
|
throw new NotFoundError(`Initiative not found: ${args.slug}`);
|
|
453
455
|
}
|
|
454
456
|
await withFileLock(getLockPath(args.slug), async () => {
|
|
455
|
-
const { frontmatter, body } = await readRawFrontmatter(briefPath);
|
|
456
|
-
setPath(
|
|
457
|
-
|
|
457
|
+
const { frontmatter: frontmatter2, body } = await readRawFrontmatter(briefPath);
|
|
458
|
+
setPath(frontmatter2, args.field, args.value);
|
|
459
|
+
frontmatter2.updated = today();
|
|
458
460
|
try {
|
|
459
|
-
await writeFrontmatter(briefPath,
|
|
461
|
+
await writeFrontmatter(briefPath, frontmatter2, body, BriefFrontmatterSchema);
|
|
460
462
|
} catch (err) {
|
|
461
463
|
const reason = err instanceof Error ? err.message : String(err);
|
|
462
464
|
throw new ValidationError(
|
|
@@ -497,9 +499,9 @@ var touch_default = defineCommand({
|
|
|
497
499
|
}
|
|
498
500
|
const updated = today();
|
|
499
501
|
await withFileLock(getLockPath(args.slug), async () => {
|
|
500
|
-
const { frontmatter, body } = await readRawFrontmatter(briefPath);
|
|
501
|
-
|
|
502
|
-
await writeFrontmatter(briefPath,
|
|
502
|
+
const { frontmatter: frontmatter2, body } = await readRawFrontmatter(briefPath);
|
|
503
|
+
frontmatter2.updated = updated;
|
|
504
|
+
await writeFrontmatter(briefPath, frontmatter2, body, BriefFrontmatterSchema);
|
|
503
505
|
});
|
|
504
506
|
return { slug: args.slug, updated };
|
|
505
507
|
}
|
|
@@ -531,8 +533,8 @@ async function loadAllBriefs() {
|
|
|
531
533
|
continue;
|
|
532
534
|
}
|
|
533
535
|
if (!stat.isFile()) continue;
|
|
534
|
-
const { frontmatter, body } = await readFrontmatter(briefPath, BriefFrontmatterSchema);
|
|
535
|
-
briefs.push({ slug: name, briefPath, frontmatter, body });
|
|
536
|
+
const { frontmatter: frontmatter2, body } = await readFrontmatter(briefPath, BriefFrontmatterSchema);
|
|
537
|
+
briefs.push({ slug: name, briefPath, frontmatter: frontmatter2, body });
|
|
536
538
|
}
|
|
537
539
|
briefs.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
538
540
|
return briefs;
|
|
@@ -1084,11 +1086,11 @@ var task_delete_default = defineCommand({
|
|
|
1084
1086
|
if (n !== null && n === onDiskMax) {
|
|
1085
1087
|
const stored = readTaskSeq(brief, onDiskMax);
|
|
1086
1088
|
if (stored < n) {
|
|
1087
|
-
const
|
|
1089
|
+
const frontmatter2 = {
|
|
1088
1090
|
...brief.frontmatter,
|
|
1089
1091
|
task_seq: n
|
|
1090
1092
|
};
|
|
1091
|
-
await writeFrontmatter(brief.path,
|
|
1093
|
+
await writeFrontmatter(brief.path, frontmatter2, brief.body, BriefFrontmatterSchema);
|
|
1092
1094
|
}
|
|
1093
1095
|
}
|
|
1094
1096
|
await fs10.unlink(file);
|
|
@@ -1515,8 +1517,8 @@ var NO_UPSTREAM = { ahead: null, behind: null };
|
|
|
1515
1517
|
function shortBranch(ref) {
|
|
1516
1518
|
return ref.replace(/^refs\/heads\//, "");
|
|
1517
1519
|
}
|
|
1518
|
-
function emptyWorktree(
|
|
1519
|
-
return { path:
|
|
1520
|
+
function emptyWorktree(path67) {
|
|
1521
|
+
return { path: path67, head: null, branch: null, detached: false, bare: false };
|
|
1520
1522
|
}
|
|
1521
1523
|
function parseWorktreePorcelain(stdout) {
|
|
1522
1524
|
const out = [];
|
|
@@ -2000,10 +2002,10 @@ var session_list_default = defineCommand({
|
|
|
2000
2002
|
for (const filename of filenames) {
|
|
2001
2003
|
const fullPath = path19.join(sessionsDir, filename);
|
|
2002
2004
|
try {
|
|
2003
|
-
const { frontmatter, body } = await readSession(fullPath);
|
|
2005
|
+
const { frontmatter: frontmatter2, body } = await readSession(fullPath);
|
|
2004
2006
|
entries.push({
|
|
2005
2007
|
filename,
|
|
2006
|
-
frontmatter,
|
|
2008
|
+
frontmatter: frontmatter2,
|
|
2007
2009
|
first_line: extractFirstLine(body)
|
|
2008
2010
|
});
|
|
2009
2011
|
} catch (err) {
|
|
@@ -2427,9 +2429,9 @@ async function verifyTaskIds(initiativeDir, ids) {
|
|
|
2427
2429
|
}
|
|
2428
2430
|
async function stampBriefUpdated(briefPath) {
|
|
2429
2431
|
const updated = today();
|
|
2430
|
-
const { frontmatter, body } = await readRawFrontmatter(briefPath);
|
|
2431
|
-
|
|
2432
|
-
await writeFrontmatter(briefPath,
|
|
2432
|
+
const { frontmatter: frontmatter2, body } = await readRawFrontmatter(briefPath);
|
|
2433
|
+
frontmatter2.updated = updated;
|
|
2434
|
+
await writeFrontmatter(briefPath, frontmatter2, body, BriefFrontmatterSchema);
|
|
2433
2435
|
return updated;
|
|
2434
2436
|
}
|
|
2435
2437
|
async function writeWrap(args, briefPath, body, ledger) {
|
|
@@ -2714,14 +2716,14 @@ var note_add_default = defineCommand({
|
|
|
2714
2716
|
throw new NotFoundError(`Initiative not found: ${args.slug}`);
|
|
2715
2717
|
}
|
|
2716
2718
|
const body = args.body ?? await fs20.readFile(args.body_file, "utf8");
|
|
2717
|
-
const
|
|
2719
|
+
const frontmatter2 = {
|
|
2718
2720
|
kind: args.kind,
|
|
2719
2721
|
title: args.title,
|
|
2720
2722
|
created: today(),
|
|
2721
2723
|
...args.tags && args.tags.length > 0 ? { tags: args.tags } : {}
|
|
2722
2724
|
};
|
|
2723
2725
|
return withFileLock(getLockPath(args.slug), async () => {
|
|
2724
|
-
const written = await writeNoteFile(initiativeDir,
|
|
2726
|
+
const written = await writeNoteFile(initiativeDir, frontmatter2, body);
|
|
2725
2727
|
return { ...written, kind: args.kind, title: args.title };
|
|
2726
2728
|
});
|
|
2727
2729
|
}
|
|
@@ -3059,13 +3061,13 @@ async function classifyWorktree(entry) {
|
|
|
3059
3061
|
if (await isWorktreePresent(entry.path)) return { keep: true };
|
|
3060
3062
|
return { keep: false, reason: "worktree path missing" };
|
|
3061
3063
|
}
|
|
3062
|
-
async function partition(entries, classify2,
|
|
3064
|
+
async function partition(entries, classify2, describe3) {
|
|
3063
3065
|
const keep = [];
|
|
3064
3066
|
const pruned = [];
|
|
3065
3067
|
for (const entry of entries) {
|
|
3066
3068
|
const verdict = await classify2(entry);
|
|
3067
3069
|
if (verdict.keep) keep.push(entry);
|
|
3068
|
-
else pruned.push({ ...
|
|
3070
|
+
else pruned.push({ ...describe3(entry), reason: verdict.reason });
|
|
3069
3071
|
}
|
|
3070
3072
|
return { keep, pruned };
|
|
3071
3073
|
}
|
|
@@ -3411,9 +3413,9 @@ async function listSources(initiativeDir) {
|
|
|
3411
3413
|
);
|
|
3412
3414
|
}
|
|
3413
3415
|
function extractSourceReferences(body) {
|
|
3414
|
-
const
|
|
3416
|
+
const matches2 = body.matchAll(/sources\/([A-Za-z0-9._/-]+\.md)/g);
|
|
3415
3417
|
const seen = /* @__PURE__ */ new Set();
|
|
3416
|
-
for (const match of
|
|
3418
|
+
for (const match of matches2) {
|
|
3417
3419
|
seen.add(match[1]);
|
|
3418
3420
|
}
|
|
3419
3421
|
return [...seen].sort();
|
|
@@ -3557,9 +3559,9 @@ async function scanInitiatives(activeRoot) {
|
|
|
3557
3559
|
continue;
|
|
3558
3560
|
}
|
|
3559
3561
|
try {
|
|
3560
|
-
const { frontmatter } = await readFrontmatter(briefPath, BriefFrontmatterSchema);
|
|
3562
|
+
const { frontmatter: frontmatter2 } = await readFrontmatter(briefPath, BriefFrontmatterSchema);
|
|
3561
3563
|
const worktrees = await readRegisteredWorktrees(path32.join(activeRoot, slug));
|
|
3562
|
-
entries.push({ slug, frontmatter, worktrees });
|
|
3564
|
+
entries.push({ slug, frontmatter: frontmatter2, worktrees });
|
|
3563
3565
|
} catch (err) {
|
|
3564
3566
|
const message = err instanceof Error ? err.message : String(err);
|
|
3565
3567
|
errors.push({ slug, error: message });
|
|
@@ -3607,13 +3609,13 @@ var audit_default = defineCommand({
|
|
|
3607
3609
|
async run() {
|
|
3608
3610
|
const activeRoot = getActiveRoot();
|
|
3609
3611
|
const { entries, errors } = await scanInitiatives(activeRoot);
|
|
3610
|
-
const initiatives = [...entries].sort(compareInitiatives).map(({ slug, frontmatter }) => ({
|
|
3612
|
+
const initiatives = [...entries].sort(compareInitiatives).map(({ slug, frontmatter: frontmatter2 }) => ({
|
|
3611
3613
|
slug,
|
|
3612
|
-
title:
|
|
3613
|
-
state:
|
|
3614
|
-
...
|
|
3615
|
-
updated:
|
|
3616
|
-
...
|
|
3614
|
+
title: frontmatter2.title,
|
|
3615
|
+
state: frontmatter2.state,
|
|
3616
|
+
...frontmatter2.rank !== void 0 ? { rank: frontmatter2.rank } : {},
|
|
3617
|
+
updated: frontmatter2.updated,
|
|
3618
|
+
...frontmatter2.ship_target !== void 0 ? { ship_target: frontmatter2.ship_target } : {}
|
|
3617
3619
|
}));
|
|
3618
3620
|
return {
|
|
3619
3621
|
initiatives,
|
|
@@ -3736,12 +3738,12 @@ function matchBody(match, base, body) {
|
|
|
3736
3738
|
return out;
|
|
3737
3739
|
}
|
|
3738
3740
|
function scanSessionFile(input, scan) {
|
|
3739
|
-
const { slug, stem, rel, id, match, frontmatter, body } = input;
|
|
3741
|
+
const { slug, stem, rel, id, match, frontmatter: frontmatter2, body } = input;
|
|
3740
3742
|
const base = { slug, source: "session", file: rel };
|
|
3741
|
-
if (id === stem || id ===
|
|
3743
|
+
if (id === stem || id === frontmatter2.session_id) {
|
|
3742
3744
|
scan.subject = { kind: "session", slug, file: rel, title: stem };
|
|
3743
3745
|
}
|
|
3744
|
-
|
|
3746
|
+
frontmatter2.next_steps.forEach((step, i) => {
|
|
3745
3747
|
if (`${stem}#${step.id}` === id) {
|
|
3746
3748
|
scan.subject = { kind: "loop", slug, file: rel, title: step.text };
|
|
3747
3749
|
return;
|
|
@@ -3753,7 +3755,7 @@ function scanSessionFile(input, scan) {
|
|
|
3753
3755
|
])
|
|
3754
3756
|
);
|
|
3755
3757
|
});
|
|
3756
|
-
|
|
3758
|
+
frontmatter2.resolves.forEach((entry, i) => {
|
|
3757
3759
|
scan.references.push(
|
|
3758
3760
|
...matchFields(match, base, [
|
|
3759
3761
|
[`resolves[${i}].ref`, entry.ref],
|
|
@@ -3768,12 +3770,12 @@ async function scanSessions(slug, id, match, scan) {
|
|
|
3768
3770
|
for (const file of await listFiles(dir, ".md")) {
|
|
3769
3771
|
const rel = path33.join("sessions", file);
|
|
3770
3772
|
try {
|
|
3771
|
-
const { frontmatter, body } = await readFrontmatter(
|
|
3773
|
+
const { frontmatter: frontmatter2, body } = await readFrontmatter(
|
|
3772
3774
|
path33.join(dir, file),
|
|
3773
3775
|
SessionFrontmatterSchema
|
|
3774
3776
|
);
|
|
3775
3777
|
const stem = file.slice(0, -".md".length);
|
|
3776
|
-
scanSessionFile({ slug, stem, rel, id, match, frontmatter, body }, scan);
|
|
3778
|
+
scanSessionFile({ slug, stem, rel, id, match, frontmatter: frontmatter2, body }, scan);
|
|
3777
3779
|
} catch (err) {
|
|
3778
3780
|
scan.errors.push({ file: rel, error: errorText(err) });
|
|
3779
3781
|
}
|
|
@@ -3897,9 +3899,9 @@ var list_default = defineCommand({
|
|
|
3897
3899
|
const backburner = [];
|
|
3898
3900
|
const paused = [];
|
|
3899
3901
|
const done = [];
|
|
3900
|
-
for (const { slug, frontmatter } of entries) {
|
|
3901
|
-
const item = toItem(slug,
|
|
3902
|
-
switch (
|
|
3902
|
+
for (const { slug, frontmatter: frontmatter2 } of entries) {
|
|
3903
|
+
const item = toItem(slug, frontmatter2);
|
|
3904
|
+
switch (frontmatter2.state) {
|
|
3903
3905
|
case "focused":
|
|
3904
3906
|
focused.push(item);
|
|
3905
3907
|
break;
|
|
@@ -3986,10 +3988,10 @@ var worktree_set_default = defineCommand({
|
|
|
3986
3988
|
const initiativeDir = path34.join(getActiveRoot(), args.slug);
|
|
3987
3989
|
const briefPath = path34.join(initiativeDir, "brief.md");
|
|
3988
3990
|
return withFileLock(getLockPath(args.slug), async () => {
|
|
3989
|
-
let
|
|
3991
|
+
let frontmatter2;
|
|
3990
3992
|
let body;
|
|
3991
3993
|
try {
|
|
3992
|
-
({ frontmatter, body } = await readFrontmatter(briefPath, BriefFrontmatterSchema));
|
|
3994
|
+
({ frontmatter: frontmatter2, body } = await readFrontmatter(briefPath, BriefFrontmatterSchema));
|
|
3993
3995
|
} catch (err) {
|
|
3994
3996
|
throw new ValidationError(err instanceof Error ? err.message : String(err));
|
|
3995
3997
|
}
|
|
@@ -4022,7 +4024,7 @@ var worktree_set_default = defineCommand({
|
|
|
4022
4024
|
});
|
|
4023
4025
|
await writeFrontmatter(
|
|
4024
4026
|
briefPath,
|
|
4025
|
-
{ ...
|
|
4027
|
+
{ ...frontmatter2, updated: today() },
|
|
4026
4028
|
body,
|
|
4027
4029
|
BriefFrontmatterSchema
|
|
4028
4030
|
);
|
|
@@ -4060,10 +4062,10 @@ var worktree_set_default_default = defineCommand({
|
|
|
4060
4062
|
const initiativeDir = path35.join(getActiveRoot(), slug);
|
|
4061
4063
|
const briefPath = path35.join(initiativeDir, "brief.md");
|
|
4062
4064
|
return withFileLock(getLockPath(slug), async () => {
|
|
4063
|
-
let
|
|
4065
|
+
let frontmatter2;
|
|
4064
4066
|
let body;
|
|
4065
4067
|
try {
|
|
4066
|
-
({ frontmatter, body } = await readFrontmatter(briefPath, BriefFrontmatterSchema));
|
|
4068
|
+
({ frontmatter: frontmatter2, body } = await readFrontmatter(briefPath, BriefFrontmatterSchema));
|
|
4067
4069
|
} catch (err) {
|
|
4068
4070
|
throw new ValidationError(err instanceof Error ? err.message : String(err));
|
|
4069
4071
|
}
|
|
@@ -4079,7 +4081,7 @@ var worktree_set_default_default = defineCommand({
|
|
|
4079
4081
|
await writeArtifactsFile(initiativeDir, { ...artifacts, worktrees });
|
|
4080
4082
|
await writeFrontmatter(
|
|
4081
4083
|
briefPath,
|
|
4082
|
-
{ ...
|
|
4084
|
+
{ ...frontmatter2, updated: today() },
|
|
4083
4085
|
body,
|
|
4084
4086
|
BriefFrontmatterSchema
|
|
4085
4087
|
);
|
|
@@ -5198,22 +5200,145 @@ import { watchTree } from "@titan-design/daemon";
|
|
|
5198
5200
|
|
|
5199
5201
|
// src/session-index/graph.ts
|
|
5200
5202
|
import Database from "better-sqlite3";
|
|
5201
|
-
import {
|
|
5203
|
+
import { openSessionGraph } from "@titan-design/session-graph";
|
|
5204
|
+
import { runMigrations, WatermarkTable } from "@titan-design/store-sqlite";
|
|
5202
5205
|
import path46 from "path";
|
|
5206
|
+
|
|
5207
|
+
// src/workspace-index/schema.ts
|
|
5208
|
+
import { kitDdl } from "@titan-design/store-sqlite";
|
|
5209
|
+
import { MIGRATIONS as SESSION_GRAPH_MIGRATIONS } from "@titan-design/session-graph";
|
|
5210
|
+
var WORKSPACE_KIT = {
|
|
5211
|
+
watermark: "workspace_file",
|
|
5212
|
+
edge: "edge",
|
|
5213
|
+
spanFts: "search"
|
|
5214
|
+
};
|
|
5215
|
+
var WORKSPACE_SPAN_SOURCE_BASE = 1e9;
|
|
5216
|
+
var DOMAIN_DDL = `
|
|
5217
|
+
CREATE TABLE IF NOT EXISTS initiative (
|
|
5218
|
+
path TEXT PRIMARY KEY,
|
|
5219
|
+
initiative_ref TEXT NOT NULL UNIQUE,
|
|
5220
|
+
slug TEXT NOT NULL,
|
|
5221
|
+
title TEXT,
|
|
5222
|
+
state TEXT,
|
|
5223
|
+
rank INTEGER,
|
|
5224
|
+
ship_target TEXT,
|
|
5225
|
+
owner TEXT,
|
|
5226
|
+
task_prefix TEXT,
|
|
5227
|
+
updated TEXT
|
|
5228
|
+
);
|
|
5229
|
+
|
|
5230
|
+
CREATE TABLE IF NOT EXISTS note (
|
|
5231
|
+
path TEXT PRIMARY KEY,
|
|
5232
|
+
note_ref TEXT NOT NULL UNIQUE,
|
|
5233
|
+
initiative TEXT NOT NULL,
|
|
5234
|
+
filename TEXT NOT NULL,
|
|
5235
|
+
kind TEXT NOT NULL,
|
|
5236
|
+
title TEXT NOT NULL,
|
|
5237
|
+
created TEXT,
|
|
5238
|
+
tags TEXT,
|
|
5239
|
+
hits INTEGER NOT NULL DEFAULT 0,
|
|
5240
|
+
promoted_at TEXT
|
|
5241
|
+
);
|
|
5242
|
+
CREATE INDEX IF NOT EXISTS idx_note_initiative ON note(initiative);
|
|
5243
|
+
|
|
5244
|
+
CREATE TABLE IF NOT EXISTS workspace_task (
|
|
5245
|
+
path TEXT PRIMARY KEY,
|
|
5246
|
+
task_ref TEXT NOT NULL,
|
|
5247
|
+
initiative TEXT NOT NULL,
|
|
5248
|
+
task_id TEXT NOT NULL,
|
|
5249
|
+
title TEXT NOT NULL,
|
|
5250
|
+
status TEXT NOT NULL,
|
|
5251
|
+
priority INTEGER,
|
|
5252
|
+
severity TEXT,
|
|
5253
|
+
estimate REAL,
|
|
5254
|
+
tags TEXT,
|
|
5255
|
+
created TEXT,
|
|
5256
|
+
updated TEXT,
|
|
5257
|
+
done_at TEXT
|
|
5258
|
+
);
|
|
5259
|
+
CREATE INDEX IF NOT EXISTS idx_workspace_task_ref ON workspace_task(task_ref);
|
|
5260
|
+
|
|
5261
|
+
CREATE TABLE IF NOT EXISTS session_record (
|
|
5262
|
+
path TEXT PRIMARY KEY,
|
|
5263
|
+
session_ref TEXT NOT NULL,
|
|
5264
|
+
initiative TEXT NOT NULL,
|
|
5265
|
+
session_id TEXT NOT NULL,
|
|
5266
|
+
started TEXT,
|
|
5267
|
+
ended TEXT,
|
|
5268
|
+
track TEXT,
|
|
5269
|
+
parent_session_id TEXT
|
|
5270
|
+
);
|
|
5271
|
+
CREATE INDEX IF NOT EXISTS idx_session_record_ref ON session_record(session_ref);
|
|
5272
|
+
|
|
5273
|
+
CREATE TABLE IF NOT EXISTS source (
|
|
5274
|
+
path TEXT PRIMARY KEY,
|
|
5275
|
+
source_ref TEXT NOT NULL UNIQUE,
|
|
5276
|
+
initiative TEXT NOT NULL,
|
|
5277
|
+
title TEXT,
|
|
5278
|
+
kind TEXT,
|
|
5279
|
+
added TEXT
|
|
5280
|
+
);
|
|
5281
|
+
`;
|
|
5282
|
+
function nextVersion(chain) {
|
|
5283
|
+
return (chain[chain.length - 1]?.version ?? 0) + 1;
|
|
5284
|
+
}
|
|
5285
|
+
var SPAN_SOURCE_INDEX = `
|
|
5286
|
+
CREATE INDEX IF NOT EXISTS idx_search_span_source ON search_span(source_id);
|
|
5287
|
+
`;
|
|
5288
|
+
var WORKSPACE_MIGRATIONS = [
|
|
5289
|
+
{
|
|
5290
|
+
version: nextVersion(SESSION_GRAPH_MIGRATIONS),
|
|
5291
|
+
name: "workspace index tables",
|
|
5292
|
+
up: (db) => {
|
|
5293
|
+
db.exec(kitDdl({ watermark: WORKSPACE_KIT.watermark }));
|
|
5294
|
+
db.exec(DOMAIN_DDL);
|
|
5295
|
+
db.exec(SPAN_SOURCE_INDEX);
|
|
5296
|
+
}
|
|
5297
|
+
}
|
|
5298
|
+
];
|
|
5299
|
+
var PRESERVE_DDL = `
|
|
5300
|
+
CREATE TABLE IF NOT EXISTS preserved_row (
|
|
5301
|
+
table_name TEXT NOT NULL,
|
|
5302
|
+
identity TEXT NOT NULL,
|
|
5303
|
+
row_key TEXT NOT NULL,
|
|
5304
|
+
payload TEXT NOT NULL,
|
|
5305
|
+
origin TEXT NOT NULL,
|
|
5306
|
+
mode TEXT NOT NULL DEFAULT 'insert',
|
|
5307
|
+
preserved_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
5308
|
+
PRIMARY KEY (table_name, row_key)
|
|
5309
|
+
);
|
|
5310
|
+
`;
|
|
5311
|
+
var PRESERVE_MIGRATION = {
|
|
5312
|
+
version: nextVersion([...SESSION_GRAPH_MIGRATIONS, ...WORKSPACE_MIGRATIONS]),
|
|
5313
|
+
name: "preserved rows",
|
|
5314
|
+
up: (db) => db.exec(PRESERVE_DDL)
|
|
5315
|
+
};
|
|
5316
|
+
var MIGRATIONS = [
|
|
5317
|
+
...SESSION_GRAPH_MIGRATIONS,
|
|
5318
|
+
...WORKSPACE_MIGRATIONS,
|
|
5319
|
+
PRESERVE_MIGRATION
|
|
5320
|
+
];
|
|
5321
|
+
|
|
5322
|
+
// src/session-index/graph.ts
|
|
5203
5323
|
var SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
|
|
5204
5324
|
function defaultGraphPath() {
|
|
5205
5325
|
return path46.join(getMinerRoot(), "graph.sqlite3");
|
|
5206
5326
|
}
|
|
5207
5327
|
function openGraph(dbPath = defaultGraphPath()) {
|
|
5208
|
-
|
|
5328
|
+
const graph = openSessionGraph(dbPath);
|
|
5329
|
+
runMigrations(graph.db, MIGRATIONS);
|
|
5330
|
+
return {
|
|
5331
|
+
...graph,
|
|
5332
|
+
workspaceFiles: new WatermarkTable(graph.db, { name: WORKSPACE_KIT.watermark })
|
|
5333
|
+
};
|
|
5209
5334
|
}
|
|
5210
5335
|
function openGraphReadOnly(dbPath = defaultGraphPath()) {
|
|
5211
5336
|
return new Database(dbPath, { readonly: true });
|
|
5212
5337
|
}
|
|
5213
5338
|
|
|
5214
5339
|
// src/session-index/refresh.ts
|
|
5215
|
-
import
|
|
5216
|
-
import { promises as
|
|
5340
|
+
import path56 from "path";
|
|
5341
|
+
import { promises as fs44 } from "fs";
|
|
5217
5342
|
import lockfile from "proper-lockfile";
|
|
5218
5343
|
import { discoverTranscripts, transcriptsRoot } from "@titan-design/session-read";
|
|
5219
5344
|
import { refreshCorpus, resetIndex } from "@titan-design/session-graph";
|
|
@@ -5274,6 +5399,873 @@ function taskResolver(root) {
|
|
|
5274
5399
|
};
|
|
5275
5400
|
}
|
|
5276
5401
|
|
|
5402
|
+
// src/workspace-index/refresh.ts
|
|
5403
|
+
import { promises as fs43 } from "fs";
|
|
5404
|
+
import matter4 from "gray-matter";
|
|
5405
|
+
|
|
5406
|
+
// src/workspace-index/refs.ts
|
|
5407
|
+
import path48 from "path";
|
|
5408
|
+
import { refKind } from "@titan-design/store-sqlite";
|
|
5409
|
+
var initiativeRef = refKind("initiative");
|
|
5410
|
+
var noteRef = refKind("note");
|
|
5411
|
+
var taskRef = refKind("task");
|
|
5412
|
+
var sessionRef = refKind("session");
|
|
5413
|
+
var sourceRef = refKind("source");
|
|
5414
|
+
function toRelative(activeRoot, absolutePath) {
|
|
5415
|
+
return path48.relative(activeRoot, absolutePath).split(path48.sep).join("/");
|
|
5416
|
+
}
|
|
5417
|
+
function toAbsolute(activeRoot, relativePath) {
|
|
5418
|
+
return path48.join(activeRoot, ...relativePath.split("/"));
|
|
5419
|
+
}
|
|
5420
|
+
|
|
5421
|
+
// src/workspace-index/mentions.ts
|
|
5422
|
+
var TASK_ID = /(?<![A-Za-z0-9])([A-Za-z][A-Za-z0-9]*-\d+)(?![A-Za-z0-9])/g;
|
|
5423
|
+
var SESSION_ID = /(?<![A-Za-z0-9_-])(session_[A-Za-z0-9]+|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?![A-Za-z0-9-])/gi;
|
|
5424
|
+
var NOTE_REF = /(?<![A-Za-z0-9])note:([A-Za-z0-9._-]+\/[A-Za-z0-9._-]+\.md)/g;
|
|
5425
|
+
var SOURCE_PATH = /sources\/([A-Za-z0-9._/-]+\.md)/g;
|
|
5426
|
+
function matches(body, pattern) {
|
|
5427
|
+
return [...body.matchAll(pattern)].map((match) => match[1]);
|
|
5428
|
+
}
|
|
5429
|
+
function extractMentions(body, initiative, index) {
|
|
5430
|
+
const found = /* @__PURE__ */ new Set();
|
|
5431
|
+
for (const id of matches(body, TASK_ID)) {
|
|
5432
|
+
if (index.taskIds.has(id.toUpperCase())) found.add(taskRef(id.toUpperCase()));
|
|
5433
|
+
}
|
|
5434
|
+
for (const id of matches(body, SESSION_ID)) {
|
|
5435
|
+
if (index.sessionIds.has(id)) found.add(sessionRef(id));
|
|
5436
|
+
}
|
|
5437
|
+
for (const id of matches(body, NOTE_REF)) {
|
|
5438
|
+
if (index.noteRefs.has(noteRef(id))) found.add(noteRef(id));
|
|
5439
|
+
}
|
|
5440
|
+
for (const path67 of matches(body, SOURCE_PATH)) {
|
|
5441
|
+
const scoped = `${initiative}/${path67}`;
|
|
5442
|
+
if (index.sourcePaths.has(scoped)) found.add(sourceRef(scoped));
|
|
5443
|
+
}
|
|
5444
|
+
return [...found].sort();
|
|
5445
|
+
}
|
|
5446
|
+
|
|
5447
|
+
// src/workspace-index/edges.ts
|
|
5448
|
+
var RELATIONS = {
|
|
5449
|
+
HOLDS: "holds",
|
|
5450
|
+
MENTIONS: "mentions",
|
|
5451
|
+
SHARES_TAG: "shares_tag"
|
|
5452
|
+
};
|
|
5453
|
+
var SHARES_TAG_MAX_NOTES = 10;
|
|
5454
|
+
function rebuildEdges(graph, notes) {
|
|
5455
|
+
const relations = Object.values(RELATIONS);
|
|
5456
|
+
const placeholders = relations.map(() => "?").join(", ");
|
|
5457
|
+
return graph.db.transaction(() => {
|
|
5458
|
+
graph.db.prepare(`DELETE FROM edge WHERE relation IN (${placeholders})`).run(...relations);
|
|
5459
|
+
return {
|
|
5460
|
+
holds: assertHolds(graph),
|
|
5461
|
+
mentions: assertMentions(graph, notes),
|
|
5462
|
+
sharesTag: assertSharesTag(graph, notes)
|
|
5463
|
+
};
|
|
5464
|
+
})();
|
|
5465
|
+
}
|
|
5466
|
+
var OWNED_REFS = `
|
|
5467
|
+
SELECT note_ref AS ref, initiative FROM note
|
|
5468
|
+
UNION ALL SELECT task_ref AS ref, initiative FROM workspace_task
|
|
5469
|
+
UNION ALL SELECT session_ref AS ref, initiative FROM session_record
|
|
5470
|
+
UNION ALL SELECT source_ref AS ref, initiative FROM source
|
|
5471
|
+
`;
|
|
5472
|
+
function assertHolds(graph) {
|
|
5473
|
+
const rows = graph.db.prepare(OWNED_REFS).all();
|
|
5474
|
+
let written = 0;
|
|
5475
|
+
for (const row of rows) {
|
|
5476
|
+
const inserted = graph.edges.assert({
|
|
5477
|
+
sourceRef: initiativeRef(row.initiative),
|
|
5478
|
+
relation: RELATIONS.HOLDS,
|
|
5479
|
+
targetRef: row.ref
|
|
5480
|
+
});
|
|
5481
|
+
if (inserted) written++;
|
|
5482
|
+
}
|
|
5483
|
+
return written;
|
|
5484
|
+
}
|
|
5485
|
+
function assertMentions(graph, notes) {
|
|
5486
|
+
const index = buildRefIndex(graph);
|
|
5487
|
+
let written = 0;
|
|
5488
|
+
for (const note of notes) {
|
|
5489
|
+
for (const target of extractMentions(note.body, note.initiative, index)) {
|
|
5490
|
+
if (target === note.ref) continue;
|
|
5491
|
+
const inserted = graph.edges.assert({
|
|
5492
|
+
sourceRef: note.ref,
|
|
5493
|
+
relation: RELATIONS.MENTIONS,
|
|
5494
|
+
targetRef: target
|
|
5495
|
+
});
|
|
5496
|
+
if (inserted) written++;
|
|
5497
|
+
}
|
|
5498
|
+
}
|
|
5499
|
+
return written;
|
|
5500
|
+
}
|
|
5501
|
+
function buildRefIndex(graph) {
|
|
5502
|
+
const column = (sql) => graph.db.prepare(sql).all().map((row) => row.value);
|
|
5503
|
+
return {
|
|
5504
|
+
taskIds: new Set(column("SELECT DISTINCT task_id AS value FROM workspace_task")),
|
|
5505
|
+
sessionIds: new Set(column("SELECT DISTINCT session_id AS value FROM session_record")),
|
|
5506
|
+
noteRefs: new Set(column("SELECT note_ref AS value FROM note")),
|
|
5507
|
+
// `source:` is 7 characters; what remains is the `<slug>/<path>` the ref was minted from.
|
|
5508
|
+
sourcePaths: new Set(column("SELECT substr(source_ref, 8) AS value FROM source"))
|
|
5509
|
+
};
|
|
5510
|
+
}
|
|
5511
|
+
function assertSharesTag(graph, notes) {
|
|
5512
|
+
let written = 0;
|
|
5513
|
+
for (const group of qualifyingTagGroups(notes)) {
|
|
5514
|
+
for (const [a, b] of crossInitiativePairs(group)) {
|
|
5515
|
+
for (const edge of [
|
|
5516
|
+
{ sourceRef: a.ref, targetRef: b.ref },
|
|
5517
|
+
{ sourceRef: b.ref, targetRef: a.ref }
|
|
5518
|
+
]) {
|
|
5519
|
+
if (graph.edges.assert({ ...edge, relation: RELATIONS.SHARES_TAG })) written++;
|
|
5520
|
+
}
|
|
5521
|
+
}
|
|
5522
|
+
}
|
|
5523
|
+
return written;
|
|
5524
|
+
}
|
|
5525
|
+
function qualifyingTagGroups(notes) {
|
|
5526
|
+
const byTag = /* @__PURE__ */ new Map();
|
|
5527
|
+
for (const note of notes) {
|
|
5528
|
+
for (const tag of new Set(note.tags)) {
|
|
5529
|
+
const group = byTag.get(tag);
|
|
5530
|
+
if (group) group.push(note);
|
|
5531
|
+
else byTag.set(tag, [note]);
|
|
5532
|
+
}
|
|
5533
|
+
}
|
|
5534
|
+
return [...byTag.values()].filter(
|
|
5535
|
+
(group) => group.length <= SHARES_TAG_MAX_NOTES && new Set(group.map((note) => note.initiative)).size > 1
|
|
5536
|
+
);
|
|
5537
|
+
}
|
|
5538
|
+
function crossInitiativePairs(group) {
|
|
5539
|
+
const sorted = [...group].sort((a, b) => a.ref.localeCompare(b.ref));
|
|
5540
|
+
const pairs = [];
|
|
5541
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
5542
|
+
for (let j = i + 1; j < sorted.length; j++) {
|
|
5543
|
+
if (sorted[i].initiative !== sorted[j].initiative) pairs.push([sorted[i], sorted[j]]);
|
|
5544
|
+
}
|
|
5545
|
+
}
|
|
5546
|
+
return pairs;
|
|
5547
|
+
}
|
|
5548
|
+
|
|
5549
|
+
// src/workspace-index/read.ts
|
|
5550
|
+
import { promises as fs36 } from "fs";
|
|
5551
|
+
import matter3 from "gray-matter";
|
|
5552
|
+
import YAML from "yaml";
|
|
5553
|
+
|
|
5554
|
+
// src/workspace-index/spans.ts
|
|
5555
|
+
var FRONTMATTER_FENCE = /^---\r?\n/;
|
|
5556
|
+
var FRONTMATTER_END = /^---[ \t]*\r?$/;
|
|
5557
|
+
function bodyByteOffset(raw) {
|
|
5558
|
+
if (!FRONTMATTER_FENCE.test(raw)) return 0;
|
|
5559
|
+
const lines = raw.split("\n");
|
|
5560
|
+
let consumed = lines[0].length + 1;
|
|
5561
|
+
for (let i = 1; i < lines.length; i++) {
|
|
5562
|
+
consumed += lines[i].length + 1;
|
|
5563
|
+
if (FRONTMATTER_END.test(lines[i])) return Buffer.byteLength(raw.slice(0, consumed), "utf8");
|
|
5564
|
+
}
|
|
5565
|
+
return 0;
|
|
5566
|
+
}
|
|
5567
|
+
function wholeFile(raw) {
|
|
5568
|
+
return { byteOffset: 0, byteLength: Buffer.byteLength(raw, "utf8") };
|
|
5569
|
+
}
|
|
5570
|
+
function locate(raw, field, text) {
|
|
5571
|
+
const trimmed = text.trim();
|
|
5572
|
+
if (trimmed.length === 0) return null;
|
|
5573
|
+
const at = raw.indexOf(trimmed);
|
|
5574
|
+
if (at < 0) return { field, text: trimmed, ...wholeFile(raw) };
|
|
5575
|
+
return {
|
|
5576
|
+
field,
|
|
5577
|
+
text: trimmed,
|
|
5578
|
+
byteOffset: Buffer.byteLength(raw.slice(0, at), "utf8"),
|
|
5579
|
+
byteLength: Buffer.byteLength(trimmed, "utf8")
|
|
5580
|
+
};
|
|
5581
|
+
}
|
|
5582
|
+
function sectionSpans(body, baseByteOffset, field = "body") {
|
|
5583
|
+
const spans = [];
|
|
5584
|
+
let cursor = baseByteOffset;
|
|
5585
|
+
for (const section of splitOnHeadings(body)) {
|
|
5586
|
+
const byteLength = Buffer.byteLength(section, "utf8");
|
|
5587
|
+
if (section.trim().length > 0) {
|
|
5588
|
+
spans.push({ field, text: section, byteOffset: cursor, byteLength });
|
|
5589
|
+
}
|
|
5590
|
+
cursor += byteLength;
|
|
5591
|
+
}
|
|
5592
|
+
return spans;
|
|
5593
|
+
}
|
|
5594
|
+
function splitOnHeadings(body) {
|
|
5595
|
+
const lines = body.split("\n");
|
|
5596
|
+
const sections = [];
|
|
5597
|
+
let current = [];
|
|
5598
|
+
for (const line of lines) {
|
|
5599
|
+
if (/^#{1,6}\s/.test(line) && current.length > 0) {
|
|
5600
|
+
sections.push(current.join("\n") + "\n");
|
|
5601
|
+
current = [];
|
|
5602
|
+
}
|
|
5603
|
+
current.push(line);
|
|
5604
|
+
}
|
|
5605
|
+
if (current.length > 0) sections.push(current.join("\n"));
|
|
5606
|
+
return sections;
|
|
5607
|
+
}
|
|
5608
|
+
|
|
5609
|
+
// src/workspace-index/read.ts
|
|
5610
|
+
var READERS = {
|
|
5611
|
+
initiative: readInitiative2,
|
|
5612
|
+
note: readNote,
|
|
5613
|
+
task: readTask,
|
|
5614
|
+
session: readSession2,
|
|
5615
|
+
source: readSource
|
|
5616
|
+
};
|
|
5617
|
+
async function readRecord(file) {
|
|
5618
|
+
const raw = await fs36.readFile(file.absolutePath, "utf8");
|
|
5619
|
+
return READERS[file.class](file, raw);
|
|
5620
|
+
}
|
|
5621
|
+
function frontmatter(raw) {
|
|
5622
|
+
const parsed = matter3(raw);
|
|
5623
|
+
return { data: coerceDates(parsed.data), body: parsed.content };
|
|
5624
|
+
}
|
|
5625
|
+
function jsonTags(tags) {
|
|
5626
|
+
return tags.length === 0 ? null : JSON.stringify(tags);
|
|
5627
|
+
}
|
|
5628
|
+
function readInitiative2(file, raw) {
|
|
5629
|
+
const { data, body } = frontmatter(raw);
|
|
5630
|
+
const brief = BriefFrontmatterSchema.parse(data);
|
|
5631
|
+
const lead = firstProse(body);
|
|
5632
|
+
return {
|
|
5633
|
+
file,
|
|
5634
|
+
ref: initiativeRef(file.slug),
|
|
5635
|
+
row: {
|
|
5636
|
+
slug: file.slug,
|
|
5637
|
+
title: brief.title,
|
|
5638
|
+
state: brief.state,
|
|
5639
|
+
rank: brief.rank ?? null,
|
|
5640
|
+
ship_target: brief.ship_target ?? null,
|
|
5641
|
+
owner: brief.owner ?? null,
|
|
5642
|
+
task_prefix: brief.task_prefix,
|
|
5643
|
+
updated: brief.updated
|
|
5644
|
+
},
|
|
5645
|
+
spans: compact([
|
|
5646
|
+
locate(raw, "title", brief.title),
|
|
5647
|
+
lead === void 0 ? null : locate(raw, "body", lead)
|
|
5648
|
+
]),
|
|
5649
|
+
tags: [],
|
|
5650
|
+
body: ""
|
|
5651
|
+
};
|
|
5652
|
+
}
|
|
5653
|
+
function readNote(file, raw) {
|
|
5654
|
+
const { data, body } = frontmatter(raw);
|
|
5655
|
+
const note = NoteFrontmatterSchema.parse(data);
|
|
5656
|
+
const tags = note.tags ?? [];
|
|
5657
|
+
return {
|
|
5658
|
+
file,
|
|
5659
|
+
ref: noteRef(`${file.slug}/${basename(file.path)}`),
|
|
5660
|
+
row: {
|
|
5661
|
+
initiative: file.slug,
|
|
5662
|
+
filename: basename(file.path),
|
|
5663
|
+
kind: note.kind,
|
|
5664
|
+
title: note.title,
|
|
5665
|
+
created: note.created,
|
|
5666
|
+
tags: jsonTags(tags),
|
|
5667
|
+
// `hits` and `promoted_at` are the promotion machinery of the design's
|
|
5668
|
+
// §4 and the one thing here not derivable from the files, so a rebuild
|
|
5669
|
+
// resets them to zero. Owner accepted that on 2026-09-10.
|
|
5670
|
+
hits: 0,
|
|
5671
|
+
promoted_at: null
|
|
5672
|
+
},
|
|
5673
|
+
spans: [
|
|
5674
|
+
...compact([locate(raw, "title", note.title)]),
|
|
5675
|
+
...sectionSpans(body, bodyByteOffset(raw))
|
|
5676
|
+
],
|
|
5677
|
+
tags,
|
|
5678
|
+
body
|
|
5679
|
+
};
|
|
5680
|
+
}
|
|
5681
|
+
function readTask(file, raw) {
|
|
5682
|
+
const task = TaskSchema.parse(coerceDates(YAML.parse(raw)));
|
|
5683
|
+
return {
|
|
5684
|
+
file,
|
|
5685
|
+
ref: taskRef(task.id),
|
|
5686
|
+
row: {
|
|
5687
|
+
initiative: file.slug,
|
|
5688
|
+
task_id: task.id,
|
|
5689
|
+
title: task.title,
|
|
5690
|
+
status: task.status,
|
|
5691
|
+
priority: task.priority,
|
|
5692
|
+
severity: task.severity ?? null,
|
|
5693
|
+
estimate: task.estimate ?? null,
|
|
5694
|
+
tags: jsonTags(task.tags ?? []),
|
|
5695
|
+
created: task.created,
|
|
5696
|
+
updated: task.updated,
|
|
5697
|
+
done_at: task.done_at
|
|
5698
|
+
},
|
|
5699
|
+
spans: compact([
|
|
5700
|
+
locate(raw, "title", task.title),
|
|
5701
|
+
task.done_when !== void 0 && locate(raw, "done_when", task.done_when),
|
|
5702
|
+
task.notes !== void 0 && locate(raw, "notes", task.notes)
|
|
5703
|
+
]),
|
|
5704
|
+
tags: [],
|
|
5705
|
+
body: ""
|
|
5706
|
+
};
|
|
5707
|
+
}
|
|
5708
|
+
function readSession2(file, raw) {
|
|
5709
|
+
const { data, body } = frontmatter(raw);
|
|
5710
|
+
const session = SessionFrontmatterSchema.parse(data);
|
|
5711
|
+
return {
|
|
5712
|
+
file,
|
|
5713
|
+
ref: sessionRef(session.session_id),
|
|
5714
|
+
row: {
|
|
5715
|
+
initiative: file.slug,
|
|
5716
|
+
session_id: session.session_id,
|
|
5717
|
+
started: session.started,
|
|
5718
|
+
ended: session.ended,
|
|
5719
|
+
track: session.track,
|
|
5720
|
+
parent_session_id: session.parent_session_id ?? null
|
|
5721
|
+
},
|
|
5722
|
+
spans: sectionSpans(body, bodyByteOffset(raw)),
|
|
5723
|
+
tags: [],
|
|
5724
|
+
body: ""
|
|
5725
|
+
};
|
|
5726
|
+
}
|
|
5727
|
+
function readSource(file, raw) {
|
|
5728
|
+
const title = firstHeading2(raw) ?? basename(file.path).replace(/\.md$/, "");
|
|
5729
|
+
return {
|
|
5730
|
+
file,
|
|
5731
|
+
ref: sourceRef(`${file.slug}/${sourcesRelative(file.path)}`),
|
|
5732
|
+
row: {
|
|
5733
|
+
initiative: file.slug,
|
|
5734
|
+
title,
|
|
5735
|
+
kind: inferSourceKind(basename(file.path)),
|
|
5736
|
+
added: null
|
|
5737
|
+
},
|
|
5738
|
+
spans: [...compact([locate(raw, "title", title)]), ...sectionSpans(raw, 0)],
|
|
5739
|
+
tags: [],
|
|
5740
|
+
body: ""
|
|
5741
|
+
};
|
|
5742
|
+
}
|
|
5743
|
+
var PR_FILENAME2 = /^pr-\d+-/;
|
|
5744
|
+
var DEEPDIVE_FILENAME2 = /^deepdive-/;
|
|
5745
|
+
var SESSION_FILENAME2 = /^\d{4}-\d{2}-\d{2}-/;
|
|
5746
|
+
function inferSourceKind(filename) {
|
|
5747
|
+
if (PR_FILENAME2.test(filename)) return "pr";
|
|
5748
|
+
if (DEEPDIVE_FILENAME2.test(filename)) return "deepdive";
|
|
5749
|
+
if (SESSION_FILENAME2.test(filename)) return "session";
|
|
5750
|
+
return "pointer";
|
|
5751
|
+
}
|
|
5752
|
+
function basename(relativePath) {
|
|
5753
|
+
return relativePath.slice(relativePath.lastIndexOf("/") + 1);
|
|
5754
|
+
}
|
|
5755
|
+
function sourcesRelative(relativePath) {
|
|
5756
|
+
return relativePath.replace(/^[^/]+\/sources\//, "");
|
|
5757
|
+
}
|
|
5758
|
+
function firstHeading2(text) {
|
|
5759
|
+
for (const line of text.split("\n", 200)) {
|
|
5760
|
+
const match = /^#{1,6}\s+(.+?)\s*$/.exec(line);
|
|
5761
|
+
if (match) return match[1];
|
|
5762
|
+
}
|
|
5763
|
+
return void 0;
|
|
5764
|
+
}
|
|
5765
|
+
function firstProse(body) {
|
|
5766
|
+
for (const block of body.split(/\n\s*\n/)) {
|
|
5767
|
+
const trimmed = block.trim();
|
|
5768
|
+
if (trimmed.length > 0 && !trimmed.startsWith("#")) return trimmed;
|
|
5769
|
+
}
|
|
5770
|
+
return void 0;
|
|
5771
|
+
}
|
|
5772
|
+
function compact(spans) {
|
|
5773
|
+
return spans.filter((span) => Boolean(span));
|
|
5774
|
+
}
|
|
5775
|
+
|
|
5776
|
+
// src/workspace-index/scan.ts
|
|
5777
|
+
import { promises as fs42 } from "fs";
|
|
5778
|
+
import path55 from "path";
|
|
5779
|
+
|
|
5780
|
+
// src/lint/index.ts
|
|
5781
|
+
import { promises as fs41 } from "fs";
|
|
5782
|
+
import path54 from "path";
|
|
5783
|
+
|
|
5784
|
+
// src/lint/brief.ts
|
|
5785
|
+
import { promises as fs37 } from "fs";
|
|
5786
|
+
import path49 from "path";
|
|
5787
|
+
|
|
5788
|
+
// src/lint/hashes.ts
|
|
5789
|
+
import { promises as fs38 } from "fs";
|
|
5790
|
+
import path50 from "path";
|
|
5791
|
+
async function lintHashes(slug, initiativeDir) {
|
|
5792
|
+
const manifest = await readArtifactHashes(initiativeDir);
|
|
5793
|
+
const findings = [];
|
|
5794
|
+
for (const [relPath, storedHash] of Object.entries(manifest)) {
|
|
5795
|
+
let content;
|
|
5796
|
+
try {
|
|
5797
|
+
content = await fs38.readFile(path50.join(initiativeDir, relPath), "utf8");
|
|
5798
|
+
} catch (err) {
|
|
5799
|
+
const code = err.code;
|
|
5800
|
+
if (code === "ENOENT") continue;
|
|
5801
|
+
throw err;
|
|
5802
|
+
}
|
|
5803
|
+
if (hashContent(content) === storedHash) continue;
|
|
5804
|
+
findings.push({
|
|
5805
|
+
level: "warn",
|
|
5806
|
+
slug,
|
|
5807
|
+
file: relPath,
|
|
5808
|
+
message: `${relPath} was hand-edited outside active-work (content no longer matches the last CLI write) \u2014 re-apply the change through the CLI/MCP tools instead of editing the file directly`
|
|
5809
|
+
});
|
|
5810
|
+
}
|
|
5811
|
+
return findings;
|
|
5812
|
+
}
|
|
5813
|
+
|
|
5814
|
+
// src/lint/open-loops.ts
|
|
5815
|
+
import path51 from "path";
|
|
5816
|
+
|
|
5817
|
+
// src/lint/task.ts
|
|
5818
|
+
import { promises as fs39 } from "fs";
|
|
5819
|
+
import path52 from "path";
|
|
5820
|
+
import YAML2 from "yaml";
|
|
5821
|
+
|
|
5822
|
+
// src/lint/zero-loops.ts
|
|
5823
|
+
import { promises as fs40 } from "fs";
|
|
5824
|
+
import path53 from "path";
|
|
5825
|
+
import YAML3 from "yaml";
|
|
5826
|
+
|
|
5827
|
+
// src/lint/index.ts
|
|
5828
|
+
async function listInitiativeSlugs2(activeRoot) {
|
|
5829
|
+
let entries;
|
|
5830
|
+
try {
|
|
5831
|
+
entries = await fs41.readdir(activeRoot, { withFileTypes: true });
|
|
5832
|
+
} catch (err) {
|
|
5833
|
+
const code = err.code;
|
|
5834
|
+
if (code === "ENOENT") return [];
|
|
5835
|
+
throw err;
|
|
5836
|
+
}
|
|
5837
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort();
|
|
5838
|
+
}
|
|
5839
|
+
|
|
5840
|
+
// src/workspace-index/scan.ts
|
|
5841
|
+
async function readDirents(dir) {
|
|
5842
|
+
try {
|
|
5843
|
+
return await fs42.readdir(dir, { withFileTypes: true });
|
|
5844
|
+
} catch {
|
|
5845
|
+
return [];
|
|
5846
|
+
}
|
|
5847
|
+
}
|
|
5848
|
+
function isVisible(entry, extensions) {
|
|
5849
|
+
return entry.isFile() && !entry.name.startsWith(".") && extensions.some((ext) => entry.name.endsWith(ext));
|
|
5850
|
+
}
|
|
5851
|
+
async function filesIn(dir, extensions) {
|
|
5852
|
+
const entries = await readDirents(dir);
|
|
5853
|
+
return entries.filter((entry) => isVisible(entry, extensions)).map((entry) => path55.join(dir, entry.name)).sort();
|
|
5854
|
+
}
|
|
5855
|
+
async function taskFiles(tasksDir) {
|
|
5856
|
+
const own = await filesIn(tasksDir, [".yml", ".yaml"]);
|
|
5857
|
+
const archive = await filesIn(path55.join(tasksDir, "archive"), [".yml", ".yaml"]);
|
|
5858
|
+
return [...own, ...archive];
|
|
5859
|
+
}
|
|
5860
|
+
async function collectSlug(activeRoot, slug) {
|
|
5861
|
+
const dir = path55.join(activeRoot, slug);
|
|
5862
|
+
const brief = path55.join(dir, "brief.md");
|
|
5863
|
+
const [notes, tasks, sessions2, sources] = await Promise.all([
|
|
5864
|
+
filesIn(path55.join(dir, "sources", "notes"), [".md"]),
|
|
5865
|
+
taskFiles(path55.join(dir, "tasks")),
|
|
5866
|
+
filesIn(path55.join(dir, "sessions"), [".md"]),
|
|
5867
|
+
filesIn(path55.join(dir, "sources"), [".md"])
|
|
5868
|
+
]);
|
|
5869
|
+
return [
|
|
5870
|
+
...notes.map((p) => ["note", p]),
|
|
5871
|
+
...tasks.map((p) => ["task", p]),
|
|
5872
|
+
...sessions2.map((p) => ["session", p]),
|
|
5873
|
+
...sources.map((p) => ["source", p]),
|
|
5874
|
+
...await exists2(brief) ? [["initiative", brief]] : []
|
|
5875
|
+
];
|
|
5876
|
+
}
|
|
5877
|
+
async function exists2(target) {
|
|
5878
|
+
try {
|
|
5879
|
+
await fs42.access(target);
|
|
5880
|
+
return true;
|
|
5881
|
+
} catch {
|
|
5882
|
+
return false;
|
|
5883
|
+
}
|
|
5884
|
+
}
|
|
5885
|
+
async function describe(activeRoot, slug, entry) {
|
|
5886
|
+
const [cls, absolutePath] = entry;
|
|
5887
|
+
try {
|
|
5888
|
+
const stat = await fs42.stat(absolutePath);
|
|
5889
|
+
return {
|
|
5890
|
+
class: cls,
|
|
5891
|
+
slug,
|
|
5892
|
+
path: toRelative(activeRoot, absolutePath),
|
|
5893
|
+
absolutePath,
|
|
5894
|
+
size: stat.size,
|
|
5895
|
+
mtime: stat.mtime.toISOString()
|
|
5896
|
+
};
|
|
5897
|
+
} catch {
|
|
5898
|
+
return null;
|
|
5899
|
+
}
|
|
5900
|
+
}
|
|
5901
|
+
async function scanWorkspace(activeRoot) {
|
|
5902
|
+
const slugs = await listInitiativeSlugs2(activeRoot);
|
|
5903
|
+
const perSlug = await Promise.all(
|
|
5904
|
+
slugs.map(async (slug) => {
|
|
5905
|
+
const entries = await collectSlug(activeRoot, slug);
|
|
5906
|
+
const described = await Promise.all(entries.map((e) => describe(activeRoot, slug, e)));
|
|
5907
|
+
return described.filter((file) => file !== null);
|
|
5908
|
+
})
|
|
5909
|
+
);
|
|
5910
|
+
return perSlug.flat().sort((a, b) => a.path.localeCompare(b.path));
|
|
5911
|
+
}
|
|
5912
|
+
|
|
5913
|
+
// src/workspace-index/write.ts
|
|
5914
|
+
var TABLES = {
|
|
5915
|
+
initiative: {
|
|
5916
|
+
table: "initiative",
|
|
5917
|
+
refColumn: "initiative_ref",
|
|
5918
|
+
columns: ["slug", "title", "state", "rank", "ship_target", "owner", "task_prefix", "updated"]
|
|
5919
|
+
},
|
|
5920
|
+
note: {
|
|
5921
|
+
table: "note",
|
|
5922
|
+
refColumn: "note_ref",
|
|
5923
|
+
columns: ["initiative", "filename", "kind", "title", "created", "tags", "hits", "promoted_at"]
|
|
5924
|
+
},
|
|
5925
|
+
task: {
|
|
5926
|
+
table: "workspace_task",
|
|
5927
|
+
refColumn: "task_ref",
|
|
5928
|
+
columns: [
|
|
5929
|
+
"initiative",
|
|
5930
|
+
"task_id",
|
|
5931
|
+
"title",
|
|
5932
|
+
"status",
|
|
5933
|
+
"priority",
|
|
5934
|
+
"severity",
|
|
5935
|
+
"estimate",
|
|
5936
|
+
"tags",
|
|
5937
|
+
"created",
|
|
5938
|
+
"updated",
|
|
5939
|
+
"done_at"
|
|
5940
|
+
]
|
|
5941
|
+
},
|
|
5942
|
+
session: {
|
|
5943
|
+
table: "session_record",
|
|
5944
|
+
refColumn: "session_ref",
|
|
5945
|
+
columns: ["initiative", "session_id", "started", "ended", "track", "parent_session_id"]
|
|
5946
|
+
},
|
|
5947
|
+
source: {
|
|
5948
|
+
table: "source",
|
|
5949
|
+
refColumn: "source_ref",
|
|
5950
|
+
columns: ["initiative", "title", "kind", "added"]
|
|
5951
|
+
}
|
|
5952
|
+
};
|
|
5953
|
+
var CLASSES = Object.keys(TABLES);
|
|
5954
|
+
function insertSql({ table, refColumn, columns }) {
|
|
5955
|
+
const all = ["path", refColumn, ...columns];
|
|
5956
|
+
const names = all.map((c) => `"${c}"`).join(", ");
|
|
5957
|
+
return `INSERT OR REPLACE INTO "${table}" (${names}) VALUES (${all.map(() => "?").join(", ")})`;
|
|
5958
|
+
}
|
|
5959
|
+
function spanSourceId(watermarkId) {
|
|
5960
|
+
return WORKSPACE_SPAN_SOURCE_BASE + watermarkId;
|
|
5961
|
+
}
|
|
5962
|
+
var WorkspaceWriter = class {
|
|
5963
|
+
constructor(graph) {
|
|
5964
|
+
this.graph = graph;
|
|
5965
|
+
this.inserts = Object.fromEntries(
|
|
5966
|
+
CLASSES.map((cls) => [cls, graph.db.prepare(insertSql(TABLES[cls]))])
|
|
5967
|
+
);
|
|
5968
|
+
this.deletes = CLASSES.map(
|
|
5969
|
+
(cls) => graph.db.prepare(`DELETE FROM "${TABLES[cls].table}" WHERE path = ?`)
|
|
5970
|
+
);
|
|
5971
|
+
this.deleteSpans = graph.db.prepare("DELETE FROM search_span WHERE source_id = ?");
|
|
5972
|
+
}
|
|
5973
|
+
graph;
|
|
5974
|
+
inserts;
|
|
5975
|
+
deletes;
|
|
5976
|
+
deleteSpans;
|
|
5977
|
+
/**
|
|
5978
|
+
* Delete every row and span one file produced.
|
|
5979
|
+
*
|
|
5980
|
+
* Spans go by `source_id`, not by owner ref: `session:` owners are shared
|
|
5981
|
+
* with the transcript side of the same table, so purging by owner would take
|
|
5982
|
+
* a mined transcript's spans with it. The offset id names the file alone.
|
|
5983
|
+
*
|
|
5984
|
+
* This is the *opposite* of the transcript policy in the same database. A
|
|
5985
|
+
* transcript that disappears keeps its rows on purpose — Claude Code prunes
|
|
5986
|
+
* transcripts, and the sessions they witnessed still happened. A workspace
|
|
5987
|
+
* file that disappears was deleted or archived by the operator, and its rows
|
|
5988
|
+
* are a claim about a file that no longer says anything.
|
|
5989
|
+
*/
|
|
5990
|
+
purge(relativePath, watermarkId) {
|
|
5991
|
+
for (const statement of this.deletes) statement.run(relativePath);
|
|
5992
|
+
this.deleteSpans.run(spanSourceId(watermarkId));
|
|
5993
|
+
}
|
|
5994
|
+
/**
|
|
5995
|
+
* Write one record's row and spans, replacing whatever the file produced
|
|
5996
|
+
* before. Not transactional on its own — the caller batches many of these
|
|
5997
|
+
* into one transaction, because a WAL commit per file is the difference
|
|
5998
|
+
* between a pass that runs in a moment and one that does not.
|
|
5999
|
+
*/
|
|
6000
|
+
apply(record, watermarkId) {
|
|
6001
|
+
const spec = TABLES[record.file.class];
|
|
6002
|
+
this.purge(record.file.path, watermarkId);
|
|
6003
|
+
this.inserts[record.file.class].run(
|
|
6004
|
+
record.file.path,
|
|
6005
|
+
record.ref,
|
|
6006
|
+
...spec.columns.map((column) => record.row[column] ?? null)
|
|
6007
|
+
);
|
|
6008
|
+
for (const span of record.spans) {
|
|
6009
|
+
this.graph.spans.index(
|
|
6010
|
+
{
|
|
6011
|
+
ownerRef: record.ref,
|
|
6012
|
+
field: span.field,
|
|
6013
|
+
sourceId: spanSourceId(watermarkId),
|
|
6014
|
+
byteOffset: span.byteOffset,
|
|
6015
|
+
byteLength: span.byteLength
|
|
6016
|
+
},
|
|
6017
|
+
span.text
|
|
6018
|
+
);
|
|
6019
|
+
}
|
|
6020
|
+
}
|
|
6021
|
+
};
|
|
6022
|
+
function resetWorkspaceIndex(graph) {
|
|
6023
|
+
graph.db.transaction(() => {
|
|
6024
|
+
for (const cls of CLASSES) graph.db.exec(`DELETE FROM "${TABLES[cls].table}"`);
|
|
6025
|
+
for (const row of graph.workspaceFiles.list()) graph.workspaceFiles.rewind(row.sourceKey);
|
|
6026
|
+
})();
|
|
6027
|
+
}
|
|
6028
|
+
|
|
6029
|
+
// src/workspace-index/refresh.ts
|
|
6030
|
+
function isUnchanged(row, file) {
|
|
6031
|
+
return row.lastOffset > 0 && row.fileSize === file.size && row.fileMtime === file.mtime;
|
|
6032
|
+
}
|
|
6033
|
+
var BATCH = 200;
|
|
6034
|
+
async function readAttempt(file, watermarkId) {
|
|
6035
|
+
try {
|
|
6036
|
+
return { file, watermarkId, record: await readRecord(file), reason: null };
|
|
6037
|
+
} catch (err) {
|
|
6038
|
+
return {
|
|
6039
|
+
file,
|
|
6040
|
+
watermarkId,
|
|
6041
|
+
record: null,
|
|
6042
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
6043
|
+
};
|
|
6044
|
+
}
|
|
6045
|
+
}
|
|
6046
|
+
function applyBatch(graph, writer, batch) {
|
|
6047
|
+
graph.db.transaction(() => {
|
|
6048
|
+
for (const attempt of batch) {
|
|
6049
|
+
if (attempt.record === null) {
|
|
6050
|
+
writer.purge(attempt.file.path, attempt.watermarkId);
|
|
6051
|
+
graph.workspaceFiles.markStatus(attempt.file.path, "quarantined", attempt.reason);
|
|
6052
|
+
continue;
|
|
6053
|
+
}
|
|
6054
|
+
writer.apply(attempt.record, attempt.watermarkId);
|
|
6055
|
+
graph.workspaceFiles.advance(attempt.file.path, {
|
|
6056
|
+
lastOffset: attempt.file.size,
|
|
6057
|
+
fileSize: attempt.file.size,
|
|
6058
|
+
fileMtime: attempt.file.mtime
|
|
6059
|
+
});
|
|
6060
|
+
}
|
|
6061
|
+
})();
|
|
6062
|
+
}
|
|
6063
|
+
function removeVanished(graph, writer, present) {
|
|
6064
|
+
let removed = 0;
|
|
6065
|
+
for (const row of graph.workspaceFiles.list()) {
|
|
6066
|
+
if (present.has(row.sourceKey)) continue;
|
|
6067
|
+
writer.purge(row.sourceKey, row.sourceId);
|
|
6068
|
+
graph.db.prepare("DELETE FROM workspace_file WHERE source_id = ?").run(row.sourceId);
|
|
6069
|
+
removed++;
|
|
6070
|
+
}
|
|
6071
|
+
return removed;
|
|
6072
|
+
}
|
|
6073
|
+
async function collectNoteFacts(activeRoot, graph) {
|
|
6074
|
+
const rows = graph.db.prepare("SELECT path, note_ref, initiative, tags FROM note ORDER BY path").all();
|
|
6075
|
+
const facts = await Promise.all(
|
|
6076
|
+
rows.map(async (row) => ({
|
|
6077
|
+
ref: row.note_ref,
|
|
6078
|
+
initiative: row.initiative,
|
|
6079
|
+
tags: JSON.parse(row.tags ?? "[]") ?? [],
|
|
6080
|
+
body: await readBody(toAbsolute(activeRoot, row.path))
|
|
6081
|
+
}))
|
|
6082
|
+
);
|
|
6083
|
+
return facts;
|
|
6084
|
+
}
|
|
6085
|
+
async function readBody(absolutePath) {
|
|
6086
|
+
try {
|
|
6087
|
+
return matter4(await fs43.readFile(absolutePath, "utf8")).content;
|
|
6088
|
+
} catch {
|
|
6089
|
+
return "";
|
|
6090
|
+
}
|
|
6091
|
+
}
|
|
6092
|
+
function countRows(graph) {
|
|
6093
|
+
const count = (table) => graph.db.prepare(`SELECT COUNT(*) AS n FROM "${table}"`).get().n;
|
|
6094
|
+
return {
|
|
6095
|
+
initiative: count("initiative"),
|
|
6096
|
+
note: count("note"),
|
|
6097
|
+
task: count("workspace_task"),
|
|
6098
|
+
session: count("session_record"),
|
|
6099
|
+
source: count("source")
|
|
6100
|
+
};
|
|
6101
|
+
}
|
|
6102
|
+
async function refreshWorkspace(graph, options = {}) {
|
|
6103
|
+
const activeRoot = options.activeRoot ?? getActiveRoot();
|
|
6104
|
+
const writer = new WorkspaceWriter(graph);
|
|
6105
|
+
const files = await scanWorkspace(activeRoot);
|
|
6106
|
+
const malformed = [];
|
|
6107
|
+
let indexed = 0;
|
|
6108
|
+
let unchanged = 0;
|
|
6109
|
+
const changed = [];
|
|
6110
|
+
graph.db.transaction(() => {
|
|
6111
|
+
for (const file of files) {
|
|
6112
|
+
const row = graph.workspaceFiles.ensure(file.path);
|
|
6113
|
+
if (!options.full && isUnchanged(row, file)) unchanged++;
|
|
6114
|
+
else changed.push({ file, watermarkId: row.sourceId });
|
|
6115
|
+
}
|
|
6116
|
+
})();
|
|
6117
|
+
for (let start = 0; start < changed.length; start += BATCH) {
|
|
6118
|
+
const batch = await Promise.all(
|
|
6119
|
+
changed.slice(start, start + BATCH).map((e) => readAttempt(e.file, e.watermarkId))
|
|
6120
|
+
);
|
|
6121
|
+
applyBatch(graph, writer, batch);
|
|
6122
|
+
indexed += batch.filter((a) => a.record !== null).length;
|
|
6123
|
+
for (const failed of batch.filter((a) => a.record === null)) {
|
|
6124
|
+
malformed.push({ path: failed.file.path, reason: failed.reason ?? "unreadable" });
|
|
6125
|
+
}
|
|
6126
|
+
}
|
|
6127
|
+
const removed = removeVanished(graph, writer, new Set(files.map((f) => f.path)));
|
|
6128
|
+
const edges = rebuildEdges(graph, await collectNoteFacts(activeRoot, graph));
|
|
6129
|
+
return {
|
|
6130
|
+
files: files.length,
|
|
6131
|
+
indexed,
|
|
6132
|
+
unchanged,
|
|
6133
|
+
removed,
|
|
6134
|
+
malformed,
|
|
6135
|
+
rows: countRows(graph),
|
|
6136
|
+
edges,
|
|
6137
|
+
orphanRatio: graph.spans.orphanRatio()
|
|
6138
|
+
};
|
|
6139
|
+
}
|
|
6140
|
+
|
|
6141
|
+
// src/session-index/preserve.ts
|
|
6142
|
+
var UPSERT = `
|
|
6143
|
+
INSERT INTO preserved_row (table_name, identity, row_key, payload, origin, mode)
|
|
6144
|
+
VALUES (@table, @identity, @key, @payload, @origin, @mode)
|
|
6145
|
+
ON CONFLICT (table_name, row_key) DO UPDATE SET
|
|
6146
|
+
identity = excluded.identity, payload = excluded.payload,
|
|
6147
|
+
origin = excluded.origin, mode = excluded.mode
|
|
6148
|
+
`;
|
|
6149
|
+
function preserveRow(graph, row) {
|
|
6150
|
+
if (row.identity.length === 0) throw new Error(`preserveRow: ${row.table} needs an identity`);
|
|
6151
|
+
if (row.mode === "merge" && row.identity.length !== 1) {
|
|
6152
|
+
throw new Error(`preserveRow: a merge over ${row.table} matches exactly one column`);
|
|
6153
|
+
}
|
|
6154
|
+
const missing = row.mode === "insert" ? row.identity.filter((c) => !(c in row.payload)) : [];
|
|
6155
|
+
if (missing.length > 0) {
|
|
6156
|
+
throw new Error(`preserveRow: ${row.table} payload is missing ${missing.join(", ")}`);
|
|
6157
|
+
}
|
|
6158
|
+
graph.db.prepare(UPSERT).run({
|
|
6159
|
+
...row,
|
|
6160
|
+
identity: JSON.stringify(row.identity),
|
|
6161
|
+
payload: JSON.stringify(row.payload)
|
|
6162
|
+
});
|
|
6163
|
+
}
|
|
6164
|
+
function listPreserved(graph) {
|
|
6165
|
+
const rows = graph.db.prepare("SELECT * FROM preserved_row ORDER BY table_name, row_key").all();
|
|
6166
|
+
return rows.map((row) => ({
|
|
6167
|
+
table: row.table_name,
|
|
6168
|
+
identity: JSON.parse(row.identity),
|
|
6169
|
+
key: row.row_key,
|
|
6170
|
+
payload: JSON.parse(row.payload),
|
|
6171
|
+
origin: row.origin,
|
|
6172
|
+
mode: row.mode
|
|
6173
|
+
}));
|
|
6174
|
+
}
|
|
6175
|
+
var UNREACHABLE_SESSIONS = `
|
|
6176
|
+
SELECT s.session_id FROM session s
|
|
6177
|
+
WHERE NOT EXISTS (
|
|
6178
|
+
SELECT 1 FROM fact f JOIN transcript t ON t.source_id = f.transcript_id
|
|
6179
|
+
WHERE f.session_id = s.session_id AND t.status = 'ok'
|
|
6180
|
+
)
|
|
6181
|
+
ORDER BY s.session_id
|
|
6182
|
+
`;
|
|
6183
|
+
var SESSION_OWNED = [
|
|
6184
|
+
{ table: "session", identity: ["session_id"], key: (r) => String(r.session_id) },
|
|
6185
|
+
{ table: "turn", identity: ["prompt_id"], key: (r) => String(r.prompt_id) },
|
|
6186
|
+
{
|
|
6187
|
+
table: "session_model_usage",
|
|
6188
|
+
identity: ["session_id", "model"],
|
|
6189
|
+
key: (r) => `${r.session_id}:${r.model}`
|
|
6190
|
+
},
|
|
6191
|
+
{
|
|
6192
|
+
table: "fact",
|
|
6193
|
+
identity: ["transcript_id", "byte_offset"],
|
|
6194
|
+
key: (r) => `${r.transcript_id}:${r.byte_offset}`
|
|
6195
|
+
},
|
|
6196
|
+
{
|
|
6197
|
+
table: "permission_phase",
|
|
6198
|
+
identity: ["session_id", "t_valid", "to_mode"],
|
|
6199
|
+
key: (r) => `${r.session_id}:${r.t_valid}:${r.to_mode}`
|
|
6200
|
+
},
|
|
6201
|
+
{
|
|
6202
|
+
table: "human_edit",
|
|
6203
|
+
identity: ["session_id", "file_path", "ts"],
|
|
6204
|
+
key: (r) => `${r.session_id}:${r.file_path}:${r.ts}`
|
|
6205
|
+
}
|
|
6206
|
+
];
|
|
6207
|
+
var SYNTHETIC_KEYS = /* @__PURE__ */ new Set(["fact_id", "phase_id", "edit_id", "checkpoint_id"]);
|
|
6208
|
+
function payloadOf(row) {
|
|
6209
|
+
return Object.fromEntries(Object.entries(row).filter(([column]) => !SYNTHETIC_KEYS.has(column)));
|
|
6210
|
+
}
|
|
6211
|
+
function preserveUnreachable(graph, origin) {
|
|
6212
|
+
const ids = graph.db.prepare(UNREACHABLE_SESSIONS).all().map(
|
|
6213
|
+
(row) => row.session_id
|
|
6214
|
+
);
|
|
6215
|
+
if (ids.length === 0) return 0;
|
|
6216
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
6217
|
+
let preserved = 0;
|
|
6218
|
+
graph.db.transaction(() => {
|
|
6219
|
+
for (const spec of SESSION_OWNED) {
|
|
6220
|
+
const rows = graph.db.prepare(`SELECT * FROM "${spec.table}" WHERE session_id IN (${placeholders})`).all(...ids);
|
|
6221
|
+
for (const row of rows) {
|
|
6222
|
+
const payload = payloadOf(row);
|
|
6223
|
+
preserveRow(graph, {
|
|
6224
|
+
table: spec.table,
|
|
6225
|
+
identity: spec.identity,
|
|
6226
|
+
key: spec.key(row),
|
|
6227
|
+
payload,
|
|
6228
|
+
origin,
|
|
6229
|
+
mode: "insert"
|
|
6230
|
+
});
|
|
6231
|
+
preserved++;
|
|
6232
|
+
}
|
|
6233
|
+
}
|
|
6234
|
+
})();
|
|
6235
|
+
return preserved;
|
|
6236
|
+
}
|
|
6237
|
+
function restore(graph, row) {
|
|
6238
|
+
const columns = Object.keys(row.payload);
|
|
6239
|
+
const names = columns.map((c) => `"${c}"`).join(", ");
|
|
6240
|
+
const values = columns.map((c) => `@${c}`).join(", ");
|
|
6241
|
+
const match = row.identity.map((c) => `"${c}" IS @${c}`).join(" AND ");
|
|
6242
|
+
const info = graph.db.prepare(
|
|
6243
|
+
`INSERT INTO "${row.table}" (${names}) SELECT ${values}
|
|
6244
|
+
WHERE NOT EXISTS (SELECT 1 FROM "${row.table}" WHERE ${match})`
|
|
6245
|
+
).run(row.payload);
|
|
6246
|
+
return info.changes > 0;
|
|
6247
|
+
}
|
|
6248
|
+
function merge(graph, row) {
|
|
6249
|
+
const assignments = Object.keys(row.payload).map((c) => `"${c}" = @${c}`).join(", ");
|
|
6250
|
+
const info = graph.db.prepare(`UPDATE "${row.table}" SET ${assignments} WHERE "${row.identity[0]}" = @__key`).run({ ...row.payload, __key: row.key });
|
|
6251
|
+
return info.changes > 0;
|
|
6252
|
+
}
|
|
6253
|
+
function replayPreserved(graph) {
|
|
6254
|
+
const summary = { restored: 0, merged: 0, skipped: 0 };
|
|
6255
|
+
graph.db.transaction(() => {
|
|
6256
|
+
for (const row of listPreserved(graph)) {
|
|
6257
|
+
if (row.mode === "merge") {
|
|
6258
|
+
if (merge(graph, row)) summary.merged++;
|
|
6259
|
+
else summary.skipped++;
|
|
6260
|
+
continue;
|
|
6261
|
+
}
|
|
6262
|
+
if (restore(graph, row)) summary.restored++;
|
|
6263
|
+
else summary.skipped++;
|
|
6264
|
+
}
|
|
6265
|
+
})();
|
|
6266
|
+
return summary;
|
|
6267
|
+
}
|
|
6268
|
+
|
|
5277
6269
|
// src/session-index/refresh.ts
|
|
5278
6270
|
var LOCK_STALE_MS = 6e4;
|
|
5279
6271
|
function refreshLockPath() {
|
|
@@ -5281,8 +6273,8 @@ function refreshLockPath() {
|
|
|
5281
6273
|
}
|
|
5282
6274
|
async function withRefreshLock(fn) {
|
|
5283
6275
|
const target = refreshLockPath();
|
|
5284
|
-
await
|
|
5285
|
-
await
|
|
6276
|
+
await fs44.mkdir(path56.dirname(target), { recursive: true });
|
|
6277
|
+
await fs44.writeFile(target, "", { flag: "a" });
|
|
5286
6278
|
const release = await lockfile.lock(target, {
|
|
5287
6279
|
realpath: false,
|
|
5288
6280
|
stale: LOCK_STALE_MS,
|
|
@@ -5300,7 +6292,11 @@ async function runRefresh(options = {}) {
|
|
|
5300
6292
|
const graph = options.graph ?? openGraph(options.dbPath ?? defaultGraphPath());
|
|
5301
6293
|
const owned = options.graph === void 0;
|
|
5302
6294
|
try {
|
|
5303
|
-
if (options.full)
|
|
6295
|
+
if (options.full) {
|
|
6296
|
+
preserveUnreachable(graph, "transcript pruned before this rebuild (TP-41)");
|
|
6297
|
+
resetIndex(graph);
|
|
6298
|
+
resetWorkspaceIndex(graph);
|
|
6299
|
+
}
|
|
5304
6300
|
const discovered = await discoverTranscripts(options.root ?? transcriptsRoot());
|
|
5305
6301
|
const visiting = discovered.slice(0, options.limit ?? discovered.length);
|
|
5306
6302
|
const verify = options.verifyHashes ?? options.full ?? false;
|
|
@@ -5310,6 +6306,11 @@ async function runRefresh(options = {}) {
|
|
|
5310
6306
|
withContentHash: verify,
|
|
5311
6307
|
resolveTasks: taskResolver(options.taskRoot)
|
|
5312
6308
|
});
|
|
6309
|
+
const workspace = options.skipWorkspace ? null : await refreshWorkspace(graph, {
|
|
6310
|
+
activeRoot: options.activeRoot ?? options.taskRoot,
|
|
6311
|
+
full: options.full
|
|
6312
|
+
});
|
|
6313
|
+
const preserved = replayPreserved(graph);
|
|
5313
6314
|
return {
|
|
5314
6315
|
startedAt: startedAt2,
|
|
5315
6316
|
durationMs: Date.now() - started,
|
|
@@ -5325,9 +6326,14 @@ async function runRefresh(options = {}) {
|
|
|
5325
6326
|
turnsRolledUp: summary.turnsRolledUp,
|
|
5326
6327
|
tasksRequested: summary.tasks.requested,
|
|
5327
6328
|
tasksApplied: summary.tasks.applied,
|
|
6329
|
+
workspace,
|
|
6330
|
+
preserved,
|
|
5328
6331
|
errors: [
|
|
5329
6332
|
...summary.tasks.failed ? [`tasks: ${summary.tasks.error ?? "resolver failed"}`] : [],
|
|
5330
|
-
...quarantineErrors(graph)
|
|
6333
|
+
...quarantineErrors(graph),
|
|
6334
|
+
...(workspace?.malformed ?? []).map(
|
|
6335
|
+
(entry) => `workspace: ${entry.path} \u2014 ${entry.reason}`
|
|
6336
|
+
)
|
|
5331
6337
|
]
|
|
5332
6338
|
};
|
|
5333
6339
|
} finally {
|
|
@@ -5798,8 +6804,8 @@ var mcp_status_default = defineCommand({
|
|
|
5798
6804
|
});
|
|
5799
6805
|
|
|
5800
6806
|
// src/commands/mcp-logs.ts
|
|
5801
|
-
import { promises as
|
|
5802
|
-
import
|
|
6807
|
+
import { promises as fs45 } from "fs";
|
|
6808
|
+
import path57 from "path";
|
|
5803
6809
|
import { z as z46 } from "zod";
|
|
5804
6810
|
var ArgsSchema42 = z46.object({
|
|
5805
6811
|
lines: z46.number().int().positive().optional()
|
|
@@ -5823,10 +6829,10 @@ var mcp_logs_default = defineCommand({
|
|
|
5823
6829
|
},
|
|
5824
6830
|
async run(args) {
|
|
5825
6831
|
const n = args.lines ?? DEFAULT_LINES;
|
|
5826
|
-
const logPath =
|
|
6832
|
+
const logPath = path57.join(getStateRoot(), "daemon.log");
|
|
5827
6833
|
let content;
|
|
5828
6834
|
try {
|
|
5829
|
-
content = await
|
|
6835
|
+
content = await fs45.readFile(logPath, "utf8");
|
|
5830
6836
|
} catch (err) {
|
|
5831
6837
|
if (err.code === "ENOENT") {
|
|
5832
6838
|
return { lines: [] };
|
|
@@ -5852,8 +6858,8 @@ import { discoverTranscripts as discoverTranscripts2, transcriptsRoot as transcr
|
|
|
5852
6858
|
import { Clusterer } from "@titan-design/cluster";
|
|
5853
6859
|
|
|
5854
6860
|
// src/drain/store.ts
|
|
5855
|
-
import { promises as
|
|
5856
|
-
import
|
|
6861
|
+
import { promises as fs46 } from "fs";
|
|
6862
|
+
import path58 from "path";
|
|
5857
6863
|
|
|
5858
6864
|
// src/schemas/template.ts
|
|
5859
6865
|
import { z as z47 } from "zod";
|
|
@@ -5883,10 +6889,10 @@ var TemplatesFileSchema = z47.object({
|
|
|
5883
6889
|
|
|
5884
6890
|
// src/drain/store.ts
|
|
5885
6891
|
function templatesPath(root) {
|
|
5886
|
-
return
|
|
6892
|
+
return path58.join(root, "templates.yml");
|
|
5887
6893
|
}
|
|
5888
6894
|
function occurrencesPath(root) {
|
|
5889
|
-
return
|
|
6895
|
+
return path58.join(root, "occurrences.jsonl");
|
|
5890
6896
|
}
|
|
5891
6897
|
async function loadTemplates(root = getMinerRoot()) {
|
|
5892
6898
|
try {
|
|
@@ -5898,15 +6904,15 @@ async function loadTemplates(root = getMinerRoot()) {
|
|
|
5898
6904
|
}
|
|
5899
6905
|
}
|
|
5900
6906
|
async function saveTemplates(templates, root = getMinerRoot()) {
|
|
5901
|
-
await
|
|
6907
|
+
await fs46.mkdir(root, { recursive: true });
|
|
5902
6908
|
await writeYaml(templatesPath(root), { templates }, TemplatesFileSchema);
|
|
5903
6909
|
}
|
|
5904
6910
|
async function appendOccurrence(occurrence, root = getMinerRoot()) {
|
|
5905
6911
|
const parsed = OccurrenceSchema.parse(occurrence);
|
|
5906
6912
|
const target = occurrencesPath(root);
|
|
5907
|
-
await
|
|
6913
|
+
await fs46.mkdir(root, { recursive: true });
|
|
5908
6914
|
await withFileLock(`${target}.lock`, async () => {
|
|
5909
|
-
await
|
|
6915
|
+
await fs46.appendFile(target, `${JSON.stringify(parsed)}
|
|
5910
6916
|
`, "utf8");
|
|
5911
6917
|
});
|
|
5912
6918
|
}
|
|
@@ -5914,17 +6920,17 @@ async function appendOccurrences(occurrences, root = getMinerRoot()) {
|
|
|
5914
6920
|
if (occurrences.length === 0) return;
|
|
5915
6921
|
const parsed = occurrences.map((o) => OccurrenceSchema.parse(o));
|
|
5916
6922
|
const target = occurrencesPath(root);
|
|
5917
|
-
await
|
|
6923
|
+
await fs46.mkdir(root, { recursive: true });
|
|
5918
6924
|
const body = `${parsed.map((o) => JSON.stringify(o)).join("\n")}
|
|
5919
6925
|
`;
|
|
5920
6926
|
await withFileLock(`${target}.lock`, async () => {
|
|
5921
|
-
await
|
|
6927
|
+
await fs46.appendFile(target, body, "utf8");
|
|
5922
6928
|
});
|
|
5923
6929
|
}
|
|
5924
6930
|
|
|
5925
6931
|
// src/drain/tree-store.ts
|
|
5926
|
-
import { promises as
|
|
5927
|
-
import
|
|
6932
|
+
import { promises as fs47 } from "fs";
|
|
6933
|
+
import path59 from "path";
|
|
5928
6934
|
import { z as z48 } from "zod";
|
|
5929
6935
|
var PartitionSchema = z48.object({
|
|
5930
6936
|
partition: z48.string().min(1),
|
|
@@ -5948,7 +6954,7 @@ var LegacySnapshotFileSchema = z48.object({
|
|
|
5948
6954
|
trees: z48.array(PartitionSchema.omit({ partition: true }).extend({ toolType: z48.string().min(1) }))
|
|
5949
6955
|
});
|
|
5950
6956
|
function snapshotPath(root) {
|
|
5951
|
-
return
|
|
6957
|
+
return path59.join(root, "drain-trees.json");
|
|
5952
6958
|
}
|
|
5953
6959
|
function empty() {
|
|
5954
6960
|
return { version: 1, partitions: [] };
|
|
@@ -5956,7 +6962,7 @@ function empty() {
|
|
|
5956
6962
|
async function loadTreeSnapshots(root = getMinerRoot()) {
|
|
5957
6963
|
let raw;
|
|
5958
6964
|
try {
|
|
5959
|
-
raw = await
|
|
6965
|
+
raw = await fs47.readFile(snapshotPath(root), "utf8");
|
|
5960
6966
|
} catch (err) {
|
|
5961
6967
|
if (err instanceof Error && "code" in err && err.code === "ENOENT") return empty();
|
|
5962
6968
|
throw err;
|
|
@@ -5976,7 +6982,7 @@ async function loadTreeSnapshots(root = getMinerRoot()) {
|
|
|
5976
6982
|
return parsed.success ? parsed.data : empty();
|
|
5977
6983
|
}
|
|
5978
6984
|
async function saveTreeSnapshots(snapshot, root = getMinerRoot()) {
|
|
5979
|
-
await
|
|
6985
|
+
await fs47.mkdir(root, { recursive: true });
|
|
5980
6986
|
await atomicWrite(
|
|
5981
6987
|
snapshotPath(root),
|
|
5982
6988
|
`${JSON.stringify(TreeSnapshotFileSchema.parse(snapshot))}
|
|
@@ -6126,8 +7132,8 @@ function extractBlobs(line, toolNames) {
|
|
|
6126
7132
|
}
|
|
6127
7133
|
|
|
6128
7134
|
// src/drain/reader-state.ts
|
|
6129
|
-
import { promises as
|
|
6130
|
-
import
|
|
7135
|
+
import { promises as fs48 } from "fs";
|
|
7136
|
+
import path60 from "path";
|
|
6131
7137
|
import { z as z49 } from "zod";
|
|
6132
7138
|
var TranscriptStateSchema = z49.object({
|
|
6133
7139
|
/** `~`-relative path, matching `session-index/discover.ts`'s display form. */
|
|
@@ -6148,11 +7154,11 @@ var ReaderStateSchema = z49.object({
|
|
|
6148
7154
|
});
|
|
6149
7155
|
var MAX_PENDING_TOOL_NAMES = 256;
|
|
6150
7156
|
function readerStatePath(root = getMinerRoot()) {
|
|
6151
|
-
return
|
|
7157
|
+
return path60.join(root, "reader-state.json");
|
|
6152
7158
|
}
|
|
6153
7159
|
async function loadReaderState(root = getMinerRoot()) {
|
|
6154
7160
|
try {
|
|
6155
|
-
const raw = await
|
|
7161
|
+
const raw = await fs48.readFile(readerStatePath(root), "utf8");
|
|
6156
7162
|
return ReaderStateSchema.parse(JSON.parse(raw));
|
|
6157
7163
|
} catch (err) {
|
|
6158
7164
|
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
@@ -6162,7 +7168,7 @@ async function loadReaderState(root = getMinerRoot()) {
|
|
|
6162
7168
|
}
|
|
6163
7169
|
}
|
|
6164
7170
|
async function saveReaderState(state, root = getMinerRoot()) {
|
|
6165
|
-
await
|
|
7171
|
+
await fs48.mkdir(root, { recursive: true });
|
|
6166
7172
|
await atomicWrite(
|
|
6167
7173
|
readerStatePath(root),
|
|
6168
7174
|
`${JSON.stringify(ReaderStateSchema.parse(state), null, 2)}
|
|
@@ -6374,6 +7380,22 @@ var ArgsSchema44 = z51.object({
|
|
|
6374
7380
|
limit: z51.coerce.number().int().positive().optional(),
|
|
6375
7381
|
verify_hashes: z51.boolean().optional()
|
|
6376
7382
|
});
|
|
7383
|
+
var WorkspaceSchema = z51.object({
|
|
7384
|
+
files: z51.number(),
|
|
7385
|
+
indexed: z51.number(),
|
|
7386
|
+
unchanged: z51.number(),
|
|
7387
|
+
removed: z51.number(),
|
|
7388
|
+
malformed: z51.array(z51.object({ path: z51.string(), reason: z51.string() })),
|
|
7389
|
+
rows: z51.object({
|
|
7390
|
+
initiative: z51.number(),
|
|
7391
|
+
note: z51.number(),
|
|
7392
|
+
task: z51.number(),
|
|
7393
|
+
session: z51.number(),
|
|
7394
|
+
source: z51.number()
|
|
7395
|
+
}),
|
|
7396
|
+
edges: z51.object({ holds: z51.number(), mentions: z51.number(), sharesTag: z51.number() }),
|
|
7397
|
+
orphanRatio: z51.number()
|
|
7398
|
+
});
|
|
6377
7399
|
var ResultSchema40 = z51.object({
|
|
6378
7400
|
startedAt: z51.string(),
|
|
6379
7401
|
durationMs: z51.number(),
|
|
@@ -6389,6 +7411,8 @@ var ResultSchema40 = z51.object({
|
|
|
6389
7411
|
turnsRolledUp: z51.number(),
|
|
6390
7412
|
tasksRequested: z51.number(),
|
|
6391
7413
|
tasksApplied: z51.number(),
|
|
7414
|
+
workspace: WorkspaceSchema.nullable(),
|
|
7415
|
+
preserved: z51.object({ restored: z51.number(), merged: z51.number(), skipped: z51.number() }),
|
|
6392
7416
|
errors: z51.array(z51.string())
|
|
6393
7417
|
});
|
|
6394
7418
|
var miner_refresh_default = defineCommand({
|
|
@@ -6425,7 +7449,7 @@ import { z as z52 } from "zod";
|
|
|
6425
7449
|
|
|
6426
7450
|
// src/session-index/liveness.ts
|
|
6427
7451
|
import { existsSync as existsSync2 } from "fs";
|
|
6428
|
-
import { RELATIONS, toAbsolutePath } from "@titan-design/session-read";
|
|
7452
|
+
import { RELATIONS as RELATIONS2, toAbsolutePath } from "@titan-design/session-read";
|
|
6429
7453
|
var SKIP_TABLES = /^(sqlite_|_migration$|search_fts)/;
|
|
6430
7454
|
var EXPECTED_EMPTY = {
|
|
6431
7455
|
"edge.t_invalid": "NULL means current; the index is rebuilt from scratch, so no edge is ever superseded in place",
|
|
@@ -6433,20 +7457,29 @@ var EXPECTED_EMPTY = {
|
|
|
6433
7457
|
"edge.attrs": "kit column; the session graph writes no per-edge attributes"
|
|
6434
7458
|
};
|
|
6435
7459
|
var REF_TABLES = {
|
|
6436
|
-
// `session` is the odd one out:
|
|
6437
|
-
// table stores the prefixed ref. Comparing without allowing for that
|
|
6438
|
-
// every session endpoint as dangling — a false alarm, which in a
|
|
6439
|
-
// is worse than no check at all.
|
|
6440
|
-
session:
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
7460
|
+
// `session` is the odd one out: `session` stores the bare id, every other
|
|
7461
|
+
// entity table stores the prefixed ref. Comparing without allowing for that
|
|
7462
|
+
// reports every session endpoint as dangling — a false alarm, which in a
|
|
7463
|
+
// diagnostic is worse than no check at all.
|
|
7464
|
+
session: [
|
|
7465
|
+
{ table: "session", column: "session_id", bare: true },
|
|
7466
|
+
{ table: "session_record", column: "session_ref" }
|
|
7467
|
+
],
|
|
7468
|
+
agent: [{ table: "subagent", column: "agent_ref" }],
|
|
7469
|
+
file: [{ table: "file", column: "file_ref" }],
|
|
7470
|
+
branch: [{ table: "branch", column: "branch_ref" }],
|
|
7471
|
+
task: [
|
|
7472
|
+
{ table: "task", column: "task_ref" },
|
|
7473
|
+
{ table: "workspace_task", column: "task_ref" }
|
|
7474
|
+
],
|
|
7475
|
+
artifact: [{ table: "artifact", column: "artifact_ref" }],
|
|
6446
7476
|
// `pr` is keyed by `pr_ref` like every other entity table. It was left
|
|
6447
7477
|
// unmapped on the belief that it was keyed by `(number, repo)` — that is
|
|
6448
7478
|
// `pr_merge_observation`, a different table (AW-107).
|
|
6449
|
-
pr: { table: "pr", column: "pr_ref" }
|
|
7479
|
+
pr: [{ table: "pr", column: "pr_ref" }],
|
|
7480
|
+
initiative: [{ table: "initiative", column: "initiative_ref" }],
|
|
7481
|
+
note: [{ table: "note", column: "note_ref" }],
|
|
7482
|
+
source: [{ table: "source", column: "source_ref" }]
|
|
6450
7483
|
};
|
|
6451
7484
|
function tableNames(db) {
|
|
6452
7485
|
const rows = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name").all();
|
|
@@ -6468,11 +7501,12 @@ function scanTable(db, table) {
|
|
|
6468
7501
|
nonNull: row[`c${index}`] ?? 0
|
|
6469
7502
|
}));
|
|
6470
7503
|
}
|
|
7504
|
+
var WORKSPACE_RELATIONS = ["holds", "mentions", "shares_tag"];
|
|
6471
7505
|
function relationLiveness(db) {
|
|
6472
7506
|
const observed = new Map(
|
|
6473
7507
|
db.prepare("SELECT relation, COUNT(*) AS n FROM edge GROUP BY relation").all().map((row) => [row.relation, row.n])
|
|
6474
7508
|
);
|
|
6475
|
-
const declared = Object.values(
|
|
7509
|
+
const declared = [...Object.values(RELATIONS2), ...WORKSPACE_RELATIONS];
|
|
6476
7510
|
const report2 = declared.map((relation) => ({
|
|
6477
7511
|
relation,
|
|
6478
7512
|
count: observed.get(relation) ?? 0,
|
|
@@ -6492,19 +7526,22 @@ function refNamespaces(db) {
|
|
|
6492
7526
|
) WHERE namespace <> '' GROUP BY namespace ORDER BY namespace`
|
|
6493
7527
|
).all();
|
|
6494
7528
|
return rows.map(({ namespace, n }) => {
|
|
6495
|
-
const
|
|
6496
|
-
if (!
|
|
6497
|
-
const
|
|
6498
|
-
const { c } = db.prepare(
|
|
6499
|
-
`SELECT COUNT(*) AS c FROM (
|
|
6500
|
-
SELECT source_ref AS ref FROM edge UNION ALL SELECT target_ref FROM edge
|
|
6501
|
-
) e
|
|
6502
|
-
WHERE e.ref LIKE @prefix
|
|
6503
|
-
AND NOT EXISTS (SELECT 1 FROM "${target.table}" t WHERE t."${target.column}" = ${lhs})`
|
|
6504
|
-
).get({ prefix: `${namespace}:%` });
|
|
7529
|
+
const targets = REF_TABLES[namespace];
|
|
7530
|
+
if (!targets) return { namespace, edges: n, dangling: null };
|
|
7531
|
+
const { c } = db.prepare(danglingSql(namespace, targets)).get({ prefix: `${namespace}:%` });
|
|
6505
7532
|
return { namespace, edges: n, dangling: c };
|
|
6506
7533
|
});
|
|
6507
7534
|
}
|
|
7535
|
+
function danglingSql(namespace, targets) {
|
|
7536
|
+
const absent = targets.map((target) => {
|
|
7537
|
+
const lhs = target.bare ? `substr(e.ref, ${namespace.length + 2})` : "e.ref";
|
|
7538
|
+
return `NOT EXISTS (SELECT 1 FROM "${target.table}" t WHERE t."${target.column}" = ${lhs})`;
|
|
7539
|
+
}).join(" AND ");
|
|
7540
|
+
return `SELECT COUNT(*) AS c FROM (
|
|
7541
|
+
SELECT source_ref AS ref FROM edge UNION ALL SELECT target_ref FROM edge
|
|
7542
|
+
) e
|
|
7543
|
+
WHERE e.ref LIKE @prefix AND ${absent}`;
|
|
7544
|
+
}
|
|
6508
7545
|
function staleTranscripts(db) {
|
|
6509
7546
|
const rows = db.prepare("SELECT source_key FROM transcript WHERE status = 'ok'").all();
|
|
6510
7547
|
return rows.filter((row) => !existsSync2(toAbsolutePath(row.source_key))).length;
|
|
@@ -6767,8 +7804,8 @@ async function readStdinJson(stream = process.stdin) {
|
|
|
6767
7804
|
}
|
|
6768
7805
|
|
|
6769
7806
|
// src/utils/agent-chat-hook-state.ts
|
|
6770
|
-
import { promises as
|
|
6771
|
-
import
|
|
7807
|
+
import { promises as fs49 } from "fs";
|
|
7808
|
+
import path61 from "path";
|
|
6772
7809
|
import { z as z54 } from "zod";
|
|
6773
7810
|
var SpawnContextSchema = z54.object({
|
|
6774
7811
|
slug: z54.string().min(1),
|
|
@@ -6788,19 +7825,19 @@ var SpawnContextSchema = z54.object({
|
|
|
6788
7825
|
briefing: z54.string().nullable().default(null)
|
|
6789
7826
|
});
|
|
6790
7827
|
function stateDir() {
|
|
6791
|
-
return
|
|
7828
|
+
return path61.join(getStateRoot(), "agent-chat-hooks");
|
|
6792
7829
|
}
|
|
6793
7830
|
function stateFile(agentId) {
|
|
6794
|
-
return
|
|
7831
|
+
return path61.join(stateDir(), `${agentId}.json`);
|
|
6795
7832
|
}
|
|
6796
7833
|
async function stashSpawnContext(agentId, context) {
|
|
6797
|
-
await
|
|
7834
|
+
await fs49.mkdir(stateDir(), { recursive: true });
|
|
6798
7835
|
await atomicWrite(stateFile(agentId), JSON.stringify(context));
|
|
6799
7836
|
}
|
|
6800
7837
|
async function readSpawnContext(agentId) {
|
|
6801
7838
|
let raw;
|
|
6802
7839
|
try {
|
|
6803
|
-
raw = await
|
|
7840
|
+
raw = await fs49.readFile(stateFile(agentId), "utf8");
|
|
6804
7841
|
} catch {
|
|
6805
7842
|
return null;
|
|
6806
7843
|
}
|
|
@@ -6809,7 +7846,7 @@ async function readSpawnContext(agentId) {
|
|
|
6809
7846
|
}
|
|
6810
7847
|
async function takeSpawnContext(agentId) {
|
|
6811
7848
|
const context = await readSpawnContext(agentId);
|
|
6812
|
-
await
|
|
7849
|
+
await fs49.rm(stateFile(agentId), { force: true });
|
|
6813
7850
|
return context;
|
|
6814
7851
|
}
|
|
6815
7852
|
async function peekSpawnContext(agentId) {
|
|
@@ -6960,9 +7997,9 @@ import { readFile, writeFile } from "fs/promises";
|
|
|
6960
7997
|
import { join } from "path";
|
|
6961
7998
|
|
|
6962
7999
|
// src/migrations/v1-to-v2-artifacts.ts
|
|
6963
|
-
import { promises as
|
|
6964
|
-
import
|
|
6965
|
-
import
|
|
8000
|
+
import { promises as fs50 } from "fs";
|
|
8001
|
+
import path62 from "path";
|
|
8002
|
+
import YAML4 from "yaml";
|
|
6966
8003
|
function asArray(value) {
|
|
6967
8004
|
return Array.isArray(value) ? value : [];
|
|
6968
8005
|
}
|
|
@@ -6987,13 +8024,13 @@ function normaliseStash(s) {
|
|
|
6987
8024
|
async function migrateOne(filePath) {
|
|
6988
8025
|
let raw;
|
|
6989
8026
|
try {
|
|
6990
|
-
raw = await
|
|
8027
|
+
raw = await fs50.readFile(filePath, "utf8");
|
|
6991
8028
|
} catch {
|
|
6992
8029
|
return { changed: false, droppedPrs: [] };
|
|
6993
8030
|
}
|
|
6994
8031
|
let parsed;
|
|
6995
8032
|
try {
|
|
6996
|
-
parsed =
|
|
8033
|
+
parsed = YAML4.parse(raw) ?? {};
|
|
6997
8034
|
} catch {
|
|
6998
8035
|
return { changed: false, droppedPrs: [] };
|
|
6999
8036
|
}
|
|
@@ -7017,38 +8054,38 @@ async function migrateOne(filePath) {
|
|
|
7017
8054
|
async function walkArtifactsFiles(activeRoot) {
|
|
7018
8055
|
const out = [];
|
|
7019
8056
|
try {
|
|
7020
|
-
const entries = await
|
|
8057
|
+
const entries = await fs50.readdir(activeRoot, { withFileTypes: true });
|
|
7021
8058
|
for (const entry of entries) {
|
|
7022
8059
|
if (!entry.isDirectory()) continue;
|
|
7023
8060
|
if (entry.name.startsWith(".")) continue;
|
|
7024
|
-
const candidate =
|
|
8061
|
+
const candidate = path62.join(activeRoot, entry.name, "artifacts.yml");
|
|
7025
8062
|
try {
|
|
7026
|
-
await
|
|
8063
|
+
await fs50.access(candidate);
|
|
7027
8064
|
out.push(candidate);
|
|
7028
8065
|
} catch {
|
|
7029
8066
|
}
|
|
7030
8067
|
}
|
|
7031
8068
|
} catch {
|
|
7032
8069
|
}
|
|
7033
|
-
const archiveRoot =
|
|
8070
|
+
const archiveRoot = path62.resolve(activeRoot, "..");
|
|
7034
8071
|
try {
|
|
7035
|
-
const domains = await
|
|
8072
|
+
const domains = await fs50.readdir(archiveRoot, { withFileTypes: true });
|
|
7036
8073
|
for (const domain of domains) {
|
|
7037
8074
|
if (!domain.isDirectory()) continue;
|
|
7038
8075
|
if (domain.name.startsWith(".")) continue;
|
|
7039
|
-
if (
|
|
7040
|
-
const archiveDir =
|
|
8076
|
+
if (path62.join(archiveRoot, domain.name) === path62.resolve(activeRoot)) continue;
|
|
8077
|
+
const archiveDir = path62.join(archiveRoot, domain.name, "archive");
|
|
7041
8078
|
let archived;
|
|
7042
8079
|
try {
|
|
7043
|
-
archived = await
|
|
8080
|
+
archived = await fs50.readdir(archiveDir, { withFileTypes: true });
|
|
7044
8081
|
} catch {
|
|
7045
8082
|
continue;
|
|
7046
8083
|
}
|
|
7047
8084
|
for (const entry of archived) {
|
|
7048
8085
|
if (!entry.isDirectory()) continue;
|
|
7049
|
-
const candidate =
|
|
8086
|
+
const candidate = path62.join(archiveDir, entry.name, "artifacts.yml");
|
|
7050
8087
|
try {
|
|
7051
|
-
await
|
|
8088
|
+
await fs50.access(candidate);
|
|
7052
8089
|
out.push(candidate);
|
|
7053
8090
|
} catch {
|
|
7054
8091
|
}
|
|
@@ -7060,15 +8097,15 @@ async function walkArtifactsFiles(activeRoot) {
|
|
|
7060
8097
|
}
|
|
7061
8098
|
async function appendMigrationLog(activeRoot, lines) {
|
|
7062
8099
|
if (lines.length === 0) return;
|
|
7063
|
-
const logPath =
|
|
8100
|
+
const logPath = path62.join(activeRoot, ".migrations.log");
|
|
7064
8101
|
const stamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
7065
8102
|
const body = lines.map((l) => `${stamp} v1->v2 ${l}
|
|
7066
8103
|
`).join("");
|
|
7067
8104
|
try {
|
|
7068
|
-
await
|
|
8105
|
+
await fs50.mkdir(activeRoot, { recursive: true });
|
|
7069
8106
|
} catch {
|
|
7070
8107
|
}
|
|
7071
|
-
await
|
|
8108
|
+
await fs50.appendFile(logPath, body, "utf8");
|
|
7072
8109
|
}
|
|
7073
8110
|
var v1ToV2Artifacts = {
|
|
7074
8111
|
from: 1,
|
|
@@ -7091,11 +8128,11 @@ var v1ToV2Artifacts = {
|
|
|
7091
8128
|
};
|
|
7092
8129
|
|
|
7093
8130
|
// src/migrations/v2-to-v3-open-loops.ts
|
|
7094
|
-
import { promises as
|
|
7095
|
-
import
|
|
8131
|
+
import { promises as fs53 } from "fs";
|
|
8132
|
+
import path64 from "path";
|
|
7096
8133
|
|
|
7097
8134
|
// src/migrations/v3-proposal.ts
|
|
7098
|
-
import { promises as
|
|
8135
|
+
import { promises as fs51 } from "fs";
|
|
7099
8136
|
import { z as z57 } from "zod";
|
|
7100
8137
|
|
|
7101
8138
|
// src/migrations/data/v3-open-loops-proposal.ts
|
|
@@ -7970,7 +9007,7 @@ async function loadProposal() {
|
|
|
7970
9007
|
if (override !== void 0 && override.length > 0) {
|
|
7971
9008
|
let raw;
|
|
7972
9009
|
try {
|
|
7973
|
-
raw = await
|
|
9010
|
+
raw = await fs51.readFile(override, "utf8");
|
|
7974
9011
|
} catch {
|
|
7975
9012
|
throw new ValidationError(`${PROPOSAL_PATH_ENV} points at an unreadable file: ${override}`);
|
|
7976
9013
|
}
|
|
@@ -7991,18 +9028,18 @@ async function loadProposal() {
|
|
|
7991
9028
|
}
|
|
7992
9029
|
|
|
7993
9030
|
// src/migrations/v3-repairs.ts
|
|
7994
|
-
import { promises as
|
|
7995
|
-
import
|
|
9031
|
+
import { promises as fs52 } from "fs";
|
|
9032
|
+
import path63 from "path";
|
|
7996
9033
|
var KNOWN_REPAIRS = [
|
|
7997
9034
|
{
|
|
7998
|
-
file:
|
|
9035
|
+
file: path63.join("audiobook", "sessions", "2026-07-23-0549-2026-07-26-book1-m4b-packaging.md"),
|
|
7999
9036
|
kind: "retrack",
|
|
8000
9037
|
why: "track is a branch name ('feat/tts-quality'), not one of canonical|sidecar|adhoc"
|
|
8001
9038
|
},
|
|
8002
9039
|
{
|
|
8003
|
-
file:
|
|
9040
|
+
file: path63.join("voltras-workspace", "sessions", "ARCHIVED-handoff-through-2026-07-15.md"),
|
|
8004
9041
|
kind: "relocate",
|
|
8005
|
-
target:
|
|
9042
|
+
target: path63.join("voltras-workspace", "sources", "ARCHIVED-handoff-through-2026-07-15.md"),
|
|
8006
9043
|
why: "not a session file \u2014 a hand-archived handoff parked in sessions/"
|
|
8007
9044
|
}
|
|
8008
9045
|
];
|
|
@@ -8016,11 +9053,11 @@ var BRIEF_REPAIRS = {
|
|
|
8016
9053
|
};
|
|
8017
9054
|
}
|
|
8018
9055
|
};
|
|
8019
|
-
function repairBriefFrontmatter(slug,
|
|
8020
|
-
const repair = BRIEF_REPAIRS[slug]?.(
|
|
8021
|
-
if (repair == null) return { frontmatter, applied: [] };
|
|
9056
|
+
function repairBriefFrontmatter(slug, frontmatter2) {
|
|
9057
|
+
const repair = BRIEF_REPAIRS[slug]?.(frontmatter2);
|
|
9058
|
+
if (repair == null) return { frontmatter: frontmatter2, applied: [] };
|
|
8022
9059
|
return {
|
|
8023
|
-
frontmatter: { ...
|
|
9060
|
+
frontmatter: { ...frontmatter2, ...repair.patch },
|
|
8024
9061
|
applied: [repair.detail]
|
|
8025
9062
|
};
|
|
8026
9063
|
}
|
|
@@ -8050,9 +9087,9 @@ async function planRetrack(fullPath, repair) {
|
|
|
8050
9087
|
repaired: { frontmatter: parsed.data, body: raw.body }
|
|
8051
9088
|
};
|
|
8052
9089
|
}
|
|
8053
|
-
async function
|
|
9090
|
+
async function exists3(p) {
|
|
8054
9091
|
try {
|
|
8055
|
-
await
|
|
9092
|
+
await fs52.access(p);
|
|
8056
9093
|
return true;
|
|
8057
9094
|
} catch {
|
|
8058
9095
|
return false;
|
|
@@ -8061,8 +9098,8 @@ async function exists2(p) {
|
|
|
8061
9098
|
async function planRepairs(activeRoot) {
|
|
8062
9099
|
const plans = [];
|
|
8063
9100
|
for (const repair of KNOWN_REPAIRS) {
|
|
8064
|
-
const fullPath =
|
|
8065
|
-
if (!await
|
|
9101
|
+
const fullPath = path63.join(activeRoot, repair.file);
|
|
9102
|
+
if (!await exists3(fullPath)) {
|
|
8066
9103
|
plans.push({ action: "skip", file: repair.file, detail: "already absent" });
|
|
8067
9104
|
continue;
|
|
8068
9105
|
}
|
|
@@ -8071,7 +9108,7 @@ async function planRepairs(activeRoot) {
|
|
|
8071
9108
|
continue;
|
|
8072
9109
|
}
|
|
8073
9110
|
const target = repair.target;
|
|
8074
|
-
if (await
|
|
9111
|
+
if (await exists3(path63.join(activeRoot, target))) {
|
|
8075
9112
|
plans.push({
|
|
8076
9113
|
action: "skip",
|
|
8077
9114
|
file: repair.file,
|
|
@@ -8085,7 +9122,7 @@ async function planRepairs(activeRoot) {
|
|
|
8085
9122
|
return plans;
|
|
8086
9123
|
}
|
|
8087
9124
|
async function applyRepair(activeRoot, plan) {
|
|
8088
|
-
const fullPath =
|
|
9125
|
+
const fullPath = path63.join(activeRoot, plan.file);
|
|
8089
9126
|
if (plan.action === "retrack" && plan.repaired !== void 0) {
|
|
8090
9127
|
await writeFrontmatter(
|
|
8091
9128
|
fullPath,
|
|
@@ -8096,34 +9133,34 @@ async function applyRepair(activeRoot, plan) {
|
|
|
8096
9133
|
return;
|
|
8097
9134
|
}
|
|
8098
9135
|
if (plan.action === "relocate") {
|
|
8099
|
-
const target =
|
|
8100
|
-
await
|
|
8101
|
-
await
|
|
9136
|
+
const target = path63.join(activeRoot, plan.target);
|
|
9137
|
+
await fs52.mkdir(path63.dirname(target), { recursive: true });
|
|
9138
|
+
await fs52.rename(fullPath, target);
|
|
8102
9139
|
}
|
|
8103
9140
|
}
|
|
8104
9141
|
|
|
8105
9142
|
// src/migrations/v2-to-v3-open-loops.ts
|
|
8106
9143
|
var HANDOFF_FILE = "handoff.md";
|
|
8107
|
-
var HANDOFF_ARCHIVE =
|
|
9144
|
+
var HANDOFF_ARCHIVE = path64.join("sources", "handoff-archive.md");
|
|
8108
9145
|
async function pathExists(p) {
|
|
8109
9146
|
try {
|
|
8110
|
-
await
|
|
9147
|
+
await fs53.access(p);
|
|
8111
9148
|
return true;
|
|
8112
9149
|
} catch {
|
|
8113
9150
|
return false;
|
|
8114
9151
|
}
|
|
8115
9152
|
}
|
|
8116
|
-
async function
|
|
9153
|
+
async function listInitiativeSlugs3(activeRoot) {
|
|
8117
9154
|
let entries;
|
|
8118
9155
|
try {
|
|
8119
|
-
entries = await
|
|
9156
|
+
entries = await fs53.readdir(activeRoot, { withFileTypes: true });
|
|
8120
9157
|
} catch {
|
|
8121
9158
|
return [];
|
|
8122
9159
|
}
|
|
8123
9160
|
const slugs = [];
|
|
8124
9161
|
for (const entry of entries) {
|
|
8125
9162
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
8126
|
-
if (await pathExists(
|
|
9163
|
+
if (await pathExists(path64.join(activeRoot, entry.name, "brief.md"))) {
|
|
8127
9164
|
slugs.push(entry.name);
|
|
8128
9165
|
}
|
|
8129
9166
|
}
|
|
@@ -8131,12 +9168,12 @@ async function listInitiativeSlugs2(activeRoot) {
|
|
|
8131
9168
|
}
|
|
8132
9169
|
async function maxOnDiskTaskNumber2(initiativeDir, prefix) {
|
|
8133
9170
|
const re = new RegExp(`^${prefix}-(\\d+)\\.yml$`);
|
|
8134
|
-
const tasksDir =
|
|
9171
|
+
const tasksDir = path64.join(initiativeDir, "tasks");
|
|
8135
9172
|
let max = 0;
|
|
8136
|
-
for (const dir of [tasksDir,
|
|
9173
|
+
for (const dir of [tasksDir, path64.join(tasksDir, "archive")]) {
|
|
8137
9174
|
let names;
|
|
8138
9175
|
try {
|
|
8139
|
-
names = await
|
|
9176
|
+
names = await fs53.readdir(dir);
|
|
8140
9177
|
} catch {
|
|
8141
9178
|
continue;
|
|
8142
9179
|
}
|
|
@@ -8147,15 +9184,15 @@ async function maxOnDiskTaskNumber2(initiativeDir, prefix) {
|
|
|
8147
9184
|
}
|
|
8148
9185
|
return max;
|
|
8149
9186
|
}
|
|
8150
|
-
async function nextTaskSeq(initiativeDir,
|
|
8151
|
-
if (
|
|
8152
|
-
const prefix =
|
|
9187
|
+
async function nextTaskSeq(initiativeDir, frontmatter2) {
|
|
9188
|
+
if (frontmatter2.task_seq !== void 0) return null;
|
|
9189
|
+
const prefix = frontmatter2.task_prefix;
|
|
8153
9190
|
if (typeof prefix !== "string" || prefix.length === 0) return null;
|
|
8154
9191
|
const max = await maxOnDiskTaskNumber2(initiativeDir, prefix);
|
|
8155
9192
|
return max > 0 ? max : null;
|
|
8156
9193
|
}
|
|
8157
9194
|
async function planBrief(initiativeDir, slug) {
|
|
8158
|
-
const raw = await readRawFrontmatter(
|
|
9195
|
+
const raw = await readRawFrontmatter(path64.join(initiativeDir, "brief.md"));
|
|
8159
9196
|
const repaired = repairBriefFrontmatter(slug, raw.frontmatter);
|
|
8160
9197
|
const taskSeq = await nextTaskSeq(initiativeDir, repaired.frontmatter);
|
|
8161
9198
|
if (taskSeq === null && repaired.applied.length === 0) return { write: null };
|
|
@@ -8172,12 +9209,12 @@ async function planBrief(initiativeDir, slug) {
|
|
|
8172
9209
|
};
|
|
8173
9210
|
}
|
|
8174
9211
|
async function planHandoff(initiativeDir) {
|
|
8175
|
-
if (!await pathExists(
|
|
8176
|
-
if (await pathExists(
|
|
9212
|
+
if (!await pathExists(path64.join(initiativeDir, HANDOFF_FILE))) return "absent";
|
|
9213
|
+
if (await pathExists(path64.join(initiativeDir, HANDOFF_ARCHIVE))) return "archive-exists";
|
|
8177
9214
|
return "archive-and-remove";
|
|
8178
9215
|
}
|
|
8179
9216
|
function buildOpenSession(entry) {
|
|
8180
|
-
const
|
|
9217
|
+
const frontmatter2 = SessionFrontmatterSchema.parse({
|
|
8181
9218
|
session_id: entry.session_id,
|
|
8182
9219
|
// A synthetic session is an instant, not an interval: it stands for a
|
|
8183
9220
|
// state read off the handoff, so `started === ended`.
|
|
@@ -8189,11 +9226,11 @@ function buildOpenSession(entry) {
|
|
|
8189
9226
|
resolves: []
|
|
8190
9227
|
});
|
|
8191
9228
|
const stem = buildSessionStem(entry.ended, entry.session_id);
|
|
8192
|
-
return { kind: "open", stem, frontmatter, body: buildOpenBody(entry, stem) };
|
|
9229
|
+
return { kind: "open", stem, frontmatter: frontmatter2, body: buildOpenBody(entry, stem) };
|
|
8193
9230
|
}
|
|
8194
9231
|
function buildAbandonSession(entry, openStem, abandonedAt) {
|
|
8195
9232
|
const sessionId = `${entry.session_id}-abandonment`;
|
|
8196
|
-
const
|
|
9233
|
+
const frontmatter2 = SessionFrontmatterSchema.parse({
|
|
8197
9234
|
session_id: sessionId,
|
|
8198
9235
|
started: abandonedAt,
|
|
8199
9236
|
ended: abandonedAt,
|
|
@@ -8209,7 +9246,7 @@ function buildAbandonSession(entry, openStem, abandonedAt) {
|
|
|
8209
9246
|
return {
|
|
8210
9247
|
kind: "abandon",
|
|
8211
9248
|
stem,
|
|
8212
|
-
frontmatter,
|
|
9249
|
+
frontmatter: frontmatter2,
|
|
8213
9250
|
body: buildAbandonBody(entry, openStem, stem)
|
|
8214
9251
|
};
|
|
8215
9252
|
}
|
|
@@ -8263,7 +9300,7 @@ function draftSessions(entry, abandonedAt) {
|
|
|
8263
9300
|
return [open, buildAbandonSession(entry, open.stem, abandonedAt)];
|
|
8264
9301
|
}
|
|
8265
9302
|
async function planInitiative(activeRoot, slug, entry, abandonedAt) {
|
|
8266
|
-
const initiativeDir =
|
|
9303
|
+
const initiativeDir = path64.join(activeRoot, slug);
|
|
8267
9304
|
const brief = await planBrief(initiativeDir, slug);
|
|
8268
9305
|
const base = {
|
|
8269
9306
|
slug,
|
|
@@ -8290,7 +9327,7 @@ function assertProposalSlugsExist(proposal, known) {
|
|
|
8290
9327
|
}
|
|
8291
9328
|
async function planV2ToV3(activeRoot) {
|
|
8292
9329
|
const { proposal, origin } = await loadProposal();
|
|
8293
|
-
const slugs = await
|
|
9330
|
+
const slugs = await listInitiativeSlugs3(activeRoot);
|
|
8294
9331
|
assertProposalSlugsExist(proposal, slugs);
|
|
8295
9332
|
const byslug = new Map(proposal.initiatives.map((i) => [i.slug, i]));
|
|
8296
9333
|
const initiatives = [];
|
|
@@ -8302,11 +9339,11 @@ async function planV2ToV3(activeRoot) {
|
|
|
8302
9339
|
return { proposalOrigin: origin, initiatives, repairs: await planRepairs(activeRoot) };
|
|
8303
9340
|
}
|
|
8304
9341
|
async function archiveHandoff(initiativeDir) {
|
|
8305
|
-
const source =
|
|
8306
|
-
const target =
|
|
8307
|
-
await
|
|
8308
|
-
await
|
|
8309
|
-
await
|
|
9342
|
+
const source = path64.join(initiativeDir, HANDOFF_FILE);
|
|
9343
|
+
const target = path64.join(initiativeDir, HANDOFF_ARCHIVE);
|
|
9344
|
+
await fs53.mkdir(path64.dirname(target), { recursive: true });
|
|
9345
|
+
await fs53.copyFile(source, target);
|
|
9346
|
+
await fs53.rm(source);
|
|
8310
9347
|
}
|
|
8311
9348
|
async function writePlannedSession(activeRoot, slug, session) {
|
|
8312
9349
|
const fm = session.frontmatter;
|
|
@@ -8323,15 +9360,15 @@ async function writePlannedSession(activeRoot, slug, session) {
|
|
|
8323
9360
|
});
|
|
8324
9361
|
}
|
|
8325
9362
|
async function applyInitiative(activeRoot, plan) {
|
|
8326
|
-
const initiativeDir =
|
|
8327
|
-
await withFileLock(
|
|
9363
|
+
const initiativeDir = path64.join(activeRoot, plan.slug);
|
|
9364
|
+
await withFileLock(path64.join(initiativeDir, ".lock"), async () => {
|
|
8328
9365
|
for (const session of plan.sessions) {
|
|
8329
9366
|
if (session.exists) continue;
|
|
8330
9367
|
await writePlannedSession(activeRoot, plan.slug, session);
|
|
8331
9368
|
}
|
|
8332
9369
|
if (plan.brief !== null) {
|
|
8333
9370
|
await writeFrontmatter(
|
|
8334
|
-
|
|
9371
|
+
path64.join(initiativeDir, "brief.md"),
|
|
8335
9372
|
plan.brief.frontmatter,
|
|
8336
9373
|
plan.brief.body,
|
|
8337
9374
|
BriefFrontmatterSchema
|
|
@@ -8361,13 +9398,13 @@ var v2ToV3OpenLoops = {
|
|
|
8361
9398
|
};
|
|
8362
9399
|
|
|
8363
9400
|
// src/migrations/v3-to-v4-worktrees.ts
|
|
8364
|
-
import { promises as
|
|
8365
|
-
import
|
|
8366
|
-
import
|
|
8367
|
-
import
|
|
9401
|
+
import { promises as fs54 } from "fs";
|
|
9402
|
+
import path65 from "path";
|
|
9403
|
+
import matter5 from "gray-matter";
|
|
9404
|
+
import YAML5 from "yaml";
|
|
8368
9405
|
function normalize(value) {
|
|
8369
|
-
const expanded = value.startsWith("~") ?
|
|
8370
|
-
return
|
|
9406
|
+
const expanded = value.startsWith("~") ? path65.join(process.env.HOME ?? "", value.slice(1)) : value;
|
|
9407
|
+
return path65.resolve(expanded);
|
|
8371
9408
|
}
|
|
8372
9409
|
function toEntries(raw) {
|
|
8373
9410
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return [];
|
|
@@ -8403,7 +9440,7 @@ function mergeWorktrees(existing, incoming) {
|
|
|
8403
9440
|
}
|
|
8404
9441
|
async function readArtifacts2(file) {
|
|
8405
9442
|
try {
|
|
8406
|
-
const raw =
|
|
9443
|
+
const raw = YAML5.parse(await fs54.readFile(file, "utf8"));
|
|
8407
9444
|
return {
|
|
8408
9445
|
branches: Array.isArray(raw?.branches) ? raw.branches : [],
|
|
8409
9446
|
stashes: Array.isArray(raw?.stashes) ? raw.stashes : [],
|
|
@@ -8414,20 +9451,20 @@ async function readArtifacts2(file) {
|
|
|
8414
9451
|
}
|
|
8415
9452
|
}
|
|
8416
9453
|
async function migrateOne2(initiativeDir) {
|
|
8417
|
-
const briefPath =
|
|
9454
|
+
const briefPath = path65.join(initiativeDir, "brief.md");
|
|
8418
9455
|
let raw;
|
|
8419
9456
|
try {
|
|
8420
|
-
raw = await
|
|
9457
|
+
raw = await fs54.readFile(briefPath, "utf8");
|
|
8421
9458
|
} catch {
|
|
8422
9459
|
return 0;
|
|
8423
9460
|
}
|
|
8424
|
-
const parsed =
|
|
9461
|
+
const parsed = matter5(raw);
|
|
8425
9462
|
const data = parsed.data;
|
|
8426
9463
|
if (!("worktrees" in data)) return 0;
|
|
8427
9464
|
const incoming = toEntries(data.worktrees);
|
|
8428
9465
|
delete data.worktrees;
|
|
8429
9466
|
if (incoming.length > 0) {
|
|
8430
|
-
const artifactsPath2 =
|
|
9467
|
+
const artifactsPath2 = path65.join(initiativeDir, "artifacts.yml");
|
|
8431
9468
|
const current = await readArtifacts2(artifactsPath2);
|
|
8432
9469
|
await writeYaml(
|
|
8433
9470
|
artifactsPath2,
|
|
@@ -8439,22 +9476,22 @@ async function migrateOne2(initiativeDir) {
|
|
|
8439
9476
|
ArtifactsSchema
|
|
8440
9477
|
);
|
|
8441
9478
|
}
|
|
8442
|
-
await atomicWrite(briefPath,
|
|
9479
|
+
await atomicWrite(briefPath, matter5.stringify(parsed.content, data));
|
|
8443
9480
|
return incoming.length;
|
|
8444
9481
|
}
|
|
8445
9482
|
async function initiativeDirs(activeRoot) {
|
|
8446
9483
|
const out = [];
|
|
8447
9484
|
let entries;
|
|
8448
9485
|
try {
|
|
8449
|
-
entries = await
|
|
9486
|
+
entries = await fs54.readdir(activeRoot, { withFileTypes: true });
|
|
8450
9487
|
} catch {
|
|
8451
9488
|
return out;
|
|
8452
9489
|
}
|
|
8453
9490
|
for (const entry of entries) {
|
|
8454
9491
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
8455
|
-
const dir =
|
|
9492
|
+
const dir = path65.join(activeRoot, entry.name);
|
|
8456
9493
|
try {
|
|
8457
|
-
await
|
|
9494
|
+
await fs54.access(path65.join(dir, "brief.md"));
|
|
8458
9495
|
out.push(dir);
|
|
8459
9496
|
} catch {
|
|
8460
9497
|
}
|
|
@@ -8469,12 +9506,12 @@ var v3ToV4Worktrees = {
|
|
|
8469
9506
|
const moved = [];
|
|
8470
9507
|
for (const dir of await initiativeDirs(activeRoot)) {
|
|
8471
9508
|
const count = await migrateOne2(dir);
|
|
8472
|
-
if (count > 0) moved.push(`${
|
|
9509
|
+
if (count > 0) moved.push(`${path65.basename(dir)} ${count} worktree(s)`);
|
|
8473
9510
|
}
|
|
8474
9511
|
if (moved.length === 0) return;
|
|
8475
9512
|
const stamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
8476
|
-
await
|
|
8477
|
-
|
|
9513
|
+
await fs54.appendFile(
|
|
9514
|
+
path65.join(activeRoot, ".migrations.log"),
|
|
8478
9515
|
moved.map((line) => `${stamp} v3->v4 ${line}
|
|
8479
9516
|
`).join(""),
|
|
8480
9517
|
"utf8"
|
|
@@ -8489,7 +9526,7 @@ var CURRENT_VERSION = targetVersion(MIGRATIONS2);
|
|
|
8489
9526
|
function targetVersion(migrations) {
|
|
8490
9527
|
return migrations.reduce((highest, m) => Math.max(highest, m.to), BASE_VERSION);
|
|
8491
9528
|
}
|
|
8492
|
-
async function
|
|
9529
|
+
async function runMigrations2(activeRoot, fromVersion, migrations = MIGRATIONS2) {
|
|
8493
9530
|
if (fromVersion === CURRENT_VERSION) {
|
|
8494
9531
|
return { ran: [] };
|
|
8495
9532
|
}
|
|
@@ -8527,10 +9564,10 @@ var SCHEMA_VERSION_FILENAME = ".schema-version";
|
|
|
8527
9564
|
var schemaVersionPath = (activeRoot) => join(activeRoot, SCHEMA_VERSION_FILENAME);
|
|
8528
9565
|
var isNodeErrnoException = (err) => typeof err === "object" && err !== null && "code" in err;
|
|
8529
9566
|
async function readSchemaVersion(activeRoot) {
|
|
8530
|
-
const
|
|
9567
|
+
const path67 = schemaVersionPath(activeRoot);
|
|
8531
9568
|
let raw;
|
|
8532
9569
|
try {
|
|
8533
|
-
raw = await readFile(
|
|
9570
|
+
raw = await readFile(path67, "utf8");
|
|
8534
9571
|
} catch (err) {
|
|
8535
9572
|
if (isNodeErrnoException(err) && err.code === "ENOENT") {
|
|
8536
9573
|
return 0;
|
|
@@ -8540,13 +9577,13 @@ async function readSchemaVersion(activeRoot) {
|
|
|
8540
9577
|
const trimmed = raw.trim();
|
|
8541
9578
|
if (trimmed === "" || !/^\d+$/.test(trimmed)) {
|
|
8542
9579
|
throw new Error(
|
|
8543
|
-
`Invalid schema version in ${
|
|
9580
|
+
`Invalid schema version in ${path67}: expected a positive integer, got ${JSON.stringify(raw)}`
|
|
8544
9581
|
);
|
|
8545
9582
|
}
|
|
8546
9583
|
const parsed = Number(trimmed);
|
|
8547
9584
|
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
8548
9585
|
throw new Error(
|
|
8549
|
-
`Invalid schema version in ${
|
|
9586
|
+
`Invalid schema version in ${path67}: expected a positive integer, got ${JSON.stringify(raw)}`
|
|
8550
9587
|
);
|
|
8551
9588
|
}
|
|
8552
9589
|
return parsed;
|
|
@@ -8559,10 +9596,10 @@ async function writeSchemaVersion(activeRoot, version) {
|
|
|
8559
9596
|
`, "utf8");
|
|
8560
9597
|
}
|
|
8561
9598
|
async function readRawSchemaVersion(activeRoot) {
|
|
8562
|
-
const
|
|
9599
|
+
const path67 = schemaVersionPath(activeRoot);
|
|
8563
9600
|
let raw;
|
|
8564
9601
|
try {
|
|
8565
|
-
raw = await readFile(
|
|
9602
|
+
raw = await readFile(path67, "utf8");
|
|
8566
9603
|
} catch (err) {
|
|
8567
9604
|
if (isNodeErrnoException(err) && err.code === "ENOENT") {
|
|
8568
9605
|
return { present: false };
|
|
@@ -8572,13 +9609,13 @@ async function readRawSchemaVersion(activeRoot) {
|
|
|
8572
9609
|
const trimmed = raw.trim();
|
|
8573
9610
|
if (trimmed === "" || !/^\d+$/.test(trimmed)) {
|
|
8574
9611
|
throw new Error(
|
|
8575
|
-
`Invalid schema version in ${
|
|
9612
|
+
`Invalid schema version in ${path67}: expected a non-negative integer, got ${JSON.stringify(raw)}`
|
|
8576
9613
|
);
|
|
8577
9614
|
}
|
|
8578
9615
|
const parsed = Number(trimmed);
|
|
8579
9616
|
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
8580
9617
|
throw new Error(
|
|
8581
|
-
`Invalid schema version in ${
|
|
9618
|
+
`Invalid schema version in ${path67}: expected a non-negative integer, got ${JSON.stringify(raw)}`
|
|
8582
9619
|
);
|
|
8583
9620
|
}
|
|
8584
9621
|
return { present: true, version: parsed };
|
|
@@ -8598,7 +9635,7 @@ async function ensureSchemaVersion(activeRoot) {
|
|
|
8598
9635
|
if (before === CURRENT_VERSION) {
|
|
8599
9636
|
return { before, after: before, migrated: false, ran: [] };
|
|
8600
9637
|
}
|
|
8601
|
-
const { ran } = await
|
|
9638
|
+
const { ran } = await runMigrations2(activeRoot, before);
|
|
8602
9639
|
await writeSchemaVersion(activeRoot, CURRENT_VERSION);
|
|
8603
9640
|
return {
|
|
8604
9641
|
before,
|
|
@@ -8619,7 +9656,7 @@ function isLinux(platform = process.platform) {
|
|
|
8619
9656
|
return platform === "linux";
|
|
8620
9657
|
}
|
|
8621
9658
|
function resolveLocalDeps(deps) {
|
|
8622
|
-
const
|
|
9659
|
+
const fs57 = deps.fs ?? fsp;
|
|
8623
9660
|
const spawn6 = deps.spawn ?? nodeSpawn;
|
|
8624
9661
|
const homeDir = deps.paths?.homeDir ?? os3.homedir();
|
|
8625
9662
|
const paths = deps.paths ?? {
|
|
@@ -8629,7 +9666,7 @@ function resolveLocalDeps(deps) {
|
|
|
8629
9666
|
homeDir
|
|
8630
9667
|
};
|
|
8631
9668
|
const cliEntry = deps.cliEntry ?? process.argv[1] ?? "active-work";
|
|
8632
|
-
return { fs:
|
|
9669
|
+
return { fs: fs57, spawn: spawn6, paths, cliEntry, platform: process.platform };
|
|
8633
9670
|
}
|
|
8634
9671
|
function getUnitDir(homeDir) {
|
|
8635
9672
|
return nodePath.join(homeDir, ".config", "systemd", "user");
|
|
@@ -8700,7 +9737,7 @@ async function isUnitActive(deps = {}) {
|
|
|
8700
9737
|
return result.code === 0;
|
|
8701
9738
|
}
|
|
8702
9739
|
async function stepInstallSupervision(deps = {}, opts = {}) {
|
|
8703
|
-
const { fs:
|
|
9740
|
+
const { fs: fs57, spawn: spawn6, paths, cliEntry, platform } = resolveLocalDeps(deps);
|
|
8704
9741
|
if (!isLinux(platform)) {
|
|
8705
9742
|
return {
|
|
8706
9743
|
ok: true,
|
|
@@ -8713,16 +9750,16 @@ async function stepInstallSupervision(deps = {}, opts = {}) {
|
|
|
8713
9750
|
const unitPath = getUnitPath(paths.homeDir);
|
|
8714
9751
|
const desired = renderUnit({ cliEntry, port: opts.port });
|
|
8715
9752
|
try {
|
|
8716
|
-
await
|
|
9753
|
+
await fs57.mkdir(unitDir, { recursive: true });
|
|
8717
9754
|
let existing = null;
|
|
8718
9755
|
try {
|
|
8719
|
-
existing = await
|
|
9756
|
+
existing = await fs57.readFile(unitPath, "utf8");
|
|
8720
9757
|
} catch (err) {
|
|
8721
9758
|
if (err.code !== "ENOENT") throw err;
|
|
8722
9759
|
}
|
|
8723
9760
|
const unchanged = existing === desired;
|
|
8724
9761
|
if (!unchanged) {
|
|
8725
|
-
await
|
|
9762
|
+
await fs57.writeFile(unitPath, desired, "utf8");
|
|
8726
9763
|
}
|
|
8727
9764
|
const reload = await runOnce(spawn6, "systemctl", ["--user", "daemon-reload"]);
|
|
8728
9765
|
if (reload.spawnError) {
|
|
@@ -8784,7 +9821,7 @@ async function stepInstallSupervision(deps = {}, opts = {}) {
|
|
|
8784
9821
|
}
|
|
8785
9822
|
}
|
|
8786
9823
|
async function uninstallSupervision(deps = {}) {
|
|
8787
|
-
const { fs:
|
|
9824
|
+
const { fs: fs57, spawn: spawn6, paths, platform } = resolveLocalDeps(deps);
|
|
8788
9825
|
if (!isLinux(platform)) {
|
|
8789
9826
|
return {
|
|
8790
9827
|
ok: true,
|
|
@@ -8797,7 +9834,7 @@ async function uninstallSupervision(deps = {}) {
|
|
|
8797
9834
|
try {
|
|
8798
9835
|
let present = true;
|
|
8799
9836
|
try {
|
|
8800
|
-
await
|
|
9837
|
+
await fs57.stat(unitPath);
|
|
8801
9838
|
} catch (err) {
|
|
8802
9839
|
if (err.code === "ENOENT") present = false;
|
|
8803
9840
|
else throw err;
|
|
@@ -8812,7 +9849,7 @@ async function uninstallSupervision(deps = {}) {
|
|
|
8812
9849
|
}
|
|
8813
9850
|
const disable = await runOnce(spawn6, "systemctl", ["--user", "disable", "--now", UNIT_NAME]);
|
|
8814
9851
|
if (disable.spawnError) {
|
|
8815
|
-
await
|
|
9852
|
+
await fs57.rm(unitPath, { force: true });
|
|
8816
9853
|
return {
|
|
8817
9854
|
ok: true,
|
|
8818
9855
|
name: STEP_SUPERVISION,
|
|
@@ -8820,7 +9857,7 @@ async function uninstallSupervision(deps = {}) {
|
|
|
8820
9857
|
message: `Removed ${unitPath} (systemctl unavailable: ${disable.spawnError.message})`
|
|
8821
9858
|
};
|
|
8822
9859
|
}
|
|
8823
|
-
await
|
|
9860
|
+
await fs57.rm(unitPath, { force: true });
|
|
8824
9861
|
const reload = await runOnce(spawn6, "systemctl", ["--user", "daemon-reload"]);
|
|
8825
9862
|
if (reload.code !== 0 && !reload.spawnError) {
|
|
8826
9863
|
return {
|
|
@@ -8856,7 +9893,7 @@ function isDarwin(platform = process.platform) {
|
|
|
8856
9893
|
return platform === "darwin";
|
|
8857
9894
|
}
|
|
8858
9895
|
function resolveLocalDeps2(deps) {
|
|
8859
|
-
const
|
|
9896
|
+
const fs57 = deps.fs ?? fsp2;
|
|
8860
9897
|
const spawn6 = deps.spawn ?? nodeSpawn2;
|
|
8861
9898
|
const homeDir = deps.paths?.homeDir ?? os4.homedir();
|
|
8862
9899
|
const paths = deps.paths ?? {
|
|
@@ -8867,7 +9904,7 @@ function resolveLocalDeps2(deps) {
|
|
|
8867
9904
|
};
|
|
8868
9905
|
const cliEntry = deps.cliEntry ?? process.argv[1] ?? "active-work";
|
|
8869
9906
|
const uid = process.getuid?.() ?? 0;
|
|
8870
|
-
return { fs:
|
|
9907
|
+
return { fs: fs57, spawn: spawn6, paths, cliEntry, uid, platform: process.platform };
|
|
8871
9908
|
}
|
|
8872
9909
|
function getAgentDir(homeDir) {
|
|
8873
9910
|
return nodePath2.join(homeDir, "Library", "LaunchAgents");
|
|
@@ -8955,7 +9992,7 @@ async function isAgentLoaded(deps = {}) {
|
|
|
8955
9992
|
return result.code === 0;
|
|
8956
9993
|
}
|
|
8957
9994
|
async function installLaunchAgent(deps = {}, opts = {}) {
|
|
8958
|
-
const { fs:
|
|
9995
|
+
const { fs: fs57, spawn: spawn6, paths, cliEntry, uid, platform } = resolveLocalDeps2(deps);
|
|
8959
9996
|
if (!isDarwin(platform)) {
|
|
8960
9997
|
return {
|
|
8961
9998
|
ok: true,
|
|
@@ -8967,11 +10004,11 @@ async function installLaunchAgent(deps = {}, opts = {}) {
|
|
|
8967
10004
|
const plistPath = getPlistPath(paths.homeDir);
|
|
8968
10005
|
const desired = renderPlist({ cliEntry, homeDir: paths.homeDir, port: opts.port });
|
|
8969
10006
|
try {
|
|
8970
|
-
await
|
|
8971
|
-
await
|
|
10007
|
+
await fs57.mkdir(getAgentDir(paths.homeDir), { recursive: true });
|
|
10008
|
+
await fs57.mkdir(getLogDir(paths.homeDir), { recursive: true });
|
|
8972
10009
|
let existing = null;
|
|
8973
10010
|
try {
|
|
8974
|
-
existing = await
|
|
10011
|
+
existing = await fs57.readFile(plistPath, "utf8");
|
|
8975
10012
|
} catch (err) {
|
|
8976
10013
|
if (err.code !== "ENOENT") throw err;
|
|
8977
10014
|
}
|
|
@@ -8985,7 +10022,7 @@ async function installLaunchAgent(deps = {}, opts = {}) {
|
|
|
8985
10022
|
};
|
|
8986
10023
|
}
|
|
8987
10024
|
if (!unchanged) {
|
|
8988
|
-
await
|
|
10025
|
+
await fs57.writeFile(plistPath, desired, "utf8");
|
|
8989
10026
|
}
|
|
8990
10027
|
const bootout = await runOnce2(spawn6, "launchctl", ["bootout", serviceTarget(uid)]);
|
|
8991
10028
|
if (bootout.spawnError) {
|
|
@@ -9026,7 +10063,7 @@ async function installLaunchAgent(deps = {}, opts = {}) {
|
|
|
9026
10063
|
}
|
|
9027
10064
|
}
|
|
9028
10065
|
async function uninstallLaunchAgent(deps = {}) {
|
|
9029
|
-
const { fs:
|
|
10066
|
+
const { fs: fs57, spawn: spawn6, paths, uid, platform } = resolveLocalDeps2(deps);
|
|
9030
10067
|
if (!isDarwin(platform)) {
|
|
9031
10068
|
return {
|
|
9032
10069
|
ok: true,
|
|
@@ -9039,7 +10076,7 @@ async function uninstallLaunchAgent(deps = {}) {
|
|
|
9039
10076
|
try {
|
|
9040
10077
|
let present = true;
|
|
9041
10078
|
try {
|
|
9042
|
-
await
|
|
10079
|
+
await fs57.stat(plistPath);
|
|
9043
10080
|
} catch (err) {
|
|
9044
10081
|
if (err.code === "ENOENT") present = false;
|
|
9045
10082
|
else throw err;
|
|
@@ -9053,7 +10090,7 @@ async function uninstallLaunchAgent(deps = {}) {
|
|
|
9053
10090
|
};
|
|
9054
10091
|
}
|
|
9055
10092
|
const bootout = await runOnce2(spawn6, "launchctl", ["bootout", serviceTarget(uid)]);
|
|
9056
|
-
await
|
|
10093
|
+
await fs57.rm(plistPath, { force: true });
|
|
9057
10094
|
if (bootout.spawnError) {
|
|
9058
10095
|
return {
|
|
9059
10096
|
ok: true,
|
|
@@ -9114,7 +10151,7 @@ function findRepoRoot() {
|
|
|
9114
10151
|
return nodePath3.resolve(nodePath3.dirname(fileURLToPath2(import.meta.url)), "..", "..");
|
|
9115
10152
|
}
|
|
9116
10153
|
function resolveDeps(deps) {
|
|
9117
|
-
const
|
|
10154
|
+
const fs57 = deps.fs ?? fsp3;
|
|
9118
10155
|
const spawn6 = deps.spawn ?? nodeSpawn3;
|
|
9119
10156
|
const prompts = deps.prompts ?? clackPrompts;
|
|
9120
10157
|
const homeDir = deps.paths?.homeDir ?? os5.homedir();
|
|
@@ -9127,7 +10164,7 @@ function resolveDeps(deps) {
|
|
|
9127
10164
|
const repoRoot = deps.repoRoot ?? findRepoRoot();
|
|
9128
10165
|
const cliEntry = deps.cliEntry ?? process.argv[1] ?? "active-work";
|
|
9129
10166
|
return {
|
|
9130
|
-
fs:
|
|
10167
|
+
fs: fs57,
|
|
9131
10168
|
spawn: spawn6,
|
|
9132
10169
|
prompts,
|
|
9133
10170
|
paths,
|
|
@@ -9170,24 +10207,24 @@ async function stepCheckNode(deps = {}) {
|
|
|
9170
10207
|
message: `Node v${process.versions.node} OK`
|
|
9171
10208
|
};
|
|
9172
10209
|
}
|
|
9173
|
-
async function ensureDir(
|
|
10210
|
+
async function ensureDir(fs57, dir) {
|
|
9174
10211
|
try {
|
|
9175
|
-
const stat = await
|
|
10212
|
+
const stat = await fs57.stat(dir);
|
|
9176
10213
|
if (stat.isDirectory()) return { created: false };
|
|
9177
10214
|
throw new Error(`${dir} exists but is not a directory`);
|
|
9178
10215
|
} catch (err) {
|
|
9179
10216
|
const code = err.code;
|
|
9180
10217
|
if (code !== "ENOENT") throw err;
|
|
9181
10218
|
}
|
|
9182
|
-
await
|
|
10219
|
+
await fs57.mkdir(dir, { recursive: true });
|
|
9183
10220
|
return { created: true };
|
|
9184
10221
|
}
|
|
9185
10222
|
async function stepCreateActiveRoot(deps = {}) {
|
|
9186
|
-
const { fs:
|
|
10223
|
+
const { fs: fs57, paths } = resolveDeps(deps);
|
|
9187
10224
|
try {
|
|
9188
10225
|
const created = [];
|
|
9189
10226
|
for (const dir of [paths.activeRoot, paths.stateRoot, paths.configRoot]) {
|
|
9190
|
-
const { created: didCreate } = await ensureDir(
|
|
10227
|
+
const { created: didCreate } = await ensureDir(fs57, dir);
|
|
9191
10228
|
if (didCreate) created.push(dir);
|
|
9192
10229
|
}
|
|
9193
10230
|
const message = created.length === 0 ? `Active/state/config dirs already present` : `Created ${created.length} dir(s)`;
|
|
@@ -9236,18 +10273,18 @@ var CONFIG_STUB = {
|
|
|
9236
10273
|
channels: ["plugin:agent-chat@agent-chat-local"]
|
|
9237
10274
|
};
|
|
9238
10275
|
async function stepWriteConfigStub(deps = {}) {
|
|
9239
|
-
const { fs:
|
|
10276
|
+
const { fs: fs57, paths, update } = resolveDeps(deps);
|
|
9240
10277
|
const configPath = nodePath3.join(paths.configRoot, "config.json");
|
|
9241
10278
|
try {
|
|
9242
|
-
await ensureDir(
|
|
9243
|
-
let
|
|
10279
|
+
await ensureDir(fs57, paths.configRoot);
|
|
10280
|
+
let exists4 = false;
|
|
9244
10281
|
try {
|
|
9245
|
-
await
|
|
9246
|
-
|
|
10282
|
+
await fs57.stat(configPath);
|
|
10283
|
+
exists4 = true;
|
|
9247
10284
|
} catch (err) {
|
|
9248
10285
|
if (err.code !== "ENOENT") throw err;
|
|
9249
10286
|
}
|
|
9250
|
-
if (
|
|
10287
|
+
if (exists4 && !update) {
|
|
9251
10288
|
return {
|
|
9252
10289
|
ok: true,
|
|
9253
10290
|
name: STEP_CONFIG,
|
|
@@ -9255,7 +10292,7 @@ async function stepWriteConfigStub(deps = {}) {
|
|
|
9255
10292
|
message: `Config exists at ${configPath} (left untouched)`
|
|
9256
10293
|
};
|
|
9257
10294
|
}
|
|
9258
|
-
await
|
|
10295
|
+
await fs57.writeFile(configPath, JSON.stringify(CONFIG_STUB, null, 2) + "\n", "utf8");
|
|
9259
10296
|
return {
|
|
9260
10297
|
ok: true,
|
|
9261
10298
|
name: STEP_CONFIG,
|
|
@@ -9270,40 +10307,40 @@ async function stepWriteConfigStub(deps = {}) {
|
|
|
9270
10307
|
};
|
|
9271
10308
|
}
|
|
9272
10309
|
}
|
|
9273
|
-
async function pathExists2(
|
|
10310
|
+
async function pathExists2(fs57, p) {
|
|
9274
10311
|
try {
|
|
9275
|
-
await
|
|
10312
|
+
await fs57.stat(p);
|
|
9276
10313
|
return true;
|
|
9277
10314
|
} catch (err) {
|
|
9278
10315
|
if (err.code === "ENOENT") return false;
|
|
9279
10316
|
throw err;
|
|
9280
10317
|
}
|
|
9281
10318
|
}
|
|
9282
|
-
async function copyTree(
|
|
9283
|
-
const cp =
|
|
10319
|
+
async function copyTree(fs57, src, dest) {
|
|
10320
|
+
const cp = fs57.cp;
|
|
9284
10321
|
if (cp) {
|
|
9285
10322
|
await cp(src, dest, { recursive: true });
|
|
9286
10323
|
return;
|
|
9287
10324
|
}
|
|
9288
|
-
await
|
|
9289
|
-
const entries = await
|
|
10325
|
+
await fs57.mkdir(dest, { recursive: true });
|
|
10326
|
+
const entries = await fs57.readdir(src, { withFileTypes: true });
|
|
9290
10327
|
for (const entry of entries) {
|
|
9291
10328
|
const from = nodePath3.join(src, entry.name);
|
|
9292
10329
|
const to = nodePath3.join(dest, entry.name);
|
|
9293
10330
|
if (entry.isDirectory()) {
|
|
9294
|
-
await copyTree(
|
|
10331
|
+
await copyTree(fs57, from, to);
|
|
9295
10332
|
} else {
|
|
9296
|
-
await
|
|
10333
|
+
await fs57.copyFile(from, to);
|
|
9297
10334
|
}
|
|
9298
10335
|
}
|
|
9299
10336
|
}
|
|
9300
10337
|
async function stepInstallSkill(deps = {}) {
|
|
9301
|
-
const { fs:
|
|
10338
|
+
const { fs: fs57, paths, repoRoot } = resolveDeps(deps);
|
|
9302
10339
|
const targetDir = nodePath3.join(paths.homeDir, ".claude", "skills", "active-work");
|
|
9303
10340
|
const targetMarker = nodePath3.join(targetDir, "SKILL.md");
|
|
9304
10341
|
const sourceDir = nodePath3.join(repoRoot, "skill");
|
|
9305
10342
|
try {
|
|
9306
|
-
if (await pathExists2(
|
|
10343
|
+
if (await pathExists2(fs57, targetMarker)) {
|
|
9307
10344
|
return {
|
|
9308
10345
|
ok: true,
|
|
9309
10346
|
name: STEP_SKILL,
|
|
@@ -9311,7 +10348,7 @@ async function stepInstallSkill(deps = {}) {
|
|
|
9311
10348
|
message: `Skill already installed at ${targetDir}`
|
|
9312
10349
|
};
|
|
9313
10350
|
}
|
|
9314
|
-
if (!await pathExists2(
|
|
10351
|
+
if (!await pathExists2(fs57, sourceDir)) {
|
|
9315
10352
|
return {
|
|
9316
10353
|
ok: true,
|
|
9317
10354
|
name: STEP_SKILL,
|
|
@@ -9319,8 +10356,8 @@ async function stepInstallSkill(deps = {}) {
|
|
|
9319
10356
|
message: `Skill source not found at ${sourceDir}; skipping`
|
|
9320
10357
|
};
|
|
9321
10358
|
}
|
|
9322
|
-
await
|
|
9323
|
-
await copyTree(
|
|
10359
|
+
await fs57.mkdir(nodePath3.dirname(targetDir), { recursive: true });
|
|
10360
|
+
await copyTree(fs57, sourceDir, targetDir);
|
|
9324
10361
|
return {
|
|
9325
10362
|
ok: true,
|
|
9326
10363
|
name: STEP_SKILL,
|
|
@@ -9336,12 +10373,12 @@ async function stepInstallSkill(deps = {}) {
|
|
|
9336
10373
|
}
|
|
9337
10374
|
}
|
|
9338
10375
|
async function stepInstallCommand(deps = {}) {
|
|
9339
|
-
const { fs:
|
|
10376
|
+
const { fs: fs57, paths, repoRoot } = resolveDeps(deps);
|
|
9340
10377
|
const targetDir = nodePath3.join(paths.homeDir, ".claude", "commands");
|
|
9341
10378
|
const target = nodePath3.join(targetDir, "aw-prompt.md");
|
|
9342
10379
|
const source = nodePath3.join(repoRoot, "claude-commands", "aw-prompt.md");
|
|
9343
10380
|
try {
|
|
9344
|
-
if (!await pathExists2(
|
|
10381
|
+
if (!await pathExists2(fs57, source)) {
|
|
9345
10382
|
return {
|
|
9346
10383
|
ok: true,
|
|
9347
10384
|
name: STEP_COMMAND,
|
|
@@ -9349,8 +10386,8 @@ async function stepInstallCommand(deps = {}) {
|
|
|
9349
10386
|
message: `Command source not found at ${source}; skipping`
|
|
9350
10387
|
};
|
|
9351
10388
|
}
|
|
9352
|
-
await
|
|
9353
|
-
await
|
|
10389
|
+
await fs57.mkdir(targetDir, { recursive: true });
|
|
10390
|
+
await fs57.copyFile(source, target);
|
|
9354
10391
|
return {
|
|
9355
10392
|
ok: true,
|
|
9356
10393
|
name: STEP_COMMAND,
|
|
@@ -9451,11 +10488,11 @@ function agentChatHooksPath(paths) {
|
|
|
9451
10488
|
return nodePath3.join(home, "hooks.json");
|
|
9452
10489
|
}
|
|
9453
10490
|
async function stepRegisterAgentChatHooks(deps = {}) {
|
|
9454
|
-
const { fs:
|
|
10491
|
+
const { fs: fs57, paths } = resolveDeps(deps);
|
|
9455
10492
|
const hooksPath = agentChatHooksPath(paths);
|
|
9456
10493
|
const agentChatHome = nodePath3.dirname(hooksPath);
|
|
9457
10494
|
try {
|
|
9458
|
-
await
|
|
10495
|
+
await fs57.stat(agentChatHome);
|
|
9459
10496
|
} catch (err) {
|
|
9460
10497
|
if (err.code === "ENOENT") {
|
|
9461
10498
|
return {
|
|
@@ -9470,7 +10507,7 @@ async function stepRegisterAgentChatHooks(deps = {}) {
|
|
|
9470
10507
|
try {
|
|
9471
10508
|
let config = {};
|
|
9472
10509
|
try {
|
|
9473
|
-
const raw = await
|
|
10510
|
+
const raw = await fs57.readFile(hooksPath, "utf8");
|
|
9474
10511
|
const parsed = JSON.parse(raw);
|
|
9475
10512
|
if (typeof parsed === "object" && parsed !== null) config = parsed;
|
|
9476
10513
|
} catch (err) {
|
|
@@ -9499,7 +10536,7 @@ async function stepRegisterAgentChatHooks(deps = {}) {
|
|
|
9499
10536
|
message: `Already registered in ${hooksPath}`
|
|
9500
10537
|
};
|
|
9501
10538
|
}
|
|
9502
|
-
await
|
|
10539
|
+
await fs57.writeFile(hooksPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
9503
10540
|
return {
|
|
9504
10541
|
ok: true,
|
|
9505
10542
|
name: STEP_AGENT_CHAT_HOOKS,
|
|
@@ -9673,10 +10710,10 @@ async function confirmStep(prompts, yes, message, initial = true) {
|
|
|
9673
10710
|
return answer === true;
|
|
9674
10711
|
}
|
|
9675
10712
|
async function uninstallSkill(deps = {}) {
|
|
9676
|
-
const { fs:
|
|
10713
|
+
const { fs: fs57, paths } = resolveDeps(deps);
|
|
9677
10714
|
const target = nodePath3.join(paths.homeDir, ".claude", "skills", "active-work");
|
|
9678
10715
|
try {
|
|
9679
|
-
if (!await pathExists2(
|
|
10716
|
+
if (!await pathExists2(fs57, target)) {
|
|
9680
10717
|
return {
|
|
9681
10718
|
ok: true,
|
|
9682
10719
|
name: STEP_SKILL,
|
|
@@ -9684,7 +10721,7 @@ async function uninstallSkill(deps = {}) {
|
|
|
9684
10721
|
message: `Skill not present at ${target}`
|
|
9685
10722
|
};
|
|
9686
10723
|
}
|
|
9687
|
-
await
|
|
10724
|
+
await fs57.rm(target, { recursive: true, force: true });
|
|
9688
10725
|
return {
|
|
9689
10726
|
ok: true,
|
|
9690
10727
|
name: STEP_SKILL,
|
|
@@ -9696,10 +10733,10 @@ async function uninstallSkill(deps = {}) {
|
|
|
9696
10733
|
}
|
|
9697
10734
|
}
|
|
9698
10735
|
async function uninstallCommand(deps = {}) {
|
|
9699
|
-
const { fs:
|
|
10736
|
+
const { fs: fs57, paths } = resolveDeps(deps);
|
|
9700
10737
|
const target = nodePath3.join(paths.homeDir, ".claude", "commands", "aw-prompt.md");
|
|
9701
10738
|
try {
|
|
9702
|
-
if (!await pathExists2(
|
|
10739
|
+
if (!await pathExists2(fs57, target)) {
|
|
9703
10740
|
return {
|
|
9704
10741
|
ok: true,
|
|
9705
10742
|
name: STEP_COMMAND,
|
|
@@ -9707,7 +10744,7 @@ async function uninstallCommand(deps = {}) {
|
|
|
9707
10744
|
message: `Command not present at ${target}`
|
|
9708
10745
|
};
|
|
9709
10746
|
}
|
|
9710
|
-
await
|
|
10747
|
+
await fs57.rm(target, { force: true });
|
|
9711
10748
|
return {
|
|
9712
10749
|
ok: true,
|
|
9713
10750
|
name: STEP_COMMAND,
|
|
@@ -9972,64 +11009,33 @@ import { promises as fsp4 } from "fs";
|
|
|
9972
11009
|
import nodePath4 from "path";
|
|
9973
11010
|
import os6 from "os";
|
|
9974
11011
|
|
|
9975
|
-
// src/
|
|
9976
|
-
import { promises as
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
const
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
const code = err.code;
|
|
9995
|
-
if (code === "ENOENT") continue;
|
|
9996
|
-
throw err;
|
|
9997
|
-
}
|
|
9998
|
-
if (hashContent(content) === storedHash) continue;
|
|
9999
|
-
findings.push({
|
|
10000
|
-
level: "warn",
|
|
10001
|
-
slug,
|
|
10002
|
-
file: relPath,
|
|
10003
|
-
message: `${relPath} was hand-edited outside active-work (content no longer matches the last CLI write) \u2014 re-apply the change through the CLI/MCP tools instead of editing the file directly`
|
|
10004
|
-
});
|
|
10005
|
-
}
|
|
10006
|
-
return findings;
|
|
11012
|
+
// src/workspace-index/doctor.ts
|
|
11013
|
+
import { promises as fs55 } from "fs";
|
|
11014
|
+
var INDEXED_REFS = `
|
|
11015
|
+
SELECT initiative_ref AS ref, path FROM initiative
|
|
11016
|
+
UNION ALL SELECT note_ref, path FROM note
|
|
11017
|
+
UNION ALL SELECT task_ref, path FROM workspace_task
|
|
11018
|
+
UNION ALL SELECT session_ref, path FROM session_record
|
|
11019
|
+
UNION ALL SELECT source_ref, path FROM source
|
|
11020
|
+
ORDER BY path
|
|
11021
|
+
`;
|
|
11022
|
+
async function checkWorkspaceIndex(graph, activeRoot) {
|
|
11023
|
+
const indexed = graph.db.prepare(INDEXED_REFS).all();
|
|
11024
|
+
const results = await Promise.all(
|
|
11025
|
+
indexed.map(async (row) => await opens(toAbsolute(activeRoot, row.path)) ? null : row)
|
|
11026
|
+
);
|
|
11027
|
+
return {
|
|
11028
|
+
checked: indexed.length,
|
|
11029
|
+
missing: results.filter((row) => row !== null).map((row) => `${row.ref} -> ${row.path}`)
|
|
11030
|
+
};
|
|
10007
11031
|
}
|
|
10008
|
-
|
|
10009
|
-
// src/lint/open-loops.ts
|
|
10010
|
-
import path60 from "path";
|
|
10011
|
-
|
|
10012
|
-
// src/lint/task.ts
|
|
10013
|
-
import { promises as fs49 } from "fs";
|
|
10014
|
-
import path61 from "path";
|
|
10015
|
-
import YAML3 from "yaml";
|
|
10016
|
-
|
|
10017
|
-
// src/lint/zero-loops.ts
|
|
10018
|
-
import { promises as fs50 } from "fs";
|
|
10019
|
-
import path62 from "path";
|
|
10020
|
-
import YAML4 from "yaml";
|
|
10021
|
-
|
|
10022
|
-
// src/lint/index.ts
|
|
10023
|
-
async function listInitiativeSlugs3(activeRoot) {
|
|
10024
|
-
let entries;
|
|
11032
|
+
async function opens(absolutePath) {
|
|
10025
11033
|
try {
|
|
10026
|
-
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
|
|
10030
|
-
throw err;
|
|
11034
|
+
await fs55.access(absolutePath);
|
|
11035
|
+
return true;
|
|
11036
|
+
} catch {
|
|
11037
|
+
return false;
|
|
10031
11038
|
}
|
|
10032
|
-
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort();
|
|
10033
11039
|
}
|
|
10034
11040
|
|
|
10035
11041
|
// src/doctor.ts
|
|
@@ -10078,19 +11084,19 @@ function parseMajor(version) {
|
|
|
10078
11084
|
const match = /^v?(\d+)\./.exec(version);
|
|
10079
11085
|
return match ? Number(match[1]) : 0;
|
|
10080
11086
|
}
|
|
10081
|
-
async function fileExists3(
|
|
11087
|
+
async function fileExists3(fs57, target) {
|
|
10082
11088
|
try {
|
|
10083
|
-
await
|
|
11089
|
+
await fs57.access(target);
|
|
10084
11090
|
return true;
|
|
10085
11091
|
} catch {
|
|
10086
11092
|
return false;
|
|
10087
11093
|
}
|
|
10088
11094
|
}
|
|
10089
11095
|
var MCP_SERVER_NAMES = ["@hjewkes/active-work", "active-work"];
|
|
10090
|
-
async function readMcpRegistered(
|
|
11096
|
+
async function readMcpRegistered(fs57, homeDir) {
|
|
10091
11097
|
const configPath = nodePath4.join(homeDir, ".claude.json");
|
|
10092
11098
|
try {
|
|
10093
|
-
const raw = await
|
|
11099
|
+
const raw = await fs57.readFile(configPath, "utf8");
|
|
10094
11100
|
const parsed = JSON.parse(raw);
|
|
10095
11101
|
const servers = parsed.mcpServers ?? {};
|
|
10096
11102
|
return MCP_SERVER_NAMES.some((name) => Boolean(servers[name]));
|
|
@@ -10112,9 +11118,9 @@ async function checkNode(deps) {
|
|
|
10112
11118
|
};
|
|
10113
11119
|
}
|
|
10114
11120
|
async function checkActiveRoot(deps) {
|
|
10115
|
-
const
|
|
11121
|
+
const fs57 = deps.fs ?? fsp4;
|
|
10116
11122
|
const activeRoot = deps.activeRoot ?? getActiveRoot();
|
|
10117
|
-
if (!await fileExists3(
|
|
11123
|
+
if (!await fileExists3(fs57, activeRoot)) {
|
|
10118
11124
|
return {
|
|
10119
11125
|
name: "active-root",
|
|
10120
11126
|
status: "fail",
|
|
@@ -10122,7 +11128,7 @@ async function checkActiveRoot(deps) {
|
|
|
10122
11128
|
};
|
|
10123
11129
|
}
|
|
10124
11130
|
const schemaFile = nodePath4.join(activeRoot, ".schema-version");
|
|
10125
|
-
if (!await fileExists3(
|
|
11131
|
+
if (!await fileExists3(fs57, schemaFile)) {
|
|
10126
11132
|
return {
|
|
10127
11133
|
name: "active-root",
|
|
10128
11134
|
status: "warn",
|
|
@@ -10159,9 +11165,9 @@ async function checkDaemon(deps) {
|
|
|
10159
11165
|
};
|
|
10160
11166
|
}
|
|
10161
11167
|
async function checkMcp(deps) {
|
|
10162
|
-
const
|
|
11168
|
+
const fs57 = deps.fs ?? fsp4;
|
|
10163
11169
|
const homeDir = deps.homeDir ?? os6.homedir();
|
|
10164
|
-
if (await readMcpRegistered(
|
|
11170
|
+
if (await readMcpRegistered(fs57, homeDir)) {
|
|
10165
11171
|
return { name: "mcp-registration", status: "ok", detail: "registered in ~/.claude.json" };
|
|
10166
11172
|
}
|
|
10167
11173
|
return {
|
|
@@ -10171,10 +11177,10 @@ async function checkMcp(deps) {
|
|
|
10171
11177
|
};
|
|
10172
11178
|
}
|
|
10173
11179
|
async function checkSkill(deps) {
|
|
10174
|
-
const
|
|
11180
|
+
const fs57 = deps.fs ?? fsp4;
|
|
10175
11181
|
const homeDir = deps.homeDir ?? os6.homedir();
|
|
10176
11182
|
const skill = nodePath4.join(homeDir, ".claude", "skills", "active-work", "SKILL.md");
|
|
10177
|
-
if (await fileExists3(
|
|
11183
|
+
if (await fileExists3(fs57, skill)) {
|
|
10178
11184
|
return { name: "skill", status: "ok", detail: skill };
|
|
10179
11185
|
}
|
|
10180
11186
|
return {
|
|
@@ -10251,7 +11257,7 @@ function sessionFilesCheck(malformed) {
|
|
|
10251
11257
|
}
|
|
10252
11258
|
async function checkSessions(deps) {
|
|
10253
11259
|
const activeRoot = deps.activeRoot ?? getActiveRoot();
|
|
10254
|
-
const slugs = await
|
|
11260
|
+
const slugs = await listInitiativeSlugs2(activeRoot);
|
|
10255
11261
|
const byKind = /* @__PURE__ */ new Map();
|
|
10256
11262
|
const malformed = [];
|
|
10257
11263
|
const badTaskRefs = [];
|
|
@@ -10293,7 +11299,7 @@ function noteTitlesCheck(entries) {
|
|
|
10293
11299
|
}
|
|
10294
11300
|
async function checkNoteTitles(deps) {
|
|
10295
11301
|
const activeRoot = deps.activeRoot ?? getActiveRoot();
|
|
10296
|
-
const slugs = await
|
|
11302
|
+
const slugs = await listInitiativeSlugs2(activeRoot);
|
|
10297
11303
|
const overlong = [];
|
|
10298
11304
|
for (const slug of slugs) {
|
|
10299
11305
|
const { notes } = await loadNotesFromDir(nodePath4.join(activeRoot, slug));
|
|
@@ -10333,9 +11339,42 @@ function artifactHashesCheck(drifted) {
|
|
|
10333
11339
|
detail: `hand-edited outside active-work: ${drifted.join("; ")}`
|
|
10334
11340
|
};
|
|
10335
11341
|
}
|
|
11342
|
+
function workspaceIndexCheck(health) {
|
|
11343
|
+
if (health === null) {
|
|
11344
|
+
return { name: "workspace-index", status: "ok", detail: "no index built yet" };
|
|
11345
|
+
}
|
|
11346
|
+
if (health.missing.length === 0) {
|
|
11347
|
+
return {
|
|
11348
|
+
name: "workspace-index",
|
|
11349
|
+
status: "ok",
|
|
11350
|
+
detail: `${health.checked} indexed refs all resolve to a file that opens`
|
|
11351
|
+
};
|
|
11352
|
+
}
|
|
11353
|
+
return {
|
|
11354
|
+
name: "workspace-index",
|
|
11355
|
+
status: "warn",
|
|
11356
|
+
// Brain's whole failure, made visible: 5,013 of 5,065 notes detached on one
|
|
11357
|
+
// directory move and nothing noticed, because nothing checked.
|
|
11358
|
+
detail: `${health.missing.length} of ${health.checked} indexed refs point at a file that no longer opens (run \`active-work miner refresh\`): ${health.missing.slice(0, 5).join("; ")}`
|
|
11359
|
+
};
|
|
11360
|
+
}
|
|
11361
|
+
async function checkWorkspaceIndexRefs(deps) {
|
|
11362
|
+
const activeRoot = deps.activeRoot ?? getActiveRoot();
|
|
11363
|
+
let graph;
|
|
11364
|
+
try {
|
|
11365
|
+
graph = openGraph();
|
|
11366
|
+
} catch {
|
|
11367
|
+
return workspaceIndexCheck(null);
|
|
11368
|
+
}
|
|
11369
|
+
try {
|
|
11370
|
+
return workspaceIndexCheck(await checkWorkspaceIndex(graph, activeRoot));
|
|
11371
|
+
} finally {
|
|
11372
|
+
graph.db.close();
|
|
11373
|
+
}
|
|
11374
|
+
}
|
|
10336
11375
|
async function checkArtifactHashes(deps) {
|
|
10337
11376
|
const activeRoot = deps.activeRoot ?? getActiveRoot();
|
|
10338
|
-
const slugs = await
|
|
11377
|
+
const slugs = await listInitiativeSlugs2(activeRoot);
|
|
10339
11378
|
const drifted = [];
|
|
10340
11379
|
for (const slug of slugs) {
|
|
10341
11380
|
const findings = await lintHashes(slug, nodePath4.join(activeRoot, slug));
|
|
@@ -10344,7 +11383,7 @@ async function checkArtifactHashes(deps) {
|
|
|
10344
11383
|
return artifactHashesCheck(drifted);
|
|
10345
11384
|
}
|
|
10346
11385
|
async function runDoctor(deps = {}) {
|
|
10347
|
-
const [installChecks, sessionChecks, noteTitles, artifactHashes, leases] = await Promise.all([
|
|
11386
|
+
const [installChecks, sessionChecks, noteTitles, artifactHashes, leases, workspaceIndex] = await Promise.all([
|
|
10348
11387
|
Promise.all([
|
|
10349
11388
|
checkNode(deps),
|
|
10350
11389
|
checkActiveRoot(deps),
|
|
@@ -10356,9 +11395,17 @@ async function runDoctor(deps = {}) {
|
|
|
10356
11395
|
checkSessions(deps),
|
|
10357
11396
|
checkNoteTitles(deps),
|
|
10358
11397
|
checkArtifactHashes(deps),
|
|
10359
|
-
checkLeases(deps)
|
|
11398
|
+
checkLeases(deps),
|
|
11399
|
+
checkWorkspaceIndexRefs(deps)
|
|
10360
11400
|
]);
|
|
10361
|
-
const checks = [
|
|
11401
|
+
const checks = [
|
|
11402
|
+
...installChecks,
|
|
11403
|
+
...sessionChecks,
|
|
11404
|
+
noteTitles,
|
|
11405
|
+
artifactHashes,
|
|
11406
|
+
leases,
|
|
11407
|
+
workspaceIndex
|
|
11408
|
+
];
|
|
10362
11409
|
return { ok: checks.every((c) => c.status !== "fail"), checks };
|
|
10363
11410
|
}
|
|
10364
11411
|
|
|
@@ -10428,7 +11475,7 @@ var ResultSchema48 = z61.object({
|
|
|
10428
11475
|
repairs: z61.array(z61.object({ action: z61.string(), file: z61.string(), detail: z61.string() })),
|
|
10429
11476
|
uncovered: z61.array(z61.string())
|
|
10430
11477
|
});
|
|
10431
|
-
function
|
|
11478
|
+
function describe2(plan, applied) {
|
|
10432
11479
|
const initiatives = plan.initiatives.map((i) => ({
|
|
10433
11480
|
slug: i.slug,
|
|
10434
11481
|
sessions: i.sessions.map((s) => ({
|
|
@@ -10514,7 +11561,7 @@ var migrate_default = defineCommand({
|
|
|
10514
11561
|
async run(args, ctx) {
|
|
10515
11562
|
if (args.apply !== true) {
|
|
10516
11563
|
const plan2 = await planV2ToV3(ctx.activeRoot);
|
|
10517
|
-
const result2 =
|
|
11564
|
+
const result2 = describe2(plan2, false);
|
|
10518
11565
|
if (ctx.format !== "json") process.stderr.write(render(result2));
|
|
10519
11566
|
return result2;
|
|
10520
11567
|
}
|
|
@@ -10530,7 +11577,7 @@ var migrate_default = defineCommand({
|
|
|
10530
11577
|
const plan = await planV2ToV3(ctx.activeRoot);
|
|
10531
11578
|
await applyV2ToV3(ctx.activeRoot, plan);
|
|
10532
11579
|
await writeSchemaVersion(ctx.activeRoot, CURRENT_VERSION);
|
|
10533
|
-
const result =
|
|
11580
|
+
const result = describe2(plan, true);
|
|
10534
11581
|
if (ctx.format !== "json") process.stderr.write(render(result));
|
|
10535
11582
|
return result;
|
|
10536
11583
|
}
|
|
@@ -10785,16 +11832,16 @@ for (const cmd of ALL_COMMANDS) {
|
|
|
10785
11832
|
import { readCommanderOption } from "@titan-design/registry";
|
|
10786
11833
|
|
|
10787
11834
|
// src/utils/usage-log.ts
|
|
10788
|
-
import { promises as
|
|
10789
|
-
import
|
|
11835
|
+
import { promises as fs56 } from "fs";
|
|
11836
|
+
import path66 from "path";
|
|
10790
11837
|
function usageLogPath() {
|
|
10791
|
-
return
|
|
11838
|
+
return path66.join(getStateRoot(), "usage.jsonl");
|
|
10792
11839
|
}
|
|
10793
11840
|
async function appendUsage(rec) {
|
|
10794
11841
|
try {
|
|
10795
11842
|
const file = usageLogPath();
|
|
10796
|
-
await
|
|
10797
|
-
await
|
|
11843
|
+
await fs56.mkdir(path66.dirname(file), { recursive: true });
|
|
11844
|
+
await fs56.appendFile(file, JSON.stringify(rec) + "\n", "utf8");
|
|
10798
11845
|
} catch {
|
|
10799
11846
|
}
|
|
10800
11847
|
}
|