@basou/core 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -102,6 +102,14 @@ function sumDurations(intervals) {
102
102
  return total;
103
103
  }
104
104
 
105
+ // src/adapters/session-label.ts
106
+ function sessionLabelDateSpan(startIso, endIso) {
107
+ const a = startIso.slice(0, 10);
108
+ const b = endIso.slice(0, 10);
109
+ if (a === b) return a;
110
+ return a < b ? `${a}..${b}` : `${b}..${a}`;
111
+ }
112
+
105
113
  // src/adapters/claude-code/transcript-importer.ts
106
114
  var CLAUDE_IMPORT_SOURCE = "claude-code-import";
107
115
  function claudeTranscriptToImportPayload(records, options) {
@@ -201,8 +209,7 @@ function claudeTranscriptToImportPayload(records, options) {
201
209
  const externalId = options.externalId ?? claudeSessionId;
202
210
  const commandCount = derived.reduce((n, e) => e.type === "command_executed" ? n + 1 : n, 0);
203
211
  const fileCount = relatedFiles.size;
204
- const date = minTs.slice(0, 10);
205
- const label = `claude-code ${date}: ${commandCount} ${commandCount === 1 ? "command" : "commands"}, ${fileCount} ${fileCount === 1 ? "file" : "files"}`;
212
+ const label = `claude-code ${sessionLabelDateSpan(minTs, maxTs)}: ${commandCount} ${commandCount === 1 ? "command" : "commands"}, ${fileCount} ${fileCount === 1 ? "file" : "files"}`;
206
213
  const active = engagementTsMs.length >= 2 ? activeTimeFromTimestamps(engagementTsMs, ACTIVE_GAP_CAP_MS) : void 0;
207
214
  const metricsFields = {
208
215
  ...outputTokens > 0 ? { output_tokens: outputTokens } : {},
@@ -414,8 +421,7 @@ function codexRolloutToImportPayload(records, options) {
414
421
  ];
415
422
  const externalId = options.externalId ?? codexSessionId;
416
423
  const commandCount = derived.length;
417
- const date = minTs.slice(0, 10);
418
- const label = `codex ${date}: ${commandCount} ${commandCount === 1 ? "command" : "commands"}`;
424
+ const label = `codex ${sessionLabelDateSpan(minTs, maxTs)}: ${commandCount} ${commandCount === 1 ? "command" : "commands"}`;
419
425
  const minTsMs = Date.parse(minTs);
420
426
  const turnIntervals = [];
421
427
  let machineActiveMs = 0;
@@ -905,7 +911,12 @@ var TaskArchivedEventSchema = BaseEventSchema.extend({
905
911
  }).strict();
906
912
  var NoteAddedEventSchema = BaseEventSchema.extend({
907
913
  type: z3.literal("note_added"),
908
- body: z3.string()
914
+ body: z3.string(),
915
+ // `next_step` marks a note authored by `basou note` as the operator's resume
916
+ // hint, which orientation surfaces as the next starting point. Absent (the
917
+ // `basou session note` default) is a plain annotation orientation does not
918
+ // surface. Optional so pre-existing note_added events remain valid.
919
+ kind: z3.enum(["note", "next_step"]).optional()
909
920
  });
910
921
  var AdapterOutputEventSchema = BaseEventSchema.extend({
911
922
  type: z3.literal("adapter_output"),
@@ -4154,42 +4165,64 @@ import { lstat as lstat3 } from "fs/promises";
4154
4165
 
4155
4166
  // src/schemas/manifest.schema.ts
4156
4167
  import { z as z9 } from "zod";
4157
- var ProjectSchema = z9.object({
4168
+ var ProjectSchema = z9.looseObject({
4158
4169
  name: z9.string().optional(),
4159
4170
  description: z9.string().optional(),
4160
4171
  repository_url: z9.string().nullable().optional()
4161
4172
  });
4162
- var CapabilitiesSchema = z9.object({
4173
+ var CapabilitiesSchema = z9.looseObject({
4163
4174
  enabled: z9.array(z9.string())
4164
4175
  });
4165
- var ApprovalConfigSchema = z9.object({
4176
+ var ApprovalConfigSchema = z9.looseObject({
4166
4177
  required_for: z9.array(z9.string()).optional(),
4167
4178
  default_risk_level: z9.enum(["low", "medium", "high", "critical"])
4168
4179
  });
4169
- var ClaudeCodeAdapterConfigSchema = z9.object({
4180
+ var ClaudeCodeAdapterConfigSchema = z9.looseObject({
4170
4181
  enabled: z9.boolean(),
4171
4182
  config_path: z9.string().optional()
4172
4183
  });
4173
- var AdaptersSchema = z9.object({
4184
+ var AdaptersSchema = z9.looseObject({
4174
4185
  "claude-code": ClaudeCodeAdapterConfigSchema
4175
4186
  });
4176
- var GitConfigSchema = z9.object({
4187
+ var GitConfigSchema = z9.looseObject({
4177
4188
  events_log: z9.enum(["ignore", "commit"]).default("ignore")
4178
4189
  });
4179
- var SOURCE_ROOT_PATTERN = /^(?![~/\\])(?![A-Za-z]:)[^\0\\]+$/;
4190
+ var SOURCE_ROOT_PATTERN = /^(?![~/\\])(?![A-Za-z]:)(?!\s)[^\0\\]*[^\0\\\s]$/;
4180
4191
  var SourceRootSchema = z9.string().min(1).regex(SOURCE_ROOT_PATTERN, {
4181
4192
  message: "source_roots entries must be relative paths (no absolute path, '~', '\\', or null byte)"
4182
4193
  });
4183
- var ImportConfigSchema = z9.object({
4194
+ var ImportConfigSchema = z9.looseObject({
4184
4195
  source_roots: z9.array(SourceRootSchema).min(1).optional()
4185
4196
  });
4186
- var WorkspaceMetaSchema = z9.object({
4197
+ var RepoVisibilitySchema = z9.enum(["public", "private", "future-public"]);
4198
+ var RepoLanguageSchema = z9.enum(["en", "ja", "en+ja"]);
4199
+ var PublishKindSchema = z9.enum(["web", "npm"]);
4200
+ var PublishTargetSchema = z9.looseObject({
4201
+ kind: PublishKindSchema,
4202
+ visibility: RepoVisibilitySchema.optional(),
4203
+ language: RepoLanguageSchema.optional()
4204
+ });
4205
+ var RepoEntrySchema = z9.looseObject({
4206
+ path: SourceRootSchema,
4207
+ visibility: RepoVisibilitySchema.optional(),
4208
+ language: RepoLanguageSchema.optional(),
4209
+ publishes: z9.array(PublishTargetSchema).optional()
4210
+ });
4211
+ var WorkspaceMetaSchema = z9.looseObject({
4187
4212
  id: WorkspaceIdSchema,
4188
4213
  name: z9.string().min(1),
4189
4214
  created_at: IsoTimestampSchema,
4190
- updated_at: IsoTimestampSchema
4215
+ updated_at: IsoTimestampSchema,
4216
+ /**
4217
+ * The generated workspace view: a throwaway directory that aggregates the
4218
+ * roster repos via symlinks (one `<repo-basename>` symlink per repo). A path
4219
+ * relative to the manifest root, reusing the machine-portable source-root
4220
+ * constraint. Absent for a solo project (no view needed); `basou project
4221
+ * workspace` reconciles the view's symlinks to the declared roster.
4222
+ */
4223
+ view: SourceRootSchema.optional()
4191
4224
  });
4192
- var ManifestSchema = z9.object({
4225
+ var ManifestSchema = z9.looseObject({
4193
4226
  schema_version: SchemaVersionSchema,
4194
4227
  basou_version: z9.literal("0.1.0"),
4195
4228
  workspace: WorkspaceMetaSchema,
@@ -4198,8 +4231,13 @@ var ManifestSchema = z9.object({
4198
4231
  approval: ApprovalConfigSchema,
4199
4232
  adapters: AdaptersSchema,
4200
4233
  git: GitConfigSchema,
4201
- import: ImportConfigSchema.optional()
4234
+ import: ImportConfigSchema.optional(),
4235
+ repos: z9.array(RepoEntrySchema).min(1).optional()
4202
4236
  });
4237
+ var KNOWN_TOP_LEVEL_KEYS = new Set(Object.keys(ManifestSchema.shape));
4238
+ function unknownManifestKeys(manifest) {
4239
+ return Object.keys(manifest).filter((k) => !KNOWN_TOP_LEVEL_KEYS.has(k)).sort();
4240
+ }
4203
4241
 
4204
4242
  // src/storage/manifest.ts
4205
4243
  function createManifest(input) {
@@ -4267,6 +4305,7 @@ function hasErrorCode3(error) {
4267
4305
  }
4268
4306
 
4269
4307
  // src/orientation/orientation-renderer.ts
4308
+ var DECISION_TRAILING_ACTIVITY_GAP_MS = 60 * 60 * 1e3;
4270
4309
  async function summarizeOrientation(input) {
4271
4310
  const limit = input.relatedFilesLimit ?? 10;
4272
4311
  const now = new Date(input.nowIso);
@@ -4275,8 +4314,17 @@ async function summarizeOrientation(input) {
4275
4314
  if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
4276
4315
  const entries = await loadSessionEntries(input.paths, loadOpts);
4277
4316
  const decisions = [];
4317
+ let latestActivityAt = null;
4318
+ let latestNote = null;
4319
+ const noteActivity = (iso) => {
4320
+ if (latestActivityAt === null || Date.parse(iso) > Date.parse(latestActivityAt)) {
4321
+ latestActivityAt = iso;
4322
+ }
4323
+ };
4278
4324
  for (const entry of entries) {
4279
4325
  const sessionDir = join14(input.paths.sessions, entry.sessionId);
4326
+ const counted = entry.session.session.status !== "archived";
4327
+ if (counted) noteActivity(entry.session.session.ended_at ?? entry.session.session.started_at);
4280
4328
  try {
4281
4329
  for await (const ev of replayEvents(sessionDir, {
4282
4330
  onWarning: (w) => input.onWarning?.(w, entry.sessionId)
@@ -4288,6 +4336,12 @@ async function summarizeOrientation(input) {
4288
4336
  occurredAt: ev.occurred_at
4289
4337
  });
4290
4338
  }
4339
+ if (counted && ev.type === "note_added" && ev.kind === "next_step") {
4340
+ if (latestNote === null || Date.parse(ev.occurred_at) > Date.parse(latestNote.occurredAt)) {
4341
+ latestNote = { body: ev.body, sessionId: entry.sessionId, occurredAt: ev.occurred_at };
4342
+ }
4343
+ }
4344
+ if (counted) noteActivity(ev.occurred_at);
4291
4345
  }
4292
4346
  } catch {
4293
4347
  input.onSessionSkip?.(entry.sessionId, "events_jsonl_unreadable");
@@ -4367,6 +4421,7 @@ async function summarizeOrientation(input) {
4367
4421
  latestSession,
4368
4422
  latestDecision: latestDecision ?? null,
4369
4423
  decisionCount: decisions.length,
4424
+ latestNote,
4370
4425
  relatedFiles: { displayed, overflow },
4371
4426
  inFlightTasks,
4372
4427
  plannedTasks,
@@ -4375,6 +4430,7 @@ async function summarizeOrientation(input) {
4375
4430
  freshness: {
4376
4431
  newestStartedAt: newest?.session.session.started_at ?? null,
4377
4432
  newestSource: newest?.session.session.source.kind ?? null,
4433
+ latestActivityAt,
4378
4434
  bySource,
4379
4435
  sourceRoots
4380
4436
  }
@@ -4418,9 +4474,16 @@ function formatOrientationBody(summary, opts) {
4418
4474
  lines.push("- \u6700\u7D42 session: (no live sessions)");
4419
4475
  }
4420
4476
  if (summary.latestDecision !== null) {
4477
+ const decAge = relativeAgeJa(summary.latestDecision.occurredAt, now);
4421
4478
  lines.push(
4422
- `- \u76F4\u8FD1\u306E\u5224\u65AD: ${summary.latestDecision.title} [${shortId(summary.latestDecision.decisionId)}]`
4479
+ `- \u76F4\u8FD1\u306E\u5224\u65AD: ${summary.latestDecision.title} [${shortId(summary.latestDecision.decisionId)}] (${decAge})`
4423
4480
  );
4481
+ const activityAt = summary.freshness.latestActivityAt;
4482
+ if (activityAt !== null && Date.parse(activityAt) - Date.parse(summary.latestDecision.occurredAt) > DECISION_TRAILING_ACTIVITY_GAP_MS) {
4483
+ lines.push(
4484
+ ` - \u6CE8: \u3053\u308C\u306F\u6700\u5F8C\u306B\u300C\u8A18\u9332\u3055\u308C\u305F\u300D\u5224\u65AD\u3067\u3059\u3002\u6700\u7D42\u6D3B\u52D5 (${relativeAgeJa(activityAt, now)}) \u306F\u3053\u308C\u3088\u308A\u5F8C\u306E\u305F\u3081\u3001\u73FE\u5728\u306E\u65B9\u91DD\u304C\u53CD\u6620\u3055\u308C\u3066\u3044\u306A\u3044\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059(\u4F1A\u8A71\u3067\u306E\u610F\u601D\u6C7A\u5B9A\u306F\u81EA\u52D5\u8A18\u9332\u3055\u308C\u307E\u305B\u3093)\u3002`
4485
+ );
4486
+ }
4424
4487
  if (summary.decisionCount > 1) {
4425
4488
  lines.push(` - ${summary.decisionCount} decisions total \u2014 see decisions.md`);
4426
4489
  }
@@ -4470,15 +4533,26 @@ function formatOrientationBody(summary, opts) {
4470
4533
  lines.push("");
4471
4534
  lines.push("## \u3069\u3053\u3078\u5411\u304B\u3046");
4472
4535
  lines.push("");
4473
- if (summary.plannedTasks.length === 0) {
4536
+ if (summary.latestNote !== null) {
4537
+ const noteAge = relativeAgeJa(summary.latestNote.occurredAt, now);
4538
+ lines.push(
4539
+ `- \u6B21\u306E\u8D77\u70B9 (\u8A18\u9332\u6E08\u307F, ${noteAge}): ${noteSummary(summary.latestNote.body)} [session ${shortId(summary.latestNote.sessionId)}]`
4540
+ );
4541
+ const activityAt = summary.freshness.latestActivityAt;
4542
+ if (activityAt !== null && Date.parse(activityAt) - Date.parse(summary.latestNote.occurredAt) > DECISION_TRAILING_ACTIVITY_GAP_MS) {
4543
+ lines.push(
4544
+ ` - \u6CE8: \u3053\u306E\u8D77\u70B9\u306E\u8A18\u9332\u5F8C (\u6700\u7D42\u6D3B\u52D5 ${relativeAgeJa(activityAt, now)}) \u3082\u4F5C\u696D\u304C\u7D9A\u3044\u3066\u3044\u307E\u3059\u3002\u518D\u958B\u70B9\u304C\u53E4\u3044\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002`
4545
+ );
4546
+ }
4547
+ }
4548
+ for (const t of summary.plannedTasks) {
4549
+ lines.push(`- ${t.title} [${shortId(t.id)}]`);
4550
+ }
4551
+ if (summary.latestNote === null && summary.plannedTasks.length === 0) {
4474
4552
  lines.push("- (no planned tasks \u2014 direction is inferred from recent decisions)");
4475
4553
  if (summary.latestDecision !== null) {
4476
4554
  lines.push(` - \u76F4\u8FD1\u306E\u5224\u65AD: ${summary.latestDecision.title}`);
4477
4555
  }
4478
- } else {
4479
- for (const t of summary.plannedTasks) {
4480
- lines.push(`- ${t.title} [${shortId(t.id)}]`);
4481
- }
4482
4556
  }
4483
4557
  lines.push("");
4484
4558
  lines.push("## \u3053\u308C\u306F\u6700\u65B0\u304B");
@@ -4492,6 +4566,11 @@ function formatOrientationBody(summary, opts) {
4492
4566
  } else {
4493
4567
  lines.push("- newest captured session: (no sessions captured yet)");
4494
4568
  }
4569
+ if (summary.freshness.latestActivityAt !== null) {
4570
+ lines.push(
4571
+ `- latest activity: ${summary.freshness.latestActivityAt} (${relativeAge(summary.freshness.latestActivityAt, now)})`
4572
+ );
4573
+ }
4495
4574
  const sourceBreakdown = summary.freshness.bySource.map(({ kind, count }) => `${kind} ${count}`).join(", ");
4496
4575
  lines.push(
4497
4576
  `- sessions: ${summary.sessionCount}${sourceBreakdown !== "" ? ` (${sourceBreakdown})` : ""}`
@@ -4549,14 +4628,22 @@ function freshnessVerdict(summary, staleness, now) {
4549
4628
  const rel = relativeAgeJa(summary.freshness.newestStartedAt, now);
4550
4629
  const tool = toolDisplayName(summary.freshness.newestSource);
4551
4630
  const suspectCount = summary.suspects.length;
4552
- const suspectClause = suspectCount > 0 ? `\u8981\u6CE8\u610F\u30BB\u30C3\u30B7\u30E7\u30F3\u304C ${suspectCount} \u4EF6\u3042\u308A\u307E\u3059\u3002` : "\u53D6\u308A\u3053\u307C\u3057\u30FB\u8981\u6CE8\u610F\u306A\u3057\u3002";
4553
4631
  if (staleness === null) {
4554
4632
  return [
4555
4633
  `\u2139\uFE0F \u53D6\u308A\u8FBC\u307F\u6E08\u307F\u306E\u72B6\u614B\u3092\u8868\u793A\u3057\u3066\u3044\u307E\u3059\u3002\u6700\u5F8C\u306E\u4F5C\u696D\u306F ${rel}(${tool})\u3002`,
4556
4634
  "\u6700\u65B0\u304B\u78BA\u8A8D\u3059\u308B\u306B\u306F `basou refresh` \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
4557
4635
  ];
4558
4636
  }
4559
- return [`\u2705 \u6700\u65B0\u3067\u3059\u3002\u6700\u5F8C\u306E\u4F5C\u696D\u306F ${rel}(${tool})\u3002${suspectClause}`];
4637
+ const lines = [
4638
+ `\u2705 \u53D6\u308A\u8FBC\u307F\u306F\u6700\u65B0\u3067\u3059\u3002\u6700\u5F8C\u306E\u4F5C\u696D\u306F ${rel}(${tool})\u3002\u672A\u53D6\u308A\u8FBC\u307F\u306E native \u30BB\u30C3\u30B7\u30E7\u30F3\u306F\u3042\u308A\u307E\u305B\u3093\u3002`
4639
+ ];
4640
+ if (suspectCount > 0) {
4641
+ lines.push(`\u305F\u3060\u3057\u8981\u6CE8\u610F\u30BB\u30C3\u30B7\u30E7\u30F3\u304C ${suspectCount} \u4EF6\u3042\u308A\u307E\u3059(\u4E0A\u8A18\u300C\u8981\u6CE8\u610F session\u300D\u53C2\u7167)\u3002`);
4642
+ }
4643
+ lines.push(
4644
+ "\u6CE8: \u3053\u306E\u5224\u5B9A\u306F\u53D6\u308A\u8FBC\u307F\u6E08\u307F native \u30BB\u30C3\u30B7\u30E7\u30F3\u306E\u9BAE\u5EA6\u3068 suspect \u306E\u6709\u7121\u3060\u3051\u3092\u898B\u307E\u3059\u3002\u8A08\u753B\u2194\u5B9F\u88C5\u306E\u30C9\u30EA\u30D5\u30C8\u3084\u672A\u8A18\u9332\u306E\u610F\u601D\u6C7A\u5B9A\u307E\u3067\u306F\u691C\u77E5\u3057\u307E\u305B\u3093\u3002"
4645
+ );
4646
+ return lines;
4560
4647
  }
4561
4648
  function relativeAgeJa(startedAt, now) {
4562
4649
  if (startedAt === null) return "(\u4E0D\u660E)";
@@ -4579,6 +4666,11 @@ function relativeAge(startedAt, now) {
4579
4666
  if (ms < 1e3) return "just now";
4580
4667
  return `${formatDurationMs(ms)} ago`;
4581
4668
  }
4669
+ var NOTE_SUMMARY_MAX = 200;
4670
+ function noteSummary(body) {
4671
+ const oneLine = body.replace(/\s+/g, " ").trim();
4672
+ return oneLine.length > NOTE_SUMMARY_MAX ? `${oneLine.slice(0, NOTE_SUMMARY_MAX - 1)}\u2026` : oneLine;
4673
+ }
4582
4674
  function suspectText(reason) {
4583
4675
  if (reason === "events_say_ended_but_yaml_running") return "ended (yaml stale)";
4584
4676
  if (reason === "running_no_end_event") return "no end event";
@@ -4590,6 +4682,581 @@ function shortId(id) {
4590
4682
  return id.slice(0, sep + 1) + id.slice(sep + 1, sep + 1 + 10);
4591
4683
  }
4592
4684
 
4685
+ // src/project/relative-path.ts
4686
+ function normalizeRelativePath(p) {
4687
+ const trimmed = p.trim();
4688
+ const absolute = trimmed.startsWith("/");
4689
+ const out = [];
4690
+ for (const seg of trimmed.split("/")) {
4691
+ if (seg === "" || seg === ".") continue;
4692
+ if (seg === "..") {
4693
+ const top = out[out.length - 1];
4694
+ if (top !== void 0 && top !== "..") {
4695
+ out.pop();
4696
+ } else if (!absolute) {
4697
+ out.push("..");
4698
+ }
4699
+ continue;
4700
+ }
4701
+ out.push(seg);
4702
+ }
4703
+ const joined = out.join("/");
4704
+ if (absolute) return `/${joined}`;
4705
+ return joined.length === 0 ? "." : joined;
4706
+ }
4707
+
4708
+ // src/project/archive.ts
4709
+ function planArchive(input) {
4710
+ const target = normalizeRelativePath(input.target);
4711
+ const repos = input.repos ?? [];
4712
+ const isAnchor = input.targetIsAnchor === true || target === ".";
4713
+ const matched = repos.filter((r) => normalizeRelativePath(r.path) === target);
4714
+ const found = matched.length > 0;
4715
+ if (isAnchor || !found) {
4716
+ return {
4717
+ target,
4718
+ found,
4719
+ isAnchor,
4720
+ nextRepos: repos,
4721
+ reposEmptied: false,
4722
+ remainingCount: repos.length,
4723
+ becomesSolo: false
4724
+ };
4725
+ }
4726
+ const nextRepos = repos.filter((r) => normalizeRelativePath(r.path) !== target);
4727
+ const remainingCount = nextRepos.length;
4728
+ let sourceRootRemoval;
4729
+ let nextSourceRoots;
4730
+ if (input.sourceRoots !== void 0) {
4731
+ const pruned = input.sourceRoots.filter((s) => normalizeRelativePath(s) !== target);
4732
+ if (pruned.length !== input.sourceRoots.length) {
4733
+ sourceRootRemoval = target;
4734
+ nextSourceRoots = pruned;
4735
+ }
4736
+ }
4737
+ return {
4738
+ target,
4739
+ found: true,
4740
+ isAnchor: false,
4741
+ rosterEntry: matched[matched.length - 1],
4742
+ nextRepos,
4743
+ reposEmptied: remainingCount === 0,
4744
+ ...sourceRootRemoval !== void 0 ? { sourceRootRemoval } : {},
4745
+ ...nextSourceRoots !== void 0 ? { nextSourceRoots } : {},
4746
+ remainingCount,
4747
+ becomesSolo: remainingCount === 1
4748
+ };
4749
+ }
4750
+
4751
+ // src/project/gitignore-plan.ts
4752
+ function isPublicFacing(v) {
4753
+ return v === "public" || v === "future-public";
4754
+ }
4755
+ function planGitignore(input) {
4756
+ const plans = [];
4757
+ const unknown = [];
4758
+ const unreachable = [];
4759
+ for (const repo of input.repos) {
4760
+ if (!repo.reachable) {
4761
+ unreachable.push(repo.path);
4762
+ continue;
4763
+ }
4764
+ if (repo.visibility === void 0) {
4765
+ unknown.push(repo.path);
4766
+ continue;
4767
+ }
4768
+ if (!isPublicFacing(repo.visibility)) continue;
4769
+ const present = /* @__PURE__ */ new Set();
4770
+ for (const line of repo.currentLines) {
4771
+ const trimmed = line.trim();
4772
+ present.add(trimmed);
4773
+ if (trimmed.startsWith("/")) present.add(trimmed.slice(1));
4774
+ }
4775
+ const toAdd = input.required.filter((p) => !present.has(p));
4776
+ if (toAdd.length > 0) plans.push({ path: repo.path, toAdd });
4777
+ }
4778
+ return {
4779
+ plans,
4780
+ unknown,
4781
+ unreachable,
4782
+ ok: plans.length === 0 && unknown.length === 0 && unreachable.length === 0
4783
+ };
4784
+ }
4785
+
4786
+ // src/project/preset.ts
4787
+ function visibilityLabel(v) {
4788
+ switch (v) {
4789
+ case "public":
4790
+ return "public(git \u5C65\u6B74\u306F\u516C\u958B)";
4791
+ case "private":
4792
+ return "private(git \u5C65\u6B74\u306F\u975E\u516C\u958B)";
4793
+ case "future-public":
4794
+ return "future-public(\u73FE\u5728\u306F\u975E\u516C\u958B\u30FB\u5C06\u6765\u516C\u958B\u4E88\u5B9A)";
4795
+ default:
4796
+ return "\u672A\u8A2D\u5B9A";
4797
+ }
4798
+ }
4799
+ function sourceLanguageLabel(l) {
4800
+ switch (l) {
4801
+ case "en":
4802
+ return "en(commit\u30FB\u30B3\u30E1\u30F3\u30C8\u30FB\u30B3\u30FC\u30C9\u306F\u82F1\u8A9E)";
4803
+ case "ja":
4804
+ return "ja(commit\u30FB\u30B3\u30E1\u30F3\u30C8\u30FB\u30B3\u30FC\u30C9\u306F\u65E5\u672C\u8A9E)";
4805
+ case "en+ja":
4806
+ return "en+ja(commit\u30FB\u30B3\u30E1\u30F3\u30C8\u30FB\u30B3\u30FC\u30C9\u306F\u65E5\u82F1)";
4807
+ default:
4808
+ return "\u672A\u8A2D\u5B9A";
4809
+ }
4810
+ }
4811
+ function publishKindLabel(k) {
4812
+ return k === "web" ? "web(\u30C7\u30D7\u30ED\u30A4)" : "npm(\u30D1\u30C3\u30B1\u30FC\u30B8)";
4813
+ }
4814
+ function publishVisibilityLabel(v) {
4815
+ switch (v) {
4816
+ case "public":
4817
+ return "\u516C\u958B";
4818
+ case "private":
4819
+ return "\u975E\u516C\u958B";
4820
+ case "future-public":
4821
+ return "\u5C06\u6765\u516C\u958B";
4822
+ default:
4823
+ return "\u53EF\u8996\u6027\u672A\u8A2D\u5B9A";
4824
+ }
4825
+ }
4826
+ function contentLanguageLabel(l) {
4827
+ return l ?? "\u8A00\u8A9E\u672A\u8A2D\u5B9A";
4828
+ }
4829
+ function isRenderable(repo) {
4830
+ return repo.visibility !== void 0 || repo.language !== void 0 || repo.publishes !== void 0 && repo.publishes.length > 0;
4831
+ }
4832
+ function renderPresetBlock(repo) {
4833
+ const lines = [];
4834
+ lines.push("## \u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u69CB\u6210(basou \u304C\u751F\u6210 \u2014 manifest \u304C\u6B63\u672C)");
4835
+ lines.push("");
4836
+ lines.push(
4837
+ "\u3053\u306E\u30BB\u30AF\u30B7\u30E7\u30F3\u306F `.basou/manifest.yaml` \u306E\u5BA3\u8A00\u304B\u3089 `basou project preset` \u304C\u751F\u6210\u3057\u307E\u3059\u3002\u7DE8\u96C6\u306F manifest \u5074\u3067\u884C\u3063\u3066\u304F\u3060\u3055\u3044(\u30DE\u30FC\u30AB\u30FC\u5916\u306E\u8A18\u8FF0\u306F\u4FDD\u6301\u3055\u308C\u307E\u3059)\u3002"
4838
+ );
4839
+ lines.push("");
4840
+ lines.push(`- \u30BD\u30FC\u30B9\u53EF\u8996\u6027: ${visibilityLabel(repo.visibility)}`);
4841
+ lines.push(`- \u30BD\u30FC\u30B9\u8A00\u8A9E: ${sourceLanguageLabel(repo.language)}`);
4842
+ const publishes = repo.publishes ?? [];
4843
+ if (publishes.length === 0) {
4844
+ lines.push("- \u914D\u4FE1\u7269: \u306A\u3057");
4845
+ } else {
4846
+ lines.push("- \u914D\u4FE1\u7269:");
4847
+ for (const p of publishes) {
4848
+ lines.push(
4849
+ ` - ${publishKindLabel(p.kind)} \u2014 ${publishVisibilityLabel(p.visibility)} / ${contentLanguageLabel(p.language)}`
4850
+ );
4851
+ }
4852
+ }
4853
+ return lines.join("\n");
4854
+ }
4855
+ function normalizeBlock(s) {
4856
+ return s.replace(/\r\n/g, "\n").replace(/\n+$/, "");
4857
+ }
4858
+ function summarizePresetPlan(facts) {
4859
+ const deduped = [];
4860
+ const seenPath = /* @__PURE__ */ new Set();
4861
+ for (const f of facts) {
4862
+ const key = normalizeRelativePath(f.path);
4863
+ if (seenPath.has(key)) continue;
4864
+ seenPath.add(key);
4865
+ deduped.push(f);
4866
+ }
4867
+ const byCanonical = /* @__PURE__ */ new Map();
4868
+ for (const f of deduped) {
4869
+ if (f.isAnchor || !f.reachable || f.canonicalName === void 0 || !isRenderable(f)) continue;
4870
+ const repos = byCanonical.get(f.canonicalName) ?? [];
4871
+ repos.push(f.path);
4872
+ byCanonical.set(f.canonicalName, repos);
4873
+ }
4874
+ const collisions = [];
4875
+ const collidingPaths = /* @__PURE__ */ new Set();
4876
+ for (const [canonicalName, repos] of byCanonical) {
4877
+ if (repos.length > 1) {
4878
+ collisions.push({ canonicalName, repos });
4879
+ for (const r of repos) collidingPaths.add(r);
4880
+ }
4881
+ }
4882
+ const plans = [];
4883
+ const inSync = [];
4884
+ const undeclared = [];
4885
+ const markerConflicts = [];
4886
+ const unreadable = [];
4887
+ const anchors = [];
4888
+ const unreachable = [];
4889
+ for (const f of deduped) {
4890
+ if (f.isAnchor) {
4891
+ anchors.push(f.path);
4892
+ continue;
4893
+ }
4894
+ if (!f.reachable) {
4895
+ unreachable.push(f.path);
4896
+ continue;
4897
+ }
4898
+ if (!isRenderable(f)) {
4899
+ undeclared.push(f.path);
4900
+ continue;
4901
+ }
4902
+ if (collidingPaths.has(f.path)) continue;
4903
+ if (f.canonicalName === void 0) {
4904
+ unreachable.push(f.path);
4905
+ continue;
4906
+ }
4907
+ const desiredBlock = renderPresetBlock(f);
4908
+ if (!f.canonicalPresent) {
4909
+ plans.push({ path: f.path, canonicalName: f.canonicalName, action: "create", desiredBlock });
4910
+ continue;
4911
+ }
4912
+ if (f.canonicalReadable === false) {
4913
+ unreadable.push(f.path);
4914
+ continue;
4915
+ }
4916
+ if (f.markerKind === "ok") {
4917
+ if (normalizeBlock(f.currentBlock ?? "") === normalizeBlock(desiredBlock)) {
4918
+ inSync.push(f.path);
4919
+ } else {
4920
+ plans.push({
4921
+ path: f.path,
4922
+ canonicalName: f.canonicalName,
4923
+ action: "update",
4924
+ desiredBlock
4925
+ });
4926
+ }
4927
+ continue;
4928
+ }
4929
+ markerConflicts.push({ repo: f.path, reason: f.markerKind ?? "no_markers" });
4930
+ }
4931
+ return {
4932
+ plans,
4933
+ inSync,
4934
+ undeclared,
4935
+ markerConflicts,
4936
+ unreadable,
4937
+ collisions,
4938
+ anchors,
4939
+ unreachable,
4940
+ ok: plans.length === 0 && markerConflicts.length === 0 && unreadable.length === 0 && collisions.length === 0 && unreachable.length === 0 && undeclared.length === 0
4941
+ };
4942
+ }
4943
+
4944
+ // src/project/rename.ts
4945
+ function pathBasename(p) {
4946
+ const parts = normalizeRelativePath(p).split("/");
4947
+ return parts[parts.length - 1];
4948
+ }
4949
+ function dedupRepos(entries) {
4950
+ const seen = /* @__PURE__ */ new Set();
4951
+ const out = [];
4952
+ for (const e of entries) {
4953
+ const k = normalizeRelativePath(e.path);
4954
+ if (seen.has(k)) continue;
4955
+ seen.add(k);
4956
+ out.push(e);
4957
+ }
4958
+ return out;
4959
+ }
4960
+ function dedupNorm(items) {
4961
+ const seen = /* @__PURE__ */ new Set();
4962
+ const out = [];
4963
+ for (const s of items) {
4964
+ const k = normalizeRelativePath(s);
4965
+ if (seen.has(k)) continue;
4966
+ seen.add(k);
4967
+ out.push(s);
4968
+ }
4969
+ return out;
4970
+ }
4971
+ function planRename(input) {
4972
+ const oldTarget = normalizeRelativePath(input.oldPath);
4973
+ const newTarget = normalizeRelativePath(input.newPath);
4974
+ const repos = input.repos ?? [];
4975
+ const basenameChanged = pathBasename(oldTarget) !== pathBasename(newTarget);
4976
+ const noop = oldTarget === newTarget;
4977
+ const isAnchor = input.oldIsAnchor === true || oldTarget === ".";
4978
+ const found = repos.some((r) => normalizeRelativePath(r.path) === oldTarget);
4979
+ const collision = !noop && repos.some((r) => normalizeRelativePath(r.path) === newTarget);
4980
+ if (noop || isAnchor || !found || collision) {
4981
+ return {
4982
+ oldTarget,
4983
+ newTarget,
4984
+ noop,
4985
+ isAnchor,
4986
+ found,
4987
+ collision,
4988
+ nextRepos: repos,
4989
+ reposChanged: false,
4990
+ basenameChanged
4991
+ };
4992
+ }
4993
+ const rosterEntry = repos.find((r) => normalizeRelativePath(r.path) === oldTarget);
4994
+ const nextRepos = dedupRepos(
4995
+ repos.map((r) => normalizeRelativePath(r.path) === oldTarget ? { ...r, path: newTarget } : r)
4996
+ );
4997
+ let sourceRootRenamed;
4998
+ let nextSourceRoots;
4999
+ if (input.sourceRoots !== void 0 && input.sourceRoots.some((s) => normalizeRelativePath(s) === oldTarget)) {
5000
+ nextSourceRoots = dedupNorm(
5001
+ input.sourceRoots.map((s) => normalizeRelativePath(s) === oldTarget ? newTarget : s)
5002
+ );
5003
+ sourceRootRenamed = oldTarget;
5004
+ }
5005
+ return {
5006
+ oldTarget,
5007
+ newTarget,
5008
+ noop: false,
5009
+ isAnchor: false,
5010
+ found: true,
5011
+ collision: false,
5012
+ rosterEntry,
5013
+ nextRepos,
5014
+ reposChanged: true,
5015
+ ...sourceRootRenamed !== void 0 ? { sourceRootRenamed } : {},
5016
+ ...nextSourceRoots !== void 0 ? { nextSourceRoots } : {},
5017
+ basenameChanged
5018
+ };
5019
+ }
5020
+
5021
+ // src/project/roster.ts
5022
+ function summarizeRosterDrift(input) {
5023
+ const captured = new Set((input.sourceRoots ?? []).map(normalizeRelativePath));
5024
+ const declared = /* @__PURE__ */ new Map();
5025
+ for (const r of input.repos ?? []) declared.set(normalizeRelativePath(r.path), r);
5026
+ const gaps = [];
5027
+ const matched = [];
5028
+ for (const [norm, entry] of declared) {
5029
+ if (captured.has(norm)) matched.push(norm);
5030
+ else gaps.push(entry);
5031
+ }
5032
+ const extra = [...captured].filter((c) => !declared.has(c)).sort();
5033
+ return {
5034
+ declaredCount: declared.size,
5035
+ capturedCount: captured.size,
5036
+ gaps,
5037
+ extra,
5038
+ matched: matched.sort(),
5039
+ ok: gaps.length === 0
5040
+ };
5041
+ }
5042
+ function reconcileSourceRoots(input) {
5043
+ const current = input.sourceRoots ?? [];
5044
+ const seen = new Set(current.map(normalizeRelativePath));
5045
+ const added = [];
5046
+ for (const r of input.repos ?? []) {
5047
+ const norm = normalizeRelativePath(r.path);
5048
+ if (seen.has(norm)) continue;
5049
+ seen.add(norm);
5050
+ added.push(norm);
5051
+ }
5052
+ return {
5053
+ next: [...current, ...added],
5054
+ added,
5055
+ unchanged: added.length === 0
5056
+ };
5057
+ }
5058
+ function planRosterAdoption(candidates) {
5059
+ const repos = [];
5060
+ const excluded = [];
5061
+ const seen = /* @__PURE__ */ new Set();
5062
+ for (const c of candidates) {
5063
+ const norm = normalizeRelativePath(c.path);
5064
+ if (seen.has(norm)) continue;
5065
+ seen.add(norm);
5066
+ if (c.kind === "repo") repos.push({ path: c.path });
5067
+ else excluded.push({ path: c.path, kind: c.kind });
5068
+ }
5069
+ return { repos, excluded };
5070
+ }
5071
+
5072
+ // src/project/symlinks.ts
5073
+ function summarizeSymlinkPlan(facts) {
5074
+ const deduped = [];
5075
+ const seenPath = /* @__PURE__ */ new Set();
5076
+ for (const f of facts) {
5077
+ const key = normalizeRelativePath(f.path);
5078
+ if (seenPath.has(key)) continue;
5079
+ seenPath.add(key);
5080
+ deduped.push(f);
5081
+ }
5082
+ const byCanonical = /* @__PURE__ */ new Map();
5083
+ for (const f of deduped) {
5084
+ if (f.isAnchor || !f.reachable || !f.canonicalPresent || f.canonicalName === void 0) {
5085
+ continue;
5086
+ }
5087
+ const repos = byCanonical.get(f.canonicalName) ?? [];
5088
+ repos.push(f.path);
5089
+ byCanonical.set(f.canonicalName, repos);
5090
+ }
5091
+ const collisions = [];
5092
+ const collidingPaths = /* @__PURE__ */ new Set();
5093
+ for (const [canonicalName, repos] of byCanonical) {
5094
+ if (repos.length > 1) {
5095
+ collisions.push({ canonicalName, repos });
5096
+ for (const r of repos) collidingPaths.add(r);
5097
+ }
5098
+ }
5099
+ const plans = [];
5100
+ const conflicts = [];
5101
+ const missingCanonical = [];
5102
+ const unreachable = [];
5103
+ for (const f of deduped) {
5104
+ if (f.isAnchor) continue;
5105
+ if (!f.reachable) {
5106
+ unreachable.push(f.path);
5107
+ continue;
5108
+ }
5109
+ if (!f.canonicalPresent) {
5110
+ missingCanonical.push(f.path);
5111
+ continue;
5112
+ }
5113
+ if (collidingPaths.has(f.path)) continue;
5114
+ const toCreate = [];
5115
+ for (const file of f.files) {
5116
+ if (file.state === "missing") {
5117
+ toCreate.push({ name: file.name, target: file.expectedTarget });
5118
+ } else if (file.state === "mismatch") {
5119
+ conflicts.push({
5120
+ repo: f.path,
5121
+ file: file.name,
5122
+ reason: "mismatch",
5123
+ ...file.actualTarget !== void 0 ? { actualTarget: file.actualTarget } : {}
5124
+ });
5125
+ } else if (file.state === "occupied") {
5126
+ conflicts.push({ repo: f.path, file: file.name, reason: "occupied" });
5127
+ } else if (file.state === "blocked") {
5128
+ conflicts.push({ repo: f.path, file: file.name, reason: "blocked" });
5129
+ }
5130
+ }
5131
+ if (toCreate.length > 0) plans.push({ path: f.path, toCreate });
5132
+ }
5133
+ return {
5134
+ plans,
5135
+ conflicts,
5136
+ missingCanonical,
5137
+ unreachable,
5138
+ collisions,
5139
+ ok: plans.length === 0 && conflicts.length === 0 && missingCanonical.length === 0 && unreachable.length === 0 && collisions.length === 0
5140
+ };
5141
+ }
5142
+
5143
+ // src/project/wiring.ts
5144
+ function isPublicFacing2(v) {
5145
+ return v === "public" || v === "future-public";
5146
+ }
5147
+ function summarizeWiring(facts) {
5148
+ const risks = [];
5149
+ const unknown = [];
5150
+ const incomplete = [];
5151
+ const unreachable = [];
5152
+ for (const f of facts) {
5153
+ if (!f.reachable) {
5154
+ unreachable.push(f.path);
5155
+ continue;
5156
+ }
5157
+ if (isPublicFacing2(f.visibility)) {
5158
+ for (const file of f.instructionFiles) {
5159
+ if (file.tracked) risks.push({ repo: f.path, visibility: f.visibility, file: file.name });
5160
+ }
5161
+ } else if (f.visibility === void 0) {
5162
+ unknown.push(f.path);
5163
+ }
5164
+ const missing = f.instructionFiles.filter((file) => !file.present).map((file) => file.name);
5165
+ if (missing.length > 0) incomplete.push({ repo: f.path, missing });
5166
+ }
5167
+ return {
5168
+ repos: facts,
5169
+ risks,
5170
+ unknown,
5171
+ incomplete,
5172
+ unreachable,
5173
+ ok: risks.length === 0 && unknown.length === 0 && unreachable.length === 0
5174
+ };
5175
+ }
5176
+
5177
+ // src/project/workspace-view.ts
5178
+ function planWorkspaceView(facts, existing = [], rosterNames = []) {
5179
+ const deduped = [];
5180
+ const seenPath = /* @__PURE__ */ new Set();
5181
+ for (const f of facts) {
5182
+ const key = normalizeRelativePath(f.path);
5183
+ if (seenPath.has(key)) continue;
5184
+ seenPath.add(key);
5185
+ deduped.push(f);
5186
+ }
5187
+ const byLinkName = /* @__PURE__ */ new Map();
5188
+ for (const f of deduped) {
5189
+ if (!f.reachable || f.linkName === void 0) continue;
5190
+ const repos = byLinkName.get(f.linkName) ?? [];
5191
+ repos.push(f.path);
5192
+ byLinkName.set(f.linkName, repos);
5193
+ }
5194
+ const collisions = [];
5195
+ const collidingPaths = /* @__PURE__ */ new Set();
5196
+ for (const [linkName, repos] of byLinkName) {
5197
+ if (repos.length > 1) {
5198
+ collisions.push({ linkName, repos });
5199
+ for (const r of repos) collidingPaths.add(r);
5200
+ }
5201
+ }
5202
+ const toCreate = [];
5203
+ const conflicts = [];
5204
+ const unreachable = [];
5205
+ let correctCount = 0;
5206
+ for (const f of deduped) {
5207
+ if (!f.reachable) {
5208
+ unreachable.push(f.path);
5209
+ continue;
5210
+ }
5211
+ if (collidingPaths.has(f.path)) continue;
5212
+ if (f.linkName === void 0 || f.expectedTarget === void 0 || f.state === void 0) {
5213
+ continue;
5214
+ }
5215
+ if (f.state === "missing") {
5216
+ toCreate.push({ name: f.linkName, target: f.expectedTarget });
5217
+ } else if (f.state === "mismatch") {
5218
+ conflicts.push({
5219
+ name: f.linkName,
5220
+ reason: "mismatch",
5221
+ ...f.actualTarget !== void 0 ? { actualTarget: f.actualTarget } : {}
5222
+ });
5223
+ } else if (f.state === "occupied") {
5224
+ conflicts.push({ name: f.linkName, reason: "occupied" });
5225
+ } else if (f.state === "blocked") {
5226
+ conflicts.push({ name: f.linkName, reason: "blocked" });
5227
+ } else {
5228
+ correctCount += 1;
5229
+ }
5230
+ }
5231
+ const ownedNames = new Set(rosterNames);
5232
+ for (const f of deduped) {
5233
+ if (f.reachable && f.linkName !== void 0) ownedNames.add(f.linkName);
5234
+ }
5235
+ const toPrune = [];
5236
+ const strayUnknown = [];
5237
+ const seenExisting = /* @__PURE__ */ new Set();
5238
+ for (const e of existing) {
5239
+ if (ownedNames.has(e.name)) continue;
5240
+ if (seenExisting.has(e.name)) continue;
5241
+ seenExisting.add(e.name);
5242
+ if (e.kind === "repo") {
5243
+ toPrune.push({ name: e.name, target: e.target });
5244
+ } else {
5245
+ strayUnknown.push({ name: e.name, target: e.target, reason: e.kind });
5246
+ }
5247
+ }
5248
+ return {
5249
+ toCreate,
5250
+ conflicts,
5251
+ collisions,
5252
+ unreachable,
5253
+ toPrune,
5254
+ strayUnknown,
5255
+ correctCount,
5256
+ ok: toCreate.length === 0 && conflicts.length === 0 && collisions.length === 0 && unreachable.length === 0 && toPrune.length === 0 && strayUnknown.length === 0
5257
+ };
5258
+ }
5259
+
4593
5260
  // src/report/report-renderer.ts
4594
5261
  import { join as join16 } from "path";
4595
5262
 
@@ -5190,6 +5857,231 @@ function formatInt(n) {
5190
5857
  return n.toLocaleString("en-US");
5191
5858
  }
5192
5859
 
5860
+ // src/review/review-gaps.ts
5861
+ import { existsSync, realpathSync } from "fs";
5862
+ import { homedir as homedir2 } from "os";
5863
+ import { basename as basename2, isAbsolute, join as join17 } from "path";
5864
+ function stripQuotes(s) {
5865
+ if (s.length >= 2 && (s[0] === '"' && s.at(-1) === '"' || s[0] === "'" && s.at(-1) === "'")) {
5866
+ return s.slice(1, -1);
5867
+ }
5868
+ return s;
5869
+ }
5870
+ var realpathCache = /* @__PURE__ */ new Map();
5871
+ function resolveRealpath(absPath) {
5872
+ const cached = realpathCache.get(absPath);
5873
+ if (cached !== void 0) return cached;
5874
+ let resolved;
5875
+ try {
5876
+ resolved = realpathSync(absPath);
5877
+ } catch {
5878
+ resolved = null;
5879
+ }
5880
+ realpathCache.set(absPath, resolved);
5881
+ return resolved;
5882
+ }
5883
+ var repoRootCache = /* @__PURE__ */ new Map();
5884
+ function isRepoRoot(realPath) {
5885
+ const cached = repoRootCache.get(realPath);
5886
+ if (cached !== void 0) return cached;
5887
+ const result = existsSync(join17(realPath, ".git"));
5888
+ repoRootCache.set(realPath, result);
5889
+ return result;
5890
+ }
5891
+ function normalizeRepoPath(p) {
5892
+ if (!p) return null;
5893
+ let s = stripQuotes(p.trim()).replace(/\/+$/, "");
5894
+ if (s.length === 0 || s === "~") return null;
5895
+ if (s.startsWith("~/")) s = homedir2() + s.slice(1);
5896
+ if (isAbsolute(s)) {
5897
+ const real = resolveRealpath(s);
5898
+ if (real !== null) {
5899
+ return isRepoRoot(real) ? real : null;
5900
+ }
5901
+ }
5902
+ s = s.replace(/\/[^/]*-workspace\/([^/]+)/, "/$1");
5903
+ const seg = s.split("/").filter((x) => x.length > 0).pop();
5904
+ if (seg === void 0) return null;
5905
+ if (/-workspace$/.test(seg) || seg.includes("$")) return null;
5906
+ return s;
5907
+ }
5908
+ function normalizeRepoKey(p) {
5909
+ const full = normalizeRepoPath(p);
5910
+ return full === null ? null : basename2(full);
5911
+ }
5912
+ function inspectCommand(args) {
5913
+ const a = args.join(" ");
5914
+ const files = /* @__PURE__ */ new Set();
5915
+ const examinedDiff = /\bgit\s+(?:diff|show|log\s+-p|add\s+-p)\b/.test(a);
5916
+ for (const re of [
5917
+ /\b(?:cat|less|bat|head|tail)\s+([^\s|&;<>]+)/g,
5918
+ /\bsed\s+-n\s+'[^']*'\s+([^\s|&;<>]+)/g,
5919
+ /\b(?:rg|grep)\b[^|&;]*?\s([^\s|&;<>]+\.[A-Za-z0-9]+)(?:\s|$)/g
5920
+ ]) {
5921
+ let m;
5922
+ while ((m = re.exec(a)) !== null) {
5923
+ const f = m[1];
5924
+ if (f !== void 0) files.add(basename2(f));
5925
+ }
5926
+ }
5927
+ return { files: [...files], examinedDiff };
5928
+ }
5929
+ function commandRepo(args, cwd) {
5930
+ const cd = args.join(" ").match(/\bcd\s+("[^"]+"|'[^']+'|[^\s&]+)\s*&&/);
5931
+ if (cd) return normalizeRepoPath(cd[1]);
5932
+ return normalizeRepoPath(cwd);
5933
+ }
5934
+ function commandFailed(exitCode) {
5935
+ return exitCode !== null && exitCode !== 0;
5936
+ }
5937
+ function commitFiles(args) {
5938
+ const a = args.join(" ");
5939
+ const add = a.match(/git add\s+([^&|;]+)/);
5940
+ if (!add?.[1]) return [];
5941
+ return add[1].split(/\s+/).filter((t) => /\.[A-Za-z]/.test(t) && !t.startsWith("-")).map((t) => basename2(t));
5942
+ }
5943
+ var REVIEW_SOURCE = "codex-import";
5944
+ var DEFAULT_WINDOW_HOURS = 24;
5945
+ async function findReviewGaps(input) {
5946
+ const now = new Date(input.nowIso);
5947
+ const windowHours = input.windowHours ?? DEFAULT_WINDOW_HOURS;
5948
+ const scope = input.scope && input.scope.length > 0 ? input.scope : null;
5949
+ const loadOpts = { now };
5950
+ if (input.onSessionSkip !== void 0) loadOpts.onSkip = input.onSessionSkip;
5951
+ if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
5952
+ const entries = await loadSessionEntries(input.paths, loadOpts);
5953
+ const reviews = [];
5954
+ const workUnits = /* @__PURE__ */ new Map();
5955
+ const unknownCommits = /* @__PURE__ */ new Map();
5956
+ for (const entry of entries) {
5957
+ const sessionDir = join17(input.paths.sessions, entry.sessionId);
5958
+ const isReview = entry.session.session.source.kind === REVIEW_SOURCE;
5959
+ const reviewRepos = /* @__PURE__ */ new Map();
5960
+ let reviewEnd = null;
5961
+ try {
5962
+ for await (const ev of replayEvents(sessionDir, {
5963
+ onWarning: (w) => input.onWarning?.(w, entry.sessionId)
5964
+ })) {
5965
+ if (ev.type !== "command_executed") continue;
5966
+ if (commandFailed(ev.exit_code)) continue;
5967
+ const at = Date.parse(ev.occurred_at);
5968
+ if (isReview) {
5969
+ const repo2 = commandRepo(ev.args, ev.cwd);
5970
+ if (repo2 === null) continue;
5971
+ const ins = inspectCommand(ev.args);
5972
+ const slot = reviewRepos.get(repo2) ?? { examinedDiff: false, files: /* @__PURE__ */ new Set() };
5973
+ if (ins.examinedDiff) slot.examinedDiff = true;
5974
+ for (const f of ins.files) slot.files.add(f);
5975
+ reviewRepos.set(repo2, slot);
5976
+ if (!Number.isNaN(at)) reviewEnd = reviewEnd === null ? at : Math.max(reviewEnd, at);
5977
+ continue;
5978
+ }
5979
+ if (!ev.args.join(" ").includes("git commit")) continue;
5980
+ const repo = commandRepo(ev.args, ev.cwd);
5981
+ if (repo === null || Number.isNaN(at)) {
5982
+ const list2 = unknownCommits.get(entry.sessionId) ?? [];
5983
+ list2.push(Number.isNaN(at) ? null : at);
5984
+ unknownCommits.set(entry.sessionId, list2);
5985
+ continue;
5986
+ }
5987
+ const byRepo = workUnits.get(entry.sessionId) ?? /* @__PURE__ */ new Map();
5988
+ const list = byRepo.get(repo) ?? [];
5989
+ list.push({ repo, at, files: commitFiles(ev.args) });
5990
+ byRepo.set(repo, list);
5991
+ workUnits.set(entry.sessionId, byRepo);
5992
+ }
5993
+ } catch {
5994
+ input.onSessionSkip?.(entry.sessionId, "events_jsonl_unreadable");
5995
+ continue;
5996
+ }
5997
+ if (isReview && reviewRepos.size > 0) {
5998
+ reviews.push({ sessionId: entry.sessionId, endedAt: reviewEnd, repos: reviewRepos });
5999
+ }
6000
+ }
6001
+ const windowMs = windowHours * 3600 * 1e3;
6002
+ const units = [];
6003
+ let newestCommit = null;
6004
+ for (const [sessionId, byRepo] of workUnits) {
6005
+ for (const [repoPath, commits] of byRepo) {
6006
+ const label = basename2(repoPath);
6007
+ if (scope !== null && !scope.includes(label)) continue;
6008
+ const times = commits.map((c) => c.at).sort((a, b) => a - b);
6009
+ const first = times[0] ?? null;
6010
+ const last = times[times.length - 1] ?? null;
6011
+ if (last !== null) newestCommit = newestCommit === null ? last : Math.max(newestCommit, last);
6012
+ const changedFiles = new Set(commits.flatMap((c) => c.files));
6013
+ const before = first ?? last ?? 0;
6014
+ const nearby = reviews.filter((r) => {
6015
+ if (!r.repos.has(repoPath) || r.endedAt === null) return false;
6016
+ return r.endedAt <= before && r.endedAt >= before - windowMs;
6017
+ });
6018
+ const bound = nearby.filter((r) => {
6019
+ const touched = r.repos.get(repoPath);
6020
+ if (touched === void 0) return false;
6021
+ if (touched.examinedDiff) return true;
6022
+ for (const f of changedFiles) if (touched.files.has(f)) return true;
6023
+ return false;
6024
+ });
6025
+ const verdict = bound.length > 0 ? "candidate" : nearby.length > 0 ? "near_unbound" : "omission";
6026
+ const cited = verdict === "candidate" ? bound : verdict === "near_unbound" ? nearby : [];
6027
+ units.push({
6028
+ repo: label,
6029
+ sessionId,
6030
+ commitCount: commits.length,
6031
+ firstCommitAt: first === null ? null : new Date(first).toISOString(),
6032
+ lastCommitAt: last === null ? null : new Date(last).toISOString(),
6033
+ verdict,
6034
+ reviews: cited.map((r) => ({
6035
+ sessionId: r.sessionId,
6036
+ examinedDiff: r.repos.get(repoPath)?.examinedDiff ?? false,
6037
+ files: [...r.repos.get(repoPath)?.files ?? []].slice(0, 8),
6038
+ endedAt: r.endedAt === null ? null : new Date(r.endedAt).toISOString()
6039
+ }))
6040
+ });
6041
+ }
6042
+ }
6043
+ if (scope === null) {
6044
+ for (const [sessionId, times] of unknownCommits) {
6045
+ const valid = times.filter((t) => t !== null).sort((a, b) => a - b);
6046
+ const first = valid[0] ?? null;
6047
+ const last = valid[valid.length - 1] ?? null;
6048
+ if (last !== null) newestCommit = newestCommit === null ? last : Math.max(newestCommit, last);
6049
+ units.push({
6050
+ repo: "(unknown)",
6051
+ sessionId,
6052
+ commitCount: times.length,
6053
+ firstCommitAt: first === null ? null : new Date(first).toISOString(),
6054
+ lastCommitAt: last === null ? null : new Date(last).toISOString(),
6055
+ verdict: "unknown",
6056
+ reviews: []
6057
+ });
6058
+ }
6059
+ }
6060
+ const recentFirst = (a, b) => (Date.parse(b.lastCommitAt ?? "") || 0) - (Date.parse(a.lastCommitAt ?? "") || 0);
6061
+ const repoKeys = [...new Set(units.map((u) => u.repo))].sort();
6062
+ const repos = repoKeys.map((repo) => {
6063
+ const us = units.filter((u) => u.repo === repo);
6064
+ return {
6065
+ repo,
6066
+ units: us.length,
6067
+ omissionUnits: us.filter((u) => u.verdict === "omission").length,
6068
+ nearUnboundUnits: us.filter((u) => u.verdict === "near_unbound").length,
6069
+ candidateUnits: us.filter((u) => u.verdict === "candidate").length,
6070
+ unknownUnits: us.filter((u) => u.verdict === "unknown").length
6071
+ };
6072
+ });
6073
+ return {
6074
+ generatedAt: input.nowIso,
6075
+ windowHours,
6076
+ scope,
6077
+ repos,
6078
+ gaps: units.filter((u) => u.verdict === "omission" || u.verdict === "near_unbound").sort(recentFirst),
6079
+ candidates: units.filter((u) => u.verdict === "candidate").sort(recentFirst),
6080
+ unknowns: units.filter((u) => u.verdict === "unknown").sort(recentFirst),
6081
+ newestCommitAt: newestCommit === null ? null : new Date(newestCommit).toISOString()
6082
+ };
6083
+ }
6084
+
5193
6085
  // src/runtime/child-process-runner.ts
5194
6086
  import { spawn as spawn2 } from "child_process";
5195
6087
  var DEFAULT_KILL_GRACE_MS = 5e3;
@@ -5437,28 +6329,28 @@ function serializeJsonSchema(schema) {
5437
6329
 
5438
6330
  // src/storage/basou-dir.ts
5439
6331
  import { lstat as lstat4, mkdir as mkdir4 } from "fs/promises";
5440
- import { join as join17 } from "path";
6332
+ import { join as join18 } from "path";
5441
6333
  function basouPaths(repositoryRoot) {
5442
- const root = join17(repositoryRoot, ".basou");
5443
- const approvalsBase = join17(root, "approvals");
6334
+ const root = join18(repositoryRoot, ".basou");
6335
+ const approvalsBase = join18(root, "approvals");
5444
6336
  return {
5445
6337
  root,
5446
- sessions: join17(root, "sessions"),
5447
- tasks: join17(root, "tasks"),
6338
+ sessions: join18(root, "sessions"),
6339
+ tasks: join18(root, "tasks"),
5448
6340
  approvals: {
5449
- pending: join17(approvalsBase, "pending"),
5450
- resolved: join17(approvalsBase, "resolved")
6341
+ pending: join18(approvalsBase, "pending"),
6342
+ resolved: join18(approvalsBase, "resolved")
5451
6343
  },
5452
- locks: join17(root, "locks"),
5453
- logs: join17(root, "logs"),
5454
- raw: join17(root, "raw"),
5455
- tmp: join17(root, "tmp"),
6344
+ locks: join18(root, "locks"),
6345
+ logs: join18(root, "logs"),
6346
+ raw: join18(root, "raw"),
6347
+ tmp: join18(root, "tmp"),
5456
6348
  files: {
5457
- manifest: join17(root, "manifest.yaml"),
5458
- status: join17(root, "status.json"),
5459
- handoff: join17(root, "handoff.md"),
5460
- decisions: join17(root, "decisions.md"),
5461
- orientation: join17(root, "orientation.md")
6349
+ manifest: join18(root, "manifest.yaml"),
6350
+ status: join18(root, "status.json"),
6351
+ handoff: join18(root, "handoff.md"),
6352
+ decisions: join18(root, "decisions.md"),
6353
+ orientation: join18(root, "orientation.md")
5462
6354
  }
5463
6355
  };
5464
6356
  }
@@ -5515,12 +6407,12 @@ function hasErrorCode4(error) {
5515
6407
 
5516
6408
  // src/storage/gitignore.ts
5517
6409
  import { readFile as readFile8, writeFile as writeFile2 } from "fs/promises";
5518
- import { join as join18 } from "path";
6410
+ import { join as join19 } from "path";
5519
6411
  var MARKER = "# Basou - default ignore";
5520
6412
  var BASOU_GITIGNORE_BLOCK = "# Basou - default ignore\n.basou/logs/\n.basou/raw/\n.basou/tmp/\n.basou/locks/\n.basou/status.json\n.basou/orientation.md\n.basou/sessions/*/events.jsonl\n.basou/sessions/*/artifacts/\n.basou/approvals/pending/\n.basou/approvals/resolved/\n\n# Basou - default commit\n# .basou/manifest.yaml\n# .basou/handoff.md\n# .basou/decisions.md\n# .basou/tasks/\n# .basou/sessions/*/session.yaml\n# .basou/sessions/*/transcript.md\n# .basou/sessions/*/changed-files.json\n";
5521
6413
  var BASOU_GITIGNORE_BLOCK_LOCAL_ONLY = "# Basou - default ignore\n# Local-only: basou's trail is never committed (personal/local state,\n# regenerable by re-importing from the agents' own logs). Recommended for\n# monitored repos and any workspace kept out of version control.\n.basou/\n";
5522
6414
  async function appendBasouGitignore(repositoryRoot, options = {}) {
5523
- const gitignorePath = join18(repositoryRoot, ".gitignore");
6415
+ const gitignorePath = join19(repositoryRoot, ".gitignore");
5524
6416
  let body;
5525
6417
  let existed;
5526
6418
  try {
@@ -5671,8 +6563,8 @@ function hasErrorCode6(error) {
5671
6563
 
5672
6564
  // src/storage/session-import.ts
5673
6565
  import { mkdir as mkdir5, readFile as readFile10, rm as rm2 } from "fs/promises";
5674
- import { homedir as homedir2 } from "os";
5675
- import { join as join19 } from "path";
6566
+ import { homedir as homedir3 } from "os";
6567
+ import { join as join20 } from "path";
5676
6568
  async function importSessionFromJson(paths, manifest, payload, options) {
5677
6569
  if (options.taskIdOverride !== void 0 && !TaskIdSchema.safeParse(options.taskIdOverride).success) {
5678
6570
  throw new Error(`Invalid task_id: ${options.taskIdOverride}`);
@@ -5697,7 +6589,7 @@ async function importSessionFromJson(paths, manifest, payload, options) {
5697
6589
  pathSanitizeReport
5698
6590
  };
5699
6591
  }
5700
- const sessionDir = join19(paths.sessions, newSessionId);
6592
+ const sessionDir = join20(paths.sessions, newSessionId);
5701
6593
  try {
5702
6594
  await mkdir5(sessionDir, { recursive: true });
5703
6595
  } catch (error) {
@@ -5711,7 +6603,7 @@ async function importSessionFromJson(paths, manifest, payload, options) {
5711
6603
  throw error;
5712
6604
  }
5713
6605
  try {
5714
- const sessionYamlPath = join19(sessionDir, "session.yaml");
6606
+ const sessionYamlPath = join20(sessionDir, "session.yaml");
5715
6607
  await linkYamlFile(sessionYamlPath, withIntegrity(sessionRecord, chainResult));
5716
6608
  } catch (error) {
5717
6609
  await rm2(sessionDir, { recursive: true, force: true }).catch(() => void 0);
@@ -5780,7 +6672,7 @@ function withIntegrity(record, chainResult) {
5780
6672
  };
5781
6673
  }
5782
6674
  function buildSessionRecord(input, manifest, newSessionId, options) {
5783
- const home = homedir2();
6675
+ const home = homedir3();
5784
6676
  const workingDirectoryRaw = input.working_directory;
5785
6677
  const workingDirectorySanitized = sanitizeWorkingDirectory(workingDirectoryRaw, {
5786
6678
  homedir: home
@@ -5879,7 +6771,7 @@ function reuseDerivedIds(priorDerived, freshDerived, sessionId) {
5879
6771
  async function reimportPreservingId(paths, manifest, priorSessionId, freshPayload, options = {}) {
5880
6772
  const sessionId = priorSessionId;
5881
6773
  const importSource = freshPayload.session.source.kind;
5882
- const sessionDir = join19(paths.sessions, priorSessionId);
6774
+ const sessionDir = join20(paths.sessions, priorSessionId);
5883
6775
  const lock = options.dryRun === true ? null : await acquireLock(paths, "session", priorSessionId);
5884
6776
  try {
5885
6777
  const priorVerdict = await verifyEventsChain(paths, priorSessionId);
@@ -5921,7 +6813,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
5921
6813
  };
5922
6814
  const updatedRecord = { schema_version: "0.1.0", session: preservedInner };
5923
6815
  if (options.dryRun !== true) {
5924
- const eventsPath = join19(sessionDir, "events.jsonl");
6816
+ const eventsPath = join20(sessionDir, "events.jsonl");
5925
6817
  let priorEventsRaw = null;
5926
6818
  try {
5927
6819
  priorEventsRaw = await readFile10(eventsPath);
@@ -5933,7 +6825,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
5933
6825
  const chainResult = await writeEventsBulk(sessionDir, mergedEvents, { chain: true });
5934
6826
  try {
5935
6827
  await overwriteYamlFile(
5936
- join19(sessionDir, "session.yaml"),
6828
+ join20(sessionDir, "session.yaml"),
5937
6829
  withIntegrity(updatedRecord, chainResult)
5938
6830
  );
5939
6831
  } catch (error) {
@@ -5957,7 +6849,7 @@ async function reimportPreservingId(paths, manifest, priorSessionId, freshPayloa
5957
6849
  }
5958
6850
  }
5959
6851
  async function rechainSessionInPlace(paths, sessionId, options = {}) {
5960
- const sessionDir = join19(paths.sessions, sessionId);
6852
+ const sessionDir = join20(paths.sessions, sessionId);
5961
6853
  let lock;
5962
6854
  try {
5963
6855
  lock = await acquireLock(paths, "session", sessionId);
@@ -5990,7 +6882,7 @@ async function rechainSessionInPlace(paths, sessionId, options = {}) {
5990
6882
  if (verdict.status !== "unchained") {
5991
6883
  return { status: "skipped", reason: "tampered" };
5992
6884
  }
5993
- const eventsPath = join19(sessionDir, "events.jsonl");
6885
+ const eventsPath = join20(sessionDir, "events.jsonl");
5994
6886
  let priorRaw;
5995
6887
  try {
5996
6888
  priorRaw = await readFile10(eventsPath);
@@ -6038,7 +6930,7 @@ async function rechainSessionInPlace(paths, sessionId, options = {}) {
6038
6930
  }
6039
6931
  try {
6040
6932
  await overwriteYamlFile(
6041
- join19(sessionDir, "session.yaml"),
6933
+ join20(sessionDir, "session.yaml"),
6042
6934
  withIntegrity(record, { headHash: chainResult.headHash, count: chainResult.count })
6043
6935
  );
6044
6936
  } catch (error) {
@@ -6120,23 +7012,34 @@ export {
6120
7012
  enumerateTaskIds,
6121
7013
  finalizeSessionYaml,
6122
7014
  findErrorCode,
7015
+ findReviewGaps,
6123
7016
  formatDurationMs,
6124
7017
  genesisHash,
6125
7018
  getDiff,
6126
7019
  getSnapshot,
6127
7020
  importSessionFromJson,
6128
7021
  inspectChainTail,
7022
+ isGitNotFound,
6129
7023
  isImportDerivedSource,
6130
7024
  isLazyExpired,
7025
+ isRenderable,
6131
7026
  isValidPrefixedId,
6132
7027
  lineHash,
6133
7028
  linkYamlFile,
6134
7029
  loadApproval,
6135
7030
  loadSessionEntries,
6136
7031
  loadTaskEntries,
7032
+ normalizeRepoKey,
7033
+ normalizeRepoPath,
6137
7034
  overwriteYamlFile,
6138
7035
  parseDuration,
6139
7036
  parseMarkers,
7037
+ pathBasename,
7038
+ planArchive,
7039
+ planGitignore,
7040
+ planRename,
7041
+ planRosterAdoption,
7042
+ planWorkspaceView,
6140
7043
  prefixedUlid,
6141
7044
  readAllEvents,
6142
7045
  readManifest,
@@ -6148,12 +7051,14 @@ export {
6148
7051
  readYamlFile,
6149
7052
  rechainSessionInPlace,
6150
7053
  reconcileAllTasks,
7054
+ reconcileSourceRoots,
6151
7055
  reconcileTask,
6152
7056
  refreshTaskLinkedSessions,
6153
7057
  reimportPreservingId,
6154
7058
  renderDecisions,
6155
7059
  renderHandoff,
6156
7060
  renderOrientation,
7061
+ renderPresetBlock,
6157
7062
  renderReport,
6158
7063
  renderWithMarkers,
6159
7064
  replayEvents,
@@ -6162,6 +7067,7 @@ export {
6162
7067
  resolveRepositoryRoot,
6163
7068
  resolveSessionId,
6164
7069
  resolveTaskId,
7070
+ safeSimpleGit,
6165
7071
  sanitizePath,
6166
7072
  sanitizeRelatedFiles,
6167
7073
  sanitizeWorkingDirectory,
@@ -6170,8 +7076,13 @@ export {
6170
7076
  sessionWorkStatsFromEvents,
6171
7077
  summarizeAdapterOutput,
6172
7078
  summarizeOrientation,
7079
+ summarizePresetPlan,
7080
+ summarizeRosterDrift,
7081
+ summarizeSymlinkPlan,
7082
+ summarizeWiring,
6173
7083
  tryRemoteUrl,
6174
7084
  ulid,
7085
+ unknownManifestKeys,
6175
7086
  updateTaskStatusWithEvent,
6176
7087
  verifyEventsChain,
6177
7088
  writeEventsBulk,