@cstart/coldstart 2.2.13 → 2.2.15

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.
@@ -16,8 +16,7 @@
16
16
  * (the coordinator only sees the final message — #61).
17
17
  */
18
18
 
19
- import { readFileSync, writeFileSync } from "node:fs";
20
- import { tmpdir } from "node:os";
19
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
21
20
  import { join } from "node:path";
22
21
  // The spec shapes are NOT written here any more. They come from the one table
23
22
  // the write guide, the MCP tool description and `kb repair` also render from —
@@ -25,25 +24,36 @@ import { join } from "node:path";
25
24
  import { shapesBlock } from "./note-shape.mjs";
26
25
 
27
26
  /**
28
- * Session worklist manifest — the DENOMINATOR for capture coverage.
27
+ * Durable capture worklist — the checklist the agent can re-Read at any point.
29
28
  *
30
- * The checklist can only ASK the agent to walk the whole worklist (rule 3a);
31
- * nothing downstream could tell it that it wrote 8 notes for 30 worked files,
32
- * because `kb write` sees one spec at a time and never saw the worklist. So the
33
- * worklist is dropped here, at the one point every host passes through, and
34
- * `kb write --session <sid>` reads it back to print "N of M".
29
+ * The old design dropped the worklist in tmpdir (keyed by session id) and let
30
+ * `kb write --session` print a running "N of M". Two failures: the manifest was
31
+ * scratch that no agent ever read directly, and the whole capture PAYLOAD (this
32
+ * checklist + the write contract) was delivered one-shot and lost to compaction
33
+ * the agent had to re-derive the note shape from memory 80 tool-calls later.
35
34
  *
36
- * CONTRACT TWIN: src/kb/session-worklist.ts same filename, same shape. Keyed
37
- * by session id in tmpdir like the pending-capture file (elicit-core.pendingPath);
38
- * never in the repo, so it cannot be committed and needs no ignore rule.
39
- * Best-effort: capture must never fail because this could not be written.
35
+ * So the pair now lives in the repo notebook, keyed by root, and holds BOTH:
36
+ * .worklist.json structured scope (the coverage source of truth)
37
+ * .worklist.md the full rendered payload worklist + write contract
38
+ * The agent re-Reads the .md whenever this scrolls away; `kb write` credits and
39
+ * trims/clears the pair (src/kb/durable-worklist.ts) so a stale snapshot never
40
+ * lingers, and the next capture fire regenerates it against live freshness.
41
+ *
42
+ * CONTRACT TWIN: src/kb/durable-worklist.ts — keep the paths and the .json shape
43
+ * (ts, sid, files:[{path,tier,needsNote}], wrote:[]) in step. Both gitignored
44
+ * (src/kb/store.ts initSkeleton). One active worklist per repo (concurrent
45
+ * sessions last-write-wins, an accepted edge). Best-effort: capture must never
46
+ * fail because these could not be written.
40
47
  */
41
- export function worklistManifestPath(sid) {
42
- return join(tmpdir(), `coldstart-kb-worklist-${sid}.json`);
48
+ export function worklistJsonPath(root) {
49
+ return join(root, ".coldstart", "notebook", ".worklist.json");
50
+ }
51
+ export function worklistMdPath(root) {
52
+ return join(root, ".coldstart", "notebook", ".worklist.md");
43
53
  }
44
54
 
45
- function recordWorklistManifest(sid, entries) {
46
- if (!sid || !entries?.length) return;
55
+ function writeDurableWorklist(root, sid, entries, mdBody) {
56
+ if (!root || !entries?.length) return;
47
57
  try {
48
58
  // needsNote: no note yet, or the note it has is stale. A file whose note is
49
59
  // already fresh is NOT an outstanding item — counting it would make full
@@ -53,7 +63,9 @@ function recordWorklistManifest(sid, entries) {
53
63
  tier: e.tier,
54
64
  needsNote: !e.notes?.length || e.notes.some((n) => n.state === "changed" || n.state === "missing"),
55
65
  }));
56
- writeFileSync(worklistManifestPath(sid), JSON.stringify({ ts: Date.now(), files, wrote: [] }));
66
+ mkdirSync(join(root, ".coldstart", "notebook"), { recursive: true });
67
+ writeFileSync(worklistJsonPath(root), JSON.stringify({ ts: Date.now(), sid, files, wrote: [] }));
68
+ writeFileSync(worklistMdPath(root), mdBody);
57
69
  } catch { /* best-effort */ }
58
70
  }
59
71
 
@@ -101,8 +113,15 @@ function worklistLines(entries) {
101
113
  return lines.join("\n");
102
114
  }
103
115
 
104
- export function buildCapturePayload({ root, cli, sid, entries, envelope }) {
105
- recordWorklistManifest(sid, entries);
116
+ export function buildCapturePayload(args) {
117
+ const payload = renderCapturePayload(args);
118
+ // Persist the WHOLE payload durably so the agent can re-Read it later, and the
119
+ // structured scope for `kb write` coverage. Best-effort; never blocks capture.
120
+ writeDurableWorklist(args.root, args.sid, args.entries, payload);
121
+ return payload;
122
+ }
123
+
124
+ function renderCapturePayload({ root, cli, sid, entries, envelope }) {
106
125
  const opening = envelope === "block"
107
126
  ? "Handle capture now, then stop."
108
127
  : envelope === "manual"
@@ -150,9 +169,10 @@ changed and what you had to understand to change it, and that is precisely the k
150
169
  cold agent lacks. The default for an edited file is a note. Walk the worklist top to bottom \
151
170
  and decide each one explicitly; do not stop at the first two or three. If you end up writing \
152
171
  notes for well under half the [edited] files, you have under-captured — say which files you \
153
- skipped and why, so the decision is visible instead of silent. Each \`kb write\` prints the \
154
- running count ("N of M worklist files noted") and lists what is still unwritten read that \
155
- line back before you finish; it is the only place your own coverage is visible to you.
172
+ skipped and why, so the decision is visible instead of silent. This whole checklist (worklist \
173
+ + the write contract below) is saved at \`.coldstart/notebook/.worklist.md\`re-Read that file \
174
+ any time this session if this scrolls out of context. Your \`kb write\` call ends with a coverage \
175
+ line naming any worked file still without a note; read it back before you finish.
156
176
 
157
177
  WORKLIST — files you actually read this session, most-worked first:
158
178
 
@@ -210,7 +230,11 @@ missing fact is a NEW flow, even in the same subsystem, even across the same fil
210
230
  an unrelated fact into a nearby flow buries it: nobody searching for your fact will find that \
211
231
  title.
212
232
 
213
- WRITE — one Bash block total: specs as heredocs, writes chained with &&.
233
+ WRITE — ONE call: put EVERY note as an object in a JSON array and write the array \
234
+ in a single \`kb write\`. Order flows before the file notes that reference them. Malformed \
235
+ notes are reported together (a bad note never silences a good one), and the call ends with \
236
+ the coverage line. Forming an array is fewer tokens than chaining, not more — same JSON, \
237
+ without the per-note heredoc and && glue.
214
238
  ${shapesBlock({ compact: true })}
215
239
  "identityAliases" and "symbols" are what make a note FINDABLE and both go missing by \
216
240
  default: a note without identityAliases is reachable only by its exact title, and agents \
@@ -227,14 +251,18 @@ however carefully you described it. Never write a flow using the file-note shape
227
251
  with several of its symbols. file-hub is ONLY for grab-bag files that have no single purpose \
228
252
  (models.py, utils, helpers) — there, knowledge lives per symbol. Touching many symbols does \
229
253
  not make a file a hub; having no one purpose does.
230
- Update = the same spec plus "id":"<id from the worklist>".
231
- node ${cli} kb write /tmp/spec1.json --root ${root} --session ${sid} --force
232
- Flow/lesson shapes: run \`node ${cli} kb write --root ${root}\` with no spec — it prints the full guide.
254
+ Update = the same spec plus "id":"<id from the worklist>". Retract a note you \
255
+ found wrong: {"op":"retract","id":"<id>","target":{"kind":"note"}} (as one array element).
256
+ cat > /tmp/notes.json <<'JSON'
257
+ [ {…note 1…}, {…note 2…} ]
258
+ JSON
259
+ node ${cli} kb write /tmp/notes.json --root ${root} --session ${sid} --force
260
+ Full shapes: run \`node ${cli} kb write --root ${root}\` with no spec — it prints the guide.
233
261
 
234
262
  FLOW DECISION — record what you decided about FLOWS (created a new one, folded into an \
235
- existing one, or none) so the flow gate can be measured. Ride it on your LAST kb write — it \
263
+ existing one, or none) so the flow gate can be measured. Ride it on the SAME batch write — it \
236
264
  is a flag, not a second command:
237
- node ${cli} kb write /tmp/specN.json --root ${root} --session ${sid} --force \\
265
+ node ${cli} kb write /tmp/notes.json --root ${root} --session ${sid} --force \\
238
266
  --decision <none|new|update> [--id <flow id>] --why "<one clause>"
239
267
  Only when you wrote NO notes at all is there no write to carry it, and then run it alone:
240
268
  node ${cli} kb flow-decision --decision none --why "<one clause>" --root ${root} --session ${sid}${tail}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cstart/coldstart",
3
- "version": "2.2.13",
3
+ "version": "2.2.15",
4
4
  "mcpName": "io.github.AkashGoenka/coldstart",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,10 +0,0 @@
1
- /** Paths this spec puts a note on. Flow steps do NOT count: a flow verifies
2
- * files, it does not give any of them the file note rule 5 asks for. */
3
- export declare function specPaths(spec: unknown): string[];
4
- /**
5
- * Record this write against the session worklist and return the coverage line
6
- * to print, or null when there is no manifest to compare against (no --session,
7
- * a manual `kb write` outside capture, a session that never armed).
8
- */
9
- export declare function noteCoverage(sid: string | undefined, spec: unknown): string | null;
10
- //# sourceMappingURL=session-worklist.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"session-worklist.d.ts","sourceRoot":"","sources":["../../src/kb/session-worklist.ts"],"names":[],"mappings":"AA8CA;yEACyE;AACzE,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,EAAE,CAIjD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CA8BlF"}
@@ -1,92 +0,0 @@
1
- /**
2
- * Capture coverage — telling the writing agent how much of its worklist it noted.
3
- *
4
- * The gap this closes: the capture checklist hands the agent a worklist of every
5
- * file it worked on, but `kb write` only ever sees ONE spec, so nothing could
6
- * report "you wrote 8 notes for 30 worked files". The agent's own count is the
7
- * one thing it cannot check — it is mid-heredoc, and under-capture is silent by
8
- * construction. So the hook drops the worklist in tmpdir when it builds the
9
- * capture prompt, and every `kb write --session <sid>` prints the running ratio.
10
- *
11
- * CONTRACT TWIN: hooks/capture-payload.mjs (worklistManifestPath) writes this
12
- * file; keep the name and shape in step. Everything here is best-effort — a
13
- * missing, stale or malformed manifest prints nothing and never fails a write.
14
- *
15
- * Local only. The manifest lives in tmpdir, never in the repo: it is scratch
16
- * state for one session, not a record, and nothing about it is transmitted.
17
- */
18
- import { readFileSync, writeFileSync } from 'node:fs';
19
- import { tmpdir } from 'node:os';
20
- import { join } from 'node:path';
21
- /** Manifests older than this are a resumed/unrelated session — ignore them. */
22
- const MAX_AGE_MS = 24 * 60 * 60 * 1000;
23
- /** Below this share of outstanding files, name the skips. */
24
- const LOW_COVERAGE = 0.5;
25
- const LIST_MAX = 6;
26
- function manifestPath(sid) {
27
- return join(tmpdir(), `coldstart-kb-worklist-${sid}.json`);
28
- }
29
- function load(sid) {
30
- try {
31
- const m = JSON.parse(readFileSync(manifestPath(sid), 'utf8'));
32
- if (!Array.isArray(m?.files) || !m.files.length)
33
- return null;
34
- if (typeof m.ts === 'number' && Date.now() - m.ts > MAX_AGE_MS)
35
- return null;
36
- m.wrote = Array.isArray(m.wrote) ? m.wrote : [];
37
- return m;
38
- }
39
- catch {
40
- return null;
41
- }
42
- }
43
- /** Paths this spec puts a note on. Flow steps do NOT count: a flow verifies
44
- * files, it does not give any of them the file note rule 5 asks for. */
45
- export function specPaths(spec) {
46
- const s = spec;
47
- if (!s || typeof s.path !== 'string')
48
- return [];
49
- return s.type === 'file-single' || s.type === 'file-hub' ? [s.path] : [];
50
- }
51
- /**
52
- * Record this write against the session worklist and return the coverage line
53
- * to print, or null when there is no manifest to compare against (no --session,
54
- * a manual `kb write` outside capture, a session that never armed).
55
- */
56
- export function noteCoverage(sid, spec) {
57
- if (!sid)
58
- return null;
59
- const m = load(sid);
60
- if (!m)
61
- return null;
62
- for (const p of specPaths(spec))
63
- if (!m.wrote.includes(p))
64
- m.wrote.push(p);
65
- try {
66
- writeFileSync(manifestPath(sid), JSON.stringify(m));
67
- }
68
- catch { /* best-effort */ }
69
- const outstanding = m.files.filter((f) => f.needsNote);
70
- if (!outstanding.length)
71
- return null;
72
- const wrote = new Set(m.wrote);
73
- const done = outstanding.filter((f) => wrote.has(f.path));
74
- const left = outstanding.filter((f) => !wrote.has(f.path));
75
- const already = m.files.length - outstanding.length;
76
- const lines = [
77
- `capture coverage: ${done.length} of ${outstanding.length} worklist files noted` +
78
- (already ? ` (${already} more already had a fresh note)` : ''),
79
- ];
80
- if (left.length) {
81
- const shown = left.slice(0, LIST_MAX).map((f) => `${f.path} [${f.tier}]`).join(', ');
82
- lines.push(` not noted yet: ${shown}${left.length > LIST_MAX ? `, +${left.length - LIST_MAX} more` : ''}`);
83
- }
84
- // The nudge fires only on the low end, and asks for a REASON rather than more
85
- // notes: a deliberate skip is a fine answer, an invisible one is not. Chained
86
- // writes each print this, so the last line of the block carries the real tally.
87
- if (left.length && done.length / outstanding.length < LOW_COVERAGE) {
88
- lines.push(' if those need no note, say which and why in your reply — an unexplained skip reads as a forgotten one.');
89
- }
90
- return lines.join('\n');
91
- }
92
- //# sourceMappingURL=session-worklist.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"session-worklist.js","sourceRoot":"","sources":["../../src/kb/session-worklist.ts"],"names":[],"mappings":"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"}