@cogit/cli 0.21.0 → 0.21.1

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cogit",
3
3
  "displayName": "cogit",
4
- "version": "0.21.0",
4
+ "version": "0.21.1",
5
5
  "description": "Git remembers what changed. cogit remembers why.",
6
6
  "author": {
7
7
  "name": "Eren Sertkaya"
@@ -20470,7 +20470,12 @@ var HookState = external_exports.object({
20470
20470
  * and a file with nothing to say is remembered too, so every edit does not
20471
20471
  * walk the history again.
20472
20472
  */
20473
- notedFiles: external_exports.array(external_exports.string().min(1).max(500)).default([])
20473
+ notedFiles: external_exports.array(external_exports.string().min(1).max(500)).default([]),
20474
+ /**
20475
+ * Closing passes a later session started for this one because its own, at
20476
+ * the session end, never finished (machine shut down, run hung or failed).
20477
+ */
20478
+ closeRetries: external_exports.number().int().min(0).optional()
20474
20479
  });
20475
20480
 
20476
20481
  // src/schema/versioned.ts
package/dist/cli.js CHANGED
@@ -81,7 +81,7 @@ import {
81
81
  updateState,
82
82
  upgradeSessionIds,
83
83
  withLock
84
- } from "./chunk-O2KOWDTP.js";
84
+ } from "./chunk-MPOCC62M.js";
85
85
 
86
86
  // ../../node_modules/cli-width/index.js
87
87
  var require_cli_width = __commonJS({
@@ -5529,7 +5529,7 @@ var program = new Command();
5529
5529
  // package.json
5530
5530
  var package_default = {
5531
5531
  name: "@cogit/cli",
5532
- version: "0.21.0",
5532
+ version: "0.21.1",
5533
5533
  description: "Git remembers what changed. cogit remembers why.",
5534
5534
  license: "MIT",
5535
5535
  keywords: [
@@ -5748,7 +5748,7 @@ async function migrateProject(root) {
5748
5748
  const next = renameGitignoreLines(text);
5749
5749
  if (next !== text) await atomicWrite(gitignore, next).catch(() => void 0);
5750
5750
  }
5751
- const { installGitHooks: installGitHooks2 } = await import("./git-hooks-WL5FS4Y7.js");
5751
+ const { installGitHooks: installGitHooks2 } = await import("./git-hooks-2F5ALCNN.js");
5752
5752
  await installGitHooks2(dir).catch(() => void 0);
5753
5753
  return true;
5754
5754
  }
@@ -6032,7 +6032,8 @@ function repairTree(nodes) {
6032
6032
  }
6033
6033
 
6034
6034
  // src/commands/build.ts
6035
- import { resolve as resolve4 } from "path";
6035
+ import { readdir, stat as stat3 } from "fs/promises";
6036
+ import { dirname as dirname3, resolve as resolve4 } from "path";
6036
6037
 
6037
6038
  // src/store/changes.ts
6038
6039
  async function readProjectChanges(root, hashes) {
@@ -6881,6 +6882,37 @@ function toViewTurn(turn) {
6881
6882
  tools: turn.tools
6882
6883
  };
6883
6884
  }
6885
+ async function unrecordedSessions(root, sessions, transcripts) {
6886
+ const dir = dirname3(projectPaths(root).state("x"));
6887
+ let names;
6888
+ try {
6889
+ names = await readdir(dir);
6890
+ } catch {
6891
+ return [];
6892
+ }
6893
+ const known = new Set(sessions.map((session) => session.sessionId));
6894
+ const found = [];
6895
+ for (const name of names) {
6896
+ if (!name.endsWith(".json")) continue;
6897
+ const id = name.slice(0, -".json".length);
6898
+ if (known.has(id) || !transcripts.has(id) || !Slug.safeParse(id).success) continue;
6899
+ const info = await stat3(resolve4(dir, name)).catch(() => void 0);
6900
+ if (!info) continue;
6901
+ const startedAt = (info.birthtimeMs > 0 ? info.birthtime : info.mtime).toISOString();
6902
+ found.push({
6903
+ schema: SESSION_SCHEMA,
6904
+ sessionId: id,
6905
+ agent: "claude-code",
6906
+ title: "",
6907
+ epoch: null,
6908
+ startedAt,
6909
+ updatedAt: info.mtime.toISOString(),
6910
+ status: { goal: "", done: [], pending: [], next: "" },
6911
+ nodes: []
6912
+ });
6913
+ }
6914
+ return found;
6915
+ }
6884
6916
  async function readTranscriptViews(sessions, transcripts, root, io) {
6885
6917
  const agent3 = defaultAgent();
6886
6918
  const chats = /* @__PURE__ */ new Map();
@@ -7013,8 +7045,9 @@ async function buildProject(opts) {
7013
7045
  const model = await withLock(
7014
7046
  paths.lock("build"),
7015
7047
  async () => {
7016
- const sessions = await loadAllSessions(root, io.warn);
7048
+ const recorded = await loadAllSessions(root, io.warn);
7017
7049
  const transcripts = await defaultAgent().collectTranscripts(io.env);
7050
+ const sessions = [...recorded, ...await unrecordedSessions(root, recorded, transcripts)];
7018
7051
  const views = await readTranscriptViews(sessions, transcripts, root, io);
7019
7052
  const trailers = await readThoughtTrailers(root, {
7020
7053
  env: io.env,
@@ -7530,6 +7563,7 @@ function closePrompt(turns, record, opts) {
7530
7563
  "- For each thread give `from`: the message whose outcome it starts from. Usually the message just before the thread's first message; for a detour the message it interrupts (the same); for a new task after a finished one, the last message of the finished thread; null only if the first message would work as the first message of a brand-new chat.",
7531
7564
  "",
7532
7565
  "Then write a branch summary for every thread (asked: what the user wanted; outcome: what it meant and what came of it; open: what is still open, or empty), and leftOff: one line saying where the session left off, so a later session can decide whether to continue it.",
7566
+ "Outcome, open and leftOff describe the state at the end: read each thread to its last message, and the whole session to its last messages. A problem or task a later message fixed, finished or dropped is done, not open; never report a mid-session state as where things stand.",
7533
7567
  `Write text in ${writing}; JSON keys stay English. Never invent what is not in the record. Never write secrets, credentials or personal data.`,
7534
7568
  "",
7535
7569
  ...candidatePart(opts.candidates),
@@ -7743,7 +7777,9 @@ function headlessEnv(env) {
7743
7777
  function claudeBin() {
7744
7778
  return process.platform === "win32" ? "claude.cmd" : "claude";
7745
7779
  }
7780
+ var CLAUDE_TIMEOUT_MS = 5 * 6e4;
7746
7781
  function runClaude(prompt, model, env) {
7782
+ const limit = Number(env.COGIT_CLAUDE_TIMEOUT_MS) || CLAUDE_TIMEOUT_MS;
7747
7783
  return new Promise((resolve12, reject) => {
7748
7784
  const child = spawn(
7749
7785
  claudeBin(),
@@ -7760,6 +7796,11 @@ function runClaude(prompt, model, env) {
7760
7796
  );
7761
7797
  let out = "";
7762
7798
  let err = "";
7799
+ let timedOut = false;
7800
+ const timer = setTimeout(() => {
7801
+ timedOut = true;
7802
+ child.kill("SIGKILL");
7803
+ }, limit);
7763
7804
  child.stdin.on("error", () => {
7764
7805
  });
7765
7806
  child.stdout.setEncoding("utf8").on("data", (chunk) => {
@@ -7769,10 +7810,14 @@ function runClaude(prompt, model, env) {
7769
7810
  err += chunk;
7770
7811
  });
7771
7812
  child.on("error", (error) => {
7813
+ clearTimeout(timer);
7772
7814
  reject(new CliError(`could not run claude: ${error.message}`));
7773
7815
  });
7774
7816
  child.on("close", (code) => {
7775
- if (code === 0) resolve12(out);
7817
+ clearTimeout(timer);
7818
+ if (timedOut)
7819
+ reject(new CliError(`claude did not answer within ${Math.round(limit / 1e3)} s`));
7820
+ else if (code === 0) resolve12(out);
7776
7821
  else reject(new CliError(`claude exited with ${code}: ${err.trim() || "no output"}`));
7777
7822
  });
7778
7823
  child.stdin.end(prompt);
@@ -7834,7 +7879,7 @@ async function backfillTail(dir, sessionId, io, opts = {}) {
7834
7879
  }
7835
7880
 
7836
7881
  // src/commands/blame.ts
7837
- import { stat as stat3 } from "fs/promises";
7882
+ import { stat as stat4 } from "fs/promises";
7838
7883
  import { isAbsolute, relative as relative2, resolve as resolve6, sep as sep2 } from "path";
7839
7884
  import { pathToFileURL as pathToFileURL2 } from "url";
7840
7885
  function parseBlameTarget(target) {
@@ -7867,7 +7912,7 @@ async function blame(opts) {
7867
7912
  const path3 = relative2(gitRoot, absolute);
7868
7913
  if (path3 === "" || path3.startsWith("..") || isAbsolute(path3))
7869
7914
  throw new CliError(`${file} is outside ${gitRoot}`);
7870
- const exists = await stat3(absolute).then(
7915
+ const exists = await stat4(absolute).then(
7871
7916
  (info) => info.isFile(),
7872
7917
  () => false
7873
7918
  );
@@ -8808,7 +8853,7 @@ async function logFile(opts) {
8808
8853
  }
8809
8854
 
8810
8855
  // src/commands/migrate.ts
8811
- import { copyFile, mkdir as mkdir3, readdir } from "fs/promises";
8856
+ import { copyFile, mkdir as mkdir3, readdir as readdir2 } from "fs/promises";
8812
8857
  import { join as join3 } from "path";
8813
8858
  var BACKUP_DIR = "backup-pre-8.5";
8814
8859
  async function migrateIds(dir, io) {
@@ -8865,7 +8910,7 @@ async function migrateIds(dir, io) {
8865
8910
  }
8866
8911
  async function backupRecords(sessions, backup) {
8867
8912
  await mkdir3(backup, { recursive: true });
8868
- for (const entry of await readdir(sessions)) {
8913
+ for (const entry of await readdir2(sessions)) {
8869
8914
  if (!entry.endsWith(".json") && !entry.endsWith(".events.jsonl")) continue;
8870
8915
  await copyFile(join3(sessions, entry), join3(backup, entry));
8871
8916
  }
@@ -10341,8 +10386,8 @@ async function search(opts) {
10341
10386
 
10342
10387
  // src/commands/why.ts
10343
10388
  import { existsSync as existsSync3 } from "fs";
10344
- import { stat as stat4 } from "fs/promises";
10345
- import { dirname as dirname3, isAbsolute as isAbsolute2, join as join5, relative as relative4, resolve as resolve10, sep as sep4 } from "path";
10389
+ import { stat as stat5 } from "fs/promises";
10390
+ import { dirname as dirname4, isAbsolute as isAbsolute2, join as join5, relative as relative4, resolve as resolve10, sep as sep4 } from "path";
10346
10391
  var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
10347
10392
  function dayStamp(iso) {
10348
10393
  const date = new Date(iso);
@@ -10443,9 +10488,9 @@ function formatWhy(hits) {
10443
10488
  return lines;
10444
10489
  }
10445
10490
  async function nearestProjectDir(start) {
10446
- for (let dir = resolve10(start); ; dir = dirname3(dir)) {
10491
+ for (let dir = resolve10(start); ; dir = dirname4(dir)) {
10447
10492
  if (existsSync3(projectPaths(dir).project) || existsSync3(join5(dir, ".git"))) return dir;
10448
- if (dirname3(dir) === dir) return void 0;
10493
+ if (dirname4(dir) === dir) return void 0;
10449
10494
  }
10450
10495
  }
10451
10496
  async function why(opts) {
@@ -10462,7 +10507,7 @@ async function why(opts) {
10462
10507
  if (!project.ok) throw new CliError(project.error);
10463
10508
  if (await findGitRoot(root) === void 0)
10464
10509
  throw new CliError(`${root} is not inside a git repository; why needs one`);
10465
- const exists = await stat4(resolve10(root, file)).then(
10510
+ const exists = await stat5(resolve10(root, file)).then(
10466
10511
  (info) => info.isFile(),
10467
10512
  () => false
10468
10513
  );
@@ -10524,8 +10569,8 @@ function tryBeforeNote(file, hits) {
10524
10569
 
10525
10570
  // src/hooks/run.ts
10526
10571
  import { spawn as spawn5 } from "child_process";
10527
- import { closeSync, existsSync as existsSync4, mkdirSync, openSync, statSync, writeSync } from "fs";
10528
- import { dirname as dirname5 } from "path";
10572
+ import { closeSync, existsSync as existsSync4, mkdirSync, openSync, statSync as statSync2, writeSync } from "fs";
10573
+ import { dirname as dirname6 } from "path";
10529
10574
  import { fileURLToPath as fileURLToPath2 } from "url";
10530
10575
 
10531
10576
  // src/hooks/pretooluse.ts
@@ -10604,17 +10649,17 @@ async function onSessionEnd(ctx) {
10604
10649
  }
10605
10650
 
10606
10651
  // src/cli-version.ts
10607
- import { readFile as readFile6, realpath, stat as stat5 } from "fs/promises";
10608
- import { delimiter, dirname as dirname4, join as join6 } from "path";
10652
+ import { readFile as readFile6, realpath, stat as stat6 } from "fs/promises";
10653
+ import { delimiter, dirname as dirname5, join as join6 } from "path";
10609
10654
  var PACKAGE = "@cogit/cli";
10610
10655
  async function installedCliVersion(env) {
10611
10656
  for (const dir of (env.PATH ?? "").split(delimiter)) {
10612
10657
  if (dir === "") continue;
10613
10658
  const bin = join6(dir, "cogit");
10614
10659
  try {
10615
- if (!(await stat5(bin)).isFile()) continue;
10660
+ if (!(await stat6(bin)).isFile()) continue;
10616
10661
  const pkg = JSON.parse(
10617
- await readFile6(join6(dirname4(dirname4(await realpath(bin))), "package.json"), "utf8")
10662
+ await readFile6(join6(dirname5(dirname5(await realpath(bin))), "package.json"), "utf8")
10618
10663
  );
10619
10664
  if (pkg.name === PACKAGE && typeof pkg.version === "string") return pkg.version;
10620
10665
  return void 0;
@@ -10642,6 +10687,55 @@ function versionNotice(running, installed) {
10642
10687
  return void 0;
10643
10688
  }
10644
10689
 
10690
+ // src/hooks/close-retry.ts
10691
+ import { statSync } from "fs";
10692
+ var CLOSE_RETRY_IDLE_MS = 10 * 6e4;
10693
+ var CLOSE_RETRY_WINDOW_MS = 3 * 24 * 60 * 6e4;
10694
+ var CLOSE_RETRY_LIMIT = 2;
10695
+ var CLOSE_RETRY_BATCH = 3;
10696
+ async function retryUnclosed(ctx) {
10697
+ const config = await loadConfig(ctx.env);
10698
+ if (!config.sessionEndCatchUp) return;
10699
+ const now = Date.parse(ctx.now);
10700
+ const sessions = await loadAllSessions(ctx.root, () => {
10701
+ });
10702
+ const candidates = sessions.filter((session) => session.sessionId !== ctx.sessionId && unclosed(session, now)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
10703
+ let started = 0;
10704
+ for (const session of candidates) {
10705
+ if (started >= CLOSE_RETRY_BATCH) break;
10706
+ const id = session.sessionId;
10707
+ const transcript = await ctx.agent.findTranscript(id, ctx.env);
10708
+ const written = transcript ? statSync(transcript, { throwIfNoEntry: false })?.mtimeMs : void 0;
10709
+ if (written !== void 0 && now - written < CLOSE_RETRY_IDLE_MS) continue;
10710
+ let allowed = false;
10711
+ await updateState(ctx.root, id, (cur) => {
10712
+ const tries = cur?.closeRetries ?? 0;
10713
+ if (tries >= CLOSE_RETRY_LIMIT) return cur ?? emptyState2();
10714
+ allowed = true;
10715
+ return { ...cur ?? emptyState2(), closeRetries: tries + 1 };
10716
+ });
10717
+ if (!allowed) continue;
10718
+ ctx.spawnDetached(["backfill", id, "--tail", "--close", "-C", ctx.root]);
10719
+ started += 1;
10720
+ }
10721
+ }
10722
+ function unclosed(session, now) {
10723
+ if (session.leftOff !== void 0) return false;
10724
+ if (!session.nodes.some((node) => node.kind === "message")) return false;
10725
+ const age = now - Date.parse(session.updatedAt);
10726
+ return age >= CLOSE_RETRY_IDLE_MS && age <= CLOSE_RETRY_WINDOW_MS;
10727
+ }
10728
+ function emptyState2() {
10729
+ return {
10730
+ schema: STATE_SCHEMA,
10731
+ turns: 0,
10732
+ turnsSinceUpdate: 0,
10733
+ recordUpdatedAt: null,
10734
+ turnsSinceSide: 0,
10735
+ notedFiles: []
10736
+ };
10737
+ }
10738
+
10645
10739
  // src/hooks/sessionstart.ts
10646
10740
  function compactRequestText(skill) {
10647
10741
  return `cogit: this session was compacted and some of its user messages are not in the lineage record yet. Run the ${skill} skill now; \`session show\` lists the messages that have no node. It records only what is missing and replies "up to date" when nothing is; do not mention this request otherwise.`;
@@ -10675,6 +10769,7 @@ async function sessionStartOutput(ctx) {
10675
10769
  if (await findGitRoot(ctx.root) === void 0) return void 0;
10676
10770
  return additionalContext(ctx, setupHint(ctx));
10677
10771
  }
10772
+ await retryUnclosed(ctx).catch(() => void 0);
10678
10773
  const config = await loadConfig(ctx.env);
10679
10774
  if (!config.startupContext) return void 0;
10680
10775
  return additionalContext(ctx, await startupContext(ctx, config.recordCommit));
@@ -10856,10 +10951,10 @@ var HOOK_EVENTS = Object.keys(HANDLERS);
10856
10951
  var LOG_LIMIT = 256 * 1024;
10857
10952
  function openLog(log2, args) {
10858
10953
  try {
10859
- mkdirSync(dirname5(log2), { recursive: true });
10954
+ mkdirSync(dirname6(log2), { recursive: true });
10860
10955
  let flag = "a";
10861
10956
  try {
10862
- if (statSync(log2).size > LOG_LIMIT) flag = "w";
10957
+ if (statSync2(log2).size > LOG_LIMIT) flag = "w";
10863
10958
  } catch {
10864
10959
  }
10865
10960
  const fd = openSync(log2, flag);
@@ -15,7 +15,7 @@ import {
15
15
  uninstallGitHooks,
16
16
  withHookBlock,
17
17
  withoutHookBlock
18
- } from "./chunk-O2KOWDTP.js";
18
+ } from "./chunk-MPOCC62M.js";
19
19
  export {
20
20
  BLOCK_END,
21
21
  BLOCK_START,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cogit/cli",
3
- "version": "0.21.0",
3
+ "version": "0.21.1",
4
4
  "description": "Git remembers what changed. cogit remembers why.",
5
5
  "license": "MIT",
6
6
  "keywords": [