@dadado/agent-kit-cli 5.15.1 → 5.17.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.
Files changed (72) hide show
  1. package/dashboard/dashboard-data.mjs +69 -65
  2. package/dashboard/lib/guards.d.mts +5 -0
  3. package/dashboard/lib/guards.mjs +31 -0
  4. package/dashboard/lib/identity-probe.mjs +56 -0
  5. package/dashboard/lib/pipeline-cache.mjs +58 -0
  6. package/dashboard/lib/processes.mjs +71 -0
  7. package/dashboard/serve.mjs +43 -1
  8. package/dashboard/start-broadcast.mjs +2 -12
  9. package/dashboard/start.mjs +2 -13
  10. package/dist/add-HTYIFKMN.js +150 -0
  11. package/dist/chunk-2IPXQFJE.js +253 -0
  12. package/dist/chunk-2RDRMGD3.js +82 -0
  13. package/dist/chunk-2YU2H7I6.js +42 -0
  14. package/dist/chunk-4IWG5CVE.js +214 -0
  15. package/dist/chunk-5VCOWTTP.js +6 -0
  16. package/dist/chunk-77V75A7O.js +39 -0
  17. package/dist/chunk-7HDQEK6T.js +26 -0
  18. package/dist/chunk-AW74INUW.js +1456 -0
  19. package/dist/chunk-B73KQ5KX.js +972 -0
  20. package/dist/chunk-CBCW5R5J.js +484 -0
  21. package/dist/chunk-DE3LQFSJ.js +18 -0
  22. package/dist/chunk-DQA434Y4.js +106 -0
  23. package/dist/chunk-F4VMYR7U.js +318 -0
  24. package/dist/chunk-F6C3GFKV.js +57 -0
  25. package/dist/chunk-HFUH3HVS.js +282 -0
  26. package/dist/chunk-HS7LVVLO.js +74 -0
  27. package/dist/chunk-IHROBTNM.js +135 -0
  28. package/dist/chunk-KD2MYBB6.js +213 -0
  29. package/dist/chunk-MAPOQEA2.js +43 -0
  30. package/dist/chunk-N3HTJVBR.js +341 -0
  31. package/dist/chunk-NQ7IQ5XT.js +26 -0
  32. package/dist/chunk-OUQZC744.js +1315 -0
  33. package/dist/chunk-PCCQUWE5.js +817 -0
  34. package/dist/chunk-PNFF6OU3.js +273 -0
  35. package/dist/chunk-PNG7LO2F.js +53 -0
  36. package/dist/chunk-QTON2GQX.js +203 -0
  37. package/dist/chunk-S6GMFDKV.js +50 -0
  38. package/dist/chunk-SMGFN3Z4.js +503 -0
  39. package/dist/chunk-STGFBAYT.js +641 -0
  40. package/dist/chunk-TFZ2JUXH.js +1081 -0
  41. package/dist/chunk-VY7MTPJ2.js +205 -0
  42. package/dist/chunk-W24CMQ32.js +525 -0
  43. package/dist/chunk-WW3YZKHW.js +165 -0
  44. package/dist/chunk-ZB43YMHW.js +20 -0
  45. package/dist/chunk-ZCTSUQRR.js +874 -0
  46. package/dist/chunk-ZXKZBTXE.js +17 -0
  47. package/dist/contribute-C6AW2P6J.js +367 -0
  48. package/dist/cursor-awareness-KTL2RGKD.js +77 -0
  49. package/dist/dashboard-YSNUGBFY.js +21 -0
  50. package/dist/dashboard-broadcast-M4CMW7RX.js +119 -0
  51. package/dist/diff-VHNR62SI.js +74 -0
  52. package/dist/doctor-PET2HRRJ.js +236 -0
  53. package/dist/dogfood-KKFVRWT6.js +366 -0
  54. package/dist/guard-EHXF76ID.js +530 -0
  55. package/dist/handoff-3UI6VKVG.js +302 -0
  56. package/dist/hook-DYIMKJYL.js +1074 -0
  57. package/dist/index.js +62 -14068
  58. package/dist/init-Q4LBM6KT.js +127 -0
  59. package/dist/install-RHOS3OYZ.js +34 -0
  60. package/dist/mission-control-LJACEVVN.js +89 -0
  61. package/dist/monitors-QV2IS3MP.js +176 -0
  62. package/dist/plan-index-JMC7Z4DW.js +44 -0
  63. package/dist/public-inbound-radar-WP3XF7GS.js +68 -0
  64. package/dist/remote-issues-SZ4HDQL5.js +44 -0
  65. package/dist/run-I3BFGULT.js +243 -0
  66. package/dist/run-plan-FO7CL4M3.js +118 -0
  67. package/dist/scan-NI2SWJ2E.js +33 -0
  68. package/dist/setup-global-M42GEODI.js +340 -0
  69. package/dist/status-XPVQVQMX.js +150 -0
  70. package/dist/update-FJ2EBZVO.js +283 -0
  71. package/dist/validate-V2CW7J4B.js +201 -0
  72. package/package.json +3 -3
@@ -0,0 +1,1074 @@
1
+ import {
2
+ validateHandoffText
3
+ } from "./chunk-2YU2H7I6.js";
4
+ import {
5
+ readStdinJson
6
+ } from "./chunk-DE3LQFSJ.js";
7
+ import {
8
+ CHANGELOG_FETCH_TIMEOUT_MS,
9
+ resolveInventoryRoot,
10
+ stampCursorUpdateCheck
11
+ } from "./chunk-CBCW5R5J.js";
12
+ import {
13
+ shouldEmitPublicInboundNudge,
14
+ stampPublicInboundCheck
15
+ } from "./chunk-2IPXQFJE.js";
16
+ import {
17
+ formatPlanIndexSection,
18
+ writePlanIndex
19
+ } from "./chunk-KD2MYBB6.js";
20
+ import {
21
+ READINESS_SNAPSHOT_RELATIVE_PATH
22
+ } from "./chunk-ZXKZBTXE.js";
23
+ import {
24
+ stampLastCheckedAt
25
+ } from "./chunk-SMGFN3Z4.js";
26
+ import {
27
+ loadContextConfig
28
+ } from "./chunk-NQ7IQ5XT.js";
29
+ import "./chunk-2RDRMGD3.js";
30
+ import "./chunk-N3HTJVBR.js";
31
+ import {
32
+ MANIFEST_RELATIVE_PATH
33
+ } from "./chunk-AW74INUW.js";
34
+ import {
35
+ HANDOFF_REL
36
+ } from "./chunk-5VCOWTTP.js";
37
+ import "./chunk-7HDQEK6T.js";
38
+ import {
39
+ fileExists
40
+ } from "./chunk-77V75A7O.js";
41
+
42
+ // src/commands/hook.ts
43
+ import path2 from "path";
44
+ import { defineCommand } from "citty";
45
+
46
+ // src/hooks/format-session-start.ts
47
+ function resolveSessionStartFormat(value) {
48
+ return value === "claude" ? "claude" : "cursor";
49
+ }
50
+ function formatSessionStartOutput(additionalContext, format) {
51
+ if (format === "claude") return additionalContext;
52
+ return JSON.stringify({ additional_context: additionalContext });
53
+ }
54
+ var SESSION_START_DEGRADED_MESSAGE = "Agent Kit session-start context is unavailable this session (internal error, fail-open mode). Nothing else is affected; retry next session.";
55
+
56
+ // src/hooks/pre-compact.ts
57
+ var CONTEXT_USAGE_CAP_PERCENT = 50;
58
+ function buildPreCompactUserMessage(payload = {}) {
59
+ const pct = payload.context_usage_percent;
60
+ const trigger = payload.trigger || "auto";
61
+ const pctTxt = pct !== void 0 && pct !== null ? `~${pct}%` : "high";
62
+ const overCap = pct === void 0 || pct === null || pct >= CONTEXT_USAGE_CAP_PERCENT;
63
+ const msg = overCap ? `Context compacting (${trigger}, usage ${pctTxt}) \u2014 over the ${CONTEXT_USAGE_CAP_PERCENT}% per-agent context-usage cap. Update \`.cursor/HANDOFF.md\` now (forced checkpoint) and open a new chat with \`/continue-plan\` so the next agent starts fresh.` : `Context compacting (${trigger}, usage ${pctTxt}). Update \`.cursor/HANDOFF.md\` and open a new chat with \`/continue-plan\` so the next agent starts fresh.`;
64
+ return { user_message: msg };
65
+ }
66
+
67
+ // src/hooks/session-start.ts
68
+ import { spawn } from "child_process";
69
+ import { readFile, stat } from "fs/promises";
70
+ import path from "path";
71
+
72
+ // src/hooks/handoff-excerpt.ts
73
+ var HANDOFF_EXCERPT_MAX_LINES = 60;
74
+ var HANDOFF_EXCERPT_MAX_BYTES = 12 * 1024;
75
+ var HANDOFF_PROSE_LINE_MIN_BYTES = 160;
76
+ var HANDOFF_MACHINE_FIELDS = /* @__PURE__ */ new Set([
77
+ "Plan",
78
+ "Last updated",
79
+ "Mode",
80
+ "In progress",
81
+ "Next",
82
+ "Next phase",
83
+ "Next to-dos",
84
+ "Backlog",
85
+ "Backlog plans",
86
+ "Parked plans",
87
+ "Run queue",
88
+ "Queue cursor",
89
+ "Queue status",
90
+ "Queue outcomes"
91
+ ]);
92
+ var FIELD_BULLET = /^- \*\*([^*\n]+?):\*\*/;
93
+ function byteLength(s) {
94
+ return Buffer.byteLength(s, "utf8");
95
+ }
96
+ function classify(lines) {
97
+ const entries = [];
98
+ let lastBullet = "structural";
99
+ for (const text of lines) {
100
+ let kind;
101
+ if (!text.trim() || /^#{1,6}\s/.test(text)) {
102
+ kind = "structural";
103
+ } else if (/^\s+\S/.test(text)) {
104
+ kind = lastBullet;
105
+ } else {
106
+ const m = FIELD_BULLET.exec(text);
107
+ kind = m?.[1] && HANDOFF_MACHINE_FIELDS.has(m[1].trim()) ? "machine" : "prose";
108
+ lastBullet = kind;
109
+ }
110
+ entries.push({ text, kind, bytes: byteLength(text) });
111
+ }
112
+ return entries;
113
+ }
114
+ function selectByFieldPriority(entries, maxLines) {
115
+ if (entries.length <= maxLines) return entries;
116
+ const mustKeep = /* @__PURE__ */ new Set();
117
+ for (let i = 0; i < entries.length; i++) {
118
+ const e = entries[i];
119
+ if (!e) continue;
120
+ if (e.kind === "machine") mustKeep.add(i);
121
+ }
122
+ if (entries[0] && /^#\s/.test(entries[0].text)) mustKeep.add(0);
123
+ const selected = new Set(mustKeep);
124
+ for (let i = 0; i < entries.length && selected.size < maxLines; i++) {
125
+ selected.add(i);
126
+ }
127
+ if (mustKeep.size > maxLines) {
128
+ const ordered = [...mustKeep].sort((a, b) => a - b).slice(0, maxLines);
129
+ return ordered.flatMap((i) => {
130
+ const e = entries[i];
131
+ return e ? [e] : [];
132
+ });
133
+ }
134
+ return [...selected].sort((a, b) => a - b).flatMap((i) => {
135
+ const e = entries[i];
136
+ return e ? [e] : [];
137
+ });
138
+ }
139
+ function truncateToBytes(text, maxBytes) {
140
+ let used = 0;
141
+ let kept = "";
142
+ let keptChars = 0;
143
+ const total = Array.from(text).length;
144
+ for (const ch of text) {
145
+ const b = byteLength(ch);
146
+ if (used + b > maxBytes) break;
147
+ used += b;
148
+ kept += ch;
149
+ keptChars += 1;
150
+ }
151
+ return { kept: kept.trimEnd(), removedChars: total - keptChars };
152
+ }
153
+ var MARKER_RESERVE_BYTES = 32;
154
+ function truncationMarker(removedChars) {
155
+ return ` \u2026[truncated ${removedChars} chars]`;
156
+ }
157
+ function jsonValue(text) {
158
+ const head = (FIELD_BULLET.exec(text) ?? /^\s*(?:[-*]\s+)?/.exec(text))?.[0] ?? "";
159
+ const value = text.slice(head.length).trimStart();
160
+ if (!value.startsWith("{") && !value.startsWith("[")) return null;
161
+ if (/^\[[ xX]\](\s|$)|^\[[^\]\n]*\]\(/.test(value)) return null;
162
+ return { prefix: head.trimEnd(), value };
163
+ }
164
+ function countJsonEntries(value) {
165
+ let depth = 0;
166
+ let entries = 0;
167
+ let inString = false;
168
+ let seenValue = false;
169
+ for (let i = 0; i < value.length; i++) {
170
+ const ch = value[i] ?? "";
171
+ if (inString) {
172
+ if (ch === "\\") i += 1;
173
+ else if (ch === '"') inString = false;
174
+ continue;
175
+ }
176
+ if (ch === "{" || ch === "[") {
177
+ depth += 1;
178
+ if (depth === 1) continue;
179
+ } else if (ch === "}" || ch === "]") {
180
+ depth -= 1;
181
+ if (depth === 0) break;
182
+ } else if (ch === "," && depth === 1) {
183
+ entries += 1;
184
+ seenValue = false;
185
+ continue;
186
+ }
187
+ if (ch === '"') inString = true;
188
+ if (depth >= 1 && ch.trim()) seenValue = true;
189
+ }
190
+ return entries + (seenValue ? 1 : 0);
191
+ }
192
+ function jsonMarker(entries) {
193
+ return `(JSON, ${entries} ${entries === 1 ? "entry" : "entries"}; read HANDOFF)`;
194
+ }
195
+ function capMachineLines(items, maxBytes, floor, join) {
196
+ const out = [...items];
197
+ const touched = /* @__PURE__ */ new Set();
198
+ let over = byteLength(join(out)) - maxBytes;
199
+ while (over > 0) {
200
+ let idx = -1;
201
+ for (let i = 0; i < out.length; i++) {
202
+ const e2 = out[i];
203
+ if (e2?.kind === "machine" && (idx < 0 || e2.bytes > (out[idx]?.bytes ?? 0))) idx = i;
204
+ }
205
+ const e = out[idx];
206
+ if (!e || e.bytes <= floor) break;
207
+ const target = Math.max(floor, e.bytes - over);
208
+ const { kept, removedChars } = truncateToBytes(e.text, target - MARKER_RESERVE_BYTES);
209
+ const text = kept + truncationMarker(removedChars);
210
+ const bytes = byteLength(text);
211
+ if (bytes >= e.bytes) break;
212
+ out[idx] = { text, kind: e.kind, bytes };
213
+ touched.add(idx);
214
+ over = byteLength(join(out)) - maxBytes;
215
+ }
216
+ return { items: out, machineTruncated: touched.size };
217
+ }
218
+ function droppedMarker(dropped) {
219
+ return `\u2026[${dropped} prose ${dropped === 1 ? "line" : "lines"} omitted by the excerpt byte cap; read .cursor/HANDOFF.md]`;
220
+ }
221
+ function proseShare(proseBytes, budget, floor) {
222
+ const n = proseBytes.length;
223
+ if (n === 0) return budget;
224
+ let share = Math.floor(budget / n);
225
+ for (let i = 0; i < 8; i++) {
226
+ const longer = proseBytes.filter((b) => b > share);
227
+ if (longer.length === 0) break;
228
+ const used = proseBytes.reduce((sum, b) => sum + Math.min(b, share), 0);
229
+ const slack = budget - used;
230
+ const step = Math.floor(slack / longer.length);
231
+ if (step <= 0) break;
232
+ share += step;
233
+ }
234
+ return Math.max(floor, share);
235
+ }
236
+ function capHandoffExcerpt(content, options = {}) {
237
+ const maxLines = options.maxLines ?? HANDOFF_EXCERPT_MAX_LINES;
238
+ const maxBytes = options.maxBytes ?? HANDOFF_EXCERPT_MAX_BYTES;
239
+ const floor = options.proseLineMinBytes ?? HANDOFF_PROSE_LINE_MIN_BYTES;
240
+ const allEntries = classify(content.split(/\r?\n/));
241
+ const entries = selectByFieldPriority(allEntries, maxLines);
242
+ const join = (items) => items.map((e) => e.text).join("\n").trim();
243
+ const untouched = join(entries);
244
+ const untouchedBytes = byteLength(untouched);
245
+ if (untouchedBytes <= maxBytes) {
246
+ return {
247
+ text: untouched,
248
+ bytes: untouchedBytes,
249
+ lines: entries.length,
250
+ truncatedLines: 0,
251
+ droppedLines: 0,
252
+ machineTruncated: 0
253
+ };
254
+ }
255
+ const fixedBytes = entries.reduce((sum, e) => sum + (e.kind === "prose" ? 0 : e.bytes) + 1, 0);
256
+ const proseBudget = Math.max(0, maxBytes - fixedBytes);
257
+ const share = proseShare(
258
+ entries.filter((e) => e.kind === "prose").map((e) => e.bytes),
259
+ proseBudget,
260
+ floor
261
+ );
262
+ let truncatedLines = 0;
263
+ const shaped = entries.map((e) => {
264
+ if (e.kind !== "prose" || e.bytes <= share) return e;
265
+ const json = jsonValue(e.text);
266
+ if (json) {
267
+ const marker = jsonMarker(countJsonEntries(json.value));
268
+ const text3 = json.prefix ? `${json.prefix} ${marker}` : marker;
269
+ truncatedLines += 1;
270
+ return { text: text3, kind: e.kind, bytes: byteLength(text3) };
271
+ }
272
+ const { kept, removedChars } = truncateToBytes(e.text, share - MARKER_RESERVE_BYTES);
273
+ const text2 = kept + truncationMarker(removedChars);
274
+ truncatedLines += 1;
275
+ return { text: text2, kind: e.kind, bytes: byteLength(text2) };
276
+ });
277
+ let total = shaped.reduce((sum, e) => sum + e.bytes + 1, 0) - 1;
278
+ let droppedLines = 0;
279
+ if (total > maxBytes) {
280
+ const keep = new Set(shaped.map((_, i) => i));
281
+ const noteBytes = byteLength(droppedMarker(99)) + 1;
282
+ for (let i = shaped.length - 1; i >= 0 && total + noteBytes > maxBytes; i--) {
283
+ const e = shaped[i];
284
+ if (!e || e.kind !== "prose") continue;
285
+ keep.delete(i);
286
+ total -= e.bytes + 1;
287
+ droppedLines += 1;
288
+ }
289
+ const remaining = shaped.filter((_, i) => keep.has(i));
290
+ remaining.push({ text: droppedMarker(droppedLines), kind: "structural", bytes: 0 });
291
+ const capped2 = capMachineLines(remaining, maxBytes, floor, join);
292
+ const text2 = join(capped2.items);
293
+ return {
294
+ text: text2,
295
+ bytes: byteLength(text2),
296
+ lines: entries.length,
297
+ truncatedLines,
298
+ droppedLines,
299
+ machineTruncated: capped2.machineTruncated
300
+ };
301
+ }
302
+ const capped = capMachineLines(shaped, maxBytes, floor, join);
303
+ const text = join(capped.items);
304
+ return {
305
+ text,
306
+ bytes: byteLength(text),
307
+ lines: entries.length,
308
+ truncatedLines,
309
+ droppedLines,
310
+ machineTruncated: capped.machineTruncated
311
+ };
312
+ }
313
+
314
+ // src/hooks/hard-rules.ts
315
+ var HARD_RULES = `# Agent Kit session hard rules (manual mode default)
316
+
317
+ 1. **One phase per chat.** Finish the current phase (or one to-do if the phase is huge), update \`.cursor/HANDOFF.md\`, then STOP and ask the user before starting the next phase.
318
+ 2. **Do not burn the window.** Never run an entire multi-phase plan in one conversation unless the user explicitly ran \`/run-plan\` (or a deprecated alias \`/run-plan-loop\` / \`/run-plan-orchestrated\`).
319
+ 3. **Context questions are not optional.** If the user asks about context / contexto / window size, run the context-guardian protocol: warn, offer handoff, do NOT dismiss with "it's fine" and keep coding.
320
+ 4. **Read HANDOFF first** when resuming. Do not restart the plan from scratch.
321
+ 5. **Git:** suggest \`/git-staging\` after a phase with a diff; never \`/git-prod\` without explicit confirmation. A \`/run-plan-all\` queue confirm with Ship auth \`per-plan-release\` is that confirmation for one ship per completed plan.
322
+ 6. **HITL slash commands win.** When waiting for confirmation on \`/git-staging\` or \`/git-prod\` (or similar), do not divert to continue-plan / phase-boundary chatter; stay on that routine until the user answers.
323
+ 7. **\`/start-project\` is plan bootstrap, not execute.** Broad Intake Review first, then two gates: (A) single composite question (with active plan: backlog+write / park+write / modify / cancel; without: write / write+backlog (\`Write plan and add to backlog\`) / modify / cancel): approve/write the plan file only, (B) approve the first unit. Goal text in the same message is NOT permission to edit product files. Never "create plan and start Phase 1" in one turn. If HANDOFF already has an active plan, disposition is merged into Gate A composite options; never park silently. Gates use Ask questions per \`.cursor/skills/core/hitl-gates/SKILL.md\` and \`.cursor/rules/hitl-ask-questions.mdc\`. Fallback: numbered list is path 1.
324
+ 8. **\`/continue-plan\` waits for yes.** Summarize next \`[to-do-id]\`, then stop until the user confirms before editing.
325
+ 9. **\`/run-plan-all\` is a pure orchestrator.** After the confirm queue Ask, dispatch one Task subagent per plan (run the \`/run-plan\` tick contract inside it). Do not implement that plan's to-dos in the orchestrator window. When Ship auth is \`per-plan-release\`, this window ships one release after each completed plan and waits until it is Done before the next plan. \`Run plans only\` does not ship. A red public sync stops the queue.
326
+ 10. **Backlog CRUD never activates.** \`/backlog-add\` enqueues (Broad Intake + write Ask + plan file + HANDOFF Backlog) without park, activate, or Gate B. \`/backlog-edit\` / \`/backlog-delete\` / \`/backlog-cancel\` require Ask confirm before mutate; delete archives from Backlog, cancel is soft in place. No Field Report cards for routine backlog CRUD.
327
+ 11. **HANDOFF machine fields are bullet fields, not \`##\` headings.** Mission Control parses \`- **Plan:**\`, \`- **Backlog plans:**\`, \`- **Parked plans:**\`, \`- **Run queue:**\` (etc.). Canonical Plan: \`- **Plan:** \\\`name.plan.md\\\`\` or \`none\`. Nested backlog/parked rows: \`- \\\`other.plan.md\\\`\`. Never invent \`## Backlog plans\` / \`## Parked plans\` / \`## Run queue\` headings in place of those fields (Checklist / Current mission go empty or idle).
328
+ 12. **Plans inventory is index + HANDOFF.** \`/continue-plan\`, \`/run-plan-all\`, and Broad Intake read \`.cursor/context/plan-index.json\` and \`.cursor/HANDOFF.md\` only. Do not glob \`.cursor/plans/*.plan.md\`. Named single-file reads of a known basename remain OK. Delegation stays (ADR \`decisions/2026-07-26_command-orchestration-delegation-pattern.md\`).`;
329
+ var DOGFOOD_INBOX_HINT = `## Dogfood inbox
330
+
331
+ Unprocessed files are listed under \`dogfood/README.md\` (factory) or \`.cursor/dogfood/README.md\` (consumer). To file a new note, use \`/dogfood <topic> [summary]\`. Follow the ingest ritual (detect \u2192 analyze \u2192 memory WRITE \u2192 triage). Do not auto-start analysis unless the user asks.`;
332
+ var UPDATE_CHECK_NUDGE = `## Agent Kit update available
333
+
334
+ Installed **v{installed}**; latest public **v{latest}**.
335
+
336
+ This is an advisory only (no files were changed). To apply, run \`/update\` and confirm via Ask questions. Bare \`agent-kit update\` is an explicit operator invoke, not a background job.`;
337
+ var CURSOR_AWARENESS_NUDGE = `## Cursor product-update awareness
338
+
339
+ Advisory gaps vs \`docs/cursor-native-audit.md\` (check-only; no apply; no Field Reports).
340
+
341
+ Run \`/cursor-update-awareness\` (or \`agent-kit cursor-awareness --check\`) and confirm routing via Ask \u2192 \`/backlog-add\` or \`/dogfood\`.`;
342
+ var PUBLIC_INBOUND_NUDGE = `## Public inbound radar
343
+
344
+ Open issues or pull requests on \`agent-kit-startup/agent-kit\` (inspect-only; no merge/close).
345
+
346
+ Run \`/public-inbound-radar\` (or \`agent-kit public-inbound-radar --json\`) and confirm routing via Ask \u2192 \`/backlog-add\`, Memory WRITE, \`/dogfood\`, \`/public-issue-triage\`, or Dismiss. Dependabot is one batch.`;
347
+
348
+ // src/hooks/session-start.ts
349
+ var NONE_PLACEHOLDERS = /* @__PURE__ */ new Set(["none", "n/a", "empty", "nil"]);
350
+ var CURSOR_AWARENESS_SPAWN_TIMEOUT_MS = CHANGELOG_FETCH_TIMEOUT_MS + 3e3;
351
+ async function readFull(filePath) {
352
+ try {
353
+ return await readFile(filePath, "utf8");
354
+ } catch {
355
+ return "";
356
+ }
357
+ }
358
+ function isMarkdownTableSeparator(line) {
359
+ const stripped = line.trim();
360
+ if (!stripped.startsWith("|")) return false;
361
+ const parts = stripped.replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
362
+ return parts.length > 0 && parts.every((cell) => /^:?-+:?$/.test(cell));
363
+ }
364
+ function parseUnprocessedDogfoodItems(readmeText) {
365
+ const items = [];
366
+ let inSection = false;
367
+ let sectionLevel = 0;
368
+ const lines = readmeText.split(/\r?\n/);
369
+ for (let i = 0; i < lines.length; i++) {
370
+ const line = lines[i] ?? "";
371
+ const unprocessedMatch = /^(#{2,3})\s+Unprocessed Files\b/.exec(line);
372
+ if (unprocessedMatch) {
373
+ const hashes = unprocessedMatch[1];
374
+ if (!hashes) continue;
375
+ inSection = true;
376
+ sectionLevel = hashes.length;
377
+ continue;
378
+ }
379
+ if (!inSection) continue;
380
+ const headingMatch = /^(#{1,6})\s+/.exec(line);
381
+ if (headingMatch) {
382
+ if (/^#{1,6}\s+Processed(?:\s+Files)?\b/.test(line)) break;
383
+ const hashes = headingMatch[1];
384
+ if (hashes && hashes.length <= sectionLevel) break;
385
+ continue;
386
+ }
387
+ if (line.trim().startsWith("|") && isMarkdownTableSeparator(lines[i + 1] ?? "")) {
388
+ continue;
389
+ }
390
+ const body = extractUnprocessedDogfoodLine(line);
391
+ if (!body) continue;
392
+ items.push(body);
393
+ }
394
+ return items;
395
+ }
396
+ function extractUnprocessedDogfoodLine(line) {
397
+ const stripped = line.trim();
398
+ if (!stripped) return null;
399
+ let raw = null;
400
+ if (stripped.startsWith("- ")) {
401
+ raw = stripped.slice(2).trim();
402
+ } else {
403
+ const numbered = /^(\d+)[.)]\s+(.+)$/.exec(stripped);
404
+ if (numbered?.[2]) {
405
+ raw = numbered[2].trim();
406
+ } else if (stripped.startsWith("|")) {
407
+ if (isMarkdownTableSeparator(stripped)) return null;
408
+ const parts = stripped.replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
409
+ if (parts.length === 0) return null;
410
+ raw = (parts[0] ?? "").trim();
411
+ }
412
+ }
413
+ if (!raw) return null;
414
+ const normalized = raw.toLowerCase().replace(/[*_]/g, "").trim();
415
+ if (!normalized || NONE_PLACEHOLDERS.has(normalized)) return null;
416
+ return raw;
417
+ }
418
+ async function l0Present(root) {
419
+ return await fileExists(path.join(root, MANIFEST_RELATIVE_PATH)) || await fileExists(path.join(root, ".cursor", "commands", "agent-kit-onboard.md")) || await fileExists(path.join(root, ".cursor", "commands", "start-project.md"));
420
+ }
421
+ function checkLabelAndRecommendation(check) {
422
+ const checkId = check.id;
423
+ if (typeof checkId !== "string" || !checkId) return null;
424
+ const actions = check.actions;
425
+ if (Array.isArray(actions)) {
426
+ for (const action of actions) {
427
+ if (!action || typeof action !== "object") continue;
428
+ const a = action;
429
+ if (typeof a.id === "string" && typeof a.recommendation === "string") {
430
+ return [a.id, a.recommendation];
431
+ }
432
+ }
433
+ }
434
+ const title = check.title;
435
+ if (typeof title === "string" && title) return [checkId, title];
436
+ return [checkId, "Resolve this readiness check"];
437
+ }
438
+ function unresolvedReadinessChecks(data) {
439
+ const essential = [];
440
+ const nonessential = [];
441
+ const pillars = data.pillars;
442
+ if (!Array.isArray(pillars)) return { essential, nonessential };
443
+ for (const pillar of pillars) {
444
+ if (!pillar || typeof pillar !== "object") continue;
445
+ const checks = pillar.checks;
446
+ if (!Array.isArray(checks)) continue;
447
+ for (const check of checks) {
448
+ if (!check || typeof check !== "object") continue;
449
+ const c = check;
450
+ if (c.status === "ready") continue;
451
+ if (c.essential === true) essential.push(c);
452
+ else nonessential.push(c);
453
+ }
454
+ }
455
+ return { essential, nonessential };
456
+ }
457
+ async function readinessSection(root) {
458
+ const snapshotPath = path.join(root, READINESS_SNAPSHOT_RELATIVE_PATH);
459
+ let data;
460
+ try {
461
+ data = JSON.parse(await readFile(snapshotPath, "utf8"));
462
+ } catch {
463
+ return null;
464
+ }
465
+ const { essential, nonessential } = unresolvedReadinessChecks(data);
466
+ if (essential[0]) {
467
+ const labeled = checkLabelAndRecommendation(essential[0]);
468
+ if (!labeled) return null;
469
+ const [actionId, recommendation] = labeled;
470
+ return `## Repository readiness
471
+
472
+ Unresolved essential check: \`${actionId}\`. ${recommendation} Run \`/agent-kit-onboard\` before \`/start-project\`. An active plan or HANDOFF remains the current work and is not replaced.`;
473
+ }
474
+ if (nonessential[0]) {
475
+ const labeled = checkLabelAndRecommendation(nonessential[0]);
476
+ if (!labeled) return null;
477
+ const [actionId, recommendation] = labeled;
478
+ return `## Repository readiness
479
+
480
+ Optional readiness item: \`${actionId}\`. ${recommendation} This does not block \`/start-project\` or active plan work. Resume later with \`/agent-kit-onboard\` if useful.`;
481
+ }
482
+ const actions = data.pendingActions;
483
+ if (!Array.isArray(actions) || !actions[0] || typeof actions[0] !== "object") return null;
484
+ const first = actions[0];
485
+ if (typeof first.id !== "string" || typeof first.recommendation !== "string") return null;
486
+ return `## Repository readiness
487
+
488
+ Optional readiness item: \`${first.id}\`. ${first.recommendation} This does not block \`/start-project\` or active plan work. Resume later with \`/agent-kit-onboard\` if useful.`;
489
+ }
490
+ async function dogfoodInboxSection(root) {
491
+ const candidateReadmes = [
492
+ path.join(root, "dogfood", "README.md"),
493
+ path.join(root, ".cursor", "dogfood", "README.md")
494
+ ];
495
+ for (const readme of candidateReadmes) {
496
+ if (!await fileExists(readme)) continue;
497
+ try {
498
+ const text = await readFile(readme, "utf8");
499
+ if (parseUnprocessedDogfoodItems(text).length) return DOGFOOD_INBOX_HINT;
500
+ } catch {
501
+ }
502
+ }
503
+ return null;
504
+ }
505
+ async function loadUpdateCheckPrefs(root) {
506
+ const data = await loadContextConfig(root);
507
+ if (!data) return null;
508
+ const uc = data.updateCheck;
509
+ if (!uc || typeof uc !== "object" || uc.enabled !== true) {
510
+ return null;
511
+ }
512
+ return uc;
513
+ }
514
+ var SESSION_START_SECTION_DEADLINE_MS = 6e3;
515
+ function withDeadline(work, ms, onDeadline) {
516
+ return new Promise((resolve) => {
517
+ let done = false;
518
+ const timer = setTimeout(() => {
519
+ if (done) return;
520
+ done = true;
521
+ try {
522
+ onDeadline?.();
523
+ } catch {
524
+ }
525
+ resolve(null);
526
+ }, ms);
527
+ const settle = (value) => {
528
+ if (done) return;
529
+ done = true;
530
+ clearTimeout(timer);
531
+ resolve(value);
532
+ };
533
+ work.then(settle, () => settle(null));
534
+ });
535
+ }
536
+ function runSectionWithDeadline(section, root, spawnFn, deadlineMs, attempt) {
537
+ const children = [];
538
+ let expired = false;
539
+ const track = (child) => {
540
+ if (attempt) attempt.spawned = true;
541
+ if (expired) releaseChild(child);
542
+ else children.push(child);
543
+ };
544
+ const onResult = (result) => {
545
+ if (attempt && !expired) attempt.result = result;
546
+ };
547
+ return withDeadline(section(root, { spawnFn, track, onResult }), deadlineMs, () => {
548
+ expired = true;
549
+ if (attempt) attempt.timedOut = true;
550
+ for (const child of children) releaseChild(child);
551
+ });
552
+ }
553
+ function sectionAttemptNeedsStamp(attempt) {
554
+ if (!attempt.spawned) return false;
555
+ if (attempt.timedOut || !attempt.result) return true;
556
+ const status = attempt.result.status;
557
+ if (typeof status !== "string") return true;
558
+ return status === "skipped-offline" || !status.startsWith("skipped-");
559
+ }
560
+ function releaseChild(child) {
561
+ try {
562
+ child.kill();
563
+ } catch {
564
+ }
565
+ try {
566
+ child.unref();
567
+ } catch {
568
+ }
569
+ }
570
+ function runUpdateCheckJson(root, track) {
571
+ return new Promise((resolve) => {
572
+ const child = spawn(
573
+ process.execPath,
574
+ [process.argv[1] ?? "", "update", "--check", "--json", "--respect-prefs", "--cwd", root],
575
+ { stdio: ["ignore", "pipe", "ignore"], timeout: 12e3 }
576
+ );
577
+ track?.(child);
578
+ let out = "";
579
+ child.stdout?.on("data", (chunk) => {
580
+ out += chunk.toString("utf8");
581
+ });
582
+ child.on("error", () => resolve(null));
583
+ child.on("close", () => {
584
+ try {
585
+ const parsed = JSON.parse(out.trim());
586
+ resolve(parsed && typeof parsed === "object" ? parsed : null);
587
+ } catch {
588
+ resolve(null);
589
+ }
590
+ });
591
+ });
592
+ }
593
+ async function updateCheckSection(root, deps = {}) {
594
+ if (await loadUpdateCheckPrefs(root) === null) return null;
595
+ const spawnFn = deps.spawnFn ?? spawn;
596
+ const runFallback = deps.runFallback ?? runUpdateCheckJson;
597
+ const result = await new Promise((resolve) => {
598
+ let settled = false;
599
+ let fallbackStarted = false;
600
+ const finish = (value) => {
601
+ if (settled) return;
602
+ settled = true;
603
+ resolve(value);
604
+ };
605
+ const startFallback = () => {
606
+ if (settled || fallbackStarted) return;
607
+ fallbackStarted = true;
608
+ void runFallback(root, deps.track).then(finish);
609
+ };
610
+ const child = spawnFn(
611
+ "agent-kit",
612
+ ["update", "--check", "--json", "--respect-prefs", "--cwd", root],
613
+ { stdio: ["ignore", "pipe", "ignore"], timeout: 12e3, shell: false }
614
+ );
615
+ deps.track?.(child);
616
+ let out = "";
617
+ child.stdout?.on("data", (chunk) => {
618
+ out += chunk.toString("utf8");
619
+ });
620
+ child.on("error", () => {
621
+ startFallback();
622
+ });
623
+ child.on("close", (code) => {
624
+ if (settled || fallbackStarted) return;
625
+ if (code !== 0 && !out.trim()) {
626
+ startFallback();
627
+ return;
628
+ }
629
+ try {
630
+ finish(JSON.parse(out.trim()));
631
+ } catch {
632
+ startFallback();
633
+ }
634
+ });
635
+ });
636
+ deps.onResult?.(result);
637
+ if (!result || result.status !== "update-available") return null;
638
+ if (result.applyRecommended === true) return null;
639
+ const installed = String(result.installedVersion ?? "?");
640
+ const latest = String(result.latestVersion ?? "?");
641
+ return UPDATE_CHECK_NUDGE.replace("{installed}", installed).replace("{latest}", latest);
642
+ }
643
+ async function loadCursorUpdateCheckPrefs(root) {
644
+ const data = await loadContextConfig(root);
645
+ if (!data) return null;
646
+ const uc = data.cursorUpdateCheck;
647
+ if (!uc || typeof uc !== "object" || uc.enabled !== true) {
648
+ return null;
649
+ }
650
+ return uc;
651
+ }
652
+ function runCursorAwarenessJson(root, track) {
653
+ return new Promise((resolve) => {
654
+ const child = spawn(
655
+ process.execPath,
656
+ [
657
+ process.argv[1] ?? "",
658
+ "cursor-awareness",
659
+ "--check",
660
+ "--json",
661
+ "--respect-prefs",
662
+ "--cwd",
663
+ root
664
+ ],
665
+ { stdio: ["ignore", "pipe", "ignore"], timeout: CURSOR_AWARENESS_SPAWN_TIMEOUT_MS }
666
+ );
667
+ track?.(child);
668
+ let out = "";
669
+ child.stdout?.on("data", (chunk) => {
670
+ out += chunk.toString("utf8");
671
+ });
672
+ child.on("error", () => resolve(null));
673
+ child.on("close", () => {
674
+ try {
675
+ const parsed = JSON.parse(out.trim());
676
+ resolve(parsed && typeof parsed === "object" ? parsed : null);
677
+ } catch {
678
+ resolve(null);
679
+ }
680
+ });
681
+ });
682
+ }
683
+ function shouldEmitCursorAwarenessNudge(result) {
684
+ if (!result || result.status !== "gaps-found") return false;
685
+ if (result.applyRecommended === true || result.fieldReportRecommended === true) return false;
686
+ const gaps = Array.isArray(result.gaps) ? result.gaps : [];
687
+ return gaps.some(
688
+ (g) => g && typeof g === "object" && g.id === "changelog-ahead"
689
+ );
690
+ }
691
+ async function cursorAwarenessSection(root, deps = {}) {
692
+ if (await loadCursorUpdateCheckPrefs(root) === null) return null;
693
+ const spawnFn = deps.spawnFn ?? spawn;
694
+ const runFallback = deps.runFallback ?? runCursorAwarenessJson;
695
+ const result = await new Promise((resolve) => {
696
+ let settled = false;
697
+ let fallbackStarted = false;
698
+ const finish = (value) => {
699
+ if (settled) return;
700
+ settled = true;
701
+ resolve(value);
702
+ };
703
+ const startFallback = () => {
704
+ if (settled || fallbackStarted) return;
705
+ fallbackStarted = true;
706
+ void runFallback(root, deps.track).then(finish);
707
+ };
708
+ const child = spawnFn(
709
+ "agent-kit",
710
+ ["cursor-awareness", "--check", "--json", "--respect-prefs", "--cwd", root],
711
+ {
712
+ stdio: ["ignore", "pipe", "ignore"],
713
+ timeout: CURSOR_AWARENESS_SPAWN_TIMEOUT_MS,
714
+ shell: false
715
+ }
716
+ );
717
+ deps.track?.(child);
718
+ let out = "";
719
+ child.stdout?.on("data", (chunk) => {
720
+ out += chunk.toString("utf8");
721
+ });
722
+ child.on("error", () => {
723
+ startFallback();
724
+ });
725
+ child.on("close", (code) => {
726
+ if (settled || fallbackStarted) return;
727
+ if (code !== 0 && !out.trim()) {
728
+ startFallback();
729
+ return;
730
+ }
731
+ try {
732
+ finish(JSON.parse(out.trim()));
733
+ } catch {
734
+ startFallback();
735
+ }
736
+ });
737
+ });
738
+ deps.onResult?.(result);
739
+ if (!shouldEmitCursorAwarenessNudge(result)) return null;
740
+ return CURSOR_AWARENESS_NUDGE;
741
+ }
742
+ var PUBLIC_INBOUND_SPAWN_TIMEOUT_MS = 3e4;
743
+ async function loadPublicInboundCheckPrefs(root) {
744
+ const data = await loadContextConfig(root);
745
+ if (!data) return null;
746
+ const uc = data.publicInboundCheck;
747
+ if (!uc || typeof uc !== "object" || uc.enabled !== true) {
748
+ return null;
749
+ }
750
+ return uc;
751
+ }
752
+ function runPublicInboundJson(root, track) {
753
+ return new Promise((resolve) => {
754
+ const child = spawn(
755
+ process.execPath,
756
+ [process.argv[1] ?? "", "public-inbound-radar", "--json", "--respect-prefs", "--cwd", root],
757
+ { stdio: ["ignore", "pipe", "ignore"], timeout: PUBLIC_INBOUND_SPAWN_TIMEOUT_MS }
758
+ );
759
+ track?.(child);
760
+ let out = "";
761
+ child.stdout?.on("data", (chunk) => {
762
+ out += chunk.toString("utf8");
763
+ });
764
+ child.on("error", () => resolve(null));
765
+ child.on("close", () => {
766
+ try {
767
+ const parsed = JSON.parse(out.trim());
768
+ resolve(parsed && typeof parsed === "object" ? parsed : null);
769
+ } catch {
770
+ resolve(null);
771
+ }
772
+ });
773
+ });
774
+ }
775
+ async function publicInboundSection(root, deps = {}) {
776
+ if (await loadPublicInboundCheckPrefs(root) === null) return null;
777
+ const spawnFn = deps.spawnFn ?? spawn;
778
+ const runFallback = deps.runFallback ?? runPublicInboundJson;
779
+ const result = await new Promise((resolve) => {
780
+ let settled = false;
781
+ let fallbackStarted = false;
782
+ const finish = (value) => {
783
+ if (settled) return;
784
+ settled = true;
785
+ resolve(value);
786
+ };
787
+ const startFallback = () => {
788
+ if (settled || fallbackStarted) return;
789
+ fallbackStarted = true;
790
+ void runFallback(root, deps.track).then(finish);
791
+ };
792
+ const child = spawnFn(
793
+ "agent-kit",
794
+ ["public-inbound-radar", "--json", "--respect-prefs", "--cwd", root],
795
+ {
796
+ stdio: ["ignore", "pipe", "ignore"],
797
+ timeout: PUBLIC_INBOUND_SPAWN_TIMEOUT_MS,
798
+ shell: false
799
+ }
800
+ );
801
+ deps.track?.(child);
802
+ let out = "";
803
+ child.stdout?.on("data", (chunk) => {
804
+ out += chunk.toString("utf8");
805
+ });
806
+ child.on("error", () => {
807
+ startFallback();
808
+ });
809
+ child.on("close", (code) => {
810
+ if (settled || fallbackStarted) return;
811
+ if (code !== 0 && !out.trim()) {
812
+ startFallback();
813
+ return;
814
+ }
815
+ try {
816
+ finish(JSON.parse(out.trim()));
817
+ } catch {
818
+ startFallback();
819
+ }
820
+ });
821
+ });
822
+ deps.onResult?.(result);
823
+ if (!shouldEmitPublicInboundNudge(result)) {
824
+ return null;
825
+ }
826
+ return PUBLIC_INBOUND_NUDGE;
827
+ }
828
+ var AUDIT_SESSION_NS_PREFIX = "agent-kit-audit-";
829
+ var AUDIT_SESSION_LIST_TIMEOUT_MS = 2e3;
830
+ function runAuditSessionCommand(cmd, args) {
831
+ return new Promise((resolve) => {
832
+ const child = spawn(cmd, args, {
833
+ stdio: ["ignore", "pipe", "ignore"],
834
+ timeout: AUDIT_SESSION_LIST_TIMEOUT_MS,
835
+ shell: false
836
+ });
837
+ let out = "";
838
+ child.stdout?.on("data", (chunk) => {
839
+ out += chunk.toString("utf8");
840
+ });
841
+ child.on("error", () => resolve(null));
842
+ child.on("close", () => resolve(out));
843
+ });
844
+ }
845
+ function parseTmuxDetachedAuditSessions(out, nowEpochSeconds) {
846
+ const sessions = [];
847
+ for (const line of out.split(/\r?\n/)) {
848
+ const m = /^(\S+)\s+(\d+)\s+(\d+)$/.exec(line.trim());
849
+ if (!m) continue;
850
+ const [, name, attached, created] = m;
851
+ if (!name || !name.startsWith(AUDIT_SESSION_NS_PREFIX)) continue;
852
+ if (Number(attached) > 0) continue;
853
+ const createdEpoch = Number(created);
854
+ const ageSeconds = Number.isFinite(createdEpoch) && nowEpochSeconds >= createdEpoch ? nowEpochSeconds - createdEpoch : -1;
855
+ sessions.push({ channel: "tmux", name, ageSeconds });
856
+ }
857
+ return sessions;
858
+ }
859
+ function parseScreenDetachedAuditSessions(listing) {
860
+ const lines = listing.split(/\r?\n/);
861
+ let sockdir = null;
862
+ for (const line of lines) {
863
+ const dirMatch = /^\d+\s+Sockets?\s+in\s+(.+)\.$/.exec(line);
864
+ if (dirMatch?.[1]) sockdir = dirMatch[1];
865
+ }
866
+ const entries = [];
867
+ for (const line of lines) {
868
+ const m = /^\s+(\d+)\.(\S+)\s+\((.*)\)/.exec(line);
869
+ if (!m) continue;
870
+ const [, pid, name, marker] = m;
871
+ if (!name || !name.startsWith(AUDIT_SESSION_NS_PREFIX)) continue;
872
+ if (marker && /[Aa]ttached/.test(marker)) continue;
873
+ entries.push({ name, socketPath: sockdir ? path.join(sockdir, `${pid}.${name}`) : null });
874
+ }
875
+ return entries;
876
+ }
877
+ function formatSessionAge(seconds) {
878
+ if (seconds >= 86400) return `${Math.floor(seconds / 86400)}d`;
879
+ if (seconds >= 3600) return `${Math.floor(seconds / 3600)}h`;
880
+ if (seconds >= 60) return `${Math.floor(seconds / 60)}m`;
881
+ return `${seconds}s`;
882
+ }
883
+ async function detachedAuditSessionsSection(deps = {}) {
884
+ try {
885
+ const run = deps.runCommand ?? runAuditSessionCommand;
886
+ const nowMs = (deps.now ?? Date.now)();
887
+ const [tmuxOut, screenOut] = await Promise.all([
888
+ run("tmux", [
889
+ "list-sessions",
890
+ "-F",
891
+ "#{session_name} #{session_attached} #{session_created}"
892
+ ]),
893
+ run("screen", ["-ls"])
894
+ ]);
895
+ const sessions = tmuxOut ? parseTmuxDetachedAuditSessions(tmuxOut, Math.floor(nowMs / 1e3)) : [];
896
+ if (screenOut) {
897
+ for (const entry of parseScreenDetachedAuditSessions(screenOut)) {
898
+ let ageSeconds = -1;
899
+ if (entry.socketPath) {
900
+ try {
901
+ const { mtimeMs } = await stat(entry.socketPath);
902
+ if (nowMs >= mtimeMs) ageSeconds = Math.floor((nowMs - mtimeMs) / 1e3);
903
+ } catch {
904
+ }
905
+ }
906
+ sessions.push({ channel: "screen", name: entry.name, ageSeconds });
907
+ }
908
+ }
909
+ if (sessions.length === 0) return null;
910
+ const knownAges = sessions.map((s) => s.ageSeconds).filter((a) => a >= 0);
911
+ const oldest = knownAges.length ? `oldest ~${formatSessionAge(Math.max(...knownAges))}` : "oldest age unknown";
912
+ const noun = sessions.length === 1 ? "session" : "sessions";
913
+ return `## Detached audit sessions (host)
914
+
915
+ ${sessions.length} detached \`agent-kit-audit-*\` PTY ${noun} on this host (${oldest}). These are external plan-review terminals: inspect with \`tmux attach -t <name>\` / \`screen -r <name>\`, or let the audit launcher's session GC dispose of them on the next spawn.`;
916
+ } catch {
917
+ return null;
918
+ }
919
+ }
920
+ async function buildSessionStartAdditionalContext(rootDir, _payload = {}, deps = {}) {
921
+ const root = path.resolve(rootDir);
922
+ const handoffPath = path.join(root, HANDOFF_REL);
923
+ const handoffFull = await readFull(handoffPath);
924
+ const handoff = capHandoffExcerpt(handoffFull);
925
+ const parts = [HARD_RULES];
926
+ if (await l0Present(root)) {
927
+ const readiness = await readinessSection(root);
928
+ if (readiness) parts.push(readiness);
929
+ }
930
+ const dogfood = await dogfoodInboxSection(root);
931
+ if (dogfood) parts.push(dogfood);
932
+ const deadlineMs = deps.deadlineMs ?? SESSION_START_SECTION_DEADLINE_MS;
933
+ const newAttempt = () => ({ spawned: false, timedOut: false, result: null });
934
+ const updateAttempt = newAttempt();
935
+ const cursorAttempt = newAttempt();
936
+ const inboundAttempt = newAttempt();
937
+ const runSection = (section, attempt) => runSectionWithDeadline(section, root, deps.spawnFn, deadlineMs, attempt);
938
+ const [updateNudge, cursorNudge, inboundNudge] = await Promise.all([
939
+ runSection(updateCheckSection, updateAttempt),
940
+ runSection(cursorAwarenessSection, cursorAttempt),
941
+ runSection(publicInboundSection, inboundAttempt)
942
+ ]);
943
+ await stampSectionAttempts(root, updateAttempt, cursorAttempt, inboundAttempt);
944
+ if (updateNudge) parts.push(updateNudge);
945
+ if (cursorNudge) parts.push(cursorNudge);
946
+ if (inboundNudge) parts.push(inboundNudge);
947
+ const auditSessions = await detachedAuditSessionsSection().catch(() => null);
948
+ if (auditSessions) parts.push(auditSessions);
949
+ const formatWarnings = validateHandoffText(handoffFull);
950
+ if (formatWarnings.length) {
951
+ const bullet = formatWarnings.map((w) => `- ${w.message}`).join("\n");
952
+ parts.push(
953
+ `## HANDOFF format warning (Mission Control)
954
+
955
+ ${bullet}
956
+
957
+ Rewrite machine lists as \`- **Field:**\` bullets before trusting Checklist / Current mission.`
958
+ );
959
+ }
960
+ if (handoff.text) {
961
+ const truncated = handoff.truncatedLines + handoff.machineTruncated;
962
+ const capped = truncated > 0 || handoff.droppedLines > 0;
963
+ const note = capped ? `
964
+
965
+ _Excerpt capped at ${Math.round(HANDOFF_EXCERPT_MAX_BYTES / 1024)} KB (${truncated} lines truncated, ${handoff.droppedLines} dropped); read \`.cursor/HANDOFF.md\` for the full entries._` : "";
966
+ parts.push(`## Current HANDOFF.md (excerpt)
967
+
968
+ ${handoff.text}${note}`);
969
+ } else {
970
+ parts.push(
971
+ "## HANDOFF.md\n\nNo handoff file yet. If starting work, create a plan with to-dos first (`/start-project`)."
972
+ );
973
+ }
974
+ const pendingPlans = await pendingPlansSection(root);
975
+ if (pendingPlans) parts.push(pendingPlans);
976
+ return { additional_context: parts.join("\n\n") };
977
+ }
978
+ async function stampSectionAttempts(root, updateAttempt, cursorAttempt, inboundAttempt) {
979
+ if (sectionAttemptNeedsStamp(updateAttempt)) {
980
+ try {
981
+ await stampLastCheckedAt(root);
982
+ } catch {
983
+ }
984
+ }
985
+ if (sectionAttemptNeedsStamp(cursorAttempt)) {
986
+ try {
987
+ const latest = cursorAttempt.result?.latestCursorVersion;
988
+ await stampCursorUpdateCheck(
989
+ await resolveInventoryRoot(root) ?? root,
990
+ typeof latest === "string" ? { lastSeenCursorVersion: latest } : {}
991
+ );
992
+ } catch {
993
+ }
994
+ }
995
+ if (sectionAttemptNeedsStamp(inboundAttempt)) {
996
+ try {
997
+ await stampPublicInboundCheck(root);
998
+ } catch {
999
+ }
1000
+ }
1001
+ }
1002
+ async function pendingPlansSection(root) {
1003
+ try {
1004
+ const index = await writePlanIndex(root);
1005
+ return formatPlanIndexSection(index);
1006
+ } catch {
1007
+ return null;
1008
+ }
1009
+ }
1010
+ function resolveSessionRoot(payload, cwd = process.cwd()) {
1011
+ const roots = payload.workspace_roots;
1012
+ if (Array.isArray(roots) && typeof roots[0] === "string" && roots[0]) {
1013
+ return roots[0];
1014
+ }
1015
+ return cwd;
1016
+ }
1017
+
1018
+ // src/commands/hook.ts
1019
+ async function runSessionStartHook(cwd, formatArg, deps = {}) {
1020
+ const format = resolveSessionStartFormat(formatArg);
1021
+ try {
1022
+ const readStdin = deps.readStdin ?? readStdinJson;
1023
+ const buildContext = deps.buildContext ?? buildSessionStartAdditionalContext;
1024
+ const payload = await readStdin();
1025
+ const root = resolveSessionRoot(payload, path2.resolve(cwd));
1026
+ const out = await buildContext(root, payload);
1027
+ return formatSessionStartOutput(out.additional_context, format);
1028
+ } catch {
1029
+ return formatSessionStartOutput(SESSION_START_DEGRADED_MESSAGE, format);
1030
+ }
1031
+ }
1032
+ var hookCommand = defineCommand({
1033
+ meta: {
1034
+ name: "hook",
1035
+ description: "Cursor + Claude Code hook adapters (session-start, pre-compact). CLI is SoT."
1036
+ },
1037
+ subCommands: {
1038
+ "session-start": defineCommand({
1039
+ meta: {
1040
+ name: "session-start",
1041
+ description: "Emit sessionStart context (stdin: host payload). --format cursor (default, JSON additional_context) | claude (plain stdout)"
1042
+ },
1043
+ args: {
1044
+ cwd: {
1045
+ type: "string",
1046
+ default: process.cwd()
1047
+ },
1048
+ format: {
1049
+ type: "string",
1050
+ default: "cursor",
1051
+ description: "cursor (default) | claude"
1052
+ }
1053
+ },
1054
+ async run({ args }) {
1055
+ const cwd = typeof args.cwd === "string" ? args.cwd : process.cwd();
1056
+ console.log(await runSessionStartHook(cwd, args.format));
1057
+ }
1058
+ }),
1059
+ "pre-compact": defineCommand({
1060
+ meta: {
1061
+ name: "pre-compact",
1062
+ description: "Emit preCompact user_message JSON (stdin: Cursor payload)"
1063
+ },
1064
+ async run() {
1065
+ const payload = await readStdinJson();
1066
+ console.log(JSON.stringify(buildPreCompactUserMessage(payload)));
1067
+ }
1068
+ })
1069
+ }
1070
+ });
1071
+ export {
1072
+ hookCommand,
1073
+ runSessionStartHook
1074
+ };