threadnote 1.4.5 → 1.6.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.
@@ -3447,7 +3447,7 @@ var DEFAULT_SEED_PATTERNS = [
3447
3447
  ];
3448
3448
 
3449
3449
  // src/hooks.ts
3450
- var import_promises8 = require("node:fs/promises");
3450
+ var import_promises9 = require("node:fs/promises");
3451
3451
  var import_node_path11 = require("node:path");
3452
3452
 
3453
3453
  // src/mcp.ts
@@ -3663,7 +3663,7 @@ function hasGlob(path2) {
3663
3663
  return path2.includes("*") || path2.includes("?");
3664
3664
  }
3665
3665
  function escapeRegExp(value) {
3666
- return value.replace(/[\\^$+?.()|[\]{}]/g, "\\$&");
3666
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3667
3667
  }
3668
3668
  async function requiredOpenVikingCli() {
3669
3669
  const command2 = await findOpenVikingCli();
@@ -4202,14 +4202,19 @@ function exactRecallTerms(query) {
4202
4202
  "after",
4203
4203
  "agent",
4204
4204
  "anything",
4205
+ "been",
4205
4206
  "branch",
4206
4207
  "case",
4207
4208
  "current",
4209
+ "does",
4208
4210
  "durable",
4209
4211
  "find",
4210
4212
  "feature",
4211
4213
  "features",
4214
+ "from",
4212
4215
  "handoff",
4216
+ "have",
4217
+ "into",
4213
4218
  "issue",
4214
4219
  "issues",
4215
4220
  "knowledge",
@@ -4223,11 +4228,22 @@ function exactRecallTerms(query) {
4223
4228
  "related",
4224
4229
  "search",
4225
4230
  "stored",
4231
+ "than",
4232
+ "that",
4233
+ "them",
4234
+ "then",
4235
+ "they",
4226
4236
  "this",
4227
4237
  "the",
4238
+ "were",
4239
+ "what",
4240
+ "when",
4241
+ "which",
4242
+ "while",
4228
4243
  "with",
4229
4244
  "workspace",
4230
- "worktree"
4245
+ "worktree",
4246
+ "your"
4231
4247
  ]);
4232
4248
  const seen = /* @__PURE__ */ new Set();
4233
4249
  const terms = [];
@@ -4261,6 +4277,34 @@ function exactRecallScopeIntents(query) {
4261
4277
  function isSummarySidecarUri(uri) {
4262
4278
  return /\.(?:overview|abstract)\.md(?:#|$)/.test(uri);
4263
4279
  }
4280
+ function isAgentArtifactPackUri(uri) {
4281
+ return /\/agent-artifacts\/packs\//.test(uri);
4282
+ }
4283
+ function isExcludedRecallUri(uri) {
4284
+ return isSummarySidecarUri(uri) || isAgentArtifactPackUri(uri);
4285
+ }
4286
+ function memoryUriProjectSegment(uri) {
4287
+ const marker = "/memories/";
4288
+ const at = uri.indexOf(marker);
4289
+ if (at < 0) {
4290
+ return void 0;
4291
+ }
4292
+ let segments = stripAnchor(uri).slice(at + marker.length).split("/").filter(Boolean);
4293
+ if (segments[0] === "shared") {
4294
+ segments = segments.slice(2);
4295
+ }
4296
+ const [kind, , project, ...rest] = segments;
4297
+ if ((kind === "durable" || kind === "handoffs" || kind === "incidents") && project && rest.length > 0) {
4298
+ return project;
4299
+ }
4300
+ return void 0;
4301
+ }
4302
+ function memoryFrontmatterField(content, field) {
4303
+ const blankLine = content.indexOf("\n\n");
4304
+ const header = blankLine < 0 ? content : content.slice(0, blankLine);
4305
+ const match = header.match(new RegExp(`^${escapeRegExp(field)}:[ \\t]*(.+)$`, "m"));
4306
+ return match?.[1]?.trim() || void 0;
4307
+ }
4264
4308
  function grepUrisFromJson(output2) {
4265
4309
  const start = output2.search(/^\{/m);
4266
4310
  if (start < 0) {
@@ -4275,7 +4319,7 @@ function grepUrisFromJson(output2) {
4275
4319
  }
4276
4320
  const uris = [];
4277
4321
  for (const match of matches) {
4278
- if (isJsonObject(match) && typeof match.uri === "string" && !isSummarySidecarUri(match.uri)) {
4322
+ if (isJsonObject(match) && typeof match.uri === "string" && !isExcludedRecallUri(match.uri)) {
4279
4323
  uris.push(match.uri);
4280
4324
  }
4281
4325
  }
@@ -4317,7 +4361,7 @@ function parseRecallHits(output2, options = {}) {
4317
4361
  continue;
4318
4362
  }
4319
4363
  for (const item of items) {
4320
- if (!isJsonObject(item) || typeof item.uri !== "string" || isSummarySidecarUri(item.uri)) {
4364
+ if (!isJsonObject(item) || typeof item.uri !== "string" || isExcludedRecallUri(item.uri)) {
4321
4365
  continue;
4322
4366
  }
4323
4367
  if (options.includeArchived !== true && isArchivedMemoryUri(item.uri)) {
@@ -4376,6 +4420,9 @@ ${hit.snippet}`;
4376
4420
  return kept;
4377
4421
  }
4378
4422
  function categoryForUri(uri) {
4423
+ if (uri.includes("/agent-artifacts/")) {
4424
+ return "skills";
4425
+ }
4379
4426
  if (uri.includes("/memories/")) {
4380
4427
  return "memories";
4381
4428
  }
@@ -4390,6 +4437,36 @@ function contextTypeForCategory(category) {
4390
4437
  }
4391
4438
  return category === "skills" ? "skill" : "resource";
4392
4439
  }
4440
+ var RECALL_EXACT_SLUG_BONUS = 4;
4441
+ function uriSlug(uri) {
4442
+ const withoutExtension = stripAnchor(uri).replace(/\.[a-z0-9]+$/i, "");
4443
+ return withoutExtension.slice(withoutExtension.lastIndexOf("/") + 1).toLowerCase();
4444
+ }
4445
+ function exactTermDocumentFrequency(matches) {
4446
+ const frequency = /* @__PURE__ */ new Map();
4447
+ for (const match of matches) {
4448
+ for (const term of new Set(match.terms.map((term2) => term2.toLowerCase()))) {
4449
+ frequency.set(term, (frequency.get(term) ?? 0) + 1);
4450
+ }
4451
+ }
4452
+ return frequency;
4453
+ }
4454
+ function slugNamesTerm(slug, term) {
4455
+ return new RegExp(`(^|[^a-z0-9])${escapeRegExp(term)}([^a-z0-9]|$)`).test(slug);
4456
+ }
4457
+ function exactMatchStrength(hit, documentFrequency) {
4458
+ if (!hit.exactTerms?.length) {
4459
+ return 0;
4460
+ }
4461
+ const slug = uriSlug(hit.uri);
4462
+ let strength = 0;
4463
+ for (const term of hit.exactTerms) {
4464
+ const normalized = term.toLowerCase();
4465
+ const rarity = 1 / (documentFrequency.get(normalized) ?? 1);
4466
+ strength += rarity * (slugNamesTerm(slug, normalized) ? RECALL_EXACT_SLUG_BONUS : 1);
4467
+ }
4468
+ return strength;
4469
+ }
4393
4470
  function applyExactMatchBoost(hits, exactMatches) {
4394
4471
  if (exactMatches.length === 0) {
4395
4472
  return hits;
@@ -4411,24 +4488,33 @@ function applyExactMatchBoost(hits, exactMatches) {
4411
4488
  uri
4412
4489
  };
4413
4490
  });
4414
- return [...annotated, ...promoted].sort(
4415
- (left, right) => recallCategoryRank(left.category) - recallCategoryRank(right.category) || (right.exactTerms?.length ?? 0) - (left.exactTerms?.length ?? 0) || right.score - left.score
4491
+ const documentFrequency = exactTermDocumentFrequency(exactMatches);
4492
+ const merged = [...annotated, ...promoted];
4493
+ const combinedRelevanceByUri = /* @__PURE__ */ new Map();
4494
+ for (const hit of merged) {
4495
+ combinedRelevanceByUri.set(stripAnchor(hit.uri), exactMatchStrength(hit, documentFrequency) + hit.score);
4496
+ }
4497
+ return merged.sort(
4498
+ (left, right) => recallCategoryRank(left.category) - recallCategoryRank(right.category) || (combinedRelevanceByUri.get(stripAnchor(right.uri)) ?? 0) - (combinedRelevanceByUri.get(stripAnchor(left.uri)) ?? 0) || right.score - left.score
4416
4499
  );
4417
4500
  }
4501
+ var RECALL_LOW_CONFIDENCE_NOTE = "\u26A0 No semantically-relevant matches \u2014 the results below only contain the query words (the corpus may not cover this topic).";
4418
4502
  function renderRecallHits(shown, overflow) {
4419
4503
  if (shown.length === 0) {
4420
4504
  return void 0;
4421
4505
  }
4422
4506
  const lines = shown.flatMap((hit, index) => {
4423
4507
  const scorePart = hit.score > 0 ? `score ${hit.score.toFixed(2)}` : void 0;
4424
- const exactPart = hit.exactTerms?.length ? `exact: ${hit.exactTerms.join(", ")}` : void 0;
4508
+ const exactLabel = hit.score > 0 ? "exact" : "keyword-only";
4509
+ const exactPart = hit.exactTerms?.length ? `${exactLabel}: ${hit.exactTerms.join(", ")}` : void 0;
4425
4510
  const head = `${index + 1}. ${[hit.contextType, scorePart, exactPart].filter(Boolean).join(" \xB7 ")} \xB7 ${hit.uri}`;
4426
4511
  return hit.snippet ? [head, ` ${hit.snippet}`] : [head];
4427
4512
  });
4428
4513
  if (overflow > 0) {
4429
4514
  lines.push(`(+${overflow} more \u2014 refine the query or read a URI above)`);
4430
4515
  }
4431
- return lines.join("\n");
4516
+ const noSemanticMatch = shown.every((hit) => hit.score === 0);
4517
+ return (noSemanticMatch ? [RECALL_LOW_CONFIDENCE_NOTE, ...lines] : lines).join("\n");
4432
4518
  }
4433
4519
  var RECALL_CATEGORY_RESERVE = 2;
4434
4520
  function selectShownHits(ranked, limit, reserve) {
@@ -7462,6 +7548,35 @@ async function inferProjectFromQuery(manifestPath, query) {
7462
7548
  return void 0;
7463
7549
  }
7464
7550
  }
7551
+ function resolveWorksetProjects(manifest, workset) {
7552
+ const byName = new Map(manifest.projects.map((project) => [project.name.toLowerCase(), project]));
7553
+ const projects = workset.projects.map((name) => byName.get(name.toLowerCase())).filter((project) => project !== void 0);
7554
+ return { name: workset.name, projects };
7555
+ }
7556
+ async function requireWorkset(manifestPath, worksetName) {
7557
+ const manifest = await readSeedManifest(manifestPath);
7558
+ const workset = manifest.worksets?.find((entry) => entry.name.toLowerCase() === worksetName.toLowerCase());
7559
+ if (!workset) {
7560
+ throw new Error(`No workset named "${worksetName}" in ${manifestPath}.`);
7561
+ }
7562
+ return resolveWorksetProjects(manifest, workset);
7563
+ }
7564
+ async function inferWorksetFromQuery(manifestPath, query) {
7565
+ try {
7566
+ const manifest = await readSeedManifest(manifestPath);
7567
+ if (!manifest.worksets || manifest.worksets.length === 0) {
7568
+ return void 0;
7569
+ }
7570
+ const workset = manifest.worksets.find((entry) => containsNameToken(query, entry.name));
7571
+ return workset ? resolveWorksetProjects(manifest, workset) : void 0;
7572
+ } catch {
7573
+ return void 0;
7574
+ }
7575
+ }
7576
+ function containsNameToken(query, name) {
7577
+ const escaped = escapeRegExp(name.toLowerCase());
7578
+ return new RegExp(`(^|[^a-z0-9])${escaped}($|[^a-z0-9])`).test(query.toLowerCase());
7579
+ }
7465
7580
  async function readSeedManifest(path2) {
7466
7581
  const raw = await (0, import_promises3.readFile)(path2, "utf8");
7467
7582
  const loaded = index_vite_proxy_tmp_default.load(raw);
@@ -7493,7 +7608,33 @@ async function readSeedManifest(path2) {
7493
7608
  uri: readString(loaded.future_monorepo, "uri")
7494
7609
  };
7495
7610
  }
7496
- return { futureMonorepo, projects, version };
7611
+ let worksets;
7612
+ if (loaded.worksets !== void 0) {
7613
+ if (!Array.isArray(loaded.worksets)) {
7614
+ throw new Error(`Manifest worksets must be an array: ${path2}`);
7615
+ }
7616
+ worksets = loaded.worksets.map((worksetValue) => {
7617
+ if (!isJsonObject(worksetValue)) {
7618
+ throw new Error(`Manifest workset must be an object: ${path2}`);
7619
+ }
7620
+ return {
7621
+ description: readOptionalString(worksetValue, "description"),
7622
+ name: readString(worksetValue, "name"),
7623
+ projects: readStringArray(worksetValue, "projects")
7624
+ };
7625
+ });
7626
+ }
7627
+ return { futureMonorepo, projects, version, worksets };
7628
+ }
7629
+ function readOptionalString(object, key) {
7630
+ const value = object[key];
7631
+ if (value === void 0) {
7632
+ return void 0;
7633
+ }
7634
+ if (typeof value !== "string") {
7635
+ throw new Error(`Expected string for ${key}`);
7636
+ }
7637
+ return value;
7497
7638
  }
7498
7639
  function readString(object, key) {
7499
7640
  const value = object[key];
@@ -7874,6 +8015,7 @@ function parseMemoryDocument(uri, content) {
7874
8015
  archivedFrom: headerValue(header, "archived_from"),
7875
8016
  kind,
7876
8017
  project: normalizeOptionalMetadata(headerValue(header, "project") ?? headerValue(header, "repo")),
8018
+ references: headerValues(header, "references"),
7877
8019
  sourceAgentClient: headerValue(header, "source_agent_client") ?? "unknown",
7878
8020
  status,
7879
8021
  supersedes: headerValue(header, "supersedes"),
@@ -8075,6 +8217,24 @@ function recallHygieneNudges(text, options) {
8075
8217
  }
8076
8218
  return [...new Set(nudges)];
8077
8219
  }
8220
+ function referencedUrisFromRecords(records, recallOutput) {
8221
+ const seen = /* @__PURE__ */ new Set();
8222
+ const result = [];
8223
+ for (const record of records) {
8224
+ for (const uri of record.metadata.references ?? []) {
8225
+ if (seen.has(uri) || recallOutput.includes(uri)) {
8226
+ continue;
8227
+ }
8228
+ seen.add(uri);
8229
+ result.push(uri);
8230
+ }
8231
+ }
8232
+ return result;
8233
+ }
8234
+ function referencedContextExcerpt(body, maxLines) {
8235
+ const lines = body.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim().length > 0).slice(0, maxLines);
8236
+ return lines.map((line) => ` ${line}`).join("\n");
8237
+ }
8078
8238
  function activePersonalMemoryUrisFromText(text, user) {
8079
8239
  const userSegment = uriSegment(user);
8080
8240
  const matches = text.matchAll(/viking:\/\/[^\s)]+/g);
@@ -8225,7 +8385,8 @@ function formatMemoryDocument(title, metadata, body) {
8225
8385
  `source_agent_client: ${metadata.sourceAgentClient}`,
8226
8386
  `timestamp: ${metadata.timestamp}`,
8227
8387
  metadata.supersedes ? `supersedes: ${metadata.supersedes}` : void 0,
8228
- metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0
8388
+ metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0,
8389
+ ...(metadata.references ?? []).map((uri) => `references: ${uri}`)
8229
8390
  ].filter((line) => line !== void 0);
8230
8391
  return [...header, "", body.trim()].join("\n");
8231
8392
  }
@@ -8233,6 +8394,11 @@ function headerValue(header, key) {
8233
8394
  const prefix = `${key}:`;
8234
8395
  return header.split("\n").find((line) => line.startsWith(prefix))?.slice(prefix.length).trim();
8235
8396
  }
8397
+ function headerValues(header, key) {
8398
+ const prefix = `${key}:`;
8399
+ const values = header.split("\n").filter((line) => line.startsWith(prefix)).map((line) => line.slice(prefix.length).trim()).filter((value) => value.length > 0);
8400
+ return values.length > 0 ? values : void 0;
8401
+ }
8236
8402
  function parseOptionalMemoryKind(value) {
8237
8403
  if (!value) {
8238
8404
  return void 0;
@@ -10217,8 +10383,8 @@ function vikingUriToWorktreeRelative(config, uri, team) {
10217
10383
  }
10218
10384
  async function isRegularFileNoSymlink(path2) {
10219
10385
  try {
10220
- const stat5 = await (0, import_promises5.lstat)(path2);
10221
- return stat5.isFile();
10386
+ const stat6 = await (0, import_promises5.lstat)(path2);
10387
+ return stat6.isFile();
10222
10388
  } catch (_err) {
10223
10389
  return false;
10224
10390
  }
@@ -10777,7 +10943,7 @@ function stripPersonalProvenance(content) {
10777
10943
  }
10778
10944
  const cleaned = [];
10779
10945
  for (let index = 0; index < headerEnd; index += 1) {
10780
- if (/^(?:supersedes|archived_from):\s/.test(lines[index])) {
10946
+ if (/^(?:supersedes|archived_from|references):\s/.test(lines[index])) {
10781
10947
  continue;
10782
10948
  }
10783
10949
  cleaned.push(lines[index]);
@@ -11291,6 +11457,7 @@ async function runRecall(config, options) {
11291
11457
  const inferredUri = options.uri ?? (options.inferScope === false ? void 0 : await inferRecallUri(config, projectQuery));
11292
11458
  const project = await inferProjectFromQuery(config.manifestPath, options.project ?? projectQuery);
11293
11459
  const nodeLimit = options.nodeLimit ? parsePositiveInteger(options.nodeLimit, "node limit") : void 0;
11460
+ const explicitWorkset = options.workset ? await requireWorkset(config.manifestPath, options.workset) : void 0;
11294
11461
  const searchArgs = (scopeUri) => [
11295
11462
  "search",
11296
11463
  query,
@@ -11316,6 +11483,15 @@ async function runRecall(config, options) {
11316
11483
  if (seededUri?.startsWith("viking://") && seededUri !== inferredUri && !options.uri && options.inferScope !== false) {
11317
11484
  passes.push(await recallSearchHits(config, ov, searchArgs(seededUri), { dryRun, includeArchived }));
11318
11485
  }
11486
+ const workset = !options.uri && explicitWorkset ? explicitWorkset : !options.uri && options.inferScope !== false ? await inferWorksetFromQuery(config.manifestPath, projectQuery) : void 0;
11487
+ if (workset && workset.projects.length > 0) {
11488
+ console.log(`Workset scope: ${workset.name} (${workset.projects.map((member) => member.name).join(", ")})`);
11489
+ const alreadyScoped = new Set([inferredUri, seededUri].filter((uri) => uri !== void 0));
11490
+ const worksetScopes = worksetScopeUris(config, workset).filter((uri) => !alreadyScoped.has(uri)).slice(0, MAX_WORKSET_PASSES);
11491
+ for (const scope of worksetScopes) {
11492
+ passes.push(await recallSearchHits(config, ov, searchArgs(scope), { dryRun, includeArchived }));
11493
+ }
11494
+ }
11319
11495
  const recallOutputs = [];
11320
11496
  const exactMatches = await collectExactMemoryMatches(config, ov, query, { dryRun, includeArchived, project });
11321
11497
  const { semanticSection, exactTail } = buildRecallSections(passes, exactMatches, nodeLimit ?? 12);
@@ -11329,8 +11505,45 @@ ${semanticSection}`);
11329
11505
  ${exactTail}`);
11330
11506
  recallOutputs.push(exactTail);
11331
11507
  }
11508
+ const referencedSection = await referencedContextSection(config, recallOutputs.join("\n"));
11509
+ if (referencedSection) {
11510
+ console.log(`
11511
+ ${referencedSection}`);
11512
+ recallOutputs.push(referencedSection);
11513
+ }
11332
11514
  await printRecallHygieneNudges(config, recallOutputs.join("\n"));
11333
11515
  }
11516
+ var MAX_REFERENCED_CONTEXT = 5;
11517
+ var REFERENCED_EXCERPT_LINES = 12;
11518
+ async function referencedContextSection(config, recallOutput) {
11519
+ const surfacedUris = activePersonalMemoryUrisFromText(recallOutput, config.user);
11520
+ if (surfacedUris.length === 0) {
11521
+ return void 0;
11522
+ }
11523
+ const surfaced = await readMemoryRecordsByUri(config, surfacedUris);
11524
+ const referenced = referencedUrisFromRecords(surfaced, recallOutput);
11525
+ if (referenced.length === 0) {
11526
+ return void 0;
11527
+ }
11528
+ const capped = referenced.slice(0, MAX_REFERENCED_CONTEXT);
11529
+ const records = await readMemoryRecordsByUri(config, capped);
11530
+ const byUri = new Map(records.map((record) => [record.uri, record]));
11531
+ const lines = ["Referenced read-only context (one-way pointers from surfaced memories):"];
11532
+ for (const uri of capped) {
11533
+ const record = byUri.get(uri);
11534
+ if (record) {
11535
+ lines.push(`- ${uri}`, referencedContextExcerpt(record.body, REFERENCED_EXCERPT_LINES));
11536
+ } else {
11537
+ lines.push(`- ${uri} [reference unavailable locally]`);
11538
+ }
11539
+ }
11540
+ if (referenced.length > capped.length) {
11541
+ lines.push(
11542
+ `- \u2026 ${referenced.length - capped.length} more referenced ${referenced.length - capped.length === 1 ? "memory" : "memories"} omitted`
11543
+ );
11544
+ }
11545
+ return lines.join("\n");
11546
+ }
11334
11547
  async function recallSearchHits(config, ov, args, options) {
11335
11548
  const jsonArgs = withIdentity(config, [...args, "--output", "json"]);
11336
11549
  console.log(`${options.dryRun ? "Would run" : "Running"}: ${formatShellCommand(ov, jsonArgs)}`);
@@ -11768,6 +11981,13 @@ async function storeMemory(config, options) {
11768
11981
  console.log(`Updated existing memory in place: ${memoryUri}`);
11769
11982
  }
11770
11983
  }
11984
+ function warnOnSharedProjectDrift(metadata, inferred) {
11985
+ if (inferred?.project && metadata.project && uriSegment(metadata.project) !== inferred.project) {
11986
+ console.log(
11987
+ `WARN keeping shared memory project "${inferred.project}" from its storage path; ignoring requested "${metadata.project}". To change a shared memory's project, forget it and store a new one under the new project.`
11988
+ );
11989
+ }
11990
+ }
11771
11991
  async function storeSharedMemoryReplacement(config, ov, options, targetUri) {
11772
11992
  if (options.metadata.kind !== "durable") {
11773
11993
  throw new Error("Shared memory replacement only supports durable memories.");
@@ -11778,9 +11998,10 @@ async function storeSharedMemoryReplacement(config, ov, options, targetUri) {
11778
11998
  }
11779
11999
  const team = await resolveTeam(config, teamName);
11780
12000
  const inferred = sharedMemoryUriParts(config, targetUri);
12001
+ warnOnSharedProjectDrift(options.metadata, inferred);
11781
12002
  const metadata = {
11782
12003
  ...options.metadata,
11783
- project: options.metadata.project ?? inferred?.project,
12004
+ project: inferred?.project ?? options.metadata.project,
11784
12005
  topic: options.metadata.topic ?? inferred?.topic
11785
12006
  };
11786
12007
  const rawMemory = formatMemoryDocument2(options.title, metadata, options.bodyText);
@@ -11838,8 +12059,8 @@ async function removeVikingResourceWithRetry(ov, config, uri) {
11838
12059
  return false;
11839
12060
  }
11840
12061
  async function vikingResourceExists2(ov, config, uri) {
11841
- const stat5 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
11842
- return stat5.exitCode === 0;
12062
+ const stat6 = await runCommand(ov, withIdentity(config, ["stat", uri]), { allowFailure: true });
12063
+ return stat6.exitCode === 0;
11843
12064
  }
11844
12065
  async function ensureDurableMemoryDirectory(ov, config) {
11845
12066
  await ensureMemoryDirectory(ov, config, durableMemoryDirectoryUri(config));
@@ -11906,6 +12127,18 @@ async function legacyLifecycleHandoffCandidates(config, limit) {
11906
12127
  function lifecycleMigrationUri(config, metadata, hash) {
11907
12128
  return `${memoryDirectoryUri(config, metadata)}/legacy-${hash.slice(0, 16)}.md`;
11908
12129
  }
12130
+ var MAX_WORKSET_PASSES = 12;
12131
+ function worksetScopeUris(config, workset) {
12132
+ const scopes = [];
12133
+ for (const member of workset.projects) {
12134
+ scopes.push(`viking://user/${uriSegment(config.user)}/memories/durable/projects/${uriSegment(member.name)}`);
12135
+ const seeded = trimTrailingSlash(member.uri);
12136
+ if (seeded.startsWith("viking://")) {
12137
+ scopes.push(seeded);
12138
+ }
12139
+ }
12140
+ return [...new Set(scopes)];
12141
+ }
11909
12142
  function exactMemoryScopes(config, includeArchived, query, project) {
11910
12143
  return exactMemoryScopeUris({
11911
12144
  agentMemoriesUri: `viking://agent/${uriSegment(config.agentId)}/memories`,
@@ -11968,7 +12201,8 @@ function formatMemoryDocument2(title, metadata, body) {
11968
12201
  `source_agent_client: ${metadata.sourceAgentClient}`,
11969
12202
  `timestamp: ${metadata.timestamp}`,
11970
12203
  metadata.supersedes ? `supersedes: ${metadata.supersedes}` : void 0,
11971
- metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0
12204
+ metadata.archivedFrom ? `archived_from: ${metadata.archivedFrom}` : void 0,
12205
+ ...(metadata.references ?? []).map((uri) => `references: ${uri}`)
11972
12206
  ].filter((line) => line !== void 0);
11973
12207
  return [...header, "", body.trim()].join("\n");
11974
12208
  }
@@ -12203,9 +12437,25 @@ function isResourceBusy(stderr, stdout2) {
12203
12437
  ${stdout2}`.toLowerCase();
12204
12438
  return output2.includes("resource is busy") || output2.includes("resource is being processed");
12205
12439
  }
12440
+ function normalizeReferenceUris(references) {
12441
+ if (!references || references.length === 0) {
12442
+ return void 0;
12443
+ }
12444
+ const seen = /* @__PURE__ */ new Set();
12445
+ for (const raw of references) {
12446
+ const uri = raw.trim();
12447
+ if (!uri || seen.has(uri)) {
12448
+ continue;
12449
+ }
12450
+ assertVikingUri(uri);
12451
+ seen.add(uri);
12452
+ }
12453
+ return seen.size > 0 ? [...seen] : void 0;
12454
+ }
12206
12455
  async function buildHandoff(options) {
12207
12456
  const repoRoot = await gitValue(["rev-parse", "--show-toplevel"]) ?? getInvocationCwd();
12208
12457
  const branch = await gitValue(["branch", "--show-current"], repoRoot) ?? "unknown";
12458
+ const commit = await gitValue(["rev-parse", "HEAD"], repoRoot) ?? "unknown";
12209
12459
  const status = await gitValue(["status", "--short"], repoRoot) ?? "";
12210
12460
  const diffStat = await gitValue(["diff", "--stat", "HEAD"], repoRoot) ?? "";
12211
12461
  const touchedFiles = await gitTouchedFiles(repoRoot);
@@ -12214,16 +12464,24 @@ async function buildHandoff(options) {
12214
12464
  const metadata = {
12215
12465
  kind: "handoff",
12216
12466
  project: normalizeOptionalMetadata2(options.project) ?? repoName,
12467
+ references: normalizeReferenceUris(options.references),
12217
12468
  sourceAgentClient: options.sourceAgentClient ?? "codex",
12218
12469
  status: "active",
12219
12470
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
12220
12471
  topic: handoffTopicForBranch(topicBranch, { timestamped: options.timestamped, topic: options.topic })
12221
12472
  };
12473
+ const reviewState = [
12474
+ options.pr ? `pr: ${options.pr}` : void 0,
12475
+ options.issue ? `issue: ${options.issue}` : void 0,
12476
+ options.ci ? `ci: ${options.ci}` : void 0
12477
+ ].filter((line) => line !== void 0);
12222
12478
  const bodyText = [
12223
12479
  `repo: ${repoName}`,
12224
12480
  `repo_path: ${repoRoot}`,
12225
12481
  `branch: ${branch || "unknown"}`,
12482
+ `commit: ${commit}`,
12226
12483
  `task: ${options.task ?? "unspecified"}`,
12484
+ ...reviewState,
12227
12485
  "",
12228
12486
  "files_touched:",
12229
12487
  formatBlock(touchedFiles, "- none"),
@@ -12241,7 +12499,9 @@ async function buildHandoff(options) {
12241
12499
  options.blockers ?? "- none recorded",
12242
12500
  "",
12243
12501
  "next_step:",
12244
- options.nextStep ?? "- inspect the current repo state and continue from this handoff"
12502
+ options.nextStep ?? "- inspect the current repo state and continue from this handoff",
12503
+ ...options.sessionId ? ["", `session_id: ${options.sessionId}`] : [],
12504
+ ...options.trace ? ["", "trace (auto-captured, heuristic):", options.trace] : []
12245
12505
  ].join("\n");
12246
12506
  return { bodyText, metadata };
12247
12507
  }
@@ -12267,9 +12527,134 @@ function formatBlock(value, emptyValue) {
12267
12527
  return trimmed.split("\n").map((line) => `- ${line}`).join("\n");
12268
12528
  }
12269
12529
 
12530
+ // src/trace.ts
12531
+ var import_promises7 = require("node:fs/promises");
12532
+ var MAX_TRANSCRIPT_BYTES = 4 * 1024 * 1024;
12533
+ var MAX_INTENTS = 5;
12534
+ var MAX_INTENT_CHARS = 160;
12535
+ var MAX_TOOLS = 12;
12536
+ function isRecord(value) {
12537
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12538
+ }
12539
+ function extractText(content) {
12540
+ if (typeof content === "string") {
12541
+ return content.trim() || void 0;
12542
+ }
12543
+ if (!Array.isArray(content)) {
12544
+ return void 0;
12545
+ }
12546
+ const parts = [];
12547
+ for (const part of content) {
12548
+ if (typeof part === "string") {
12549
+ parts.push(part);
12550
+ } else if (isRecord(part) && part.type === "text" && typeof part.text === "string") {
12551
+ parts.push(part.text);
12552
+ }
12553
+ }
12554
+ const joined = parts.join(" ").trim();
12555
+ return joined || void 0;
12556
+ }
12557
+ function extractToolNames(content) {
12558
+ if (!Array.isArray(content)) {
12559
+ return [];
12560
+ }
12561
+ const names = [];
12562
+ for (const part of content) {
12563
+ if (isRecord(part) && part.type === "tool_use" && typeof part.name === "string") {
12564
+ names.push(part.name);
12565
+ }
12566
+ }
12567
+ return names;
12568
+ }
12569
+ function truncate(value, max) {
12570
+ const oneLine = value.replace(/\s+/g, " ").trim();
12571
+ return oneLine.length > max ? `${oneLine.slice(0, max - 1)}\u2026` : oneLine;
12572
+ }
12573
+ async function distillTrace(transcriptPath) {
12574
+ const raw = await readTranscriptTail(transcriptPath);
12575
+ if (raw === void 0) {
12576
+ return void 0;
12577
+ }
12578
+ if (!raw.trim()) {
12579
+ return void 0;
12580
+ }
12581
+ let events = 0;
12582
+ const tools = /* @__PURE__ */ new Set();
12583
+ const intents = [];
12584
+ for (const line of raw.split("\n")) {
12585
+ const trimmed = line.trim();
12586
+ if (!trimmed) {
12587
+ continue;
12588
+ }
12589
+ let parsed;
12590
+ try {
12591
+ parsed = JSON.parse(trimmed);
12592
+ } catch {
12593
+ continue;
12594
+ }
12595
+ if (!isRecord(parsed)) {
12596
+ continue;
12597
+ }
12598
+ events += 1;
12599
+ const message = isRecord(parsed.message) ? parsed.message : parsed;
12600
+ const role = typeof message.role === "string" ? message.role : typeof parsed.type === "string" ? parsed.type : void 0;
12601
+ if (role === "user") {
12602
+ const text = extractText(message.content);
12603
+ if (text) {
12604
+ const scrubbed = applyScrubber(text, { redact: true });
12605
+ if (scrubbed.blocker) {
12606
+ return void 0;
12607
+ }
12608
+ intents.push(truncate(scrubbed.cleaned, MAX_INTENT_CHARS));
12609
+ }
12610
+ } else if (role === "assistant") {
12611
+ for (const name of extractToolNames(message.content)) {
12612
+ tools.add(name);
12613
+ }
12614
+ }
12615
+ }
12616
+ if (events === 0) {
12617
+ return void 0;
12618
+ }
12619
+ const lines = [`- ${events} transcript events`];
12620
+ if (tools.size > 0) {
12621
+ lines.push(`- tools used: ${[...tools].slice(0, MAX_TOOLS).join(", ")}`);
12622
+ }
12623
+ const recentIntents = intents.slice(-MAX_INTENTS);
12624
+ if (recentIntents.length > 0) {
12625
+ lines.push("- recent intents:");
12626
+ for (const intent of recentIntents) {
12627
+ lines.push(` - ${intent}`);
12628
+ }
12629
+ }
12630
+ return lines.join("\n");
12631
+ }
12632
+ async function readTranscriptTail(transcriptPath) {
12633
+ try {
12634
+ const { size } = await (0, import_promises7.stat)(transcriptPath);
12635
+ if (size === 0) {
12636
+ return void 0;
12637
+ }
12638
+ if (size <= MAX_TRANSCRIPT_BYTES) {
12639
+ return await (0, import_promises7.readFile)(transcriptPath, "utf8");
12640
+ }
12641
+ const start = size - MAX_TRANSCRIPT_BYTES;
12642
+ const buffer = Buffer.alloc(MAX_TRANSCRIPT_BYTES);
12643
+ const file = await (0, import_promises7.open)(transcriptPath, "r");
12644
+ try {
12645
+ const { bytesRead } = await file.read(buffer, 0, MAX_TRANSCRIPT_BYTES, start);
12646
+ return buffer.subarray(0, bytesRead).toString("utf8").replace(/^[^\n]*(?:\n|$)/, "");
12647
+ } finally {
12648
+ await file.close();
12649
+ }
12650
+ } catch {
12651
+ return void 0;
12652
+ }
12653
+ }
12654
+
12270
12655
  // src/update-check.ts
12271
12656
  var import_node_child_process3 = require("node:child_process");
12272
- var import_promises7 = require("node:fs/promises");
12657
+ var import_promises8 = require("node:fs/promises");
12273
12658
  var import_node_path9 = require("node:path");
12274
12659
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
12275
12660
  var NPM_LATEST_URL = "https://registry.npmjs.org/threadnote/latest";
@@ -12323,7 +12708,7 @@ function isCacheFresh(cache) {
12323
12708
  }
12324
12709
  async function readUpdateCache(cachePath) {
12325
12710
  try {
12326
- const raw = await (0, import_promises7.readFile)(cachePath, "utf8");
12711
+ const raw = await (0, import_promises8.readFile)(cachePath, "utf8");
12327
12712
  const parsed = JSON.parse(raw);
12328
12713
  if (parsed.version !== 1 || typeof parsed.latestVersion !== "string" || typeof parsed.checkedAt !== "string") {
12329
12714
  return void 0;
@@ -12335,8 +12720,8 @@ async function readUpdateCache(cachePath) {
12335
12720
  }
12336
12721
  async function writeUpdateCache(cachePath, contents) {
12337
12722
  try {
12338
- await (0, import_promises7.mkdir)((0, import_node_path9.dirname)(cachePath), { recursive: true });
12339
- await (0, import_promises7.writeFile)(cachePath, `${JSON.stringify(contents)}
12723
+ await (0, import_promises8.mkdir)((0, import_node_path9.dirname)(cachePath), { recursive: true });
12724
+ await (0, import_promises8.writeFile)(cachePath, `${JSON.stringify(contents)}
12340
12725
  `, { encoding: "utf8", mode: 384 });
12341
12726
  } catch {
12342
12727
  }
@@ -12408,7 +12793,7 @@ async function runHooksInstall(config, agent, options) {
12408
12793
  }
12409
12794
  async function runClaudeHooksInstall(options) {
12410
12795
  const path2 = expandPath(CLAUDE_SETTINGS_PATH);
12411
- const existingRaw = await exists(path2) ? await (0, import_promises8.readFile)(path2, "utf8") : "{}";
12796
+ const existingRaw = await exists(path2) ? await (0, import_promises9.readFile)(path2, "utf8") : "{}";
12412
12797
  const parsed = parseJsonConfigObject(existingRaw) ?? {};
12413
12798
  const next = options.remove ? withoutThreadnoteHooks(parsed) : withThreadnoteHooks(parsed);
12414
12799
  const before = JSON.stringify(parsed);
@@ -12425,11 +12810,11 @@ async function runClaudeHooksInstall(options) {
12425
12810
  console.log("\nRe-run with --apply to actually modify the file.");
12426
12811
  return;
12427
12812
  }
12428
- await (0, import_promises8.mkdir)((0, import_node_path11.dirname)(path2), { recursive: true });
12813
+ await (0, import_promises9.mkdir)((0, import_node_path11.dirname)(path2), { recursive: true });
12429
12814
  const serialized = `${JSON.stringify(next, void 0, 2)}
12430
12815
  `;
12431
- await (0, import_promises8.writeFile)(path2, serialized, { encoding: "utf8", mode: 384 });
12432
- await (0, import_promises8.chmod)(path2, 384);
12816
+ await (0, import_promises9.writeFile)(path2, serialized, { encoding: "utf8", mode: 384 });
12817
+ await (0, import_promises9.chmod)(path2, 384);
12433
12818
  console.log(`${options.remove ? "Removed" : "Installed"} threadnote-managed Claude hooks.`);
12434
12819
  }
12435
12820
  function withThreadnoteHooks(input2) {
@@ -12507,7 +12892,7 @@ async function hasManagedClaudeHooks() {
12507
12892
  if (!await exists(path2)) {
12508
12893
  return false;
12509
12894
  }
12510
- const raw = await (0, import_promises8.readFile)(path2, "utf8");
12895
+ const raw = await (0, import_promises9.readFile)(path2, "utf8");
12511
12896
  const parsed = parseJsonConfigObject(raw);
12512
12897
  if (!parsed || !isJsonObject(parsed.hooks)) {
12513
12898
  return false;
@@ -12525,15 +12910,18 @@ async function hasManagedClaudeHooks() {
12525
12910
  async function runPreCompactHook(config, options = {}) {
12526
12911
  try {
12527
12912
  const project = await resolveRepoName() ?? "general";
12913
+ const { sessionId, trace } = await captureTraceContext();
12528
12914
  await runHandoff(config, {
12529
12915
  blockers: "- none recorded",
12530
12916
  dryRun: options.dryRun === true,
12531
12917
  nextStep: "Continue from this auto-snapshot. A manual `threadnote handoff` will produce a richer write-up if you have more context.",
12532
12918
  project,
12919
+ sessionId,
12533
12920
  sourceAgentClient: "claude",
12534
12921
  task: "Auto-snapshot captured at Claude PreCompact (deterministic safety net before context compaction).",
12535
12922
  tests: "- not recorded (auto-snapshot)",
12536
- topic: HOOK_AUTO_PRECOMPACT_TOPIC
12923
+ topic: HOOK_AUTO_PRECOMPACT_TOPIC,
12924
+ trace
12537
12925
  });
12538
12926
  } catch (err) {
12539
12927
  process.stderr.write(
@@ -12566,6 +12954,76 @@ async function runSessionStartHook(config, options = {}) {
12566
12954
  );
12567
12955
  }
12568
12956
  }
12957
+ async function captureTraceContext() {
12958
+ try {
12959
+ const payload = await readHookPayload();
12960
+ if (!payload) {
12961
+ return {};
12962
+ }
12963
+ const rawTrace = payload.transcriptPath ? await distillTrace(payload.transcriptPath) : void 0;
12964
+ return { sessionId: payload.sessionId, trace: rawTrace ? scrubTrace(rawTrace) : void 0 };
12965
+ } catch {
12966
+ return {};
12967
+ }
12968
+ }
12969
+ function scrubTrace(trace) {
12970
+ const result = applyScrubber(trace, { redact: true });
12971
+ return result.blocker ? void 0 : result.cleaned;
12972
+ }
12973
+ async function readHookPayload() {
12974
+ if (process.stdin.isTTY) {
12975
+ return void 0;
12976
+ }
12977
+ const raw = await readStdinWithTimeout(1500, 512 * 1024);
12978
+ if (!raw.trim()) {
12979
+ return void 0;
12980
+ }
12981
+ let parsed;
12982
+ try {
12983
+ parsed = JSON.parse(raw);
12984
+ } catch {
12985
+ return void 0;
12986
+ }
12987
+ if (!isJsonObject(parsed)) {
12988
+ return void 0;
12989
+ }
12990
+ return {
12991
+ sessionId: typeof parsed.session_id === "string" ? parsed.session_id : void 0,
12992
+ transcriptPath: typeof parsed.transcript_path === "string" ? parsed.transcript_path : void 0
12993
+ };
12994
+ }
12995
+ function readStdinWithTimeout(timeoutMs, maxBytes) {
12996
+ return new Promise((resolve2) => {
12997
+ const stdin = process.stdin;
12998
+ let data = "";
12999
+ let settled = false;
13000
+ const finish = () => {
13001
+ if (settled) {
13002
+ return;
13003
+ }
13004
+ settled = true;
13005
+ stdin.off("data", onData);
13006
+ stdin.off("end", finish);
13007
+ stdin.off("error", finish);
13008
+ clearTimeout(timer);
13009
+ stdin.pause();
13010
+ resolve2(data);
13011
+ };
13012
+ const onData = (chunk) => {
13013
+ data += chunk;
13014
+ if (data.length >= maxBytes) {
13015
+ data = data.slice(0, maxBytes);
13016
+ finish();
13017
+ }
13018
+ };
13019
+ const timer = setTimeout(finish, timeoutMs);
13020
+ stdin.setEncoding("utf8");
13021
+ stdin.on("data", onData);
13022
+ stdin.on("end", finish);
13023
+ stdin.on("error", finish);
13024
+ stdin.resume();
13025
+ });
13026
+ }
12569
13027
  async function emitUpdateBannerIfOutdated(config) {
12570
13028
  try {
12571
13029
  const result = await checkForThreadnoteUpdate({
@@ -12594,8 +13052,144 @@ async function emitUpdateBannerIfOutdated(config) {
12594
13052
  }
12595
13053
 
12596
13054
  // src/seeding.ts
12597
- var import_promises9 = require("node:fs/promises");
13055
+ var import_promises11 = require("node:fs/promises");
13056
+ var import_node_path13 = require("node:path");
13057
+
13058
+ // src/graph.ts
13059
+ var import_promises10 = require("node:fs/promises");
12598
13060
  var import_node_path12 = require("node:path");
13061
+ async function readIfExists(path2) {
13062
+ try {
13063
+ return await (0, import_promises10.readFile)(path2, "utf8");
13064
+ } catch {
13065
+ return void 0;
13066
+ }
13067
+ }
13068
+ function parsePackageJson(content) {
13069
+ let parsed;
13070
+ try {
13071
+ parsed = JSON.parse(content);
13072
+ } catch {
13073
+ return void 0;
13074
+ }
13075
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
13076
+ return void 0;
13077
+ }
13078
+ const object = parsed;
13079
+ const dependencies = /* @__PURE__ */ new Set();
13080
+ for (const key of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
13081
+ const section = object[key];
13082
+ if (section && typeof section === "object" && !Array.isArray(section)) {
13083
+ for (const name of Object.keys(section)) {
13084
+ dependencies.add(name);
13085
+ }
13086
+ }
13087
+ }
13088
+ return { dependencies: [...dependencies], publishedName: typeof object.name === "string" ? object.name : void 0 };
13089
+ }
13090
+ function parseGoMod(content) {
13091
+ const dependencies = /* @__PURE__ */ new Set();
13092
+ let publishedName;
13093
+ let inRequireBlock = false;
13094
+ for (const rawLine of content.split("\n")) {
13095
+ const line = rawLine.replace(/\/\/.*$/, "").trim();
13096
+ if (!line) {
13097
+ continue;
13098
+ }
13099
+ const moduleMatch = /^module\s+(\S+)/.exec(line);
13100
+ if (moduleMatch) {
13101
+ publishedName = moduleMatch[1];
13102
+ continue;
13103
+ }
13104
+ if (/^require\s*\($/.test(line)) {
13105
+ inRequireBlock = true;
13106
+ continue;
13107
+ }
13108
+ if (inRequireBlock) {
13109
+ if (line === ")") {
13110
+ inRequireBlock = false;
13111
+ continue;
13112
+ }
13113
+ const blockMatch = /^(\S+)\s+v\S+/.exec(line);
13114
+ if (blockMatch) {
13115
+ dependencies.add(blockMatch[1]);
13116
+ }
13117
+ continue;
13118
+ }
13119
+ const singleMatch = /^require\s+(\S+)\s+v\S+/.exec(line);
13120
+ if (singleMatch) {
13121
+ dependencies.add(singleMatch[1]);
13122
+ }
13123
+ }
13124
+ return { dependencies: [...dependencies], publishedName };
13125
+ }
13126
+ async function extractDependencyFacts(projectRoot) {
13127
+ const dependencies = /* @__PURE__ */ new Set();
13128
+ const ecosystems = [];
13129
+ const manifestFiles = [];
13130
+ let publishedName;
13131
+ const packageJson = await readIfExists((0, import_node_path12.join)(projectRoot, "package.json"));
13132
+ if (packageJson !== void 0) {
13133
+ const parsed = parsePackageJson(packageJson);
13134
+ if (parsed) {
13135
+ manifestFiles.push("package.json");
13136
+ ecosystems.push("npm");
13137
+ publishedName = publishedName ?? parsed.publishedName;
13138
+ for (const dependency of parsed.dependencies) {
13139
+ dependencies.add(dependency);
13140
+ }
13141
+ }
13142
+ }
13143
+ const goMod = await readIfExists((0, import_node_path12.join)(projectRoot, "go.mod"));
13144
+ if (goMod !== void 0) {
13145
+ const parsed = parseGoMod(goMod);
13146
+ manifestFiles.push("go.mod");
13147
+ ecosystems.push("go");
13148
+ publishedName = publishedName ?? parsed.publishedName;
13149
+ for (const dependency of parsed.dependencies) {
13150
+ dependencies.add(dependency);
13151
+ }
13152
+ }
13153
+ return { dependencies: [...dependencies].sort(), ecosystems, manifestFiles, publishedName };
13154
+ }
13155
+ function buildGraphDocument(params) {
13156
+ const { externalCount, facts, internalEdges, projectName } = params;
13157
+ const lines = [
13158
+ `# ${projectName} \u2014 dependency facts`,
13159
+ "",
13160
+ "Auto-generated by `threadnote seed --graph` from declarative manifest files. A static snapshot of package metadata, not a live dependency graph.",
13161
+ "",
13162
+ `provides: ${facts.publishedName ?? "(none declared)"}`,
13163
+ `ecosystems: ${facts.ecosystems.length > 0 ? facts.ecosystems.join(", ") : "(none detected)"}`,
13164
+ "",
13165
+ "manifests:",
13166
+ ...facts.manifestFiles.length > 0 ? facts.manifestFiles.map((file) => `- ${file}`) : ["- (none)"],
13167
+ "",
13168
+ "depends_on (within this workspace):",
13169
+ ...internalEdges.length > 0 ? internalEdges.map((edge) => `- [[${edge.project}]] (via ${edge.dependency})`) : ["- (no in-workspace dependencies detected)"],
13170
+ "",
13171
+ `external dependencies: ${externalCount} declared`
13172
+ ];
13173
+ return `${lines.join("\n")}
13174
+ `;
13175
+ }
13176
+ function resolveGraphEdges(projectName, dependencies, projectByPublishedName) {
13177
+ const internalEdges = [];
13178
+ const seenTargets = /* @__PURE__ */ new Set();
13179
+ let externalCount = 0;
13180
+ for (const dependency of dependencies) {
13181
+ const target = projectByPublishedName.get(dependency.toLowerCase());
13182
+ if (target && target !== projectName && !seenTargets.has(target)) {
13183
+ seenTargets.add(target);
13184
+ internalEdges.push({ dependency, project: target });
13185
+ } else if (!target) {
13186
+ externalCount += 1;
13187
+ }
13188
+ }
13189
+ return { externalCount, internalEdges };
13190
+ }
13191
+
13192
+ // src/seeding.ts
12599
13193
  function parseSeedWatchIntervalMinutes(rawValue) {
12600
13194
  if (rawValue === void 0) {
12601
13195
  return void 0;
@@ -12614,7 +13208,7 @@ async function runSeed(config, options) {
12614
13208
  const ignorePatterns = await loadIgnorePatterns();
12615
13209
  const ov = await openVikingCliForMode(options.dryRun === true);
12616
13210
  const watchIntervalMinutes = parseSeedWatchIntervalMinutes(process.env[SEED_WATCH_INTERVAL_ENV]);
12617
- const statePath = (0, import_node_path12.join)(config.agentContextHome, SEED_STATE_FILE);
13211
+ const statePath = (0, import_node_path13.join)(config.agentContextHome, SEED_STATE_FILE);
12618
13212
  const state = options.force === true ? { files: {}, version: 1 } : await readSeedState(statePath);
12619
13213
  const projects = filterProjects(manifest.projects, options.only);
12620
13214
  let importedCount = 0;
@@ -12666,6 +13260,70 @@ async function runSeed(config, options) {
12666
13260
  console.log(
12667
13261
  `Seed complete: ${importedCount} candidate(s), ${unchangedCount} unchanged, ${skippedCount} skipped for safety.`
12668
13262
  );
13263
+ if (options.graph === true) {
13264
+ await seedDependencyGraphs(config, ov, manifest, projects, options.dryRun === true);
13265
+ }
13266
+ }
13267
+ async function seedDependencyGraphs(config, ov, manifest, targetProjects, dryRun) {
13268
+ const factsByProject = /* @__PURE__ */ new Map();
13269
+ for (const project of manifest.projects) {
13270
+ const projectRoot = expandPath(project.path);
13271
+ if (!await exists(projectRoot)) {
13272
+ continue;
13273
+ }
13274
+ factsByProject.set(project.name, await extractDependencyFacts(projectRoot));
13275
+ }
13276
+ const projectByPublishedName = /* @__PURE__ */ new Map();
13277
+ for (const [name, facts] of factsByProject) {
13278
+ if (facts.publishedName) {
13279
+ projectByPublishedName.set(facts.publishedName.toLowerCase(), name);
13280
+ }
13281
+ }
13282
+ let written = 0;
13283
+ let skipped = 0;
13284
+ for (const project of targetProjects) {
13285
+ const facts = factsByProject.get(project.name);
13286
+ if (!facts || facts.manifestFiles.length === 0) {
13287
+ continue;
13288
+ }
13289
+ const { externalCount, internalEdges } = resolveGraphEdges(project.name, facts.dependencies, projectByPublishedName);
13290
+ const document = buildGraphDocument({ externalCount, facts, internalEdges, projectName: project.name });
13291
+ const secretMatches = detectSecretMatches(document);
13292
+ if (secretMatches.length > 0) {
13293
+ skipped += 1;
13294
+ console.log(
13295
+ `SKIP ${project.name}/.graph.md: possible secret (${secretMatches.slice(0, MAX_SECRET_MATCHES_TO_PRINT).join(", ")})`
13296
+ );
13297
+ continue;
13298
+ }
13299
+ const destinationUri = `${trimTrailingSlash(project.uri)}/.graph.md`;
13300
+ if (dryRun) {
13301
+ console.log(`Would seed dependency facts: ${destinationUri} (${internalEdges.length} in-workspace edge(s))`);
13302
+ written += 1;
13303
+ continue;
13304
+ }
13305
+ const graphPath = (0, import_node_path13.join)(config.agentContextHome, "graph", graphCacheFileName(project.name));
13306
+ await ensureDirectory((0, import_node_path13.dirname)(graphPath), false);
13307
+ await (0, import_promises11.writeFile)(graphPath, document, { encoding: "utf8", mode: 384 });
13308
+ await (0, import_promises11.chmod)(graphPath, 384);
13309
+ await maybeRun(
13310
+ false,
13311
+ ov,
13312
+ withIdentity(config, [
13313
+ "add-resource",
13314
+ graphPath,
13315
+ "--to",
13316
+ destinationUri,
13317
+ "--reason",
13318
+ `Dependency facts for ${project.name}`,
13319
+ "--wait"
13320
+ ])
13321
+ );
13322
+ written += 1;
13323
+ }
13324
+ console.log(
13325
+ `Dependency graph seed complete: ${written} .graph.md resource(s)${skipped > 0 ? `, ${skipped} skipped for safety` : ""}.`
13326
+ );
12669
13327
  }
12670
13328
  function filterProjects(projects, only) {
12671
13329
  if (!only || only.length === 0) {
@@ -12682,7 +13340,7 @@ function filterProjects(projects, only) {
12682
13340
  }
12683
13341
  async function statSeedFile(path2) {
12684
13342
  try {
12685
- const result = await (0, import_promises9.stat)(path2);
13343
+ const result = await (0, import_promises11.stat)(path2);
12686
13344
  return { mtimeMs: result.mtimeMs, size: result.size };
12687
13345
  } catch (_err) {
12688
13346
  return void 0;
@@ -12690,7 +13348,7 @@ async function statSeedFile(path2) {
12690
13348
  }
12691
13349
  async function readSeedState(path2) {
12692
13350
  try {
12693
- const raw = await (0, import_promises9.readFile)(path2, "utf8");
13351
+ const raw = await (0, import_promises11.readFile)(path2, "utf8");
12694
13352
  const parsed = JSON.parse(raw);
12695
13353
  if (parsed.version !== 1 || !isJsonObject(parsed.files)) {
12696
13354
  return { files: {}, version: 1 };
@@ -12707,13 +13365,13 @@ async function readSeedState(path2) {
12707
13365
  }
12708
13366
  }
12709
13367
  async function writeSeedState(path2, state) {
12710
- await ensureDirectory((0, import_node_path12.dirname)(path2), false);
12711
- await (0, import_promises9.writeFile)(path2, `${JSON.stringify(state, void 0, 2)}
13368
+ await ensureDirectory((0, import_node_path13.dirname)(path2), false);
13369
+ await (0, import_promises11.writeFile)(path2, `${JSON.stringify(state, void 0, 2)}
12712
13370
  `, { encoding: "utf8", mode: 384 });
12713
13371
  }
12714
13372
  async function runInitManifest(config, options) {
12715
13373
  const manifestPath = expandPath(
12716
- options.path ?? process.env.THREADNOTE_MANIFEST ?? (0, import_node_path12.join)(config.agentContextHome, USER_MANIFEST_NAME)
13374
+ options.path ?? process.env.THREADNOTE_MANIFEST ?? (0, import_node_path13.join)(config.agentContextHome, USER_MANIFEST_NAME)
12717
13375
  );
12718
13376
  const repoInputs = options.repo && options.repo.length > 0 ? options.repo : [getInvocationCwd()];
12719
13377
  const existingManifest = options.replace === true || !await exists(manifestPath) ? void 0 : await readSeedManifest(manifestPath);
@@ -12750,20 +13408,58 @@ async function runInitManifest(config, options) {
12750
13408
  uri: existingManifest.futureMonorepo.uri
12751
13409
  };
12752
13410
  }
13411
+ if (existingManifest?.worksets) {
13412
+ outputManifest.worksets = existingManifest.worksets.map((workset) => ({
13413
+ name: workset.name,
13414
+ ...workset.description !== void 0 ? { description: workset.description } : {},
13415
+ projects: [...workset.projects]
13416
+ }));
13417
+ }
12753
13418
  const output2 = index_vite_proxy_tmp_default.dump(outputManifest, { lineWidth: 120, noRefs: true });
12754
13419
  if (options.dryRun === true) {
12755
13420
  console.log(`# Would write ${manifestPath}`);
12756
13421
  console.log(output2.trimEnd());
12757
13422
  return;
12758
13423
  }
12759
- await ensureDirectory((0, import_node_path12.dirname)(manifestPath), false);
12760
- await (0, import_promises9.writeFile)(manifestPath, output2, { encoding: "utf8", mode: 384 });
12761
- await (0, import_promises9.chmod)(manifestPath, 384);
13424
+ await ensureDirectory((0, import_node_path13.dirname)(manifestPath), false);
13425
+ await (0, import_promises11.writeFile)(manifestPath, output2, { encoding: "utf8", mode: 384 });
13426
+ await (0, import_promises11.chmod)(manifestPath, 384);
12762
13427
  console.log(`Wrote manifest: ${manifestPath}`);
12763
13428
  console.log("Seed with:");
12764
13429
  console.log(" threadnote seed --dry-run");
12765
13430
  console.log(" threadnote seed");
12766
13431
  }
13432
+ async function runWorksetList(config) {
13433
+ const manifest = await readSeedManifest(config.manifestPath);
13434
+ const worksets = manifest.worksets ?? [];
13435
+ if (worksets.length === 0) {
13436
+ console.log(
13437
+ "No worksets defined. Add a top-level `worksets:` list to the seed manifest to group related projects."
13438
+ );
13439
+ return;
13440
+ }
13441
+ console.log(`Worksets (${worksets.length}):`);
13442
+ for (const workset of worksets) {
13443
+ const summary = workset.description ? ` \u2014 ${workset.description}` : "";
13444
+ console.log(`- ${workset.name} (${workset.projects.length} project(s))${summary}`);
13445
+ }
13446
+ }
13447
+ async function runWorksetShow(config, name) {
13448
+ const manifest = await readSeedManifest(config.manifestPath);
13449
+ const workset = manifest.worksets?.find((entry) => entry.name.toLowerCase() === name.toLowerCase());
13450
+ if (!workset) {
13451
+ throw new Error(`No workset named "${name}" in ${config.manifestPath}.`);
13452
+ }
13453
+ console.log(`Workset: ${workset.name}`);
13454
+ if (workset.description) {
13455
+ console.log(workset.description);
13456
+ }
13457
+ console.log("Projects:");
13458
+ for (const memberName of workset.projects) {
13459
+ const project = manifest.projects.find((entry) => entry.name.toLowerCase() === memberName.toLowerCase());
13460
+ console.log(project ? `- ${project.name} (${project.uri})` : `- ${memberName} [not found in manifest projects]`);
13461
+ }
13462
+ }
12767
13463
  async function runSeedSkills(config, options) {
12768
13464
  const ov = await openVikingCliForMode(options.dryRun === true);
12769
13465
  const catalogItems = await collectSkillCandidates(config);
@@ -12804,13 +13500,13 @@ async function resolveRepoRoot(repoInput) {
12804
13500
  async function projectIdentity(path2) {
12805
13501
  const expanded = expandPath(path2);
12806
13502
  try {
12807
- return await (0, import_promises9.realpath)(expanded);
13503
+ return await (0, import_promises11.realpath)(expanded);
12808
13504
  } catch (_err) {
12809
13505
  return expanded;
12810
13506
  }
12811
13507
  }
12812
13508
  function projectManifestForRepo(repoRoot, existingProjects) {
12813
- const baseName = uriSegment((0, import_node_path12.basename)(repoRoot));
13509
+ const baseName = uriSegment((0, import_node_path13.basename)(repoRoot));
12814
13510
  const usedNames = new Set(existingProjects.map((project) => project.name));
12815
13511
  const usedUris = new Set(existingProjects.map((project) => project.uri));
12816
13512
  let name = baseName;
@@ -12832,7 +13528,7 @@ async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
12832
13528
  for (const pattern of project.seed) {
12833
13529
  const files = await resolveProjectPattern(projectRoot, pattern);
12834
13530
  for (const filePath of files) {
12835
- const relativePath = toPosixPath((0, import_node_path12.relative)(projectRoot, filePath));
13531
+ const relativePath = toPosixPath((0, import_node_path13.relative)(projectRoot, filePath));
12836
13532
  if (seen.has(relativePath) || matchesIgnore(relativePath, ignorePatterns)) {
12837
13533
  continue;
12838
13534
  }
@@ -12850,20 +13546,20 @@ async function collectSeedCandidates(project, projectRoot, ignorePatterns) {
12850
13546
  async function resolveProjectPattern(projectRoot, pattern) {
12851
13547
  const normalizedPattern = toPosixPath(pattern);
12852
13548
  if (!hasGlob(normalizedPattern)) {
12853
- const filePath = (0, import_node_path12.join)(projectRoot, normalizedPattern);
13549
+ const filePath = (0, import_node_path13.join)(projectRoot, normalizedPattern);
12854
13550
  return await isFile(filePath) ? [filePath] : [];
12855
13551
  }
12856
13552
  const globBase = getGlobBase(normalizedPattern);
12857
- const basePath = (0, import_node_path12.join)(projectRoot, globBase);
13553
+ const basePath = (0, import_node_path13.join)(projectRoot, globBase);
12858
13554
  if (!await exists(basePath)) {
12859
13555
  return [];
12860
13556
  }
12861
13557
  const regex = globToRegExp(normalizedPattern);
12862
13558
  const files = await walkFiles(basePath);
12863
- return files.filter((filePath) => regex.test(toPosixPath((0, import_node_path12.relative)(projectRoot, filePath))));
13559
+ return files.filter((filePath) => regex.test(toPosixPath((0, import_node_path13.relative)(projectRoot, filePath))));
12864
13560
  }
12865
13561
  async function prepareSeedFile(config, candidate, dryRun) {
12866
- const content = await (0, import_promises9.readFile)(candidate.filePath, "utf8");
13562
+ const content = await (0, import_promises11.readFile)(candidate.filePath, "utf8");
12867
13563
  const redactedContent = shouldRedactPath(candidate.relativePath) ? redactContent(candidate.relativePath, content) : content;
12868
13564
  const secretMatches = detectSecretMatches(redactedContent);
12869
13565
  if (secretMatches.length > 0) {
@@ -12875,21 +13571,24 @@ async function prepareSeedFile(config, candidate, dryRun) {
12875
13571
  if (redactedContent === content) {
12876
13572
  return candidate.filePath;
12877
13573
  }
12878
- const redactedPath = (0, import_node_path12.join)(config.agentContextHome, "redacted", candidate.projectName, candidate.relativePath);
13574
+ const redactedPath = (0, import_node_path13.join)(config.agentContextHome, "redacted", candidate.projectName, candidate.relativePath);
12879
13575
  if (dryRun) {
12880
13576
  console.log(`Would write redacted copy: ${redactedPath}`);
12881
13577
  return redactedPath;
12882
13578
  }
12883
- await ensureDirectory((0, import_node_path12.dirname)(redactedPath), false);
12884
- await (0, import_promises9.writeFile)(redactedPath, redactedContent, { encoding: "utf8", mode: 384 });
12885
- await (0, import_promises9.chmod)(redactedPath, 384);
13579
+ await ensureDirectory((0, import_node_path13.dirname)(redactedPath), false);
13580
+ await (0, import_promises11.writeFile)(redactedPath, redactedContent, { encoding: "utf8", mode: 384 });
13581
+ await (0, import_promises11.chmod)(redactedPath, 384);
12886
13582
  return redactedPath;
12887
13583
  }
12888
13584
  function seedResourceReason(candidate) {
12889
13585
  return `Project guidance for ${candidate.projectName}: ${candidate.relativePath}`;
12890
13586
  }
13587
+ function graphCacheFileName(projectName) {
13588
+ return `${uriSegment(projectName)}-${sha256(projectName).slice(0, 8)}.graph.md`;
13589
+ }
12891
13590
  function skillResourceReason(skill) {
12892
- return `${skill.kind === "command" ? "Agent command" : "Agent skill"} catalog item from ${skill.source}: ${(0, import_node_path12.basename)(
13591
+ return `${skill.kind === "command" ? "Agent command" : "Agent skill"} catalog item from ${skill.source}: ${(0, import_node_path13.basename)(
12893
13592
  skill.filePath
12894
13593
  )}`;
12895
13594
  }
@@ -12922,7 +13621,7 @@ async function collectSkillCandidates(config) {
12922
13621
  for (const source of sources) {
12923
13622
  const files = await resolveAbsolutePattern(expandPath(source.pattern));
12924
13623
  for (const filePath of files) {
12925
- const content = await (0, import_promises9.readFile)(filePath, "utf8");
13624
+ const content = await (0, import_promises11.readFile)(filePath, "utf8");
12926
13625
  const matches = detectSecretMatches(content);
12927
13626
  if (matches.length > 0) {
12928
13627
  console.log(`SKIP skill with possible secret: ${filePath}`);
@@ -12956,16 +13655,16 @@ function skillResourceUri(skill) {
12956
13655
  return `viking://resources/agent-skills/${uriSegment(skill.source)}/${skillResourceName(skill)}-${skill.hash.slice(0, 12)}.md`;
12957
13656
  }
12958
13657
  function skillResourceName(skill) {
12959
- const fileName = (0, import_node_path12.basename)(skill.filePath);
13658
+ const fileName = (0, import_node_path13.basename)(skill.filePath);
12960
13659
  if (fileName.toLowerCase() === "skill.md") {
12961
- return uriSegment((0, import_node_path12.basename)((0, import_node_path12.dirname)(skill.filePath)));
13660
+ return uriSegment((0, import_node_path13.basename)((0, import_node_path13.dirname)(skill.filePath)));
12962
13661
  }
12963
13662
  const extensionIndex = fileName.lastIndexOf(".");
12964
13663
  const stem = extensionIndex > 0 ? fileName.slice(0, extensionIndex) : fileName;
12965
13664
  return uriSegment(stem);
12966
13665
  }
12967
13666
  async function loadIgnorePatterns() {
12968
- const raw = await (0, import_promises9.readFile)((0, import_node_path12.join)(toolRoot(), ".threadnoteignore"), "utf8");
13667
+ const raw = await (0, import_promises11.readFile)((0, import_node_path13.join)(toolRoot(), ".threadnoteignore"), "utf8");
12969
13668
  return raw.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
12970
13669
  }
12971
13670
  function matchesIgnore(relativePath, patterns) {
@@ -13038,18 +13737,18 @@ function detectSecretMatches(content) {
13038
13737
  // src/lifecycle.ts
13039
13738
  var import_node_child_process4 = require("node:child_process");
13040
13739
  var import_node_fs7 = require("node:fs");
13041
- var import_promises12 = require("node:fs/promises");
13740
+ var import_promises14 = require("node:fs/promises");
13042
13741
  var import_node_os6 = require("node:os");
13043
- var import_node_path14 = require("node:path");
13742
+ var import_node_path15 = require("node:path");
13044
13743
  var import_node_process4 = require("node:process");
13045
- var import_promises13 = require("node:readline/promises");
13744
+ var import_promises15 = require("node:readline/promises");
13046
13745
 
13047
13746
  // src/update.ts
13048
13747
  var import_node_fs6 = require("node:fs");
13049
- var import_promises10 = require("node:fs/promises");
13748
+ var import_promises12 = require("node:fs/promises");
13050
13749
  var import_node_os5 = require("node:os");
13051
- var import_node_path13 = require("node:path");
13052
- var import_promises11 = require("node:readline/promises");
13750
+ var import_node_path14 = require("node:path");
13751
+ var import_promises13 = require("node:readline/promises");
13053
13752
  var import_node_process3 = require("node:process");
13054
13753
 
13055
13754
  // src/release_notes.ts
@@ -13403,14 +14102,14 @@ async function waitForOpenVikingPortClosed(config, timeoutMs) {
13403
14102
  return false;
13404
14103
  }
13405
14104
  function launchAgentPlistPath() {
13406
- return (0, import_node_path13.join)((0, import_node_os5.homedir)(), "Library", "LaunchAgents", "io.threadnote.openviking.plist");
14105
+ return (0, import_node_path14.join)((0, import_node_os5.homedir)(), "Library", "LaunchAgents", "io.threadnote.openviking.plist");
13407
14106
  }
13408
14107
  async function isLaunchAgentInstalled() {
13409
14108
  if (process.platform !== "darwin") {
13410
14109
  return false;
13411
14110
  }
13412
14111
  try {
13413
- await (0, import_promises10.access)(launchAgentPlistPath(), import_node_fs6.constants.F_OK);
14112
+ await (0, import_promises12.access)(launchAgentPlistPath(), import_node_fs6.constants.F_OK);
13414
14113
  return true;
13415
14114
  } catch (_err) {
13416
14115
  return false;
@@ -13481,11 +14180,11 @@ async function readFreshCache(config, registry) {
13481
14180
  }
13482
14181
  async function writeUpdateCache2(config, cache) {
13483
14182
  await ensureDirectory(config.agentContextHome, false);
13484
- await (0, import_promises10.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
14183
+ await (0, import_promises12.writeFile)(updateCachePath(config), `${JSON.stringify(cache, null, 2)}
13485
14184
  `, { encoding: "utf8", mode: 384 });
13486
14185
  }
13487
14186
  function updateCachePath(config) {
13488
- return (0, import_node_path13.join)(config.agentContextHome, "update-check.json");
14187
+ return (0, import_node_path14.join)(config.agentContextHome, "update-check.json");
13489
14188
  }
13490
14189
  async function runApplicablePostUpdateMigrations(config, options) {
13491
14190
  const state = await readPostUpdateState(config);
@@ -13549,7 +14248,7 @@ async function applicablePostUpdateMigrations(config, options) {
13549
14248
  return applicable;
13550
14249
  }
13551
14250
  async function readPostUpdateMigrations() {
13552
- const raw = await readFileIfExists((0, import_node_path13.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
14251
+ const raw = await readFileIfExists((0, import_node_path14.join)(toolRoot(), "config", POST_UPDATE_MIGRATIONS_FILE));
13553
14252
  if (!raw) {
13554
14253
  return [];
13555
14254
  }
@@ -13588,7 +14287,7 @@ function printPostUpdateMigration(migration) {
13588
14287
  }
13589
14288
  }
13590
14289
  async function confirmPostUpdateMigration(prompt, defaultYes = false) {
13591
- const readline = (0, import_promises11.createInterface)({ input: import_node_process3.stdin, output: import_node_process3.stdout });
14290
+ const readline = (0, import_promises13.createInterface)({ input: import_node_process3.stdin, output: import_node_process3.stdout });
13592
14291
  try {
13593
14292
  const answer = (await readline.question(prompt)).trim().toLowerCase();
13594
14293
  if (answer === "") {
@@ -13623,11 +14322,11 @@ async function readPostUpdateState(config) {
13623
14322
  }
13624
14323
  async function writePostUpdateState(config, state) {
13625
14324
  await ensureDirectory(config.agentContextHome, false);
13626
- await (0, import_promises10.writeFile)(postUpdateStatePath(config), `${JSON.stringify(state, null, 2)}
14325
+ await (0, import_promises12.writeFile)(postUpdateStatePath(config), `${JSON.stringify(state, null, 2)}
13627
14326
  `, { encoding: "utf8", mode: 384 });
13628
14327
  }
13629
14328
  function postUpdateStatePath(config) {
13630
- return (0, import_node_path13.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
14329
+ return (0, import_node_path14.join)(config.agentContextHome, POST_UPDATE_STATE_FILE);
13631
14330
  }
13632
14331
  async function resolveUpdateRuntime(runtime) {
13633
14332
  if (runtime !== "auto") {
@@ -13657,14 +14356,14 @@ async function runtimeThreadnoteBin(runtime) {
13657
14356
  if (runtime === "npm") {
13658
14357
  const result = await runCommand("npm", ["prefix", "--global"], { allowFailure: true });
13659
14358
  const prefix = result.stdout.trim();
13660
- return prefix ? (0, import_node_path13.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
14359
+ return prefix ? (0, import_node_path14.join)(prefix, "bin", NPM_PACKAGE_NAME) : void 0;
13661
14360
  }
13662
14361
  if (runtime === "bun") {
13663
14362
  const result = await runCommand("bun", ["pm", "bin", "-g"], { allowFailure: true });
13664
14363
  const binDir = result.stdout.trim();
13665
- return binDir ? (0, import_node_path13.join)(binDir, NPM_PACKAGE_NAME) : void 0;
14364
+ return binDir ? (0, import_node_path14.join)(binDir, NPM_PACKAGE_NAME) : void 0;
13666
14365
  }
13667
- return (0, import_node_path13.join)(process.env.DENO_INSTALL ?? (0, import_node_path13.join)((0, import_node_os5.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
14366
+ return (0, import_node_path14.join)(process.env.DENO_INSTALL ?? (0, import_node_path14.join)((0, import_node_os5.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
13668
14367
  }
13669
14368
  function updatePackageCommand(runtime, registry) {
13670
14369
  if (runtime === "npm") {
@@ -13736,9 +14435,10 @@ async function collectDoctorChecks(config, options = {}) {
13736
14435
  checks.push(...await userAgentInstructionsChecks());
13737
14436
  checks.push(await manifestCheck(config.manifestPath));
13738
14437
  checks.push(await recallIndexFreshnessCheck(config));
13739
- checks.push(await fileCheck((0, import_node_path14.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
13740
- checks.push(await fileCheck((0, import_node_path14.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
13741
- checks.push(await fileCheck((0, import_node_path14.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
14438
+ checks.push(await memoryProjectConsistencyCheck(config));
14439
+ checks.push(await fileCheck((0, import_node_path15.join)(toolRoot(), ".threadnoteignore"), "ignore file"));
14440
+ checks.push(await fileCheck((0, import_node_path15.join)(toolRoot(), "config", "ov.conf.template.json"), "server config template"));
14441
+ checks.push(await fileCheck((0, import_node_path15.join)(toolRoot(), "config", "ovcli.conf.template.json"), "cli config template"));
13742
14442
  checks.push(await healthCheck(config));
13743
14443
  return checks;
13744
14444
  }
@@ -13746,9 +14446,9 @@ async function runInstall(config, options) {
13746
14446
  const repairInvalidConfigs = options.repairInvalidConfigs === true;
13747
14447
  const dryRun = options.dryRun === true;
13748
14448
  await ensureDirectory(config.agentContextHome, dryRun);
13749
- await ensureDirectory((0, import_node_path14.join)(config.agentContextHome, "logs"), dryRun);
13750
- await ensureDirectory((0, import_node_path14.join)(config.agentContextHome, "redacted"), dryRun);
13751
- await ensureDirectory((0, import_node_path14.join)(config.agentContextHome, "mcp"), dryRun);
14449
+ await ensureDirectory((0, import_node_path15.join)(config.agentContextHome, "logs"), dryRun);
14450
+ await ensureDirectory((0, import_node_path15.join)(config.agentContextHome, "redacted"), dryRun);
14451
+ await ensureDirectory((0, import_node_path15.join)(config.agentContextHome, "mcp"), dryRun);
13752
14452
  await installCommandShim(dryRun);
13753
14453
  await installUserAgentInstructions(dryRun);
13754
14454
  const serverPath = await findOpenVikingServer();
@@ -13793,17 +14493,17 @@ async function runInstall(config, options) {
13793
14493
  }
13794
14494
  await writeTemplateIfMissing({
13795
14495
  config,
13796
- destinationPath: (0, import_node_path14.join)(config.agentContextHome, "ov.conf"),
14496
+ destinationPath: (0, import_node_path15.join)(config.agentContextHome, "ov.conf"),
13797
14497
  dryRun,
13798
14498
  shouldRepair: (content) => shouldRepairOpenVikingConfig(content, config) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
13799
- templatePath: (0, import_node_path14.join)(toolRoot(), "config", "ov.conf.template.json")
14499
+ templatePath: (0, import_node_path15.join)(toolRoot(), "config", "ov.conf.template.json")
13800
14500
  });
13801
14501
  await writeTemplateIfMissing({
13802
14502
  config,
13803
- destinationPath: (0, import_node_path14.join)(config.agentContextHome, "ovcli.conf"),
14503
+ destinationPath: (0, import_node_path15.join)(config.agentContextHome, "ovcli.conf"),
13804
14504
  dryRun,
13805
14505
  shouldRepair: (content) => shouldRepairLegacyOvCliConfig(content) || repairInvalidConfigs && parseJsonConfigObject(content) === void 0,
13806
- templatePath: (0, import_node_path14.join)(toolRoot(), "config", "ovcli.conf.template.json")
14506
+ templatePath: (0, import_node_path15.join)(toolRoot(), "config", "ovcli.conf.template.json")
13807
14507
  });
13808
14508
  await configureOpenVikingCliLanguage(config, dryRun);
13809
14509
  if (options.start !== false) {
@@ -13860,7 +14560,7 @@ async function runUninstall(config, options) {
13860
14560
  }
13861
14561
  console.log("Uninstalling local Threadnote setup.");
13862
14562
  await runStop(config, { dryRun });
13863
- await removePathIfExists((0, import_node_path14.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
14563
+ await removePathIfExists((0, import_node_path15.join)(config.agentContextHome, "openviking-server.pid"), "pid file", dryRun);
13864
14564
  await removeLaunchAgent(dryRun);
13865
14565
  await removeMcpConfigs(options.mcp ?? "available", dryRun);
13866
14566
  await removeMcpSnippets(config, dryRun);
@@ -13917,16 +14617,16 @@ async function repairManifest(config, dryRun) {
13917
14617
  console.log(output2.trimEnd());
13918
14618
  return;
13919
14619
  }
13920
- await ensureDirectory((0, import_node_path14.dirname)(config.manifestPath), false);
14620
+ await ensureDirectory((0, import_node_path15.dirname)(config.manifestPath), false);
13921
14621
  const currentContent = await readFileIfExists(config.manifestPath);
13922
14622
  if (currentContent !== void 0) {
13923
14623
  const backupPath = `${config.manifestPath}.legacy-${safeTimestamp()}`;
13924
- await (0, import_promises12.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
13925
- await (0, import_promises12.chmod)(backupPath, 384);
14624
+ await (0, import_promises14.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
14625
+ await (0, import_promises14.chmod)(backupPath, 384);
13926
14626
  console.log(`Backup: ${backupPath}`);
13927
14627
  }
13928
- await (0, import_promises12.writeFile)(config.manifestPath, output2, { encoding: "utf8", mode: 384 });
13929
- await (0, import_promises12.chmod)(config.manifestPath, 384);
14628
+ await (0, import_promises14.writeFile)(config.manifestPath, output2, { encoding: "utf8", mode: 384 });
14629
+ await (0, import_promises14.chmod)(config.manifestPath, 384);
13930
14630
  console.log(`Wrote replacement manifest: ${config.manifestPath}`);
13931
14631
  }
13932
14632
  async function repairRecallIndex(config, dryRun) {
@@ -14001,7 +14701,7 @@ async function findOpenVikingServer() {
14001
14701
  return onPath;
14002
14702
  }
14003
14703
  for (const candidateDir of await openVikingServerCandidateDirs()) {
14004
- const candidate = (0, import_node_path14.join)(candidateDir, OPENVIKING_SERVER_COMMAND);
14704
+ const candidate = (0, import_node_path15.join)(candidateDir, OPENVIKING_SERVER_COMMAND);
14005
14705
  if (await isExecutable(candidate)) {
14006
14706
  return candidate;
14007
14707
  }
@@ -14047,7 +14747,7 @@ async function maybePrintOpenVikingPathHint(serverPath) {
14047
14747
  if (onPath) {
14048
14748
  return;
14049
14749
  }
14050
- const binDir = (0, import_node_path14.dirname)(serverPath);
14750
+ const binDir = (0, import_node_path15.dirname)(serverPath);
14051
14751
  const rcHint = suggestedShellRc(process.env.SHELL, (0, import_node_os6.platform)());
14052
14752
  console.log(
14053
14753
  `Note: ${serverPath} is installed but ${binDir} is not on this shell's PATH. Add \`export PATH="${binDir}:$PATH"\` to ${rcHint} so other tools can find openviking-server.`
@@ -14091,7 +14791,7 @@ async function runStart(config, options) {
14091
14791
  );
14092
14792
  }
14093
14793
  const logPath = openVikingLogPath(config);
14094
- await ensureDirectory((0, import_node_path14.dirname)(logPath), false);
14794
+ await ensureDirectory((0, import_node_path15.dirname)(logPath), false);
14095
14795
  if (options.foreground === true) {
14096
14796
  const result = await runInteractive(server, args);
14097
14797
  process.exitCode = result;
@@ -14100,7 +14800,7 @@ async function runStart(config, options) {
14100
14800
  const logFd = (0, import_node_fs7.openSync)(logPath, "a");
14101
14801
  const child = spawnDetachedServerWithLog(server, args, logFd);
14102
14802
  child.unref();
14103
- await (0, import_promises12.writeFile)((0, import_node_path14.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
14803
+ await (0, import_promises14.writeFile)((0, import_node_path15.join)(config.agentContextHome, "openviking-server.pid"), `${child.pid}
14104
14804
  `, "utf8");
14105
14805
  const health = await waitForOpenVikingHealth(
14106
14806
  config,
@@ -14137,7 +14837,7 @@ async function runStop(config, options) {
14137
14837
  console.log(`No LaunchAgent found: ${launchAgentPath}`);
14138
14838
  }
14139
14839
  }
14140
- const pidPath = (0, import_node_path14.join)(config.agentContextHome, "openviking-server.pid");
14840
+ const pidPath = (0, import_node_path15.join)(config.agentContextHome, "openviking-server.pid");
14141
14841
  const pidText = await readFileIfExists(pidPath);
14142
14842
  if (!pidText) {
14143
14843
  console.log("No pid file found for detached OpenViking server.");
@@ -14183,7 +14883,7 @@ async function openVikingServerCheck() {
14183
14883
  }
14184
14884
  const result = await runCommand(executable, ["--help"], { allowFailure: true });
14185
14885
  const onPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
14186
- const detail = onPath ? executable : `${executable} (found outside PATH; add ${(0, import_node_path14.dirname)(executable)} to PATH)`;
14886
+ const detail = onPath ? executable : `${executable} (found outside PATH; add ${(0, import_node_path15.dirname)(executable)} to PATH)`;
14187
14887
  return {
14188
14888
  name,
14189
14889
  status: result.exitCode === 0 ? "ok" : "warn",
@@ -14224,7 +14924,7 @@ async function openVikingCliCheck() {
14224
14924
  }
14225
14925
  const result = await runCommand(executable, ["--help"], { allowFailure: true });
14226
14926
  const onPath = await findExecutable(["ov", "openviking"]);
14227
- const detail = onPath ? executable : `${executable} (found outside PATH; add ${(0, import_node_path14.dirname)(executable)} to PATH)`;
14927
+ const detail = onPath ? executable : `${executable} (found outside PATH; add ${(0, import_node_path15.dirname)(executable)} to PATH)`;
14228
14928
  return {
14229
14929
  name: "openviking cli",
14230
14930
  status: result.exitCode === 0 ? "ok" : "warn",
@@ -14317,7 +15017,7 @@ async function pythonSystemCertificatesCheck() {
14317
15017
  };
14318
15018
  }
14319
15019
  async function commandShimCheck() {
14320
- const shimPath = (0, import_node_path14.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
15020
+ const shimPath = (0, import_node_path15.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
14321
15021
  const content = await readFileIfExists(shimPath);
14322
15022
  if (content === void 0) {
14323
15023
  return { name: "threadnote shim", status: "warn", detail: `${shimPath} missing; repair will create it` };
@@ -14383,11 +15083,11 @@ async function hasPythonModule(serverPath, moduleName) {
14383
15083
  async function siblingPythonForExecutable(executablePath) {
14384
15084
  let resolvedPath;
14385
15085
  try {
14386
- resolvedPath = await (0, import_promises12.realpath)(executablePath);
15086
+ resolvedPath = await (0, import_promises14.realpath)(executablePath);
14387
15087
  } catch (_err) {
14388
15088
  return void 0;
14389
15089
  }
14390
- const pythonPath = (0, import_node_path14.join)((0, import_node_path14.dirname)(resolvedPath), "python");
15090
+ const pythonPath = (0, import_node_path15.join)((0, import_node_path15.dirname)(resolvedPath), "python");
14391
15091
  return await exists(pythonPath) ? pythonPath : void 0;
14392
15092
  }
14393
15093
  async function manifestCheck(path2) {
@@ -14428,6 +15128,61 @@ async function recallIndexFreshnessCheck(config) {
14428
15128
  return { name: "recall index freshness", status: "warn", detail: errorMessage(err) };
14429
15129
  }
14430
15130
  }
15131
+ async function memoryProjectConsistencyCheck(config) {
15132
+ const name = "memory project consistency";
15133
+ const memoriesRoot = (0, import_node_path15.join)(
15134
+ config.agentContextHome,
15135
+ "data",
15136
+ "viking",
15137
+ config.account,
15138
+ "user",
15139
+ uriSegment(config.user),
15140
+ "memories"
15141
+ );
15142
+ try {
15143
+ let entries;
15144
+ try {
15145
+ entries = await (0, import_promises14.readdir)(memoriesRoot, { recursive: true });
15146
+ } catch {
15147
+ return { name, status: "ok", detail: "no memories directory yet" };
15148
+ }
15149
+ const mismatches = [];
15150
+ let checked = 0;
15151
+ for (const entry of entries) {
15152
+ if (!entry.endsWith(".md") || isSummarySidecarUri(entry)) {
15153
+ continue;
15154
+ }
15155
+ const uri = `viking://user/${uriSegment(config.user)}/memories/${entry.split(import_node_path15.sep).join("/")}`;
15156
+ const pathProject = memoryUriProjectSegment(uri);
15157
+ if (!pathProject) {
15158
+ continue;
15159
+ }
15160
+ let content;
15161
+ try {
15162
+ content = await (0, import_promises14.readFile)((0, import_node_path15.join)(memoriesRoot, entry), "utf8");
15163
+ } catch {
15164
+ continue;
15165
+ }
15166
+ checked += 1;
15167
+ const frontProject = memoryFrontmatterField(content, "project");
15168
+ if (frontProject && uriSegment(frontProject) !== pathProject) {
15169
+ mismatches.push(`${uri} (frontmatter "${frontProject}" vs path "${pathProject}")`);
15170
+ }
15171
+ }
15172
+ if (mismatches.length === 0) {
15173
+ return { name, status: "ok", detail: `${checked} project-scoped memories consistent` };
15174
+ }
15175
+ const sample = mismatches.slice(0, 3).join("; ");
15176
+ const extra = Math.max(0, mismatches.length - 3);
15177
+ return {
15178
+ name,
15179
+ status: "warn",
15180
+ detail: `${mismatches.length} memory(ies) whose frontmatter project differs from their storage path; re-store under the correct project to fix: ${sample}${extra > 0 ? `, +${extra} more` : ""}`
15181
+ };
15182
+ } catch (err) {
15183
+ return { name, status: "warn", detail: errorMessage(err) };
15184
+ }
15185
+ }
14431
15186
  async function fileCheck(path2, label) {
14432
15187
  return await exists(path2) ? { name: label, status: "ok", detail: path2 } : { name: label, status: "fail", detail: `${path2} missing` };
14433
15188
  }
@@ -14659,7 +15414,7 @@ async function offerToInstallUv() {
14659
15414
  );
14660
15415
  return false;
14661
15416
  }
14662
- const readline = (0, import_promises13.createInterface)({ input: import_node_process4.stdin, output: import_node_process4.stdout });
15417
+ const readline = (0, import_promises15.createInterface)({ input: import_node_process4.stdin, output: import_node_process4.stdout });
14663
15418
  let answer;
14664
15419
  try {
14665
15420
  answer = (await readline.question(
@@ -14817,38 +15572,38 @@ function printInstallNextSteps(options) {
14817
15572
  }
14818
15573
  async function writeTemplateIfMissing(options) {
14819
15574
  if (await exists(options.destinationPath)) {
14820
- const currentContent = await (0, import_promises12.readFile)(options.destinationPath, "utf8");
15575
+ const currentContent = await (0, import_promises14.readFile)(options.destinationPath, "utf8");
14821
15576
  if (options.shouldRepair?.(currentContent) !== true) {
14822
15577
  console.log(`Already exists: ${options.destinationPath}`);
14823
15578
  return;
14824
15579
  }
14825
- const rendered2 = renderTemplate(await (0, import_promises12.readFile)(options.templatePath, "utf8"), options.config);
15580
+ const rendered2 = renderTemplate(await (0, import_promises14.readFile)(options.templatePath, "utf8"), options.config);
14826
15581
  if (options.dryRun) {
14827
15582
  console.log(`Would repair generated config: ${options.destinationPath}`);
14828
15583
  return;
14829
15584
  }
14830
15585
  const backupPath = `${options.destinationPath}.legacy-${safeTimestamp()}`;
14831
- await (0, import_promises12.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
14832
- await (0, import_promises12.chmod)(backupPath, 384);
14833
- await (0, import_promises12.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
14834
- await (0, import_promises12.chmod)(options.destinationPath, 384);
15586
+ await (0, import_promises14.writeFile)(backupPath, currentContent, { encoding: "utf8", mode: 384 });
15587
+ await (0, import_promises14.chmod)(backupPath, 384);
15588
+ await (0, import_promises14.writeFile)(options.destinationPath, rendered2, { encoding: "utf8", mode: 384 });
15589
+ await (0, import_promises14.chmod)(options.destinationPath, 384);
14835
15590
  console.log(`Repaired generated config: ${options.destinationPath}`);
14836
15591
  console.log(`Backup: ${backupPath}`);
14837
15592
  return;
14838
15593
  }
14839
- const rendered = renderTemplate(await (0, import_promises12.readFile)(options.templatePath, "utf8"), options.config);
15594
+ const rendered = renderTemplate(await (0, import_promises14.readFile)(options.templatePath, "utf8"), options.config);
14840
15595
  if (options.dryRun) {
14841
15596
  console.log(`Would write ${options.destinationPath}`);
14842
15597
  return;
14843
15598
  }
14844
- await ensureDirectory((0, import_node_path14.dirname)(options.destinationPath), false);
14845
- await (0, import_promises12.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
14846
- await (0, import_promises12.chmod)(options.destinationPath, 384);
15599
+ await ensureDirectory((0, import_node_path15.dirname)(options.destinationPath), false);
15600
+ await (0, import_promises14.writeFile)(options.destinationPath, rendered, { encoding: "utf8", mode: 384 });
15601
+ await (0, import_promises14.chmod)(options.destinationPath, 384);
14847
15602
  console.log(`Wrote ${options.destinationPath}`);
14848
15603
  }
14849
15604
  async function installCommandShim(dryRun) {
14850
15605
  const binDir = expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin");
14851
- const shimPath = (0, import_node_path14.join)(binDir, "threadnote");
15606
+ const shimPath = (0, import_node_path15.join)(binDir, "threadnote");
14852
15607
  const existingContent = await readFileIfExists(shimPath);
14853
15608
  if (existingContent && !isManagedCommandShim(existingContent)) {
14854
15609
  console.log(`WARN not overwriting existing command shim: ${shimPath}`);
@@ -14864,12 +15619,12 @@ async function installCommandShim(dryRun) {
14864
15619
  return;
14865
15620
  }
14866
15621
  await ensureDirectory(binDir, false);
14867
- await (0, import_promises12.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
14868
- await (0, import_promises12.chmod)(shimPath, 493);
15622
+ await (0, import_promises14.writeFile)(shimPath, content, { encoding: "utf8", mode: 493 });
15623
+ await (0, import_promises14.chmod)(shimPath, 493);
14869
15624
  console.log(`Wrote command shim: ${shimPath}`);
14870
15625
  }
14871
15626
  async function removeCommandShim(dryRun) {
14872
- const shimPath = (0, import_node_path14.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
15627
+ const shimPath = (0, import_node_path15.join)(expandPath(process.env.THREADNOTE_BIN_DIR ?? "~/.local/bin"), "threadnote");
14873
15628
  const content = await readFileIfExists(shimPath);
14874
15629
  if (content === void 0) {
14875
15630
  console.log(`Already absent: ${shimPath}`);
@@ -14903,8 +15658,8 @@ async function installUserAgentInstructions(dryRun) {
14903
15658
  console.log(currentContent === void 0 ? `Would write ${targetPath}` : `Would update ${targetPath}`);
14904
15659
  continue;
14905
15660
  }
14906
- await ensureDirectory((0, import_node_path14.dirname)(targetPath), false);
14907
- await (0, import_promises12.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
15661
+ await ensureDirectory((0, import_node_path15.dirname)(targetPath), false);
15662
+ await (0, import_promises14.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
14908
15663
  console.log(currentContent === void 0 ? `Wrote ${targetPath}` : `Updated ${targetPath}`);
14909
15664
  }
14910
15665
  }
@@ -14941,7 +15696,7 @@ async function removeUserAgentInstructions(dryRun) {
14941
15696
  console.log(`Would update ${targetPath}`);
14942
15697
  continue;
14943
15698
  }
14944
- await (0, import_promises12.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
15699
+ await (0, import_promises14.writeFile)(targetPath, nextContent, { encoding: "utf8", mode: 420 });
14945
15700
  console.log(`Updated ${targetPath}`);
14946
15701
  }
14947
15702
  }
@@ -14961,7 +15716,7 @@ async function renderUserAgentInstructions(target) {
14961
15716
  ].join("\n");
14962
15717
  }
14963
15718
  async function renderUserAgentInstructionsBlock() {
14964
- const instructions = (await (0, import_promises12.readFile)((0, import_node_path14.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
15719
+ const instructions = (await (0, import_promises14.readFile)((0, import_node_path15.join)(toolRoot(), "docs", "agent-instructions.md"), "utf8")).trim();
14965
15720
  return `${USER_INSTRUCTIONS_START_MARKER}
14966
15721
  ${instructions}
14967
15722
  ${USER_INSTRUCTIONS_END_MARKER}`;
@@ -15045,9 +15800,9 @@ async function installLaunchAgent(config, dryRun) {
15045
15800
  `Cannot install LaunchAgent: ${OPENVIKING_SERVER_COMMAND} was not found in PATH, uv tool bin dir, $UV_TOOL_BIN_DIR, or ~/.local/bin. Run \`threadnote install\` first.`
15046
15801
  );
15047
15802
  }
15048
- const source = (0, import_node_path14.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
15803
+ const source = (0, import_node_path15.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
15049
15804
  const destination = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
15050
- const rendered = renderTemplate(await (0, import_promises12.readFile)(source, "utf8"), config, {
15805
+ const rendered = renderTemplate(await (0, import_promises14.readFile)(source, "utf8"), config, {
15051
15806
  OPENVIKING_SERVER_PATH: resolvedServer ?? OPENVIKING_SERVER_COMMAND
15052
15807
  });
15053
15808
  if (dryRun) {
@@ -15059,9 +15814,9 @@ async function installLaunchAgent(config, dryRun) {
15059
15814
  console.log(`Would run: launchctl start ${LAUNCHD_LABEL}`);
15060
15815
  return;
15061
15816
  }
15062
- await ensureDirectory((0, import_node_path14.dirname)(destination), false);
15063
- await ensureDirectory((0, import_node_path14.dirname)(openVikingLogPath(config)), false);
15064
- await (0, import_promises12.writeFile)(destination, rendered, "utf8");
15817
+ await ensureDirectory((0, import_node_path15.dirname)(destination), false);
15818
+ await ensureDirectory((0, import_node_path15.dirname)(openVikingLogPath(config)), false);
15819
+ await (0, import_promises14.writeFile)(destination, rendered, "utf8");
15065
15820
  await maybeRun(false, "launchctl", ["unload", destination], { allowFailure: true });
15066
15821
  await maybeRun(false, "launchctl", ["load", destination]);
15067
15822
  await maybeRun(false, "launchctl", ["start", LAUNCHD_LABEL]);
@@ -15128,7 +15883,7 @@ function isGeneratedLocalPilotConfig(parsed, config) {
15128
15883
  if (typeof parsed.default_user !== "string") {
15129
15884
  return false;
15130
15885
  }
15131
- if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path14.join)(config.agentContextHome, "data")) {
15886
+ if (!isJsonObject(parsed.storage) || parsed.storage.workspace !== (0, import_node_path15.join)(config.agentContextHome, "data")) {
15132
15887
  return false;
15133
15888
  }
15134
15889
  return isJsonObject(parsed.server) && parsed.server.host === config.host && String(parsed.server.port) === String(config.port);
@@ -15190,9 +15945,9 @@ function printWhatsNew(whatsNew) {
15190
15945
  // src/manager.ts
15191
15946
  var import_node_http2 = require("node:http");
15192
15947
  var import_node_crypto2 = require("node:crypto");
15193
- var import_promises14 = require("node:fs/promises");
15948
+ var import_promises16 = require("node:fs/promises");
15194
15949
  var import_node_os7 = require("node:os");
15195
- var import_node_path15 = require("node:path");
15950
+ var import_node_path16 = require("node:path");
15196
15951
  var STATIC_FILES = {
15197
15952
  "/": { contentType: "text/html; charset=utf-8", path: "index.html" },
15198
15953
  "/index.html": { contentType: "text/html; charset=utf-8", path: "index.html" },
@@ -15263,18 +16018,18 @@ async function readManagedMemory(config, uri) {
15263
16018
  if (!path2) {
15264
16019
  throw new Error(`Manager can only read current-user memory URIs: ${uri}`);
15265
16020
  }
15266
- const [content, pathStat] = await Promise.all([(0, import_promises14.readFile)(path2, "utf8"), (0, import_promises14.stat)(path2)]);
15267
- const relativePath = (0, import_node_path15.relative)(localMemoriesRoot(config), path2).split(import_node_path15.sep).join("/");
16021
+ const [content, pathStat] = await Promise.all([(0, import_promises16.readFile)(path2, "utf8"), (0, import_promises16.stat)(path2)]);
16022
+ const relativePath = (0, import_node_path16.relative)(localMemoriesRoot(config), path2).split(import_node_path16.sep).join("/");
15268
16023
  const record = parseMemoryDocument(uri, content);
15269
16024
  return {
15270
16025
  content,
15271
16026
  node: {
15272
16027
  isDir: false,
15273
16028
  isShared: isInSharedNamespace(config, uri),
15274
- isSystem: isSystemMemoryName(path2.split(import_node_path15.sep).at(-1) ?? ""),
16029
+ isSystem: isSystemMemoryName(path2.split(import_node_path16.sep).at(-1) ?? ""),
15275
16030
  metadata: record?.metadata,
15276
16031
  modTime: pathStat.mtime.toISOString(),
15277
- name: path2.split(import_node_path15.sep).at(-1) ?? uri,
16032
+ name: path2.split(import_node_path16.sep).at(-1) ?? uri,
15278
16033
  relativePath,
15279
16034
  sharedTeam: sharedTeamNameForUri(config, uri),
15280
16035
  size: pathStat.size,
@@ -15562,7 +16317,7 @@ async function handleRequest(context, request, response) {
15562
16317
  }
15563
16318
  async function serveStatic(context, url, response) {
15564
16319
  const file = STATIC_FILES[url.pathname] ?? STATIC_FILES["/"];
15565
- const content = await (0, import_promises14.readFile)((0, import_node_path15.join)(toolRoot(), file.root ?? "manager", file.path));
16320
+ const content = await (0, import_promises16.readFile)((0, import_node_path16.join)(toolRoot(), file.root ?? "manager", file.path));
15566
16321
  const headers = { "content-type": file.contentType };
15567
16322
  if (file.root !== "docs") {
15568
16323
  headers["cache-control"] = "no-store";
@@ -15571,11 +16326,11 @@ async function serveStatic(context, url, response) {
15571
16326
  response.end(content);
15572
16327
  }
15573
16328
  async function readTree(config, path2, uri, relativePath, options = {}) {
15574
- const pathStat = await (0, import_promises14.stat)(path2);
16329
+ const pathStat = await (0, import_promises16.stat)(path2);
15575
16330
  const name = relativePath ? relativePath.split("/").at(-1) ?? relativePath : options.rootName ?? "memories";
15576
16331
  const isDir = pathStat.isDirectory();
15577
16332
  if (!isDir) {
15578
- const record = options.parseMemoryDocuments === false ? void 0 : parseMemoryDocument(uri, await (0, import_promises14.readFile)(path2, "utf8").catch(() => ""));
16333
+ const record = options.parseMemoryDocuments === false ? void 0 : parseMemoryDocument(uri, await (0, import_promises16.readFile)(path2, "utf8").catch(() => ""));
15579
16334
  return {
15580
16335
  isDir: false,
15581
16336
  isShared: isInSharedNamespace(config, uri),
@@ -15589,13 +16344,13 @@ async function readTree(config, path2, uri, relativePath, options = {}) {
15589
16344
  uri
15590
16345
  };
15591
16346
  }
15592
- const entries = await (0, import_promises14.readdir)(path2, { withFileTypes: true });
16347
+ const entries = await (0, import_promises16.readdir)(path2, { withFileTypes: true });
15593
16348
  const children = await Promise.all(
15594
16349
  entries.sort(
15595
16350
  (left, right) => Number(right.isDirectory()) - Number(left.isDirectory()) || left.name.localeCompare(right.name)
15596
16351
  ).map((entry) => {
15597
16352
  const childRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
15598
- return readTree(config, (0, import_node_path15.join)(path2, entry.name), `${uri}/${entry.name}`, childRelative, options);
16353
+ return readTree(config, (0, import_node_path16.join)(path2, entry.name), `${uri}/${entry.name}`, childRelative, options);
15599
16354
  })
15600
16355
  );
15601
16356
  return {
@@ -15769,27 +16524,27 @@ async function removeManagedFolder(config, uri) {
15769
16524
  if (!path2) {
15770
16525
  throw new Error(`Manager can only remove current-user memory folders: ${uri}`);
15771
16526
  }
15772
- const pathStat = await (0, import_promises14.stat)(path2);
16527
+ const pathStat = await (0, import_promises16.stat)(path2);
15773
16528
  if (!pathStat.isDirectory()) {
15774
16529
  throw new Error(`Not a folder: ${uri}`);
15775
16530
  }
15776
- const relativePath = (0, import_node_path15.relative)(localMemoriesRoot(config), path2);
15777
- if (!relativePath || relativePath.startsWith("..") || relativePath.split(import_node_path15.sep).includes("..")) {
16531
+ const relativePath = (0, import_node_path16.relative)(localMemoriesRoot(config), path2);
16532
+ if (!relativePath || relativePath.startsWith("..") || relativePath.split(import_node_path16.sep).includes("..")) {
15778
16533
  throw new Error("Refusing to remove a folder outside the memories tree.");
15779
16534
  }
15780
16535
  const fileUris = await fileUrisUnderFolder(config, path2);
15781
16536
  for (const fileUri of fileUris) {
15782
16537
  await runForget(config, fileUri, {});
15783
16538
  }
15784
- await (0, import_promises14.rm)(path2, { force: true, recursive: true });
16539
+ await (0, import_promises16.rm)(path2, { force: true, recursive: true });
15785
16540
  console.log(`Removed folder: ${uri}`);
15786
16541
  console.log(`Forgot ${fileUris.length} file${fileUris.length === 1 ? "" : "s"}.`);
15787
16542
  }
15788
16543
  async function fileUrisUnderFolder(config, folderPath) {
15789
- const entries = await (0, import_promises14.readdir)(folderPath, { withFileTypes: true });
16544
+ const entries = await (0, import_promises16.readdir)(folderPath, { withFileTypes: true });
15790
16545
  const uris = [];
15791
16546
  for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
15792
- const path2 = (0, import_node_path15.join)(folderPath, entry.name);
16547
+ const path2 = (0, import_node_path16.join)(folderPath, entry.name);
15793
16548
  if (entry.isDirectory()) {
15794
16549
  uris.push(...await fileUrisUnderFolder(config, path2));
15795
16550
  } else if (entry.isFile()) {
@@ -15887,11 +16642,11 @@ async function runConsolidationAgent(agent, sources) {
15887
16642
  throw new Error(`${agent} executable was not found.`);
15888
16643
  }
15889
16644
  const prompt = consolidationPrompt(sources);
15890
- const stagingDir = await (0, import_promises14.mkdtemp)((0, import_node_path15.join)((0, import_node_os7.tmpdir)(), "threadnote-consolidate-"));
15891
- const promptPath = (0, import_node_path15.join)(stagingDir, "prompt.txt");
16645
+ const stagingDir = await (0, import_promises16.mkdtemp)((0, import_node_path16.join)((0, import_node_os7.tmpdir)(), "threadnote-consolidate-"));
16646
+ const promptPath = (0, import_node_path16.join)(stagingDir, "prompt.txt");
15892
16647
  try {
15893
- await (0, import_promises14.writeFile)(promptPath, prompt, { encoding: "utf8", mode: 384 });
15894
- await (0, import_promises14.chmod)(promptPath, 384);
16648
+ await (0, import_promises16.writeFile)(promptPath, prompt, { encoding: "utf8", mode: 384 });
16649
+ await (0, import_promises16.chmod)(promptPath, 384);
15895
16650
  const script = consolidationAgentScript(agent, executable);
15896
16651
  const result = await runCommand("sh", ["-lc", script, "threadnote-consolidate", promptPath], {
15897
16652
  allowFailure: true,
@@ -15907,7 +16662,7 @@ async function runConsolidationAgent(agent, sources) {
15907
16662
  }
15908
16663
  return draft;
15909
16664
  } finally {
15910
- await (0, import_promises14.rm)(stagingDir, { force: true, recursive: true });
16665
+ await (0, import_promises16.rm)(stagingDir, { force: true, recursive: true });
15911
16666
  }
15912
16667
  }
15913
16668
  function consolidationAgentScript(agent, executable) {
@@ -16045,10 +16800,10 @@ function memoryDirectoryUri2(config, kind, status, projectSegment) {
16045
16800
  }
16046
16801
  }
16047
16802
  function localMemoriesRoot(config) {
16048
- return (0, import_node_path15.join)(config.agentContextHome, "data", "viking", config.account, "user", uriSegment(config.user), "memories");
16803
+ return (0, import_node_path16.join)(config.agentContextHome, "data", "viking", config.account, "user", uriSegment(config.user), "memories");
16049
16804
  }
16050
16805
  function localResourcesRoot(config) {
16051
- return (0, import_node_path15.join)(config.agentContextHome, "data", "viking", config.account, "resources");
16806
+ return (0, import_node_path16.join)(config.agentContextHome, "data", "viking", config.account, "resources");
16052
16807
  }
16053
16808
  function localPathForMemoryUri(config, uri) {
16054
16809
  const prefix = `viking://user/${uriSegment(config.user)}/memories`;
@@ -16060,17 +16815,17 @@ function localPathForMemoryUri(config, uri) {
16060
16815
  if (segments.some((segment) => segment === "." || segment === "..")) {
16061
16816
  return void 0;
16062
16817
  }
16063
- return (0, import_node_path15.join)(localMemoriesRoot(config), ...segments);
16818
+ return (0, import_node_path16.join)(localMemoriesRoot(config), ...segments);
16064
16819
  }
16065
16820
  function isMissingPathError(err) {
16066
16821
  return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
16067
16822
  }
16068
16823
  function localPathToMemoryUri(config, path2) {
16069
- const relativePath = (0, import_node_path15.relative)(localMemoriesRoot(config), path2);
16070
- if (!relativePath || relativePath.startsWith("..") || relativePath.split(import_node_path15.sep).includes("..")) {
16824
+ const relativePath = (0, import_node_path16.relative)(localMemoriesRoot(config), path2);
16825
+ if (!relativePath || relativePath.startsWith("..") || relativePath.split(import_node_path16.sep).includes("..")) {
16071
16826
  throw new Error(`Path is outside the memories tree: ${path2}`);
16072
16827
  }
16073
- return `viking://user/${uriSegment(config.user)}/memories/${relativePath.split(import_node_path15.sep).join("/")}`;
16828
+ return `viking://user/${uriSegment(config.user)}/memories/${relativePath.split(import_node_path16.sep).join("/")}`;
16074
16829
  }
16075
16830
  async function ensurePersonalDirectoryChain2(config, ov, directoryUri) {
16076
16831
  const prefix = "viking://";
@@ -16264,7 +17019,10 @@ async function main() {
16264
17019
  ).option("--preserve-memories", "Preserve THREADNOTE_HOME and OpenViking memories (default)").option("--erase-memories", "Delete THREADNOTE_HOME, including all OpenViking memories").action(async (options) => {
16265
17020
  await runUninstall(getRuntimeConfig(program2), options);
16266
17021
  });
16267
- program2.command("seed").description("Seed curated context from the manifest; never indexes whole repos by default").option("--dry-run", "Print files and ov commands without importing").option("--force", "Re-upload every candidate even if mtime+size match the recorded state").option("--manifest <path>", "Manifest path for this seed run").option(
17022
+ program2.command("seed").description("Seed curated context from the manifest; never indexes whole repos by default").option("--dry-run", "Print files and ov commands without importing").option("--force", "Re-upload every candidate even if mtime+size match the recorded state").option(
17023
+ "--graph",
17024
+ "Also seed a per-project .graph.md dependency-facts resource (package.json/go.mod), with [[project]] cross-repo edges"
17025
+ ).option("--manifest <path>", "Manifest path for this seed run").option(
16268
17026
  "--only <project>",
16269
17027
  "Restrict seeding to one or more manifest projects by name; repeat for multiple",
16270
17028
  collectOption,
@@ -16310,9 +17068,19 @@ async function main() {
16310
17068
  program2.command("migrate-lifecycle").description("Move clear legacy handoff memories into lifecycle-aware archive paths").option("--apply", "Perform the migration; without this, prints a dry run").option("--dry-run", "Print migration actions without writing or removing memories").option("--limit <count>", "Maximum number of legacy handoffs to migrate").action(async (options) => {
16311
17069
  await runMigrateLifecycle(getRuntimeConfig(program2), options);
16312
17070
  });
16313
- program2.command("recall").description("Search shared OpenViking context").requiredOption("--query <query>", "Search query").option("--dry-run", "Print ov command without searching").option("--include-archived", "Include archived memories in recall results").option("-n, --node-limit <count>", "Maximum number of search results").option("--no-infer-scope", "Disable query-based scope inference").option("--project <name>", "Prioritize a project: add a scoped pass over its memories alongside the global search").option("--threshold <score>", "Minimum relevance score 0-1 (default 0.45); lower to broaden when recall is empty").option("--uri <uri>", "Restrict search to a viking:// URI").action(async (options) => {
17071
+ program2.command("recall").description("Search shared OpenViking context").requiredOption("--query <query>", "Search query").option("--dry-run", "Print ov command without searching").option("--include-archived", "Include archived memories in recall results").option("-n, --node-limit <count>", "Maximum number of search results").option("--no-infer-scope", "Disable query-based scope inference").option("--project <name>", "Prioritize a project: add a scoped pass over its memories alongside the global search").option("--threshold <score>", "Minimum relevance score 0-1 (default 0.45); lower to broaden when recall is empty").option("--uri <uri>", "Restrict search to a viking:// URI").option(
17072
+ "--workset <name>",
17073
+ "Recall across a named seed-manifest workset (a set of related repos) as one working set"
17074
+ ).action(async (options) => {
16314
17075
  await runRecall(getRuntimeConfig(program2), options);
16315
17076
  });
17077
+ const workset = program2.command("workset").description("Inspect seed-manifest worksets (named sets of related repos recalled as one working set)");
17078
+ workset.command("list").description("List worksets defined in the seed manifest").action(async () => {
17079
+ await runWorksetList(getRuntimeConfig(program2));
17080
+ });
17081
+ workset.command("show").description("Show the member projects of a workset").argument("<name>", "Workset name").action(async (name) => {
17082
+ await runWorksetShow(getRuntimeConfig(program2), name);
17083
+ });
16316
17084
  program2.command("compact").description("Plan or apply scoped memory hygiene for active personal memories").requiredOption("--project <name>", "Project/repo namespace to inspect").option("--apply", "Apply the compact plan; without this, prints a dry run").option("--dry-run", "Print the compact plan without changing anything").option("--kind <kind>", "durable, handoff, or incident", parseCompactKind).option("--topic <name>", "Stable topic name to inspect").action(async (options) => {
16317
17085
  await runCompact(getRuntimeConfig(program2), options);
16318
17086
  });
@@ -16322,8 +17090,13 @@ async function main() {
16322
17090
  program2.command("list").alias("ls").description("List a viking:// directory").argument("[uri]", "viking:// URI to list", "viking://").option("-a, --all", "Show hidden files such as .abstract.md and .overview.md").option("--dry-run", "Print ov command without listing").option("-n, --node-limit <count>", "Maximum number of nodes to list").option("-r, --recursive", "List subdirectories recursively").option("-s, --simple", "Print only paths").action(async (uri, options) => {
16323
17091
  await runList(getRuntimeConfig(program2), uri, options);
16324
17092
  });
16325
- program2.command("handoff").description("Capture current repo state as a durable cross-agent handoff memory").option("--blockers <text>", "Known blockers").option("--dry-run", "Print handoff without storing").option("--next-step <text>", "Suggested next step").option("--project <name>", "Project/repo namespace; defaults to current repo basename").option("--replace <uri>", "Supersede an existing viking:// memory after the new handoff is stored").option("--source-agent-client <name>", "codex, claude, cursor, copilot, or another client name", "codex").option("--task <text>", "Current task summary").option("--tests <text>", "Tests or checks run").option("--timestamped", "Store a historical timestamped handoff instead of updating the current branch handoff").option("--topic <name>", "Stable topic name; active handoffs with the same project/topic update one file").action(async (options) => {
16326
- await runHandoff(getRuntimeConfig(program2), options);
17093
+ program2.command("handoff").description("Capture current repo state as a durable cross-agent handoff memory").option("--blockers <text>", "Known blockers").option("--ci <text>", "Captured CI status snapshot (free text; not a live status board)").option("--dry-run", "Print handoff without storing").option("--issue <text>", "Related issue reference (number or URL)").option("--next-step <text>", "Suggested next step").option("--pr <text>", "Related pull request reference (number or URL)").option("--project <name>", "Project/repo namespace; defaults to current repo basename").option(
17094
+ "--reference <uri>",
17095
+ "viking:// memory to record as one-way read-only prior context; repeat for multiple",
17096
+ collectOption,
17097
+ []
17098
+ ).option("--replace <uri>", "Supersede an existing viking:// memory after the new handoff is stored").option("--source-agent-client <name>", "codex, claude, cursor, copilot, or another client name", "codex").option("--task <text>", "Current task summary").option("--tests <text>", "Tests or checks run").option("--timestamped", "Store a historical timestamped handoff instead of updating the current branch handoff").option("--topic <name>", "Stable topic name; active handoffs with the same project/topic update one file").action(async (options) => {
17099
+ await runHandoff(getRuntimeConfig(program2), { ...options, references: options.reference });
16327
17100
  });
16328
17101
  program2.command("archive").description("Move a memory into the archived lifecycle tree, then remove the original after the archive is stored").argument("<uri>", "viking:// memory URI to archive").option("--dry-run", "Print archive content and ov commands without changing anything").option("--kind <kind>", "durable, handoff, incident, preference, or smoke", parseMemoryKind).option("--project <name>", "Override inferred project/repo namespace").option("--topic <name>", "Override inferred topic").action(async (uri, options) => {
16329
17102
  await runArchive(getRuntimeConfig(program2), uri, options);