@cstart/coldstart 2.2.15 → 2.2.17

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.
@@ -36,7 +36,7 @@ import { existsSync, writeFileSync, appendFileSync, readFileSync, readdirSync, s
36
36
  import { extractEvidence, segmentStats } from "./evidence.mjs";
37
37
  import { initialState, step } from "./trigger.mjs";
38
38
  import { loadIgnore } from "./ignore.mjs";
39
- import { buildCapturePayload } from "./capture-payload.mjs";
39
+ import { buildCapturePayload, worklistJsonPath } from "./capture-payload.mjs";
40
40
  import {
41
41
  worklistEntries, freshNotedSet, gitHead, logCaptureEvent, writePendingCapture, MAX_WORKLIST,
42
42
  } from "./elicit-core.mjs";
@@ -129,10 +129,12 @@ process.on("unhandledRejection", (e) => { log(`unhandled ${e?.stack || e}`); pro
129
129
 
130
130
  // --- Manual capture (`--manual --root <dir>`) --------------------------------
131
131
  // The `/capture-notes` command runs this to fire capture ON DEMAND, bypassing
132
- // the trigger score gate. There is no hook stdin, so we self-discover the
133
- // session: the freshest marker in tmpdir whose recorded (relative) files still
134
- // resolve under <root> is this repo's active session. We then emit the SAME
135
- // capture payload an automatic fire would, built from accumulated evidence.
132
+ // the trigger score gate. There is no hook stdin, so there is no session id to
133
+ // trust by default. When the host names it (Claude passes `--session
134
+ // ${CLAUDE_SESSION_ID}`), that session's marker is resolved exactly, never
135
+ // guessed among several. Hosts with no session-id variable on their command
136
+ // surface (Cursor, Codex) pass none; soleMarkerUnderRoot below covers the
137
+ // common single-session case for them without ever guessing wrong.
136
138
  //
137
139
  // It marks the files it LISTED as captured (and nothing else). Without that, a
138
140
  // manual capture left every file uncaptured, so the next automatic fire re-asked
@@ -144,41 +146,77 @@ function argValue(name) {
144
146
  const i = process.argv.indexOf(name);
145
147
  return i >= 0 ? process.argv[i + 1] : undefined;
146
148
  }
147
- function markerMtime(p) { try { return statSync(p).mtimeMs; } catch { return 0; } }
148
- function freshestMarkerUnderRoot(root) {
149
- let names;
150
- try { names = readdirSync(tmpdir()); } catch { return null; }
151
- const markers = names
152
- .filter((n) => /^coldstart-kb-.+-main\.json$/.test(n))
153
- .map((n) => ({ n, p: join(tmpdir(), n), mtime: 0 }))
154
- .map((m) => ({ ...m, mtime: markerMtime(m.p) }))
155
- .sort((a, b) => b.mtime - a.mtime);
156
- for (const m of markers) {
157
- let state;
158
- try { state = JSON.parse(readFileSync(m.p, "utf8")); } catch { continue; }
159
- const files = state && state.files ? Object.keys(state.files) : [];
160
- if (!files.length) continue;
161
- // Belongs to THIS repo iff a recorded file still resolves under root.
162
- if (files.some((rel) => existsSync(join(root, rel)))) {
163
- return { sid: m.n.slice("coldstart-kb-".length, -"-main.json".length), state, path: m.p };
164
- }
149
+ // The main-agent evidence marker for a KNOWN session id. Returns null when the
150
+ // sid is empty or its marker is absent/foreign (files don't resolve under root).
151
+ function markerForSession(root, sid) {
152
+ if (!sid) return null;
153
+ const p = join(tmpdir(), `coldstart-kb-${sid}-main.json`);
154
+ let state;
155
+ try { state = JSON.parse(readFileSync(p, "utf8")); } catch { return null; }
156
+ const files = state && state.files ? Object.keys(state.files) : [];
157
+ if (!files.length || !files.some((rel) => existsSync(join(root, rel)))) return null;
158
+ return { sid, state, path: p };
159
+ }
160
+ // Hosts with no verified session-id variable on their command surface (Cursor,
161
+ // Codex their commands are plain prompt templates the agent itself runs, with
162
+ // no `!`-style substitution to carry a session id into the invocation) can't name
163
+ // a session at all. Guessing among several is the wrong-session risk `--session`
164
+ // exists to avoid, but when exactly ONE main-agent marker under this root exists
165
+ // there is nothing to disambiguate — restore that single-session case instead of
166
+ // refusing outright. `-main.json` only: subagent markers never own a worklist.
167
+ function soleMarkerUnderRoot(root) {
168
+ const re = /^coldstart-kb-(.+)-main\.json$/;
169
+ let entries;
170
+ try { entries = readdirSync(tmpdir()); } catch { return { marker: null, ambiguous: false }; }
171
+ const candidates = [];
172
+ for (const name of entries) {
173
+ const m = re.exec(name);
174
+ if (!m) continue;
175
+ const found = markerForSession(root, m[1]);
176
+ if (found) candidates.push(found);
165
177
  }
166
- return null;
178
+ if (candidates.length === 1) return { marker: candidates[0], ambiguous: false };
179
+ if (candidates.length > 1) return { marker: null, ambiguous: true };
180
+ return { marker: null, ambiguous: false };
167
181
  }
168
182
  if (process.argv.includes("--manual")) {
169
183
  try {
170
184
  const rootArg = argValue("--root");
171
185
  const root = rootArg ? resolve(rootArg) : process.cwd();
172
186
  setLogRoot(root);
173
- const found = freshestMarkerUnderRoot(root);
174
- if (!found) {
187
+ const sidArg = String(argValue("--session") || "").replace(/[^A-Za-z0-9_-]/g, "");
188
+ let found = sidArg ? markerForSession(root, sidArg) : null;
189
+ if (!sidArg) {
190
+ const sole = soleMarkerUnderRoot(root);
191
+ if (sole.ambiguous) {
192
+ process.stdout.write(
193
+ "Multiple sessions have notebook-capture evidence in this repo, and this host can't tell\n" +
194
+ "/capture-notes which one you mean — pass --session <id> if your host exposes one, or close\n" +
195
+ "the other sessions working here so only one remains.\n",
196
+ );
197
+ log(`MANUAL ambiguous-no-session root=${root}`);
198
+ process.exit(0);
199
+ }
200
+ found = sole.marker;
201
+ if (found) log(`MANUAL sole-marker-fallback sid=${found.sid} root=${root}`);
202
+ }
203
+ if (!sidArg && !found) {
175
204
  process.stdout.write(
176
205
  "No notebook-capture evidence for this repo yet — it accrues as you read and edit files.\n" +
177
206
  "Do a turn or two of real work here, then run /capture-notes again.\n",
178
207
  );
179
- log(`MANUAL no-marker root=${root}`);
208
+ log(`MANUAL no-session root=${root}`);
209
+ process.exit(0);
210
+ }
211
+ if (!found) {
212
+ process.stdout.write(
213
+ "No notebook-capture evidence for THIS session in this repo yet — it accrues as you read\n" +
214
+ "and edit files. Do a turn or two of real work here, then run /capture-notes again.\n",
215
+ );
216
+ log(`MANUAL no-marker-for-session sid=${sidArg} root=${root}`);
180
217
  process.exit(0);
181
218
  }
219
+ log(`MANUAL session sid=${found.sid} root=${root}`);
182
220
  const { sid, state, path: markerPath } = found;
183
221
  // A manual /capture-notes is an EXPLICIT request to write up THIS session's
184
222
  // work, so its scope is everything worked on — not merely what an automatic
@@ -189,10 +227,17 @@ if (process.argv.includes("--manual")) {
189
227
  // note unwritten and a now-stale note unfixed (the Bug-1 report). So:
190
228
  // include every edited file regardless of the flag, and drop only READ-ONLY
191
229
  // files that were already offered (re-listing those on demand is just
192
- // noise). Rank most-worked first (edits retouches reads) so new/edited
193
- // files lead; the per-file annotations flag when a fresh note needs nothing.
230
+ // noise) AND whose worklist is still live. A captured read-only file whose
231
+ // durable worklist pair is gone (write failure, or hand-deleted since) has
232
+ // no live checklist anywhere naming it — re-including it here is the only
233
+ // way it is ever offered again, and it can't double-nag since there is
234
+ // nothing else currently showing it. Rank most-worked first (edits →
235
+ // retouches → reads) so new/edited files lead; the per-file annotations
236
+ // flag when a fresh note needs nothing.
237
+ const worklistLost = !existsSync(worklistJsonPath(root, sid, "main"));
238
+ if (worklistLost) log(`MANUAL worklist-artifact-missing session=${sid} — recovering captured read-only files too`);
194
239
  const files = Object.entries(state.files)
195
- .filter(([, f]) => (f.reads + f.edits + f.gs) > 0 && (f.edits > 0 || !f.captured))
240
+ .filter(([, f]) => (f.reads + f.edits + f.gs) > 0 && (f.edits > 0 || !f.captured || worklistLost))
196
241
  .sort((a, b) => (b[1].edits - a[1].edits)
197
242
  || ((b[1].retouches || 0) - (a[1].retouches || 0))
198
243
  || (b[1].reads - a[1].reads))
@@ -203,7 +248,9 @@ if (process.argv.includes("--manual")) {
203
248
  process.exit(0);
204
249
  }
205
250
  const entries = worklistEntries(CLI, root, files, state.files, log);
206
- const payload = buildCapturePayload({ root, cli: CLI, sid, entries, envelope: "manual" });
251
+ // Manual capture resolves the invoking session's MAIN marker (markerForSession
252
+ // targets `-main.json`), so its worklist is the main agent's.
253
+ const payload = buildCapturePayload({ root, cli: CLI, sid, aid: "main", entries, envelope: "manual" });
207
254
  // Mark ONLY the files actually LISTED (the worklist caps its display to
208
255
  // MAX_WORKLIST) captured — any overflow stays uncaptured so it rolls into
209
256
  // the next capture rather than being marked done but never shown. Re-read
@@ -345,7 +392,7 @@ if (process.argv.includes("--manual")) {
345
392
  writeFileSync(marker, JSON.stringify(state));
346
393
  if (!fresh.length) { log(`FAST-EXIT subagent no-new-files session=${sid} agent=${aid}`); process.exit(0); }
347
394
  const entries = worklistEntries(CLI, root, fresh, Object.fromEntries(fresh.map((rel) => [rel, delta.get(rel)])), log);
348
- const payload = buildCapturePayload({ root, cli: CLI, sid, entries, envelope: "subagent" });
395
+ const payload = buildCapturePayload({ root, cli: CLI, sid, aid, entries, envelope: "subagent" });
349
396
  logCaptureEvent(root, { event: "fire", reason: "subagent", session: sid, agent: aid, files: fresh.length });
350
397
  log(`FIRE subagent session=${sid} agent=${aid} files=${fresh.length}`);
351
398
  process.stdout.write(JSON.stringify({ decision: "block", reason: payload }));
@@ -381,11 +428,11 @@ if (process.argv.includes("--manual")) {
381
428
  log(`FIRE ${decision.fire} mode=${decision.mode} session=${sid} score=${decision.score} files=${decision.files.length}`);
382
429
 
383
430
  if (decision.mode === "block") {
384
- const payload = buildCapturePayload({ root, cli: CLI, sid, entries, envelope: "block" });
431
+ const payload = buildCapturePayload({ root, cli: CLI, sid, aid, entries, envelope: "block" });
385
432
  process.stdout.write(JSON.stringify({ decision: "block", reason: payload }));
386
433
  } else {
387
434
  // Non-blocking: kb-recall delivers this with the user's next prompt.
388
- const payload = buildCapturePayload({ root, cli: CLI, sid, entries, envelope: "inject" });
435
+ const payload = buildCapturePayload({ root, cli: CLI, sid, aid, entries, envelope: "inject" });
389
436
  writePendingCapture(sid, decision.fire, payload);
390
437
  }
391
438
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cstart/coldstart",
3
- "version": "2.2.15",
3
+ "version": "2.2.17",
4
4
  "mcpName": "io.github.AkashGoenka/coldstart",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -40,7 +40,7 @@
40
40
  ],
41
41
  "license": "MIT",
42
42
  "author": "Akash Goenka (https://github.com/AkashGoenka)",
43
- "homepage": "https://akashgoenka.github.io/coldstart/",
43
+ "homepage": "https://coldstartmcp.dev/",
44
44
  "repository": {
45
45
  "type": "git",
46
46
  "url": "git+https://github.com/AkashGoenka/coldstart.git"