@isaacriehm/cairn-core 0.10.4 → 0.11.0

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.
@@ -8,16 +8,16 @@
8
8
  * `decision: block` with an instructional reason that prompts main
9
9
  * Claude to render the question.
10
10
  *
11
- * Threshold defaults to 50 % of the active model's window:
12
- * - claude-opus-* → 1_000_000 tokens, fire at 500_000
13
- * - claude-sonnet-* → 200_000 tokens, fire at 100_000
14
- * - claude-haiku-* → 200_000 tokens, fire at 100_000
15
- * - unknown model → assume Opus shape (1M / 500k threshold)
11
+ * Single source of truth: Claude Code's statusline payload ships a
12
+ * `context_window` block with `total_tokens` (the active model's
13
+ * window — 200k Sonnet, 1M Opus-1m) + `remaining_percentage`. The
14
+ * statusline hook persists those numbers to
15
+ * `.cairn/sessions/<id>/ctx.json` on every prompt. The Stop hook reads
16
+ * that snapshot — there is no model-keyed fallback and no transcript-
17
+ * usage estimator. If CC omits the block, ctx.json is absent or stale
18
+ * and the threshold check stays silent rather than firing on a guess.
16
19
  *
17
- * Token count is estimated from the transcript file size (`bytes / 4`)
18
- * — overcounts a little on JSON whitespace, undercounts on unicode-
19
- * heavy turns. Good enough to fire near the threshold; not a budget
20
- * check.
20
+ * Threshold defaults to 50 % of CC's reported window.
21
21
  *
22
22
  * Suppress re-fire within the same session by stamping
23
23
  * `.cairn/sessions/<id>/ctx-threshold-warned.json`. Once stamped, the
@@ -25,54 +25,22 @@
25
25
  * past the last warning.
26
26
  */
27
27
  export interface ContextThresholdInput {
28
- transcriptPath: string | null;
29
28
  repoRoot: string;
30
29
  sessionId: string;
31
- /** Override the model lookup (rarely needed). */
32
- modelOverride?: string | null;
33
30
  /** Override the threshold fraction (default 0.5). */
34
31
  thresholdFraction?: number;
35
- /** Override the window size in tokens (default keyed on model). */
36
- windowOverride?: number;
37
32
  }
38
33
  export interface ContextThresholdHit {
39
34
  hit: true;
40
- estimatedTokens: number;
35
+ usedTokens: number;
41
36
  windowTokens: number;
42
37
  pct: number;
43
- model: string;
44
38
  taskId: string | null;
45
39
  }
46
40
  export interface ContextThresholdMiss {
47
41
  hit: false;
48
42
  }
49
43
  export type ContextThresholdResult = ContextThresholdHit | ContextThresholdMiss;
50
- export declare function modelWindow(model: string): number;
51
- /**
52
- * Walk the last ~64 KB of the transcript looking for the most recent
53
- * `model` field. Claude Code transcript lines are JSON; each assistant
54
- * turn carries a `message.model` string. Skipping the full file keeps
55
- * the hook fast on long sessions.
56
- */
57
- export declare function readModelFromTranscript(path: string): string | null;
58
- /**
59
- * Fallback estimator when no persisted ctx snapshot is available.
60
- *
61
- * Walks the last ~256 KB of the transcript backward looking for the
62
- * most recent `message.usage` block from an assistant turn. Claude
63
- * Code transcript lines are JSON; assistant turns carry an exact token
64
- * count under `message.usage.{input_tokens, cache_creation_input_tokens,
65
- * cache_read_input_tokens}` — summing those three gives the actual
66
- * prompt size at that turn (output_tokens are produced, not consumed
67
- * by context).
68
- *
69
- * Returns null when no usage block is found (e.g. fresh session, no
70
- * assistant turn yet). Caller can choose to skip threshold check or
71
- * fall back to bytes/4. We never fall back here — bytes/4 over-counts
72
- * 1.5–2x because the transcript JSONL accumulates every tool I/O blob
73
- * since session start, which is what produced the 113%-of-window bug.
74
- */
75
- export declare function estimateTokensFromTranscript(transcriptPath: string): number | null;
76
44
  /**
77
45
  * Returns the current threshold result. Stamps the warned-state file
78
46
  * on a hit so re-fires within the same session are suppressed until
@@ -8,79 +8,31 @@
8
8
  * `decision: block` with an instructional reason that prompts main
9
9
  * Claude to render the question.
10
10
  *
11
- * Threshold defaults to 50 % of the active model's window:
12
- * - claude-opus-* → 1_000_000 tokens, fire at 500_000
13
- * - claude-sonnet-* → 200_000 tokens, fire at 100_000
14
- * - claude-haiku-* → 200_000 tokens, fire at 100_000
15
- * - unknown model → assume Opus shape (1M / 500k threshold)
11
+ * Single source of truth: Claude Code's statusline payload ships a
12
+ * `context_window` block with `total_tokens` (the active model's
13
+ * window — 200k Sonnet, 1M Opus-1m) + `remaining_percentage`. The
14
+ * statusline hook persists those numbers to
15
+ * `.cairn/sessions/<id>/ctx.json` on every prompt. The Stop hook reads
16
+ * that snapshot — there is no model-keyed fallback and no transcript-
17
+ * usage estimator. If CC omits the block, ctx.json is absent or stale
18
+ * and the threshold check stays silent rather than firing on a guess.
16
19
  *
17
- * Token count is estimated from the transcript file size (`bytes / 4`)
18
- * — overcounts a little on JSON whitespace, undercounts on unicode-
19
- * heavy turns. Good enough to fire near the threshold; not a budget
20
- * check.
20
+ * Threshold defaults to 50 % of CC's reported window.
21
21
  *
22
22
  * Suppress re-fire within the same session by stamping
23
23
  * `.cairn/sessions/<id>/ctx-threshold-warned.json`. Once stamped, the
24
24
  * threshold prompt re-fires only when usage climbs another +10 %
25
25
  * past the last warning.
26
26
  */
27
- import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
27
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
28
28
  import { join } from "node:path";
29
- const MODEL_WINDOW_FALLBACK = 1_000_000;
30
- export function modelWindow(model) {
31
- // Opus 4.7 ships with a 1M-token window when the `[1m]` variant is
32
- // selected; the base alias still resolves to 200k. Treat any model
33
- // string that contains `1m` (the variant suffix Claude Code prints)
34
- // as the 1M tier so the statusline percentage reads correctly.
35
- if (/1m/i.test(model))
36
- return 1_000_000;
37
- if (/opus/i.test(model))
38
- return 1_000_000;
39
- if (/sonnet/i.test(model))
40
- return 200_000;
41
- if (/haiku/i.test(model))
42
- return 200_000;
43
- return MODEL_WINDOW_FALLBACK;
44
- }
45
- /**
46
- * Walk the last ~64 KB of the transcript looking for the most recent
47
- * `model` field. Claude Code transcript lines are JSON; each assistant
48
- * turn carries a `message.model` string. Skipping the full file keeps
49
- * the hook fast on long sessions.
50
- */
51
- export function readModelFromTranscript(path) {
52
- try {
53
- const stat = statSync(path);
54
- const tail = Math.min(stat.size, 65_536);
55
- const fd = readFileSync(path, "utf8");
56
- const slice = fd.slice(Math.max(0, fd.length - tail));
57
- const lines = slice.split(/\r?\n/);
58
- for (let i = lines.length - 1; i >= 0; i--) {
59
- const line = lines[i];
60
- if (line === undefined || line.length === 0)
61
- continue;
62
- try {
63
- const obj = JSON.parse(line);
64
- const m = obj.message?.model;
65
- if (typeof m === "string" && m.length > 0)
66
- return m;
67
- }
68
- catch {
69
- // skip malformed lines
70
- }
71
- }
72
- }
73
- catch {
74
- return null;
75
- }
76
- return null;
77
- }
78
29
  const CTX_SNAPSHOT_STALE_MS = 5 * 60 * 1000;
79
30
  /**
80
31
  * Read the latest persisted ctx snapshot from the statusline writer.
81
32
  * Statusline runs on every prompt so a fresh snapshot is normally
82
33
  * <1s old. Returns null when missing, malformed, or older than 5min
83
- * (e.g. session crashed, statusline hook misconfigured).
34
+ * (e.g. session crashed, statusline hook misconfigured, or CC did
35
+ * not ship a `context_window` block on the last prompt).
84
36
  */
85
37
  function readPersistedCtx(repoRoot, sessionId) {
86
38
  const path = join(repoRoot, ".cairn", "sessions", sessionId, "ctx.json");
@@ -92,6 +44,8 @@ function readPersistedCtx(repoRoot, sessionId) {
92
44
  parsed !== null &&
93
45
  typeof parsed.usedPct === "number" &&
94
46
  typeof parsed.usedTokens === "number" &&
47
+ typeof parsed.windowTokens === "number" &&
48
+ parsed.windowTokens > 0 &&
95
49
  typeof parsed.ts === "number") {
96
50
  if (Date.now() - parsed.ts > CTX_SNAPSHOT_STALE_MS)
97
51
  return null;
@@ -103,60 +57,6 @@ function readPersistedCtx(repoRoot, sessionId) {
103
57
  }
104
58
  return null;
105
59
  }
106
- /**
107
- * Fallback estimator when no persisted ctx snapshot is available.
108
- *
109
- * Walks the last ~256 KB of the transcript backward looking for the
110
- * most recent `message.usage` block from an assistant turn. Claude
111
- * Code transcript lines are JSON; assistant turns carry an exact token
112
- * count under `message.usage.{input_tokens, cache_creation_input_tokens,
113
- * cache_read_input_tokens}` — summing those three gives the actual
114
- * prompt size at that turn (output_tokens are produced, not consumed
115
- * by context).
116
- *
117
- * Returns null when no usage block is found (e.g. fresh session, no
118
- * assistant turn yet). Caller can choose to skip threshold check or
119
- * fall back to bytes/4. We never fall back here — bytes/4 over-counts
120
- * 1.5–2x because the transcript JSONL accumulates every tool I/O blob
121
- * since session start, which is what produced the 113%-of-window bug.
122
- */
123
- export function estimateTokensFromTranscript(transcriptPath) {
124
- try {
125
- const stat = statSync(transcriptPath);
126
- const tail = Math.min(stat.size, 262_144);
127
- const fd = readFileSync(transcriptPath, "utf8");
128
- const slice = fd.slice(Math.max(0, fd.length - tail));
129
- const lines = slice.split(/\r?\n/);
130
- for (let i = lines.length - 1; i >= 0; i--) {
131
- const line = lines[i];
132
- if (line === undefined || line.length === 0)
133
- continue;
134
- try {
135
- const obj = JSON.parse(line);
136
- const u = obj.message?.usage;
137
- if (u === undefined)
138
- continue;
139
- const i_t = typeof u.input_tokens === "number" ? u.input_tokens : 0;
140
- const cc = typeof u.cache_creation_input_tokens === "number"
141
- ? u.cache_creation_input_tokens
142
- : 0;
143
- const cr = typeof u.cache_read_input_tokens === "number"
144
- ? u.cache_read_input_tokens
145
- : 0;
146
- const total = i_t + cc + cr;
147
- if (total > 0)
148
- return total;
149
- }
150
- catch {
151
- // skip malformed lines
152
- }
153
- }
154
- }
155
- catch {
156
- return null;
157
- }
158
- return null;
159
- }
160
60
  function warnedStatePath(repoRoot, sessionId) {
161
61
  return join(repoRoot, ".cairn", "sessions", sessionId, "ctx-threshold-warned.json");
162
62
  }
@@ -192,39 +92,28 @@ function writeWarned(repoRoot, sessionId, state) {
192
92
  * usage climbs another +10 % of the window.
193
93
  */
194
94
  export function checkContextThreshold(input) {
195
- if (input.transcriptPath === null || input.transcriptPath.length === 0) {
196
- return { hit: false };
197
- }
198
- if (!existsSync(input.transcriptPath))
95
+ const snapshot = readPersistedCtx(input.repoRoot, input.sessionId);
96
+ if (snapshot === null)
199
97
  return { hit: false };
200
- const model = input.modelOverride ?? readModelFromTranscript(input.transcriptPath) ?? "unknown";
201
- const windowTokens = input.windowOverride ?? modelWindow(model);
98
+ const windowTokens = snapshot.windowTokens;
202
99
  const fraction = input.thresholdFraction ?? 0.5;
203
100
  const thresholdTokens = Math.floor(windowTokens * fraction);
204
- const snapshot = readPersistedCtx(input.repoRoot, input.sessionId);
205
- const fallback = estimateTokensFromTranscript(input.transcriptPath);
206
- // Skip the check when neither source is available — better to stay
207
- // silent than fire on bytes/4 garbage like we used to.
208
- if (snapshot === null && fallback === null)
209
- return { hit: false };
210
- const estimated = snapshot !== null ? snapshot.usedTokens : (fallback ?? 0);
211
- if (estimated < thresholdTokens)
101
+ if (snapshot.usedTokens < thresholdTokens)
212
102
  return { hit: false };
213
103
  const warned = readWarned(input.repoRoot, input.sessionId);
214
104
  const reFireSlackTokens = Math.floor(windowTokens * 0.1);
215
- if (warned !== null && estimated < warned.warned_at_tokens + reFireSlackTokens) {
105
+ if (warned !== null && snapshot.usedTokens < warned.warned_at_tokens + reFireSlackTokens) {
216
106
  return { hit: false };
217
107
  }
218
108
  writeWarned(input.repoRoot, input.sessionId, {
219
109
  ts: Date.now(),
220
- warned_at_tokens: estimated,
110
+ warned_at_tokens: snapshot.usedTokens,
221
111
  });
222
112
  return {
223
113
  hit: true,
224
- estimatedTokens: estimated,
114
+ usedTokens: snapshot.usedTokens,
225
115
  windowTokens,
226
- pct: Math.min(100, Math.round((estimated / windowTokens) * 100)),
227
- model,
116
+ pct: Math.min(100, Math.round((snapshot.usedTokens / windowTokens) * 100)),
228
117
  taskId: null,
229
118
  };
230
119
  }
@@ -242,7 +131,7 @@ export function renderContextThresholdHint(hit, taskId) {
242
131
  const header = [
243
132
  `## Cairn — context threshold reached`,
244
133
  "",
245
- `Estimated **${hit.estimatedTokens.toLocaleString()} / ${hit.windowTokens.toLocaleString()} tokens (${hit.pct}%)** for \`${hit.model}\`. Trust degrades as context climbs — best to compact now.`,
134
+ `**${hit.usedTokens.toLocaleString()} / ${hit.windowTokens.toLocaleString()} tokens (${hit.pct}%)** in use. Trust degrades as context climbs — best to compact now.`,
246
135
  "",
247
136
  ];
248
137
  if (taskId === null) {
@@ -252,7 +141,7 @@ export function renderContextThresholdHint(hit, taskId) {
252
141
  "",
253
142
  "Render this question via the `AskUserQuestion` tool — do not skip:",
254
143
  "",
255
- "> Context at " + hit.pct + "% of " + hit.model + " window. Pick:",
144
+ "> Context at " + hit.pct + "% of window. Pick:",
256
145
  "> ",
257
146
  "> - `[a]` keep going (warn re-fires every +10 %)",
258
147
  "> - `[b]` `/clear` and start fresh (no task to resume)",
@@ -266,7 +155,7 @@ export function renderContextThresholdHint(hit, taskId) {
266
155
  "",
267
156
  "Render this question via the `AskUserQuestion` tool — do not skip:",
268
157
  "",
269
- "> Context at " + hit.pct + "% of " + hit.model + " window. Pick:",
158
+ "> Context at " + hit.pct + "% of window. Pick:",
270
159
  "> ",
271
160
  "> - `[a]` keep going (warn re-fires every +10 %)",
272
161
  "> - `[b]` `/clear` and resume now (Cairn writes the resume prompt)",
@@ -1 +1 @@
1
- {"version":3,"file":"context-threshold.js","sourceRoot":"","sources":["../../../src/hooks/runners/context-threshold.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"context-threshold.js","sourceRoot":"","sources":["../../../src/hooks/runners/context-threshold.ts"],"names":[],"mappings":"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"}
@@ -6,7 +6,6 @@
6
6
  export { runSessionStartHook } from "./session-start.js";
7
7
  export { runSessionEndHook } from "./session-end.js";
8
8
  export { runStopHook } from "./stop.js";
9
- export { estimateTokensFromTranscript, modelWindow, readModelFromTranscript, } from "./context-threshold.js";
10
9
  export { runUserPromptSubmitHook } from "./user-prompt-submit.js";
11
10
  export { runGcAutotriggerCheck } from "./gc-autotrigger.js";
12
11
  export type { GcAutotriggerArgv, GcAutotriggerOptions, GcAutotriggerReason, GcAutotriggerResult, } from "./gc-autotrigger.js";
@@ -6,7 +6,6 @@
6
6
  export { runSessionStartHook } from "./session-start.js";
7
7
  export { runSessionEndHook } from "./session-end.js";
8
8
  export { runStopHook } from "./stop.js";
9
- export { estimateTokensFromTranscript, modelWindow, readModelFromTranscript, } from "./context-threshold.js";
10
9
  export { runUserPromptSubmitHook } from "./user-prompt-submit.js";
11
10
  export { runGcAutotriggerCheck } from "./gc-autotrigger.js";
12
11
  export { renderBypassHint, scanBypassedCommits, } from "../bypass-detection.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/hooks/runners/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EACL,4BAA4B,EAC5B,WAAW,EACX,uBAAuB,GACxB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAO5D,OAAO,EACL,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,wBAAwB,CAAC;AAKhC,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,EACL,kBAAkB,EAClB,UAAU,EACV,gBAAgB,EAChB,aAAa,EACb,eAAe,GAChB,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/hooks/runners/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAO5D,OAAO,EACL,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,wBAAwB,CAAC;AAKhC,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,EACL,kBAAkB,EAClB,UAAU,EACV,gBAAgB,EAChB,aAAa,EACb,eAAe,GAChB,MAAM,cAAc,CAAC"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Phase-ready surface relocation.
3
+ *
4
+ * `phase-ready-to-exit` events used to inject straight into the Stop
5
+ * hook's `decision: block` reason. Claude Code labels every Stop
6
+ * decision-block as "Stop hook error" in the operator UI — even when
7
+ * the block carries informational Cairn context — which the operator
8
+ * reads as a real failure. That visual signal is what we wanted to
9
+ * avoid, so the surface moved off Stop entirely:
10
+ *
11
+ * - Stop hook collects phase-ready hints from drained events as
12
+ * before, but writes them to
13
+ * `.cairn/sessions/<id>/phase-ready-pending.json` instead of
14
+ * emitting `decision: block`. If no other surface is pending,
15
+ * Stop returns `{continue: true}` and the operator sees no
16
+ * banner.
17
+ * - UserPromptSubmit hook, fired on the next operator prompt,
18
+ * reads the pending file, renders it as `additionalContext`,
19
+ * and deletes the file. Claude Code stitches the
20
+ * `additionalContext` straight into the model's next turn — no
21
+ * red banner, no `decision: block`.
22
+ *
23
+ * Idempotency for the phase-ready emission itself is handled upstream
24
+ * in `task-link.ts` via the per-phase `ready_emitted` flag on
25
+ * `phase_progress`. This module is purely the Stop → UPS shuttle.
26
+ */
27
+ export interface PhaseReadyHint {
28
+ mission_id: string;
29
+ mission_title: string;
30
+ phase_id: string;
31
+ phase_title: string;
32
+ exit_criteria: string;
33
+ }
34
+ /**
35
+ * Persist the hints the Stop hook collected so the next UPS hook can
36
+ * inject them as additionalContext. Overwrites any prior pending file
37
+ * for the session — the latest hint set wins (the operator hasn't
38
+ * seen the old one yet so there's nothing to merge).
39
+ */
40
+ export declare function writePhaseReadyPending(repoRoot: string, sessionId: string, hints: PhaseReadyHint[]): void;
41
+ /**
42
+ * Read the pending file and delete it in one shot — UPS hook semantics
43
+ * are "show once, then forget". When the file is missing or malformed
44
+ * returns null so the caller can no-op cleanly.
45
+ */
46
+ export declare function readAndConsumePhaseReadyPending(repoRoot: string, sessionId: string): PhaseReadyHint[] | null;
47
+ /**
48
+ * Render the operator-facing phase-ready prompt. Keeps option labels
49
+ * in plain English — no MCP tool-call syntax, no internal ids exposed
50
+ * raw — and instructs the model to surface via `AskUserQuestion`.
51
+ *
52
+ * The mission/phase ids stay in the body for traceability but the
53
+ * AskUserQuestion option labels themselves are human-readable phase
54
+ * titles, not phase ids.
55
+ */
56
+ export declare function renderPhaseReadyHint(hints: PhaseReadyHint[]): string;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Phase-ready surface relocation.
3
+ *
4
+ * `phase-ready-to-exit` events used to inject straight into the Stop
5
+ * hook's `decision: block` reason. Claude Code labels every Stop
6
+ * decision-block as "Stop hook error" in the operator UI — even when
7
+ * the block carries informational Cairn context — which the operator
8
+ * reads as a real failure. That visual signal is what we wanted to
9
+ * avoid, so the surface moved off Stop entirely:
10
+ *
11
+ * - Stop hook collects phase-ready hints from drained events as
12
+ * before, but writes them to
13
+ * `.cairn/sessions/<id>/phase-ready-pending.json` instead of
14
+ * emitting `decision: block`. If no other surface is pending,
15
+ * Stop returns `{continue: true}` and the operator sees no
16
+ * banner.
17
+ * - UserPromptSubmit hook, fired on the next operator prompt,
18
+ * reads the pending file, renders it as `additionalContext`,
19
+ * and deletes the file. Claude Code stitches the
20
+ * `additionalContext` straight into the model's next turn — no
21
+ * red banner, no `decision: block`.
22
+ *
23
+ * Idempotency for the phase-ready emission itself is handled upstream
24
+ * in `task-link.ts` via the per-phase `ready_emitted` flag on
25
+ * `phase_progress`. This module is purely the Stop → UPS shuttle.
26
+ */
27
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
28
+ import { dirname, join } from "node:path";
29
+ function pendingPath(repoRoot, sessionId) {
30
+ return join(repoRoot, ".cairn", "sessions", sessionId, "phase-ready-pending.json");
31
+ }
32
+ /**
33
+ * Persist the hints the Stop hook collected so the next UPS hook can
34
+ * inject them as additionalContext. Overwrites any prior pending file
35
+ * for the session — the latest hint set wins (the operator hasn't
36
+ * seen the old one yet so there's nothing to merge).
37
+ */
38
+ export function writePhaseReadyPending(repoRoot, sessionId, hints) {
39
+ if (hints.length === 0)
40
+ return;
41
+ const path = pendingPath(repoRoot, sessionId);
42
+ try {
43
+ mkdirSync(dirname(path), { recursive: true });
44
+ const payload = {
45
+ ts: new Date().toISOString(),
46
+ session_id: sessionId,
47
+ hints,
48
+ };
49
+ writeFileSync(path, JSON.stringify(payload, null, 2), "utf8");
50
+ }
51
+ catch {
52
+ // best-effort
53
+ }
54
+ }
55
+ /**
56
+ * Read the pending file and delete it in one shot — UPS hook semantics
57
+ * are "show once, then forget". When the file is missing or malformed
58
+ * returns null so the caller can no-op cleanly.
59
+ */
60
+ export function readAndConsumePhaseReadyPending(repoRoot, sessionId) {
61
+ const path = pendingPath(repoRoot, sessionId);
62
+ if (!existsSync(path))
63
+ return null;
64
+ let raw;
65
+ try {
66
+ raw = readFileSync(path, "utf8");
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ let parsed;
72
+ try {
73
+ parsed = JSON.parse(raw);
74
+ }
75
+ catch {
76
+ try {
77
+ unlinkSync(path);
78
+ }
79
+ catch { /* ignore */ }
80
+ return null;
81
+ }
82
+ if (typeof parsed !== "object" || parsed === null) {
83
+ try {
84
+ unlinkSync(path);
85
+ }
86
+ catch { /* ignore */ }
87
+ return null;
88
+ }
89
+ const p = parsed;
90
+ if (!Array.isArray(p.hints) || p.hints.length === 0) {
91
+ try {
92
+ unlinkSync(path);
93
+ }
94
+ catch { /* ignore */ }
95
+ return null;
96
+ }
97
+ try {
98
+ unlinkSync(path);
99
+ }
100
+ catch { /* ignore */ }
101
+ return p.hints;
102
+ }
103
+ /**
104
+ * Render the operator-facing phase-ready prompt. Keeps option labels
105
+ * in plain English — no MCP tool-call syntax, no internal ids exposed
106
+ * raw — and instructs the model to surface via `AskUserQuestion`.
107
+ *
108
+ * The mission/phase ids stay in the body for traceability but the
109
+ * AskUserQuestion option labels themselves are human-readable phase
110
+ * titles, not phase ids.
111
+ */
112
+ export function renderPhaseReadyHint(hints) {
113
+ const h = hints[0];
114
+ if (h === undefined)
115
+ return "";
116
+ const lines = [];
117
+ lines.push(`## Cairn — phase ready to exit`);
118
+ lines.push("");
119
+ lines.push(`**Mission:** ${h.mission_title}`);
120
+ lines.push(`**Phase:** ${h.phase_title}`);
121
+ lines.push("");
122
+ lines.push(`Exit criteria: ${h.exit_criteria}`);
123
+ lines.push("");
124
+ lines.push("Surface this question to the operator via `AskUserQuestion`. Do NOT call `cairn_mission_advance` yourself — the operator's answer is the only valid input.");
125
+ lines.push("");
126
+ lines.push(`> Phase \`${h.phase_title}\` looks done. Move on?`);
127
+ lines.push(">");
128
+ lines.push("> - `[a]` Mark phase done, advance to next phase");
129
+ lines.push("> - `[b]` Keep working on this phase");
130
+ lines.push("");
131
+ lines.push(`On \`[a]\`, call \`cairn_mission_advance({phase_id: "${h.phase_id}", choice: "exit"})\`. On \`[b]\`, call \`cairn_mission_advance({phase_id: "${h.phase_id}", choice: "not_yet"})\`.`);
132
+ return lines.join("\n");
133
+ }
134
+ //# sourceMappingURL=phase-ready-surface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"phase-ready-surface.js","sourceRoot":"","sources":["../../../src/hooks/runners/phase-ready-surface.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACzF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAgB1C,SAAS,WAAW,CAAC,QAAgB,EAAE,SAAiB;IACtD,OAAO,IAAI,CACT,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,SAAS,EACT,0BAA0B,CAC3B,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CACpC,QAAgB,EAChB,SAAiB,EACjB,KAAuB;IAEvB,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAC/B,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IAC9C,IAAI,CAAC;QACH,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9C,MAAM,OAAO,GAAgB;YAC3B,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YAC5B,UAAU,EAAE,SAAS;YACrB,KAAK;SACN,CAAC;QACF,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACP,cAAc;IAChB,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,+BAA+B,CAC7C,QAAgB,EAChB,SAAiB;IAEjB,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IAC9C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,CAAC;YAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;QAChD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QAClD,IAAI,CAAC;YAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;QAChD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,CAAC,GAAG,MAA8B,CAAC;IACzC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpD,IAAI,CAAC;YAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;QAChD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC;QAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;IAChD,OAAO,CAAC,CAAC,KAAyB,CAAC;AACrC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAuB;IAC1D,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IACnB,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IAE/B,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAC;IAC7C,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC;IAC9C,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;IAC1C,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC;IAChD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CACR,4JAA4J,CAC7J,CAAC;IACF,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,WAAW,yBAAyB,CAAC,CAAC;IAChE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAChB,KAAK,CAAC,IAAI,CAAC,kDAAkD,CAAC,CAAC;IAC/D,KAAK,CAAC,IAAI,CAAC,sCAAsC,CAAC,CAAC;IACnD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CACR,wDAAwD,CAAC,CAAC,QAAQ,+EAA+E,CAAC,CAAC,QAAQ,2BAA2B,CACvL,CAAC;IAEF,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}