@cstart/coldstart 2.1.1 → 2.2.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.
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +32 -0
- package/dist/cli.js.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +1 -6
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +45 -1
- package/dist/init.js.map +1 -1
- package/dist/kb/cli.d.ts.map +1 -1
- package/dist/kb/cli.js +11 -4
- package/dist/kb/cli.js.map +1 -1
- package/dist/kb/lint.d.ts +6 -1
- package/dist/kb/lint.d.ts.map +1 -1
- package/dist/kb/lint.js +28 -0
- package/dist/kb/lint.js.map +1 -1
- package/dist/kb/write-guide.d.ts +15 -0
- package/dist/kb/write-guide.d.ts.map +1 -0
- package/dist/kb/write-guide.js +112 -0
- package/dist/kb/write-guide.js.map +1 -0
- package/dist/server/mcp.d.ts +5 -1
- package/dist/server/mcp.d.ts.map +1 -1
- package/dist/server/mcp.js +15 -4
- package/dist/server/mcp.js.map +1 -1
- package/dist/unwire.d.ts.map +1 -1
- package/dist/unwire.js +2 -0
- package/dist/unwire.js.map +1 -1
- package/hooks/capture-payload.mjs +158 -0
- package/hooks/codex-kb-elicit.mjs +77 -272
- package/hooks/cursor-kb-elicit.mjs +121 -266
- package/hooks/cursor-kb-recall.mjs +39 -15
- package/hooks/elicit-core.mjs +126 -0
- package/hooks/evidence.mjs +318 -0
- package/hooks/ignore.mjs +84 -0
- package/hooks/kb-elicit.mjs +114 -288
- package/hooks/kb-recall.mjs +43 -17
- package/hooks/trigger.mjs +140 -0
- package/package.json +1 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trigger.mjs — the capture arming/firing state machine. Pure: no I/O, no
|
|
3
|
+
* clock, no client specifics. The hook feeds it one stop's observations and
|
|
4
|
+
* it returns the updated state + a fire decision.
|
|
5
|
+
*
|
|
6
|
+
* FROZEN SPEC (2026-07-15, replay + wave-lab validated):
|
|
7
|
+
* score = uncaptured contentRead files ×1
|
|
8
|
+
* + settled edited files ×2 (settled = 3 active stops w/o re-edit)
|
|
9
|
+
* + active stops since last fire (synthesis turns count as active)
|
|
10
|
+
* fresh-noted files contribute NOTHING — genuinely new knowledge drives firing.
|
|
11
|
+
* arm at score ≥ T(10), requiring ≥2 active stops AND ≥2 uncaptured files.
|
|
12
|
+
* fire armed + descent (2 quiet stops) → non-blocking (inject)
|
|
13
|
+
* armed + surge (≥2 new files after a quiet) → non-blocking (inject)
|
|
14
|
+
* score ≥ CAP(20) → non-blocking (backlog
|
|
15
|
+
* rescue — replay showed dense sessions starve descent and hit cap
|
|
16
|
+
* repeatedly; blocking each cap re-created the v4 agitation)
|
|
17
|
+
* .git/HEAD drift, ≥2 uncaptured files → BLOCKING (instant —
|
|
18
|
+
* the one boundary where waiting for a next prompt loses the moment)
|
|
19
|
+
* NEVER: first-stop fire, wall-clock, gap/resume, conversation classification.
|
|
20
|
+
*
|
|
21
|
+
* State lives in the session marker (JSON-serializable, owned by the caller).
|
|
22
|
+
* Files enter state ONLY if they passed the ignore filter and have contentRead
|
|
23
|
+
* evidence — mentions and ignored files never arm anything.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export const T_ARM = 10;
|
|
27
|
+
export const T_CAP = 20;
|
|
28
|
+
export const SETTLE_ACTIVE_STOPS = 3;
|
|
29
|
+
export const DESCENT_QUIET = 2;
|
|
30
|
+
export const SURGE_NEW_FILES = 2;
|
|
31
|
+
export const MIN_FILES = 2;
|
|
32
|
+
|
|
33
|
+
export function initialState() {
|
|
34
|
+
return {
|
|
35
|
+
v: 2,
|
|
36
|
+
stop: 0, // stops processed
|
|
37
|
+
activeStops: 0, // ACTIVE stops since last fire
|
|
38
|
+
quietRun: 0, // consecutive quiet stops
|
|
39
|
+
wasQuiet: false, // previous stop was quiet (surge detection)
|
|
40
|
+
armed: false,
|
|
41
|
+
fires: 0,
|
|
42
|
+
lineCount: 0, // transcript lines already consumed (caller-owned)
|
|
43
|
+
head: "", // .git/HEAD fingerprint at last stop (caller-owned)
|
|
44
|
+
files: {}, // rel → { reads, edits, gs, firstStop, lastStop,
|
|
45
|
+
// lastEditActive, retouches, captured, fresh }
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* step(state, obs) → { state, decision }
|
|
51
|
+
* obs = {
|
|
52
|
+
* delta: Map/obj rel → {reads, edits, gs} (this stop's NEW evidence,
|
|
53
|
+
* ignore-filtered, contentRead tiers only — no mentions),
|
|
54
|
+
* synthesis: bool (prose-heavy tool-light segment),
|
|
55
|
+
* freshNoted: Set<rel> (files whose note is currently fresh),
|
|
56
|
+
* headDrift: bool (.git/HEAD changed since last stop — manual commits too),
|
|
57
|
+
* }
|
|
58
|
+
* decision = null | { fire: "descent"|"surge"|"cap"|"head-drift",
|
|
59
|
+
* mode: "inject"|"block", files: [rel…] }
|
|
60
|
+
*/
|
|
61
|
+
export function step(state, obs) {
|
|
62
|
+
const s = state;
|
|
63
|
+
s.stop++;
|
|
64
|
+
|
|
65
|
+
// ---- merge this stop's evidence -------------------------------------------
|
|
66
|
+
let newFiles = 0;
|
|
67
|
+
let editsThisStop = 0;
|
|
68
|
+
const entries = obs.delta instanceof Map ? obs.delta.entries() : Object.entries(obs.delta || {});
|
|
69
|
+
const freshNoted = obs.freshNoted || new Set();
|
|
70
|
+
for (const [rel, d] of entries) {
|
|
71
|
+
let f = s.files[rel];
|
|
72
|
+
if (!f) {
|
|
73
|
+
// A file whose note is currently FRESH contributes nothing to the score
|
|
74
|
+
// (genuinely new knowledge drives firing). An edit clears the discount:
|
|
75
|
+
// the note no longer covers what the file just became.
|
|
76
|
+
f = { reads: 0, edits: 0, gs: 0, firstStop: s.stop, lastStop: 0, lastEditActive: -1, retouches: 0, captured: false, fresh: freshNoted.has(rel) };
|
|
77
|
+
s.files[rel] = f;
|
|
78
|
+
newFiles++;
|
|
79
|
+
} else if (f.lastStop !== s.stop && f.lastStop < s.stop) {
|
|
80
|
+
f.retouches++;
|
|
81
|
+
}
|
|
82
|
+
f.reads += d.reads || 0;
|
|
83
|
+
f.gs += d.gs || 0;
|
|
84
|
+
if (d.edits) {
|
|
85
|
+
f.edits += d.edits;
|
|
86
|
+
editsThisStop++;
|
|
87
|
+
f.fresh = false;
|
|
88
|
+
f.captured = false; // re-edit after a capture = new knowledge to capture again
|
|
89
|
+
f.lastEditActive = -2; // provisional: fixed up after active-stop accounting below
|
|
90
|
+
}
|
|
91
|
+
f.lastStop = s.stop;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---- active vs quiet --------------------------------------------------------
|
|
95
|
+
const active = newFiles > 0 || editsThisStop > 0 || obs.synthesis === true;
|
|
96
|
+
if (active) { s.activeStops++; s.quietRun = 0; } else { s.quietRun++; }
|
|
97
|
+
for (const f of Object.values(s.files)) {
|
|
98
|
+
if (f.lastEditActive === -2) f.lastEditActive = s.activeStops; // edit stamped at current active count
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---- score -------------------------------------------------------------------
|
|
102
|
+
const uncaptured = Object.entries(s.files).filter(([, f]) => !f.captured && !f.fresh);
|
|
103
|
+
const readPts = uncaptured.length;
|
|
104
|
+
const settledEdits = uncaptured.filter(([, f]) =>
|
|
105
|
+
f.edits > 0 && f.lastEditActive >= 0 && s.activeStops - f.lastEditActive >= SETTLE_ACTIVE_STOPS,
|
|
106
|
+
).length;
|
|
107
|
+
const score = readPts + settledEdits * 2 + s.activeStops;
|
|
108
|
+
|
|
109
|
+
// ---- arm / fire ----------------------------------------------------------------
|
|
110
|
+
if (!s.armed && score >= T_ARM && s.activeStops >= 2 && uncaptured.length >= MIN_FILES) {
|
|
111
|
+
s.armed = true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let fire = null;
|
|
115
|
+
if (obs.headDrift && uncaptured.length >= MIN_FILES) {
|
|
116
|
+
fire = { fire: "head-drift", mode: "block" };
|
|
117
|
+
} else if (score >= T_CAP && uncaptured.length >= MIN_FILES) {
|
|
118
|
+
fire = { fire: "cap", mode: "inject" };
|
|
119
|
+
} else if (s.armed && s.quietRun >= DESCENT_QUIET) {
|
|
120
|
+
fire = { fire: "descent", mode: "inject" };
|
|
121
|
+
} else if (s.armed && s.wasQuiet && newFiles >= SURGE_NEW_FILES) {
|
|
122
|
+
fire = { fire: "surge", mode: "inject" };
|
|
123
|
+
}
|
|
124
|
+
s.wasQuiet = !active;
|
|
125
|
+
|
|
126
|
+
let decision = null;
|
|
127
|
+
if (fire) {
|
|
128
|
+
// worklist = the uncaptured set, most-worked first (edits, then retouches)
|
|
129
|
+
const files = uncaptured
|
|
130
|
+
.sort((a, b) => (b[1].edits - a[1].edits) || (b[1].retouches - a[1].retouches) || (b[1].reads - a[1].reads))
|
|
131
|
+
.map(([rel]) => rel);
|
|
132
|
+
for (const [, f] of uncaptured) f.captured = true;
|
|
133
|
+
s.armed = false;
|
|
134
|
+
s.activeStops = 0;
|
|
135
|
+
s.quietRun = 0;
|
|
136
|
+
s.fires++;
|
|
137
|
+
decision = { ...fire, files, score };
|
|
138
|
+
}
|
|
139
|
+
return { state: s, decision };
|
|
140
|
+
}
|