runcastle 1.4.7 → 1.5.3
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/bin/runcastle.js +11150 -7145
- package/drizzle/0045_nappy_adam_destine.sql +3 -0
- package/drizzle/0046_polite_nightcrawler.sql +20 -0
- package/drizzle/0046_project_scenario_runner.sql +1 -0
- package/drizzle/0047_clear_betty_brant.sql +3 -0
- package/drizzle/0048_new_northstar.sql +1 -0
- package/drizzle/0050_tan_manta.sql +1 -0
- package/drizzle/0051_ambiguous_talkback.sql +1 -0
- package/drizzle/meta/0045_snapshot.json +1220 -0
- package/drizzle/meta/0046_snapshot.json +1227 -0
- package/drizzle/meta/0047_snapshot.json +1349 -0
- package/drizzle/meta/0048_snapshot.json +1356 -0
- package/drizzle/meta/0050_snapshot.json +1349 -0
- package/drizzle/meta/0051_snapshot.json +1356 -0
- package/drizzle/meta/_journal.json +49 -0
- package/hook-client.ts +110 -9
- package/index.js +11148 -7143
- package/package.json +1 -1
- package/skills/burner/implement-ticket.md +0 -1
- package/skills/burner/review-ticket.md +7 -5
- package/skills/burner/verify-fixes.md +4 -2
- package/skills/packs/README.md +1 -0
- package/skills/packs/runcastle/.claude-plugin/plugin.json +1 -1
- package/skills/packs/runcastle/skills/design/SKILL.md +73 -0
- package/skills/packs/runcastle/skills/ideate/SKILL.md +11 -8
- package/skills/packs/runcastle/skills/ideate/references/preview-starter.html +16 -1
- package/skills/packs/runcastle/skills/ideate/references/preview.md +35 -1
- package/skills/packs/runcastle/skills/lead/SKILL.md +104 -0
- package/skills/packs/runcastle/skills/prepare/SKILL.md +19 -5
- package/skills/packs/runcastle/skills/prepare/references/recipes.md +6 -2
- package/skills/packs/runcastle/skills/project/SKILL.md +103 -4
- package/skills/packs/runcastle/skills/project/references/charter.md +3 -3
- package/skills/packs/runcastle/skills/project/references/triage.md +1 -1
- package/skills/packs/runcastle/skills/revisit/SKILL.md +12 -35
- package/skills/packs/runcastle/skills/tickets/SKILL.md +33 -5
- package/web/assets/index-BYSs5Hdy.js +186 -0
- package/web/assets/index-bpL_iO_C.css +1 -0
- package/web/index.html +2 -2
- package/web/assets/index-bNgQedte.css +0 -1
- package/web/assets/index-e056iqcB.js +0 -188
|
@@ -316,6 +316,55 @@
|
|
|
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
|
|
319
368
|
}
|
|
320
369
|
]
|
|
321
370
|
}
|
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
|
|
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
|
|
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).
|