@alecsibilia/luca 14.0.0-alpha.4 → 14.0.0-alpha.6

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 (47) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +5 -2
  3. package/board/client/board-panel.tsx +44 -70
  4. package/board/client/stuck-actions.tsx +269 -0
  5. package/board/client/timeline-rows.tsx +41 -14
  6. package/board/index.server.ts +7 -1
  7. package/board/package.json +1 -1
  8. package/board/server/board-server.ts +65 -1
  9. package/board/server/board-vocabulary.ts +18 -5
  10. package/board/server/luca-version.ts +1 -1
  11. package/board/server/make-rows.ts +8 -0
  12. package/board/server/reduce-board.ts +43 -14
  13. package/board/server/reply-post.ts +407 -0
  14. package/board/shared/board-rows.ts +9 -0
  15. package/board/shared/board-rpc.ts +42 -0
  16. package/board/shared/board-state.ts +8 -0
  17. package/board/shared/reply-actions.ts +201 -0
  18. package/engine/agents/role-prompts.ts +22 -0
  19. package/engine/cli/computer-adapters-real.ts +14 -2
  20. package/engine/cli/computer-adapters.ts +2 -0
  21. package/engine/cli/computer-checks.ts +51 -3
  22. package/engine/cli/doctor.ts +14 -2
  23. package/engine/cli/init.ts +17 -1
  24. package/engine/cli/luca-skills.ts +159 -0
  25. package/engine/cli/upgrade-command.ts +1 -0
  26. package/engine/cli/upgrade.ts +23 -6
  27. package/engine/core/decide-build.ts +27 -12
  28. package/engine/core/decide-run-budget.ts +12 -3
  29. package/engine/core/execute-build.ts +95 -41
  30. package/engine/core/execute-changeset.ts +6 -8
  31. package/engine/core/execute-final-review.ts +86 -10
  32. package/engine/core/execute-pull-request.ts +84 -0
  33. package/engine/core/execute-stuck.ts +7 -9
  34. package/engine/core/execute.ts +27 -2
  35. package/engine/core/final-review-text.ts +13 -4
  36. package/engine/core/pull-request-text.ts +140 -0
  37. package/engine/core/stuck-text.ts +29 -0
  38. package/engine/gates/leftover-scan.ts +58 -4
  39. package/engine/git/git-adapter.ts +29 -0
  40. package/engine/journal/journal-record.ts +37 -5
  41. package/engine/journal/replay.ts +27 -10
  42. package/engine/tracker/github-tracker.ts +19 -9
  43. package/engine/tracker/in-memory-tracker.ts +3 -2
  44. package/engine/tracker/tracker.ts +37 -4
  45. package/package.json +2 -1
  46. package/skills/luca-unstick/SKILL.md +149 -0
  47. package/skills/luca-unstick/scripts/stuck-summary.ts +1005 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @alecsibilia/luca
2
2
 
3
+ ## 14.0.0-alpha.6
4
+
5
+ No changes in this release.
6
+
7
+ ## 14.0.0-alpha.5
8
+
9
+ No changes in this release.
10
+
3
11
  ## 14.0.0-alpha.4
4
12
 
5
13
  No changes in this release.
package/README.md CHANGED
@@ -38,14 +38,16 @@ This is Luca v14, in alpha. Old Luca (v13) is still on npm; see [Coming from v13
38
38
  5. Write a spec with `/to-spec`, then split it into tickets with `/to-tickets`.
39
39
  6. In a Paseo chat, type `/luca-run <spec number>`. The board shows the run in the side panel, and the run ends in one pull request.
40
40
 
41
+ If a ticket gets stuck, Luca comments on the spec and waits for your one-word reply. Type `/luca-unstick` in Claude Code or a Paseo chat, in the repo: it reads why, says what to do, and posts the reply once you say yes.
42
+
41
43
  ## Commands
42
44
 
43
45
  | Command | What it does |
44
46
  | ------- | ------------ |
45
- | `luca init` | Sets up this computer, once (safe to run again): MuninnDB and its Claude Code entry, the board in Paseo with its engine and Bun paths, and the planning skills (`to-spec`, `to-tickets`, `setup-matt-pocock-skills`, `grilling`, `domain-modeling`). It asks before turning on Paseo's plugins, and leaves skills you already have alone. `--skip-skills` skips the skills. Inside a git repo, it then runs `luca setup` for that repo. |
47
+ | `luca init` | Sets up this computer, once (safe to run again): MuninnDB and its Claude Code entry, the board in Paseo with its engine and Bun paths, the planning skills (`to-spec`, `to-tickets`, `setup-matt-pocock-skills`, `grilling`, `domain-modeling`), and Luca's own `/luca-unstick` skill. It asks before turning on Paseo's plugins, and leaves skills you already have alone (it only replaces its own `/luca-unstick`). `--skip-skills` skips the planning skills. Inside a git repo, it then runs `luca setup` for that repo. |
46
48
  | `luca setup` | Gets the repo in this folder ready: the labels a run needs (and the `release:*` labels when the repo uses changesets), `.luca/config.json` (a new one gets the repo's GitHub name as its vault; an existing vault is kept), and a check of GitHub sub-issues and dependencies. `--base <branch>` names the base branch. |
47
49
  | `luca doctor` | Checks this computer, this repo when you're in one, and what v13 left behind. Each problem comes with its exact fix. `luca doctor --fix` fixes what's safe without asking: it never deletes (v13 files go to a dated backup folder) and never commits, and it lists the repo files to commit. |
48
- | `luca upgrade` | Moves to the newest Luca on your channel: `alpha` stays on `alpha`, and it never goes back to v13. It refuses while any run is going (stuck runs and limit waits count) and lists them. A run that crashed, with no engine running it, doesn't stop it: upgrade says how to resume it on the new version. Then it reloads the board and says when to run `/reload-skills`. |
50
+ | `luca upgrade` | Moves to the newest Luca on your channel: `alpha` stays on `alpha`, and it never goes back to v13. It refuses while any run is going (stuck runs and limit waits count) and lists them. A run that crashed, with no engine running it, doesn't stop it: upgrade says how to resume it on the new version. Then it reloads the board, copies the new `/luca-unstick` skill, and says when to run `/reload-skills`. |
49
51
  | `luca upgrade --to <version>` | Installs that exact version, an older one too, to go back when a version breaks. |
50
52
 
51
53
  `init`, `setup`, and `upgrade` end with their part of `luca doctor`'s checks.
@@ -88,6 +90,7 @@ This is the one package that goes to npm. It's TypeScript source with no build a
88
90
  | ------ | ---------- |
89
91
  | `engine/` | The engine's source, from [`packages/engine/src`](../engine/src). Both bins point into it. |
90
92
  | `board/` | The board plugin's folder, from [`packages/board`](../board): `paseo-plugin.json`, its entry points, its client, server, and shared code, and its own `package.json`, so Paseo can install it as a folder source. |
93
+ | `skills/` | Luca's own Claude Code skills, such as `luca-unstick`, from [`packages/engine/skills`](../engine/skills). `luca init` and `luca upgrade` copy them to `~/.claude/skills`. |
91
94
  | `LICENSE` | The repo's license. |
92
95
 
93
96
  Test files and test helpers stay out. The engine and the board stay private workspace packages, and this package lists every package they import at runtime as its own dependency.
@@ -6,12 +6,7 @@ import {
6
6
  useWorkspace,
7
7
  type PluginWorkspacePanelProps,
8
8
  } from '@getpaseo/plugin/client'
9
- import {
10
- Icon,
11
- ScrollView,
12
- copyText,
13
- useToast,
14
- } from '@getpaseo/plugin/client/react-native'
9
+ import { Icon, ScrollView } from '@getpaseo/plugin/client/react-native'
15
10
  import { ExternalLink } from '@getpaseo/plugin/client/ui'
16
11
  import { useQuery } from '@tanstack/react-query'
17
12
  import { Pressable, StyleSheet, Text, View } from 'react-native'
@@ -31,6 +26,7 @@ import {
31
26
  usageLineWaitText,
32
27
  useNow,
33
28
  } from './board-look'
29
+ import { StuckActions } from './stuck-actions'
34
30
 
35
31
  import { boardReadRpc } from '../shared/board-rpc'
36
32
  import {
@@ -54,6 +50,7 @@ import {
54
50
  type TicketCard,
55
51
  type TicketStage,
56
52
  } from '../shared/board-state'
53
+ import { postedText, type PostedReply } from '../shared/reply-actions'
57
54
 
58
55
  /**
59
56
  * The side panel: one run of this workspace, top to bottom. Plan usage, a
@@ -254,21 +251,6 @@ const makeStyles = ({
254
251
  fontSize: 12,
255
252
  fontWeight: '700',
256
253
  },
257
- replyRow: {
258
- flexDirection: 'row',
259
- flexWrap: 'wrap',
260
- alignItems: 'center',
261
- gap: 5,
262
- },
263
- reply: {
264
- color: colors.foreground,
265
- backgroundColor: colors.surface2,
266
- borderRadius: 4,
267
- paddingHorizontal: 6,
268
- paddingVertical: 2,
269
- fontSize: 12,
270
- fontFamily: MONO,
271
- },
272
254
  rule: { height: 1, backgroundColor: colors.border, marginVertical: 4 },
273
255
  reviewTitle: {
274
256
  color: colors.foreground,
@@ -439,56 +421,21 @@ const StepLine = ({
439
421
  )
440
422
  }
441
423
 
442
- /** The exact replies; tap one to copy it. */
443
- const Replies = ({
444
- replies,
445
- spec_number,
446
- styles,
447
- }: {
448
- replies: string[]
449
- spec_number: number | null
450
- styles: Styles
451
- }) => {
452
- const toast = useToast()
453
- const copy = async ({ reply }: { reply: string }) => {
454
- try {
455
- await copyText(reply)
456
- toast.show(`Copied "${reply}"`, { variant: 'success' })
457
- } catch {
458
- toast.error('Could not copy. Select the text and use Copy.')
459
- }
460
- }
461
- return (
462
- <View style={styles.replyRow}>
463
- <Text style={styles.label}>
464
- Reply on{' '}
465
- {spec_number === null
466
- ? 'the spec issue'
467
- : `spec issue #${spec_number}`}{' '}
468
- with
469
- </Text>
470
- {replies.map((reply) => (
471
- <Pressable
472
- key={reply}
473
- accessibilityRole="button"
474
- accessibilityLabel={`Copy the reply ${reply}`}
475
- onPress={() => void copy({ reply })}
476
- >
477
- <Text style={styles.reply} selectable>
478
- {reply}
479
- </Text>
480
- </Pressable>
481
- ))}
482
- </View>
483
- )
484
- }
485
-
486
424
  const NeedsYouCard = ({
487
425
  item,
426
+ run_id,
488
427
  spec_number,
428
+ posted,
429
+ chat_agent_id,
489
430
  theme,
490
431
  styles,
491
- }: { item: NeedsYou; spec_number: number | null } & Look) => (
432
+ }: {
433
+ item: NeedsYou
434
+ run_id: string
435
+ spec_number: number | null
436
+ posted: PostedReply[]
437
+ chat_agent_id: string | null
438
+ } & Look) => (
492
439
  <View style={[styles.card, styles.cardStuck]}>
493
440
  <View style={styles.cardTop}>
494
441
  <Icon
@@ -518,10 +465,13 @@ const NeedsYouCard = ({
518
465
  ))}
519
466
  </>
520
467
  ) : null}
521
- <Replies
522
- replies={item.replies}
468
+ <StuckActions
469
+ run_id={run_id}
470
+ item={item}
523
471
  spec_number={spec_number}
524
- styles={styles}
472
+ posted={posted}
473
+ chat_agent_id={chat_agent_id}
474
+ theme={theme}
525
475
  />
526
476
  </View>
527
477
  )
@@ -1096,6 +1046,15 @@ export const BoardPanel = ({
1096
1046
 
1097
1047
  const { run } = state
1098
1048
  const runs = board.data?.runs ?? []
1049
+ const posted = board.data?.posted ?? []
1050
+ const chat_agent_id = board.data?.chat_agent_id ?? null
1051
+ // Replies the board posted whose stuck item is gone: say what came of them.
1052
+ const answered = posted.filter(
1053
+ (entry) =>
1054
+ !state.needs_you.some(
1055
+ (item) => item.key === entry.key && item.since === entry.since
1056
+ )
1057
+ )
1099
1058
  const card = (ticket: TicketCard) => {
1100
1059
  const key = `ticket-${ticket.number}`
1101
1060
  return (
@@ -1257,14 +1216,29 @@ export const BoardPanel = ({
1257
1216
  <View style={styles.stageBody}>
1258
1217
  {state.needs_you.map((item) => (
1259
1218
  <NeedsYouCard
1260
- key={item.key}
1219
+ key={`${item.key}-${item.since}`}
1261
1220
  item={item}
1221
+ run_id={run.run_id}
1262
1222
  spec_number={run.spec_number}
1223
+ posted={posted}
1224
+ chat_agent_id={chat_agent_id}
1263
1225
  {...look}
1264
1226
  />
1265
1227
  ))}
1266
1228
  </View>
1267
1229
  ) : null}
1230
+ {answered.length > 0 ? (
1231
+ <View style={styles.stageBody}>
1232
+ {answered.map((entry) => (
1233
+ <Text
1234
+ key={`${entry.key}-${entry.since}`}
1235
+ style={styles.muted}
1236
+ >
1237
+ {postedText({ posted: entry })}
1238
+ </Text>
1239
+ ))}
1240
+ </View>
1241
+ ) : null}
1268
1242
  </Section>
1269
1243
 
1270
1244
  {TICKET_STAGES.map((entry) => {
@@ -0,0 +1,269 @@
1
+ import { useMemo, useState } from 'react'
2
+
3
+ import type { PluginTheme } from '@getpaseo/plugin'
4
+ import { usePaseo, useRpc } from '@getpaseo/plugin/client'
5
+ import { copyText, useToast } from '@getpaseo/plugin/client/react-native'
6
+ import { Pressable, StyleSheet, Text, View } from 'react-native'
7
+
8
+ import { MONO } from './board-look'
9
+
10
+ import { replyPostRpc } from '../shared/board-rpc'
11
+ import {
12
+ confirmReplyText,
13
+ postedFor,
14
+ postedText,
15
+ replyButtons,
16
+ unstickCommand,
17
+ type PostedReply,
18
+ type ReplyButton,
19
+ } from '../shared/reply-actions'
20
+
21
+ /**
22
+ * The actions on stuck work (#503), in the side panel and in the chat's
23
+ * stuck rows: a button per reply the engine takes, which asks to confirm
24
+ * and then posts the reply on the spec issue through `reply.post`, and
25
+ * "Help me", which sends `/luca-unstick` to a chat's agent.
26
+ */
27
+
28
+ const makeStyles = ({ theme }: { theme: PluginTheme }) => {
29
+ const { colors } = theme
30
+ return StyleSheet.create({
31
+ box: { gap: 6 },
32
+ row: {
33
+ flexDirection: 'row',
34
+ flexWrap: 'wrap',
35
+ alignItems: 'center',
36
+ gap: 6,
37
+ },
38
+ label: {
39
+ color: colors.foregroundMuted,
40
+ fontSize: 12,
41
+ fontWeight: '700',
42
+ },
43
+ text: { color: colors.foreground, fontSize: 12 },
44
+ muted: { color: colors.foregroundMuted, fontSize: 12 },
45
+ danger: { color: colors.statusDanger, fontSize: 12 },
46
+ success: { color: colors.statusSuccess, fontSize: 12 },
47
+ button: {
48
+ borderWidth: 1,
49
+ borderColor: colors.border,
50
+ backgroundColor: colors.surface2,
51
+ borderRadius: 6,
52
+ paddingHorizontal: 9,
53
+ paddingVertical: 4,
54
+ },
55
+ buttonText: {
56
+ color: colors.foreground,
57
+ fontSize: 12,
58
+ fontFamily: MONO,
59
+ },
60
+ primary: {
61
+ borderRadius: 6,
62
+ paddingHorizontal: 10,
63
+ paddingVertical: 4,
64
+ backgroundColor: colors.accent,
65
+ },
66
+ primaryText: {
67
+ color: colors.accentForeground,
68
+ fontSize: 12,
69
+ fontWeight: '700',
70
+ },
71
+ posted: { borderColor: colors.statusSuccess },
72
+ postedText: { color: colors.statusSuccess },
73
+ locked: { opacity: 0.45 },
74
+ })
75
+ }
76
+
77
+ type Styles = ReturnType<typeof makeStyles>
78
+
79
+ /** What a reply button reads, for its state. */
80
+ const buttonLabel = ({ button }: { button: ReplyButton }): string => {
81
+ switch (button.state) {
82
+ case 'sending':
83
+ return `${button.reply} · posting…`
84
+ case 'posted':
85
+ return `${button.reply} · posted`
86
+ default:
87
+ return button.reply
88
+ }
89
+ }
90
+
91
+ const ReplyButtonView = ({
92
+ button,
93
+ onPress,
94
+ styles,
95
+ }: {
96
+ button: ReplyButton
97
+ onPress: () => void
98
+ styles: Styles
99
+ }) => (
100
+ <Pressable
101
+ accessibilityRole="button"
102
+ accessibilityLabel={`Post the reply ${button.reply}`}
103
+ accessibilityState={{ disabled: button.state !== 'ready' }}
104
+ disabled={button.state !== 'ready'}
105
+ onPress={onPress}
106
+ style={[
107
+ styles.button,
108
+ button.state === 'posted' ? styles.posted : null,
109
+ button.state === 'locked' ? styles.locked : null,
110
+ ]}
111
+ >
112
+ <Text
113
+ style={[
114
+ styles.buttonText,
115
+ button.state === 'posted' ? styles.postedText : null,
116
+ ]}
117
+ >
118
+ {buttonLabel({ button })}
119
+ </Text>
120
+ </Pressable>
121
+ )
122
+
123
+ /**
124
+ * A stuck item's reply buttons and "Help me". `posted` is what the board
125
+ * says it posted (from `board.read`); a post made here shows at once too.
126
+ * `chat_agent_id` is the chat "Help me" sends `/luca-unstick` to; without
127
+ * one, the command is copied instead.
128
+ */
129
+ export const StuckActions = ({
130
+ run_id,
131
+ item,
132
+ spec_number,
133
+ posted,
134
+ chat_agent_id,
135
+ theme,
136
+ }: {
137
+ run_id: string
138
+ item: { key: string; ticket: number | null; since: string }
139
+ spec_number: number | null
140
+ posted: PostedReply[]
141
+ chat_agent_id: string | null
142
+ theme: PluginTheme
143
+ }) => {
144
+ const styles = useMemo(() => makeStyles({ theme }), [theme])
145
+ const postReply = useRpc(replyPostRpc)
146
+ const paseo = usePaseo()
147
+ const toast = useToast()
148
+ const [confirming, setConfirming] = useState<string | null>(null)
149
+ const [sending, setSending] = useState<string | null>(null)
150
+ const [mine, setMine] = useState<PostedReply | null>(null)
151
+ const [note, setNote] = useState<{ ok: boolean; text: string } | null>(null)
152
+
153
+ const known = [...posted, ...(mine ? [mine] : [])]
154
+ const buttons = replyButtons({ item, posted: known, sending })
155
+ const done = postedFor({ item, posted: known })
156
+ const command = unstickCommand({ run_id, ticket: item.ticket })
157
+
158
+ const post = async ({ reply }: { reply: string }) => {
159
+ setConfirming(null)
160
+ setSending(reply)
161
+ setNote(null)
162
+ try {
163
+ const output = await postReply({ run_id, key: item.key, reply })
164
+ if (output.posted) setMine(output.posted)
165
+ setNote({ ok: output.ok, text: output.message })
166
+ } catch (error) {
167
+ setNote({
168
+ ok: false,
169
+ text: `Couldn't post \`${reply}\`: ${String(error)}`,
170
+ })
171
+ } finally {
172
+ setSending(null)
173
+ }
174
+ }
175
+
176
+ const helpMe = async () => {
177
+ if (chat_agent_id !== null) {
178
+ try {
179
+ await paseo.agents.ref(chat_agent_id).send(command)
180
+ toast.show(`Sent ${command} to the run's chat.`, {
181
+ variant: 'success',
182
+ })
183
+ return
184
+ } catch {
185
+ // The chat may be gone: copy it instead.
186
+ }
187
+ }
188
+ try {
189
+ await copyText(command)
190
+ toast.show(`Copied ${command}. Paste it in a chat.`, {
191
+ variant: 'success',
192
+ })
193
+ } catch {
194
+ toast.error(`Could not copy. Type ${command} in a chat.`)
195
+ }
196
+ }
197
+
198
+ return (
199
+ <View style={styles.box}>
200
+ <Text style={styles.label}>
201
+ Reply on{' '}
202
+ {spec_number === null
203
+ ? 'the spec issue'
204
+ : `spec issue #${spec_number}`}
205
+ </Text>
206
+ <View style={styles.row}>
207
+ {buttons.map((button) => (
208
+ <ReplyButtonView
209
+ key={button.reply}
210
+ button={button}
211
+ onPress={() => {
212
+ setNote(null)
213
+ setConfirming(button.reply)
214
+ }}
215
+ styles={styles}
216
+ />
217
+ ))}
218
+ </View>
219
+ {confirming !== null && done === null && sending === null ? (
220
+ <View style={styles.row}>
221
+ <Text style={styles.text}>
222
+ {confirmReplyText({ reply: confirming, spec_number })}
223
+ </Text>
224
+ <Pressable
225
+ accessibilityRole="button"
226
+ accessibilityLabel={`Yes, post ${confirming}`}
227
+ onPress={() => void post({ reply: confirming })}
228
+ style={styles.primary}
229
+ >
230
+ <Text style={styles.primaryText}>Post</Text>
231
+ </Pressable>
232
+ <Pressable
233
+ accessibilityRole="button"
234
+ accessibilityLabel="Cancel"
235
+ onPress={() => setConfirming(null)}
236
+ style={styles.button}
237
+ >
238
+ <Text style={styles.buttonText}>Cancel</Text>
239
+ </Pressable>
240
+ </View>
241
+ ) : null}
242
+ {note !== null && !(note.ok && done !== null) ? (
243
+ <Text style={note.ok ? styles.success : styles.danger}>
244
+ {note.text}
245
+ </Text>
246
+ ) : null}
247
+ {done !== null ? (
248
+ <Text style={styles.success}>
249
+ {postedText({ posted: done })}
250
+ </Text>
251
+ ) : null}
252
+ <View style={styles.row}>
253
+ <Pressable
254
+ accessibilityRole="button"
255
+ accessibilityLabel={`Help me: ${command}`}
256
+ onPress={() => void helpMe()}
257
+ style={styles.button}
258
+ >
259
+ <Text style={styles.buttonText}>Help me</Text>
260
+ </Pressable>
261
+ <Text style={styles.muted}>
262
+ {chat_agent_id === null
263
+ ? `copies ${command}`
264
+ : `sends ${command} to the run's chat`}
265
+ </Text>
266
+ </View>
267
+ </View>
268
+ )
269
+ }
@@ -20,6 +20,7 @@ import {
20
20
  usageColor,
21
21
  useNow,
22
22
  } from './board-look'
23
+ import { StuckActions } from './stuck-actions'
23
24
 
24
25
  import type { EventRow, LimitRow, RunRow, StuckRow } from '../shared/board-rows'
25
26
  import { currentStepText, USAGE_LABEL } from '../shared/board-state'
@@ -294,9 +295,14 @@ export const EventRowLine = ({
294
295
  )
295
296
  }
296
297
 
297
- /** Stuck work, with the exact replies; resolved in place. */
298
+ /**
299
+ * Stuck work, with a button per reply and "Help me", which sends
300
+ * `/luca-unstick` to this chat (#503); resolved in place. A row added
301
+ * before the buttons shows the replies as text.
302
+ */
298
303
  export const StuckRowCard = ({
299
304
  item,
305
+ agentId,
300
306
  theme,
301
307
  layout,
302
308
  }: PluginTimelineItemProps<StuckRow>) => {
@@ -333,20 +339,41 @@ export const StuckRowCard = ({
333
339
  · {line}
334
340
  </Text>
335
341
  ))}
336
- <View style={styles.replyRow}>
337
- <Text style={styles.muted}>
338
- Reply with a comment on{' '}
339
- {row.spec_number === null
340
- ? 'the spec issue'
341
- : `spec issue #${row.spec_number}`}
342
- :
343
- </Text>
344
- {row.replies.map((reply) => (
345
- <Text key={reply} style={styles.reply} selectable>
346
- {reply}
342
+ {row.run_id !== null &&
343
+ row.key !== null &&
344
+ row.since !== null ? (
345
+ <StuckActions
346
+ run_id={row.run_id}
347
+ item={{
348
+ key: row.key,
349
+ ticket: row.ticket,
350
+ since: row.since,
351
+ }}
352
+ spec_number={row.spec_number}
353
+ posted={[]}
354
+ chat_agent_id={agentId}
355
+ theme={theme}
356
+ />
357
+ ) : (
358
+ <View style={styles.replyRow}>
359
+ <Text style={styles.muted}>
360
+ Reply with a comment on{' '}
361
+ {row.spec_number === null
362
+ ? 'the spec issue'
363
+ : `spec issue #${row.spec_number}`}
364
+ :
347
365
  </Text>
348
- ))}
349
- </View>
366
+ {row.replies.map((reply) => (
367
+ <Text
368
+ key={reply}
369
+ style={styles.reply}
370
+ selectable
371
+ >
372
+ {reply}
373
+ </Text>
374
+ ))}
375
+ </View>
376
+ )}
350
377
  </>
351
378
  ) : (
352
379
  <Text style={styles.muted}>{row.resolution}</Text>
@@ -23,6 +23,7 @@ import {
23
23
  boardReadRpc,
24
24
  boardVersionRpc,
25
25
  engineEventRpc,
26
+ replyPostRpc,
26
27
  runStartRpc,
27
28
  } from './shared/board-rpc'
28
29
  import { PLUGIN_ID } from './shared/board-state'
@@ -40,7 +41,8 @@ const log = (message: string) => console.error(`[${PLUGIN_ID}] ${message}`)
40
41
  * The board plugin's daemon side: the engine settings (their usage lines
41
42
  * kept in a file for the engine), `run.start` (launch a
42
43
  * run from `/luca-run`), `engine.event` (the engine's journal records in),
43
- * and `board.read` (the side panel's poll). On start, and every 15 s after,
44
+ * `board.read` (the side panel's poll), and `reply.post` (a reply button on
45
+ * stuck work posts its word on the spec, #503). On start, and every 15 s after,
44
46
  * it checks for runs whose engine is gone and restarts the ones that can go
45
47
  * on (#375). The logic lives in `createBoardServer`; this entry only wires it
46
48
  * to Paseo.
@@ -130,6 +132,10 @@ export default function contribute(server: PluginServerContext) {
130
132
  connect({ context })
131
133
  return board.readBoard(input)
132
134
  })
135
+ server.handle(replyPostRpc, (input, context) => {
136
+ connect({ context })
137
+ return board.postReply(input)
138
+ })
133
139
  // Fixed as loaded: after an upgrade it differs until a reload.
134
140
  server.handle(boardVersionRpc, () => ({ version: LUCA_VERSION }))
135
141
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luca/board",
3
- "version": "14.0.0-alpha.4",
3
+ "version": "14.0.0-alpha.6",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }