@cogit/cli 0.31.0 → 0.31.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.31.0",
4
+ "version": "0.31.1",
5
5
  "description": "Git remembers what changed. cogit remembers why.",
6
6
  "author": {
7
7
  "name": "Eren Sertkaya"
package/README.md CHANGED
@@ -8,11 +8,47 @@ Git remembers what changed. cogit remembers why.
8
8
 
9
9
  cogit records every message you send to an AI coding agent as a *thought commit*: what you
10
10
  asked, what came of it, and the message it answered. Your code commits point back to the
11
- thought behind them, so `git log` tells you what changed and cogit tells you why. It works with
12
- Claude Code today.
11
+ thought behind them, so `cogit why` takes any line of code back to the reason it exists. It works
12
+ with Claude Code today.
13
13
 
14
- It runs as a Claude Code plugin and hooks in at SessionStart, Stop and SessionEnd, so the
15
- record is written while you work, not reconstructed from the diff afterwards.
14
+ Agent memory helps your agent remember. cogit helps you understand.
15
+
16
+ It runs as a Claude Code plugin and works through Claude Code hooks, so the record is written
17
+ while you work, not reconstructed from the diff afterwards.
18
+
19
+ **[Click through a saved map](https://erensrtky.github.io/cogit/demo/)**: no install, no server.
20
+ It's the page `cogit open` writes, built from sample data and read-only.
21
+
22
+ ## Why
23
+
24
+ I started by copying code out of ChatGPT, then moved to browser tools, editor extensions and now
25
+ terminal agents. Each step moved the code further from me, and I kept feeling something was
26
+ slipping away. When you write code yourself, it sinks in bit by bit: you know why that table
27
+ exists and why you wrote that class. When an agent writes it, that feeling goes away.
28
+
29
+ The information isn't really gone. It sits in your messages, the agent's replies and your commits,
30
+ just in separate places. cogit ties them together, so you can always walk back and see why
31
+ something was done. It helps most when you juggle projects: I run 5-10 agents at once across 2-3
32
+ of them, and a few minutes later I can lose track of what I did where.
33
+
34
+ <img src="docs/images/why-abstraction.png" alt="A conceptual chart from machine code to software factories: abstraction keeps rising, while how much of the why you can still trace falls sharply once AI starts writing code. cogit keeps what falls away.">
35
+
36
+ It's bigger than one person's memory: a reviewer sees what changed, not why.
37
+ [More, with research and sources](docs/guide/why.md).
38
+
39
+ cogit isn't:
40
+
41
+ - **Agent memory** (OpenViking, Mem0): that helps your agent recall what you talked about.
42
+ - **A token saver** (RTK, Headroom): that sends the model less.
43
+ - **A code map** (graphify): that shows how a codebase fits together.
44
+ - **An orchestrator** (Herdr, Conductor): that runs many agents at once.
45
+ - **A planning framework** (BMAD, Spec Kit): that decides what to build next.
46
+ - **An AI review bot** (CodeRabbit, Greptile): that comments on whether a diff looks right.
47
+
48
+ It answers the one question they leave open: why is this code the way it is? I use several of
49
+ them alongside it.
50
+
51
+ ## See it
16
52
 
17
53
  <img src="docs/images/map.png" alt="The map: sessions as a git graph, each with its messages, branches, commit chips and a left off here marker">
18
54
 
@@ -26,18 +62,14 @@ what the change does, and the diff.*
26
62
 
27
63
  <img src="docs/images/tasks.png" alt="The Tasks panel: suggested items with Accept and Dismiss, open items with Done, and a Done section">
28
64
 
29
- *Tasks — what your sessions left undone, drafted when a session closes. You accept, dismiss or
30
- close each one.*
65
+ *Tasks — work that came up in a session and didn't get finished, caught when the session closes.
66
+ You accept, dismiss or close each one.*
31
67
 
32
68
  <img src="docs/images/files.png" alt="The Files view: a changed file opened, listing the messages that touched it with each message's reason and the option it rejected">
33
69
 
34
70
  *A changed file opens its history: every message that touched it, and the reason behind each —
35
71
  including the option that was rejected.*
36
72
 
37
- **[Click through a saved map](https://erensrtky.github.io/cogit/demo/)** — no install, no server:
38
- the same page `cogit open` writes, built from sample data. Read-only, so the Tasks buttons are
39
- inert.
40
-
41
73
  ## What it does
42
74
 
43
75
  - **Every message becomes a thought commit**: the question, the outcome, the decisions with
@@ -50,25 +82,21 @@ inert.
50
82
  - **Intent, Shape, Code**: a message that landed commits shows why it happened, a small
51
83
  picture of what the change does (a diagram, a before/after or interface table, an example
52
84
  or plain steps), and the diff, in one place.
53
- - **Tasks**: when a session ends, a small model drafts a few user-level items from what it
54
- left undone; you accept, dismiss or close them (`cogit captures`, and on the map).
85
+ - **Tasks**: when a session ends, a small model catches the work that came up and didn't get
86
+ finished as a few user-level items; you accept, dismiss or close them (`cogit captures`, and
87
+ on the map).
55
88
  - **Pull request descriptions with the decisions**: `cogit pr` writes the goal, the
56
89
  decisions and the rejected options, never the raw chat.
57
90
  - **Teams share the record through git**: after a pull, your teammates' sessions are on the
58
91
  map too, with who ran them and what changed most.
59
- - **The agent remembers**: a new session starts where the last one left off, and before
60
- editing a file the agent hears what was already tried and rejected there.
61
92
  - **Cheap by default**: a small side model records in the background; in our sessions it
62
93
  cost about 1–2% of a session's tokens.
63
- - **Private by default**: no server, no account. The raw conversation stays on your machine
64
- and keys are redacted.
65
-
66
- ## Why
67
-
68
- AI coding agents made writing code faster and moved the bottleneck to review: in a study of
69
- 718 firms, they opened 23% more pull requests while review time rose 49% and finished work did
70
- not measurably grow. A reviewer sees what changed, not why; the why stays in a chat nobody else
71
- reads. cogit keeps it next to the code. [More, with sources](docs/guide/why.md).
94
+ - **No server, no account**: it runs on your machine. Your chats are not committed. The record
95
+ (titles, decisions and reasons) is, so it travels with your branch; `cogit init` lets you keep
96
+ it local instead. Key and token patterns are redacted.
97
+ - **The agent gets the context too**: a new session starts where the last one left off, and
98
+ before editing a file the agent hears what was already tried and rejected there. It works
99
+ alongside memory tools; it doesn't replace them.
72
100
 
73
101
  ## Install
74
102
 
@@ -21206,7 +21206,7 @@ async function migrateProject(root) {
21206
21206
  const next = renameGitignoreLines(text);
21207
21207
  if (next !== text) await atomicWrite(gitignore, next).catch(() => void 0);
21208
21208
  }
21209
- const { installGitHooks: installGitHooks2 } = await import("./git-hooks-SGXDRKNY.js");
21209
+ const { installGitHooks: installGitHooks2 } = await import("./git-hooks-KQ3BN2VC.js");
21210
21210
  await installGitHooks2(dir).catch(() => void 0);
21211
21211
  return true;
21212
21212
  }
@@ -23793,6 +23793,9 @@ function isStoredTurn(value) {
23793
23793
  const turn = value;
23794
23794
  return typeof turn.uuid === "string" && turn.uuid !== "" && typeof turn.at === "string" && typeof turn.text === "string" && Array.isArray(turn.replies) && turn.replies.every((reply) => typeof reply === "string") && turn.tools !== null && typeof turn.tools === "object";
23795
23795
  }
23796
+ function answerSize(turn) {
23797
+ return turn.replies.reduce((sum, reply) => sum + reply.length, 0) + JSON.stringify(turn.tools).length;
23798
+ }
23796
23799
  async function readConversationStore(root, sessionId) {
23797
23800
  let text;
23798
23801
  try {
@@ -23819,16 +23822,28 @@ async function readConversationStore(root, sessionId) {
23819
23822
  order.push(value.uuid);
23820
23823
  continue;
23821
23824
  }
23822
- if (prev.hash === void 0 && value.hash !== void 0) byUuid.set(value.uuid, value);
23825
+ const fuller = answerSize(value) > answerSize(prev) ? value : prev;
23826
+ const hash2 = prev.hash ?? value.hash;
23827
+ byUuid.set(value.uuid, {
23828
+ ...prev,
23829
+ ...hash2 === void 0 ? {} : { hash: hash2 },
23830
+ replies: fuller.replies,
23831
+ tools: fuller.tools
23832
+ });
23823
23833
  }
23824
23834
  return order.map((uuid3) => byUuid.get(uuid3));
23825
23835
  }
23826
23836
  async function appendConversationTurns(root, sessionId, turns) {
23827
- const known = new Set((await readConversationStore(root, sessionId)).map((turn) => turn.uuid));
23837
+ const stored = new Map(
23838
+ (await readConversationStore(root, sessionId)).map((turn) => [turn.uuid, turn])
23839
+ );
23840
+ const seen = /* @__PURE__ */ new Set();
23828
23841
  const fresh = [];
23829
23842
  for (const turn of turns) {
23830
- if (turn.uuid === void 0 || known.has(turn.uuid)) continue;
23831
- known.add(turn.uuid);
23843
+ if (turn.uuid === void 0 || seen.has(turn.uuid)) continue;
23844
+ seen.add(turn.uuid);
23845
+ const held = stored.get(turn.uuid);
23846
+ if (held && answerSize(turn) <= answerSize(held)) continue;
23832
23847
  fresh.push({
23833
23848
  uuid: turn.uuid,
23834
23849
  ...turn.hash === void 0 ? {} : { hash: turn.hash },
@@ -23839,13 +23854,17 @@ async function appendConversationTurns(root, sessionId, turns) {
23839
23854
  });
23840
23855
  }
23841
23856
  if (fresh.length === 0) return 0;
23842
- const { value } = redactDeep(fresh);
23857
+ const value = redactDeep(fresh).value.filter((turn) => {
23858
+ const held = stored.get(turn.uuid);
23859
+ return !held || answerSize(turn) > answerSize(held);
23860
+ });
23861
+ if (value.length === 0) return 0;
23843
23862
  const chunk = `${value.map((turn) => JSON.stringify(turn)).join("\n")}
23844
23863
  `;
23845
23864
  const file2 = projectPaths(root).conversation(sessionId);
23846
23865
  await mkdir4(dirname6(file2), { recursive: true });
23847
23866
  await appendFile(file2, gzipSync(Buffer.from(chunk, "utf8")));
23848
- return fresh.length;
23867
+ return value.length;
23849
23868
  }
23850
23869
  async function appendConversationHashes(root, sessionId, hashes) {
23851
23870
  if (hashes.size === 0) return 0;
@@ -24517,9 +24536,8 @@ function embedViewModel(template, model) {
24517
24536
  if (!template.includes(EMPTY_DATA_ELEMENT)) {
24518
24537
  throw new Error(`the template has no empty <script id="${DATA_ELEMENT_ID}"> element`);
24519
24538
  }
24520
- const data = `<script id="${DATA_ELEMENT_ID}" type="application/json">${escapeForScript(
24521
- JSON.stringify(model)
24522
- )}</script>`;
24539
+ const json2 = JSON.stringify(model, null, 1).replace(/\n +/g, "\n");
24540
+ const data = `<script id="${DATA_ELEMENT_ID}" type="application/json">${escapeForScript(json2)}</script>`;
24523
24541
  return template.replace(EMPTY_DATA_ELEMENT, () => data);
24524
24542
  }
24525
24543
 
@@ -25578,7 +25596,7 @@ async function onSessionEnd(ctx) {
25578
25596
  // package.json
25579
25597
  var package_default = {
25580
25598
  name: "@cogit/cli",
25581
- version: "0.31.0",
25599
+ version: "0.31.1",
25582
25600
  description: "Git remembers what changed. cogit remembers why.",
25583
25601
  license: "MIT",
25584
25602
  keywords: [
package/dist/cli.js CHANGED
@@ -121,7 +121,7 @@ import {
121
121
  versionNotice,
122
122
  why,
123
123
  withLock
124
- } from "./chunk-XMQKZVCS.js";
124
+ } from "./chunk-DJFQNOZT.js";
125
125
 
126
126
  // node_modules/commander/lib/error.js
127
127
  var CommanderError = class extends Error {
@@ -4194,8 +4194,7 @@ async function readTranscriptViews(sessions, transcripts, root, io) {
4194
4194
  const file = transcripts.get(session.sessionId);
4195
4195
  const recordsMessages = session.nodes.some((node) => node.kind === "message");
4196
4196
  const roots = [session.worktree?.path, root].filter((r) => Boolean(r));
4197
- const stored = await readConversationStore(root, session.sessionId);
4198
- const storedByUuid = new Map(stored.map((turn) => [turn.uuid, turn]));
4197
+ let stored = await readConversationStore(root, session.sessionId);
4199
4198
  let transcript = null;
4200
4199
  if (file) {
4201
4200
  try {
@@ -4209,21 +4208,18 @@ async function readTranscriptViews(sessions, transcripts, root, io) {
4209
4208
  }
4210
4209
  const fromTranscript = transcript && transcript.length > 0 ? hashTurns(session.sessionId, transcript) : null;
4211
4210
  if (fromTranscript) {
4212
- const fresh = fromTranscript.filter(
4213
- (turn) => turn.uuid !== void 0 && !storedByUuid.has(turn.uuid)
4214
- );
4215
4211
  const hashes = new Map(
4216
4212
  fromTranscript.flatMap(
4217
4213
  (turn) => turn.uuid === void 0 ? [] : [[turn.uuid, turn.hash]]
4218
4214
  )
4219
4215
  );
4220
4216
  try {
4221
- if (fresh.length > 0)
4222
- await appendConversationTurns(
4223
- root,
4224
- session.sessionId,
4225
- fresh.map((turn) => agent3.displayTurn(turn, roots))
4226
- );
4217
+ const appended = await appendConversationTurns(
4218
+ root,
4219
+ session.sessionId,
4220
+ fromTranscript.map((turn) => agent3.displayTurn(turn, roots))
4221
+ );
4222
+ if (appended > 0) stored = await readConversationStore(root, session.sessionId);
4227
4223
  await appendConversationHashes(root, session.sessionId, hashes);
4228
4224
  } catch (err) {
4229
4225
  io.warn(
@@ -4239,6 +4235,7 @@ async function readTranscriptViews(sessions, transcripts, root, io) {
4239
4235
  nodes: applyEvents(upgraded.nodes, events)
4240
4236
  };
4241
4237
  prepared.push(effective);
4238
+ const storedByUuid = new Map(stored.map((turn) => [turn.uuid, turn]));
4242
4239
  let turns;
4243
4240
  if (fromTranscript) {
4244
4241
  turns = fromTranscript.map((turn) => {
@@ -5826,6 +5823,8 @@ function rules(language, messageSummaries, recorder) {
5826
5823
  "- parent: an earlier message id, or null when the message starts a new thread; never a topic or phase node.",
5827
5824
  "- Back to an older thread: that message is the parent and the previous one goes in links (at most 5). Unsure: use the previous message.",
5828
5825
  '- When the message itself made a decision (the user asked for it, picked an option or approved a proposal): decidedBy ("user" or "agreed"), the reason in why, and the options not taken in alternatives [{"option": "...", "why": "..."}].',
5826
+ "- alternatives lists only options someone actually named in the conversation before the decision. Never infer one from the decision, never list the current state as an option, and leave the field out when none was named. Its why says what the conversation said: no durations, sizes or reasons it does not contain. An option only postponed was not rejected.",
5827
+ '- decidedBy "agreed" needs the user approving the proposal in this message. A proposal the user has not answered is not a decision.',
5829
5828
  `- The agent's work belongs inside the message node that caused it, never in nodes of its own: work (short items), agentDecisions ([{"title": "...", "why": "...", "alternatives": [...]}]) only for technical choices with a reason \u2014 never for proposals or work items \u2014 commits (short hashes) and files (a few paths).`,
5830
5829
  "- Write a branch summary ({title, asked, outcome, open}) on the first message of a branch when the branch ends.",
5831
5830
  `- Never send "at"; the CLI takes each message's time from the transcript.`,
@@ -15,7 +15,7 @@ import {
15
15
  uninstallGitHooks,
16
16
  withHookBlock,
17
17
  withoutHookBlock
18
- } from "./chunk-XMQKZVCS.js";
18
+ } from "./chunk-DJFQNOZT.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.31.0",
3
+ "version": "0.31.1",
4
4
  "description": "Git remembers what changed. cogit remembers why.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -55,7 +55,11 @@ transcript is unreadable: send only `status`.
55
55
  - `summary`: what came of it, ONLY when `messageSummaries` is true; otherwise leave it out,
56
56
  like a commit with a subject and no body.
57
57
  - A decision: `decidedBy` (`"user"` or `"agreed"`), `why`, `alternatives`
58
- (`{ "option", "why" }`).
58
+ (`{ "option", "why" }`). `alternatives` lists only options someone actually named in the
59
+ conversation before the decision: never one inferred from the decision, never the current
60
+ state, and no durations, sizes or reasons the conversation does not contain. An option only
61
+ postponed was not rejected. `"agreed"` needs the user approving the proposal in this message;
62
+ a proposal the user has not answered is not a decision.
59
63
  - Your work belongs to the message that caused it: `work`, `agentDecisions`
60
64
  (`{ "title", "why", "alternatives" }`), `commits` (short hashes), `files` (a few paths).
61
65
  Work spanning messages goes on the one it ended in; update an earlier node only to correct it.