runcastle 1.4.8 → 1.5.4

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 (52) hide show
  1. package/bin/runcastle.js +10604 -7555
  2. package/drizzle/0045_nappy_adam_destine.sql +3 -0
  3. package/drizzle/0046_polite_nightcrawler.sql +20 -0
  4. package/drizzle/0046_project_scenario_runner.sql +1 -0
  5. package/drizzle/0047_clear_betty_brant.sql +3 -0
  6. package/drizzle/0048_new_northstar.sql +1 -0
  7. package/drizzle/0050_tan_manta.sql +1 -0
  8. package/drizzle/0051_ambiguous_talkback.sql +1 -0
  9. package/drizzle/0052_friendly_black_tarantula.sql +1 -0
  10. package/drizzle/0053_soft_karma.sql +12 -0
  11. package/drizzle/meta/0045_snapshot.json +1220 -0
  12. package/drizzle/meta/0046_snapshot.json +1227 -0
  13. package/drizzle/meta/0047_snapshot.json +1349 -0
  14. package/drizzle/meta/0048_snapshot.json +1356 -0
  15. package/drizzle/meta/0050_snapshot.json +1349 -0
  16. package/drizzle/meta/0051_snapshot.json +1356 -0
  17. package/drizzle/meta/0052_snapshot.json +1364 -0
  18. package/drizzle/meta/0053_snapshot.json +1283 -0
  19. package/drizzle/meta/_journal.json +63 -0
  20. package/hook-client.ts +110 -9
  21. package/index.js +10601 -7552
  22. package/package.json +1 -1
  23. package/skills/burner/implement-ticket.md +0 -1
  24. package/skills/burner/review-ticket.md +7 -5
  25. package/skills/burner/verify-fixes.md +4 -2
  26. package/skills/packs/README.md +24 -28
  27. package/skills/packs/runcastle/.claude-plugin/plugin.json +1 -1
  28. package/skills/packs/runcastle/skills/feature/SKILL.md +72 -0
  29. package/skills/packs/runcastle/skills/feature/references/design.md +132 -0
  30. package/skills/packs/runcastle/skills/feature/references/grilling.md +99 -0
  31. package/skills/packs/runcastle/skills/feature/references/planning-alone.md +70 -0
  32. package/skills/packs/runcastle/skills/{ideate → feature}/references/preview-starter.html +27 -17
  33. package/skills/packs/runcastle/skills/{ideate → feature}/references/preview.md +67 -8
  34. package/skills/packs/runcastle/skills/feature/references/rounds.md +91 -0
  35. package/skills/packs/runcastle/skills/feature/references/spec.md +54 -0
  36. package/skills/packs/runcastle/skills/feature/references/tickets.md +229 -0
  37. package/skills/packs/runcastle/skills/prepare/SKILL.md +19 -5
  38. package/skills/packs/runcastle/skills/prepare/references/recipes.md +6 -2
  39. package/skills/packs/runcastle/skills/project/SKILL.md +115 -14
  40. package/skills/packs/runcastle/skills/project/references/charter.md +3 -3
  41. package/skills/packs/runcastle/skills/project/references/health-sweeps.md +5 -5
  42. package/skills/packs/runcastle/skills/project/references/triage.md +21 -24
  43. package/skills/packs/runcastle/skills/qa/SKILL.md +1 -2
  44. package/web/assets/index-CT9k1kz0.css +1 -0
  45. package/web/assets/index-DVR7VbWW.js +186 -0
  46. package/web/index.html +2 -2
  47. package/skills/packs/runcastle/skills/ideate/SKILL.md +0 -128
  48. package/skills/packs/runcastle/skills/revisit/SKILL.md +0 -78
  49. package/skills/packs/runcastle/skills/spec/SKILL.md +0 -47
  50. package/skills/packs/runcastle/skills/tickets/SKILL.md +0 -119
  51. package/web/assets/index-CUoLAKY6.js +0 -188
  52. package/web/assets/index-bNgQedte.css +0 -1
@@ -316,6 +316,69 @@
316
316
  "when": 1790860897661,
317
317
  "tag": "0044_icy_daimon_hellstrom",
318
318
  "breakpoints": true
319
+ },
320
+ {
321
+ "idx": 45,
322
+ "version": "6",
323
+ "when": 1792053714532,
324
+ "tag": "0045_nappy_adam_destine",
325
+ "breakpoints": true
326
+ },
327
+ {
328
+ "idx": 46,
329
+ "version": "6",
330
+ "when": 1792224938618,
331
+ "tag": "0046_project_scenario_runner",
332
+ "breakpoints": true
333
+ },
334
+ {
335
+ "idx": 47,
336
+ "version": "6",
337
+ "when": 1792228865475,
338
+ "tag": "0046_polite_nightcrawler",
339
+ "breakpoints": true
340
+ },
341
+ {
342
+ "idx": 48,
343
+ "version": "6",
344
+ "when": 1792301267550,
345
+ "tag": "0047_clear_betty_brant",
346
+ "breakpoints": true
347
+ },
348
+ {
349
+ "idx": 49,
350
+ "version": "6",
351
+ "when": 1792345583347,
352
+ "tag": "0048_new_northstar",
353
+ "breakpoints": true
354
+ },
355
+ {
356
+ "idx": 50,
357
+ "version": "6",
358
+ "when": 1792482127192,
359
+ "tag": "0050_tan_manta",
360
+ "breakpoints": true
361
+ },
362
+ {
363
+ "idx": 51,
364
+ "version": "6",
365
+ "when": 1792569018654,
366
+ "tag": "0051_ambiguous_talkback",
367
+ "breakpoints": true
368
+ },
369
+ {
370
+ "idx": 52,
371
+ "version": "6",
372
+ "when": 1792847895970,
373
+ "tag": "0052_friendly_black_tarantula",
374
+ "breakpoints": true
375
+ },
376
+ {
377
+ "idx": 53,
378
+ "version": "6",
379
+ "when": 1792865670658,
380
+ "tag": "0053_soft_karma",
381
+ "breakpoints": true
319
382
  }
320
383
  ]
321
384
  }
package/hook-client.ts CHANGED
@@ -5,12 +5,23 @@
5
5
  * It reads the hook JSON from stdin, POSTs `{ event, sessionId, payload }` to
6
6
  * `RUNCASTLE_SERVER_URL/api/hooks/<event>`, and prints the server's response body
7
7
  * VERBATIM to stdout (the server returns the final hook JSON that Claude Code
8
- * consumes as context). Zero dependencies (plain `fetch`), 3s timeout. On ANY
8
+ * consumes as context). Zero dependencies (plain `fetch`), 3s timeout (5s and
9
+ * retried for `session-start`, see {@link postHook}). On ANY
9
10
  * error it prints `{}` and exits 0 — a hook must never break the user's session.
11
+ *
12
+ * One argument is not a route event: `wait-messages`, the background message
13
+ * waiter of the orchestrator and of a feature's lead (see {@link waitMessages}).
10
14
  */
11
15
 
12
16
  const FETCH_TIMEOUT_MS = 3000
13
17
  const STDIN_TIMEOUT_MS = 2500
18
+ /**
19
+ * `session-start` is retried ({@link postHook}): 3 attempts of 5s with a 0.5s
20
+ * pause stay inside the 20s its settings give it (`renderSettings`).
21
+ */
22
+ const SESSION_START_ATTEMPTS = 3
23
+ const SESSION_START_FETCH_TIMEOUT_MS = 5000
24
+ const SESSION_START_RETRY_MS = 500
14
25
 
15
26
  async function readStdin(): Promise<string> {
16
27
  const chunks: Buffer[] = []
@@ -33,11 +44,70 @@ async function readStdin(): Promise<string> {
33
44
  })
34
45
  }
35
46
 
47
+ /** The hook-client argument that runs the orchestrator's message waiter. */
48
+ export const WAIT_MESSAGES_MODE = 'wait-messages'
49
+
50
+ /** Longer than the server's hold, so a held poll is answered rather than cut off. */
51
+ const WAIT_FETCH_TIMEOUT_MS = 40_000
52
+ /** The pause after a failed poll (server restarting, network blip) before the next. */
53
+ const WAIT_RETRY_MS = 3000
54
+
55
+ export interface WaitMessagesDeps {
56
+ fetch?: typeof fetch
57
+ sleep?: (ms: number) => Promise<void>
58
+ waiterId?: string
59
+ }
60
+
61
+ /**
62
+ * `wait-messages` mode (`whole-cuts-plan-themselves` decision 20). Runs as an
63
+ * orchestrate session's `asyncRewake` hook: long-polls
64
+ * `GET /api/hooks/orchestrator-messages` until the server hands it lines, then
65
+ * resolves exit code 2 with them joined by newlines — Claude Code wakes the
66
+ * session with that output as one system reminder. `superseded` (a newer waiter
67
+ * took over) and `ended` (the session is over) resolve 0 with nothing to say. A
68
+ * hold that ran out polls again; a failed poll waits and retries. Never 2
69
+ * without lines.
70
+ */
71
+ export async function waitMessages(
72
+ serverUrl: string,
73
+ sessionId: string | undefined,
74
+ deps: WaitMessagesDeps = {},
75
+ ): Promise<{ code: 0 | 2; output: string }> {
76
+ if (!sessionId) return { code: 0, output: '' }
77
+ const doFetch = deps.fetch ?? fetch
78
+ const sleep = deps.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)))
79
+ const waiter = deps.waiterId ?? crypto.randomUUID()
80
+ const url =
81
+ `${serverUrl}/api/hooks/orchestrator-messages` +
82
+ `?sessionId=${encodeURIComponent(sessionId)}&waiter=${encodeURIComponent(waiter)}`
83
+ for (;;) {
84
+ let answer: { status?: string; lines?: unknown }
85
+ try {
86
+ const res = await doFetch(url, { signal: AbortSignal.timeout(WAIT_FETCH_TIMEOUT_MS) })
87
+ answer = (await res.json()) as typeof answer
88
+ } catch {
89
+ await sleep(WAIT_RETRY_MS)
90
+ continue
91
+ }
92
+ if (answer.status === 'superseded' || answer.status === 'ended') return { code: 0, output: '' }
93
+ const lines = Array.isArray(answer.lines) ? answer.lines.filter((l) => typeof l === 'string') : []
94
+ if (answer.status === 'lines' && lines.length > 0) return { code: 2, output: lines.join('\n') }
95
+ // Anything but a hold that ran out answered at once: never spin on it.
96
+ if (answer.status !== 'empty') await sleep(WAIT_RETRY_MS)
97
+ }
98
+ }
99
+
36
100
  export async function main(argv: string[] = process.argv.slice(2)): Promise<void> {
37
101
  const event = argv[0] ?? ''
38
102
  const serverUrl = process.env.RUNCASTLE_SERVER_URL ?? 'http://localhost:4512'
39
103
  const sessionId = process.env.RUNCASTLE_SESSION_ID
40
104
 
105
+ if (event === WAIT_MESSAGES_MODE) {
106
+ const { code, output } = await waitMessages(serverUrl, sessionId)
107
+ if (output) process.stderr.write(output)
108
+ process.exit(code)
109
+ }
110
+
41
111
  const raw = await readStdin()
42
112
  let payload: unknown
43
113
  try {
@@ -46,17 +116,48 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<void
46
116
  payload = { raw }
47
117
  }
48
118
 
49
- const res = await fetch(`${serverUrl}/api/hooks/${event}`, {
50
- method: 'POST',
51
- headers: { 'content-type': 'application/json' },
52
- body: JSON.stringify({ event, sessionId, payload }),
53
- signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
54
- })
55
-
56
- const text = await res.text()
119
+ const text = await postHook(serverUrl, event, { event, sessionId, payload })
57
120
  process.stdout.write(text.trim().length > 0 ? text : '{}')
58
121
  }
59
122
 
123
+ export interface PostHookDeps {
124
+ fetch?: typeof fetch
125
+ sleep?: (ms: number) => Promise<void>
126
+ }
127
+
128
+ /**
129
+ * POST one hook to the server and return its response body. `session-start` is
130
+ * the one that records the conversation id every later resume needs, and its
131
+ * handler is idempotent (it already fires once per source), so a failed or
132
+ * timed-out attempt is retried: one slow server moment must not leave a session
133
+ * unrecorded. Every other hook gets one attempt — it blocks a turn or a tool.
134
+ */
135
+ export async function postHook(
136
+ serverUrl: string,
137
+ event: string,
138
+ body: unknown,
139
+ deps: PostHookDeps = {},
140
+ ): Promise<string> {
141
+ const doFetch = deps.fetch ?? fetch
142
+ const sleep = deps.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)))
143
+ const startHook = event === 'session-start'
144
+ const attempts = startHook ? SESSION_START_ATTEMPTS : 1
145
+ for (let attempt = 1; ; attempt++) {
146
+ try {
147
+ const res = await doFetch(`${serverUrl}/api/hooks/${event}`, {
148
+ method: 'POST',
149
+ headers: { 'content-type': 'application/json' },
150
+ body: JSON.stringify(body),
151
+ signal: AbortSignal.timeout(startHook ? SESSION_START_FETCH_TIMEOUT_MS : FETCH_TIMEOUT_MS),
152
+ })
153
+ return await res.text()
154
+ } catch (err) {
155
+ if (attempt >= attempts) throw err
156
+ await sleep(SESSION_START_RETRY_MS)
157
+ }
158
+ }
159
+ }
160
+
60
161
  if (import.meta.main) {
61
162
  main().catch((err) => {
62
163
  // Never break the user's session: log, print `{}`, exit 0 (SPEC §5.5).