@alecsibilia/luca 14.0.0-alpha.2 → 14.0.0-alpha.21
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/CHANGELOG.md +76 -0
- package/README.md +7 -2
- package/board/client/board-look.ts +31 -1
- package/board/client/board-panel-parts.tsx +150 -0
- package/board/client/board-panel-stages.ts +99 -0
- package/board/client/board-panel-styles.ts +223 -0
- package/board/client/board-panel.tsx +77 -1275
- package/board/client/engine-life.ts +48 -0
- package/board/client/final-review-stack.tsx +144 -0
- package/board/client/heartbeat.tsx +61 -0
- package/board/client/helper-lines.tsx +69 -0
- package/board/client/live-feed.tsx +86 -0
- package/board/client/live-line.ts +73 -0
- package/board/client/model-line.tsx +178 -0
- package/board/client/needs-you-section.tsx +133 -0
- package/board/client/one-line.tsx +193 -0
- package/board/client/repo-wait-banner.tsx +69 -0
- package/board/client/run-footer.tsx +68 -0
- package/board/client/run-header.tsx +189 -0
- package/board/client/run-picker.tsx +98 -0
- package/board/client/status-bar.tsx +131 -0
- package/board/client/step-dots.tsx +127 -0
- package/board/client/stuck-actions.tsx +269 -0
- package/board/client/ticket-card-view.tsx +238 -0
- package/board/client/ticket-detail.tsx +87 -0
- package/board/client/ticket-stack.tsx +119 -0
- package/board/client/ticket-wait.ts +188 -0
- package/board/client/timeline-rows.tsx +41 -14
- package/board/client/usage-line.tsx +51 -0
- package/board/client/wait-line.tsx +39 -0
- package/board/index.client.tsx +1 -1
- package/board/index.server.ts +7 -1
- package/board/package.json +1 -1
- package/board/server/board-panel-state-helpers.ts +67 -0
- package/board/server/board-server-check.ts +294 -0
- package/board/server/board-server-engine.ts +129 -0
- package/board/server/board-server-events.ts +134 -0
- package/board/server/board-server-read.ts +190 -0
- package/board/server/board-server-resume.ts +164 -0
- package/board/server/board-server-start.ts +174 -0
- package/board/server/board-server-store.ts +281 -0
- package/board/server/board-server-types.ts +114 -0
- package/board/server/board-server-util.ts +4 -0
- package/board/server/board-server.ts +52 -943
- package/board/server/board-vocabulary-engine.ts +144 -0
- package/board/server/board-vocabulary-review.ts +115 -0
- package/board/server/board-vocabulary-run.ts +145 -0
- package/board/server/board-vocabulary-shapes.ts +153 -0
- package/board/server/board-vocabulary.ts +20 -375
- package/board/server/current-step-panel-helpers.ts +108 -0
- package/board/server/describe-record.ts +403 -0
- package/board/server/engine-launch.ts +20 -3
- package/board/server/engine-restart-helpers.ts +105 -0
- package/board/server/engine-watch.ts +45 -5
- package/board/server/journal-rebuild-helpers.ts +169 -0
- package/board/server/live-file.ts +130 -0
- package/board/server/luca-version.ts +1 -1
- package/board/server/make-rows.ts +14 -629
- package/board/server/record-texts.ts +350 -0
- package/board/server/reduce-board-agent-start.ts +224 -0
- package/board/server/reduce-board-agents.ts +160 -0
- package/board/server/reduce-board-counters.ts +138 -0
- package/board/server/reduce-board-create.ts +148 -0
- package/board/server/reduce-board-final-review.ts +174 -0
- package/board/server/reduce-board-needs-you.ts +283 -0
- package/board/server/reduce-board-reasons.ts +59 -0
- package/board/server/reduce-board-run.ts +255 -0
- package/board/server/reduce-board-settle.ts +88 -0
- package/board/server/reduce-board-steps.ts +226 -0
- package/board/server/reduce-board-text.ts +139 -0
- package/board/server/reduce-board-ticket-checks.ts +168 -0
- package/board/server/reduce-board-ticket-join.ts +144 -0
- package/board/server/reduce-board-ticket-start.ts +132 -0
- package/board/server/reduce-board-update.ts +117 -0
- package/board/server/reduce-board-usage.ts +247 -0
- package/board/server/reduce-board.ts +100 -1917
- package/board/server/reply-post.ts +407 -0
- package/board/server/run-registry.ts +8 -4
- package/board/server/ticket-stages-panel-helpers.ts +296 -0
- package/board/shared/board-rows.ts +9 -0
- package/board/shared/board-rpc.ts +42 -0
- package/board/shared/board-state-counts.ts +77 -0
- package/board/shared/board-state-live.ts +99 -0
- package/board/shared/board-state-review.ts +59 -0
- package/board/shared/board-state-run.ts +236 -0
- package/board/shared/board-state-ticket.ts +257 -0
- package/board/shared/board-state-usage.ts +107 -0
- package/board/shared/board-state.ts +97 -567
- package/board/shared/reply-actions.ts +207 -0
- package/engine/agents/agent-launcher.ts +27 -4
- package/engine/agents/claude-launcher-helpers.ts +233 -0
- package/engine/agents/claude-launcher.ts +28 -669
- package/engine/agents/claude-messages.ts +212 -0
- package/engine/agents/claude-options.ts +43 -8
- package/engine/agents/common-instructions.ts +138 -0
- package/engine/agents/input-channel.ts +36 -0
- package/engine/agents/launch-paths.ts +29 -0
- package/engine/agents/launch-session.ts +227 -0
- package/engine/agents/learner-instructions.ts +31 -0
- package/engine/agents/open-session.ts +210 -0
- package/engine/agents/prompt-sections.ts +95 -0
- package/engine/agents/rejoin-prompts.ts +135 -0
- package/engine/agents/reviewer-instructions.ts +99 -0
- package/engine/agents/role-instructions.ts +31 -244
- package/engine/agents/role-prompts.ts +39 -109
- package/engine/agents/role-results.ts +106 -7
- package/engine/agents/scripted-launcher.ts +21 -6
- package/engine/agents/session-pool.ts +124 -0
- package/engine/agents/turn-progress.ts +283 -0
- package/engine/agents/writer-instructions.ts +151 -0
- package/engine/cli/computer-adapters-real.ts +15 -3
- package/engine/cli/computer-adapters.ts +2 -0
- package/engine/cli/computer-checks.ts +87 -4
- package/engine/cli/config-reload-text.ts +39 -0
- package/engine/cli/decision-model-setup.ts +70 -0
- package/engine/cli/doctor-command.ts +1 -0
- package/engine/cli/doctor.ts +21 -3
- package/engine/cli/going-runs.ts +192 -36
- package/engine/cli/init-command.ts +1 -0
- package/engine/cli/init.ts +23 -1
- package/engine/cli/live-runs.ts +73 -0
- package/engine/cli/luca-run.ts +42 -7
- package/engine/cli/luca-skills.ts +183 -0
- package/engine/cli/luca.ts +8 -1
- package/engine/cli/models-command.ts +119 -0
- package/engine/cli/read-run.ts +81 -0
- package/engine/cli/run-args.ts +33 -6
- package/engine/cli/run-demo.ts +143 -0
- package/engine/cli/run-end.ts +184 -0
- package/engine/cli/run-modes-helpers.ts +81 -0
- package/engine/cli/run-modes.ts +62 -439
- package/engine/cli/setup.ts +5 -48
- package/engine/cli/unfinished-runs.ts +122 -0
- package/engine/cli/upgrade-command.ts +212 -30
- package/engine/cli/upgrade-finish.ts +149 -0
- package/engine/cli/upgrade.ts +112 -71
- package/engine/config/config-fields.ts +127 -0
- package/engine/config/config-reload.ts +250 -0
- package/engine/config/engine-config-schema.ts +235 -0
- package/engine/config/engine-config.ts +72 -142
- package/engine/config/test-commands.ts +102 -0
- package/engine/core/agent-guards-helpers.ts +84 -0
- package/engine/core/agent-turn.ts +395 -0
- package/engine/core/already-done.ts +44 -1
- package/engine/core/build-action.ts +214 -0
- package/engine/core/build-context.ts +135 -0
- package/engine/core/build-launch.ts +259 -0
- package/engine/core/commit-in.ts +282 -0
- package/engine/core/decide-build-early.ts +108 -0
- package/engine/core/decide-build-ending.ts +145 -0
- package/engine/core/decide-build-helpers.ts +87 -0
- package/engine/core/decide-build-tickets.ts +207 -0
- package/engine/core/decide-build-view.ts +103 -0
- package/engine/core/decide-build.ts +101 -1368
- package/engine/core/decide-final-fix.ts +203 -0
- package/engine/core/decide-final-review.ts +55 -310
- package/engine/core/decide-red-green.ts +398 -0
- package/engine/core/decide-rejoin.ts +259 -0
- package/engine/core/decide-repo-wait.ts +129 -0
- package/engine/core/decide-run-budget.ts +86 -12
- package/engine/core/decide-stuck-helpers.ts +127 -0
- package/engine/core/decide-stuck.ts +60 -8
- package/engine/core/decide-ticket-review.ts +95 -0
- package/engine/core/decide-ticket.ts +267 -0
- package/engine/core/decide-waits.ts +47 -0
- package/engine/core/decide.ts +74 -23
- package/engine/core/engine-chores.ts +163 -0
- package/engine/core/environment-rerun.ts +83 -0
- package/engine/core/evidence-text.ts +84 -0
- package/engine/core/execute-action.ts +303 -0
- package/engine/core/execute-already-done-check.ts +133 -0
- package/engine/core/execute-build.ts +68 -944
- package/engine/core/execute-changeset.ts +5 -7
- package/engine/core/execute-final-review.ts +125 -13
- package/engine/core/execute-gates.ts +182 -0
- package/engine/core/execute-intake.ts +130 -0
- package/engine/core/execute-join.ts +276 -0
- package/engine/core/execute-memory.ts +12 -0
- package/engine/core/execute-pull-request.ts +84 -0
- package/engine/core/execute-red-check.ts +219 -0
- package/engine/core/execute-repo-wait.ts +129 -0
- package/engine/core/execute-stuck.ts +25 -10
- package/engine/core/execute-waits.ts +158 -0
- package/engine/core/execute-with-jev.ts +118 -0
- package/engine/core/execute.ts +124 -831
- package/engine/core/final-failure-cap.ts +38 -0
- package/engine/core/final-review-action.ts +125 -0
- package/engine/core/final-review-text.ts +63 -17
- package/engine/core/final-stuck-text.ts +86 -0
- package/engine/core/fix-loop-text.ts +49 -2
- package/engine/core/left-out.ts +120 -0
- package/engine/core/live-file.ts +372 -0
- package/engine/core/loop-caps.ts +25 -2
- package/engine/core/not-in-run.ts +7 -3
- package/engine/core/pull-request-text.ts +255 -6
- package/engine/core/repo-wait-text.ts +81 -0
- package/engine/core/review-text.ts +55 -1
- package/engine/core/run-crash-recovery-helpers.ts +16 -0
- package/engine/core/run-session-close-helpers.ts +96 -0
- package/engine/core/run-start.ts +148 -0
- package/engine/core/run-stuck-text.ts +86 -0
- package/engine/core/schedule-rules.ts +193 -0
- package/engine/core/stuck-detail-text.ts +89 -0
- package/engine/core/stuck-format.ts +22 -0
- package/engine/core/stuck-text.ts +29 -332
- package/engine/core/ticket-stuck-text.ts +192 -0
- package/engine/core/worktree-changes.ts +106 -0
- package/engine/gates/already-done-check.ts +120 -0
- package/engine/gates/feature-map.ts +96 -0
- package/engine/gates/gate-runner.ts +108 -108
- package/engine/gates/gate-schemas.ts +61 -2
- package/engine/gates/leftover-scan.ts +154 -31
- package/engine/gates/new-tests.ts +45 -0
- package/engine/gates/own-files-check.ts +98 -0
- package/engine/gates/prepare-check.ts +60 -0
- package/engine/gates/prepare-slots.ts +99 -0
- package/engine/gates/red-check.ts +184 -16
- package/engine/gates/shell-check.ts +64 -0
- package/engine/gates/slot-waits.ts +55 -0
- package/engine/gates/slow-test-runner.ts +59 -0
- package/engine/gates/test-checks.ts +161 -0
- package/engine/gates/test-runner.ts +164 -5
- package/engine/gates/test-slots.ts +48 -0
- package/engine/git/git-adapter.ts +55 -396
- package/engine/git/git-commits.ts +132 -0
- package/engine/git/git-join.ts +139 -0
- package/engine/git/git-read.ts +123 -0
- package/engine/git/git-run.ts +32 -0
- package/engine/git/git-worktrees.ts +143 -0
- package/engine/git/outside-links.ts +55 -0
- package/engine/guards/after-turn-check.ts +31 -5
- package/engine/guards/guard-hook.ts +60 -12
- package/engine/guards/guard-roles.ts +68 -0
- package/engine/guards/helper-rules.ts +92 -0
- package/engine/guards/permission-rules.ts +161 -0
- package/engine/guards/role-rules-helpers.ts +48 -0
- package/engine/guards/role-rules.ts +34 -606
- package/engine/guards/shell-rules.ts +344 -0
- package/engine/guards/worktree-git.ts +208 -0
- package/engine/guards/worktree-snapshot.ts +412 -0
- package/engine/guards/worktree-state.ts +28 -825
- package/engine/guards/worktree-undo.ts +260 -0
- package/engine/guards/write-rules.ts +149 -0
- package/engine/index.ts +30 -9
- package/engine/intake/criteria-checks.ts +114 -0
- package/engine/intake/intake-checks.ts +164 -30
- package/engine/intake/intake-schemas.ts +37 -2
- package/engine/jev/clef-client.ts +259 -0
- package/engine/jev/decision-model-credentials.ts +212 -0
- package/engine/jev/jev-jobs.ts +144 -60
- package/engine/jev/jev-schemas.ts +86 -5
- package/engine/jev/jev-shadow-helpers.ts +218 -0
- package/engine/jev/jev-shadow.ts +109 -64
- package/engine/jev/model-pick.ts +275 -0
- package/engine/journal/agent-entries.ts +167 -0
- package/engine/journal/agent-message-entries.ts +72 -0
- package/engine/journal/agent-turn-replay.ts +203 -0
- package/engine/journal/already-done-entries.ts +55 -0
- package/engine/journal/commit-entries.ts +56 -0
- package/engine/journal/cross-ticket-replay.ts +222 -0
- package/engine/journal/decision-model-entries.ts +86 -0
- package/engine/journal/entry-fields.ts +10 -0
- package/engine/journal/final-review-entries.ts +128 -0
- package/engine/journal/final-review-replay.ts +324 -0
- package/engine/journal/final-review-round.ts +320 -0
- package/engine/journal/final-review-state.ts +180 -0
- package/engine/journal/gate-entries.ts +81 -0
- package/engine/journal/intake-entries.ts +87 -0
- package/engine/journal/join-entries.ts +126 -0
- package/engine/journal/journal-entry-schema.ts +218 -0
- package/engine/journal/journal-kinds.ts +97 -0
- package/engine/journal/journal-record-schema.ts +218 -0
- package/engine/journal/journal-record.ts +43 -1400
- package/engine/journal/journal.ts +6 -1
- package/engine/journal/limit-entries.ts +90 -0
- package/engine/journal/memory-entries.ts +124 -0
- package/engine/journal/memory-replay.ts +157 -0
- package/engine/journal/plan-replay.ts +139 -0
- package/engine/journal/pull-request-entries.ts +41 -0
- package/engine/journal/replay.ts +157 -2144
- package/engine/journal/reply-replay.ts +144 -0
- package/engine/journal/repo-wait-entries.ts +53 -0
- package/engine/journal/run-entries.ts +172 -0
- package/engine/journal/run-state.ts +264 -0
- package/engine/journal/shared-failure-replay.ts +133 -0
- package/engine/journal/stuck-entries.ts +193 -0
- package/engine/journal/ticket-progress.ts +392 -0
- package/engine/journal/ticket-replay.ts +411 -0
- package/engine/journal/ticket-result-replay.ts +364 -0
- package/engine/journal/ticket-retry-replay.ts +142 -0
- package/engine/journal/worktree-entries.ts +122 -0
- package/engine/models/model-score.ts +512 -0
- package/engine/shell/run-command.ts +187 -18
- package/engine/testing/agent-turn-fixtures.ts +287 -0
- package/engine/testing/already-done-fixtures.ts +97 -0
- package/engine/testing/build-fixtures.ts +68 -973
- package/engine/testing/checkouts-fixtures.ts +48 -0
- package/engine/testing/final-review-fixtures.ts +62 -1
- package/engine/testing/final-review-steps.ts +143 -0
- package/engine/testing/limit-fixtures.ts +85 -0
- package/engine/testing/live-file-fixtures.ts +57 -0
- package/engine/testing/practice-repo.ts +10 -4
- package/engine/testing/practice-run.ts +1 -1
- package/engine/testing/reply-fixtures.ts +88 -0
- package/engine/testing/run-budget-fixtures.ts +165 -0
- package/engine/testing/run-fixtures.ts +219 -0
- package/engine/testing/sdk-play.ts +177 -0
- package/engine/testing/setup-repo.ts +194 -0
- package/engine/testing/step-record-fixtures.ts +55 -0
- package/engine/testing/ticket-step-fixtures.ts +272 -0
- package/engine/testing/wait-scenarios.ts +186 -0
- package/engine/tracker/gh-retry.ts +217 -0
- package/engine/tracker/github-tracker.ts +310 -90
- package/engine/tracker/in-memory-tracker.ts +8 -4
- package/engine/tracker/tracker.ts +131 -6
- package/package.json +2 -1
- package/skills/luca-retro/SKILL.md +74 -0
- package/skills/luca-retro/scripts/retro-summary.ts +1356 -0
- package/skills/luca-unstick/SKILL.md +153 -0
- package/skills/luca-unstick/scripts/stuck-summary.ts +1063 -0
- package/engine/jev/jev-client.ts +0 -173
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
import type { CommandResult, RunCommand } from './board-server'
|
|
6
|
+
|
|
7
|
+
import type { ReplyPostInput, ReplyPostOutput } from '../shared/board-rpc'
|
|
8
|
+
import type { BoardState } from '../shared/board-state'
|
|
9
|
+
import {
|
|
10
|
+
allowedReplies,
|
|
11
|
+
replyParts,
|
|
12
|
+
repoOfIssueUrl,
|
|
13
|
+
type PostedReply,
|
|
14
|
+
} from '../shared/reply-actions'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `reply.post` (#503): a reply button on stuck work posts its word on the
|
|
18
|
+
* spec issue with `gh issue comment`, so the owner doesn't have to go to
|
|
19
|
+
* GitHub. It never trusts the button: it checks again, from the run's
|
|
20
|
+
* board, that the item is stuck and the word is one the engine takes for
|
|
21
|
+
* it, and that the signed-in `gh` user owns the spec (the engine counts
|
|
22
|
+
* only the spec owner's replies). It posts once per stuck item. `gh` runs
|
|
23
|
+
* with an argument list, never through a shell, and needs no engine token,
|
|
24
|
+
* so runs the board didn't start can be answered too.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** How long one `gh` call may take. */
|
|
28
|
+
export const GH_TIMEOUT_MS = 30_000
|
|
29
|
+
|
|
30
|
+
const GH_HINT =
|
|
31
|
+
'Install the GitHub CLI (gh), or set LUCA_GH to its absolute path for the Paseo daemon.'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Finds `gh`: `LUCA_GH`, then the daemon's `PATH`, then the usual install
|
|
35
|
+
* folders. Only an absolute path that exists counts, since Paseo's daemon
|
|
36
|
+
* may run with a short `PATH`.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* resolveGh({ env: process.env, home_dir: homedir(), file_exists })
|
|
40
|
+
* // '/opt/homebrew/bin/gh', or null
|
|
41
|
+
*/
|
|
42
|
+
export const resolveGh = ({
|
|
43
|
+
env,
|
|
44
|
+
home_dir,
|
|
45
|
+
file_exists,
|
|
46
|
+
}: {
|
|
47
|
+
env: Record<string, string | undefined>
|
|
48
|
+
home_dir: string
|
|
49
|
+
file_exists: ({ path }: { path: string }) => boolean
|
|
50
|
+
}): string | null =>
|
|
51
|
+
[
|
|
52
|
+
env.LUCA_GH ?? '',
|
|
53
|
+
...(env.PATH ?? '').split(':').map((dir) => join(dir, 'gh')),
|
|
54
|
+
'/opt/homebrew/bin/gh',
|
|
55
|
+
'/usr/local/bin/gh',
|
|
56
|
+
join(home_dir, '.local/bin/gh'),
|
|
57
|
+
].find((path) => path.startsWith('/') && file_exists({ path })) ?? null
|
|
58
|
+
|
|
59
|
+
/** A run as `reply.post` sees it. */
|
|
60
|
+
export type ReplyRun = {
|
|
61
|
+
state: BoardState
|
|
62
|
+
/** The run's repo folder; `null` when the board doesn't know it. */
|
|
63
|
+
repo_dir: string | null
|
|
64
|
+
/** The next journal seq the board wants: a later `reply_received` is new. */
|
|
65
|
+
next_seq: number
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A posted reply, with the run it is for and the seq it waits past. */
|
|
69
|
+
type PostedEntry = PostedReply & {
|
|
70
|
+
run_id: string
|
|
71
|
+
after_seq: number
|
|
72
|
+
/** True while `gh` is posting it. */
|
|
73
|
+
sending: boolean
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const IssueViewSchema = z.object({
|
|
77
|
+
author: z.object({ login: z.string() }),
|
|
78
|
+
url: z.string(),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const firstLine = ({ text }: { text: string }): string =>
|
|
82
|
+
text.trim().split('\n')[0]?.slice(0, 300) ?? ''
|
|
83
|
+
|
|
84
|
+
const ghProblem = ({ result }: { result: CommandResult }): string =>
|
|
85
|
+
result.exit_code === null
|
|
86
|
+
? `it didn't finish in ${GH_TIMEOUT_MS / 1000} s`
|
|
87
|
+
: firstLine({ text: result.stderr || result.stdout }) ||
|
|
88
|
+
`it exited with ${result.exit_code}`
|
|
89
|
+
|
|
90
|
+
const refuse = (message: string): ReplyPostOutput => ({
|
|
91
|
+
ok: false,
|
|
92
|
+
status: 'refused',
|
|
93
|
+
message,
|
|
94
|
+
posted: null,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const fail = (message: string): ReplyPostOutput => ({
|
|
98
|
+
ok: false,
|
|
99
|
+
status: 'failed',
|
|
100
|
+
message,
|
|
101
|
+
posted: null,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const toPosted = ({ entry }: { entry: PostedEntry }): PostedReply => ({
|
|
105
|
+
key: entry.key,
|
|
106
|
+
since: entry.since,
|
|
107
|
+
reply: entry.reply,
|
|
108
|
+
posted_at: entry.posted_at,
|
|
109
|
+
comment_url: entry.comment_url,
|
|
110
|
+
taken_at: entry.taken_at,
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The reply poster, with `gh` run through `run_command`. It keeps what it
|
|
115
|
+
* posted in memory (lost on a plugin restart), marks a reply taken when the
|
|
116
|
+
* engine journals `reply_received` for it, and lists a run's posted replies
|
|
117
|
+
* for `board.read`.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* const poster = createReplyPoster({ run_command, env, home_dir, file_exists, now, log })
|
|
121
|
+
* const output = await poster.postReply({ run, input: { run_id, key: 'ticket-13', reply: 'retry #13' } })
|
|
122
|
+
*/
|
|
123
|
+
export const createReplyPoster = ({
|
|
124
|
+
run_command,
|
|
125
|
+
env,
|
|
126
|
+
home_dir,
|
|
127
|
+
file_exists,
|
|
128
|
+
now,
|
|
129
|
+
log,
|
|
130
|
+
}: {
|
|
131
|
+
run_command: RunCommand
|
|
132
|
+
env: Record<string, string | undefined>
|
|
133
|
+
home_dir: string
|
|
134
|
+
file_exists: ({ path }: { path: string }) => boolean
|
|
135
|
+
now: () => Date
|
|
136
|
+
log: (message: string) => void
|
|
137
|
+
}) => {
|
|
138
|
+
const posted = new Map<string, PostedEntry>()
|
|
139
|
+
|
|
140
|
+
const ghEnv = (): Record<string, string> => {
|
|
141
|
+
const gh_env: Record<string, string> = {}
|
|
142
|
+
for (const [key, value] of Object.entries(env)) {
|
|
143
|
+
if (value !== undefined) gh_env[key] = value
|
|
144
|
+
}
|
|
145
|
+
gh_env.GH_PROMPT_DISABLED = '1'
|
|
146
|
+
return gh_env
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Checks the owner and posts the comment. A refusal or failure says why;
|
|
151
|
+
* `comment_url` is what `gh` printed.
|
|
152
|
+
*/
|
|
153
|
+
const send = async ({
|
|
154
|
+
run,
|
|
155
|
+
spec_number,
|
|
156
|
+
reply,
|
|
157
|
+
}: {
|
|
158
|
+
run: ReplyRun
|
|
159
|
+
spec_number: number
|
|
160
|
+
reply: string
|
|
161
|
+
}): Promise<
|
|
162
|
+
| { ok: true; comment_url: string | null }
|
|
163
|
+
| { ok: false; output: ReplyPostOutput }
|
|
164
|
+
> => {
|
|
165
|
+
const gh = resolveGh({ env, home_dir, file_exists })
|
|
166
|
+
if (gh === null) {
|
|
167
|
+
return { ok: false, output: fail(`Couldn't find gh. ${GH_HINT}`) }
|
|
168
|
+
}
|
|
169
|
+
const cwd = run.repo_dir ?? home_dir
|
|
170
|
+
const runGh = (args: string[]) =>
|
|
171
|
+
run_command({
|
|
172
|
+
command: gh,
|
|
173
|
+
args,
|
|
174
|
+
cwd,
|
|
175
|
+
env: ghEnv(),
|
|
176
|
+
timeout_ms: GH_TIMEOUT_MS,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
let owner = run.state.run.spec_author
|
|
180
|
+
let repo = run.state.run.spec_repo
|
|
181
|
+
if (owner === null || repo === null) {
|
|
182
|
+
if (repo === null && run.repo_dir === null) {
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
output: fail(
|
|
186
|
+
`The board doesn't know spec #${spec_number}'s repo, so it can't post there.`
|
|
187
|
+
),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const view = await runGh([
|
|
191
|
+
'issue',
|
|
192
|
+
'view',
|
|
193
|
+
String(spec_number),
|
|
194
|
+
...(repo === null ? [] : ['--repo', repo]),
|
|
195
|
+
'--json',
|
|
196
|
+
'author,url',
|
|
197
|
+
])
|
|
198
|
+
const parsed =
|
|
199
|
+
view.exit_code === 0
|
|
200
|
+
? IssueViewSchema.safeParse(safeJson({ text: view.stdout }))
|
|
201
|
+
: null
|
|
202
|
+
if (!parsed?.success) {
|
|
203
|
+
return {
|
|
204
|
+
ok: false,
|
|
205
|
+
output: fail(
|
|
206
|
+
`Couldn't read spec #${spec_number}'s owner with gh issue view: ${parsed ? 'it gave an answer the board could not read' : ghProblem({ result: view })}.`
|
|
207
|
+
),
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
owner ??= parsed.data.author.login
|
|
211
|
+
repo ??= repoOfIssueUrl({ url: parsed.data.url })
|
|
212
|
+
}
|
|
213
|
+
if (repo === null) {
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
output: fail(
|
|
217
|
+
`The board couldn't tell spec #${spec_number}'s repo, so it can't post there.`
|
|
218
|
+
),
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const user = await runGh(['api', 'user', '--jq', '.login'])
|
|
223
|
+
const login = user.stdout.trim()
|
|
224
|
+
if (user.exit_code !== 0 || login === '') {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
output: fail(
|
|
228
|
+
`Couldn't tell who is signed in to gh: ${ghProblem({ result: user })}. Run gh auth login.`
|
|
229
|
+
),
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (login.toLowerCase() !== owner.toLowerCase()) {
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
output: refuse(
|
|
236
|
+
`You're signed in to gh as ${login}, but spec #${spec_number} is ${owner}'s. The engine only counts ${owner}'s replies, so the board didn't post \`${reply}\`. Switch gh to ${owner} (gh auth switch), or reply as ${owner} on GitHub.`
|
|
237
|
+
),
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const comment = await runGh([
|
|
242
|
+
'issue',
|
|
243
|
+
'comment',
|
|
244
|
+
String(spec_number),
|
|
245
|
+
'--repo',
|
|
246
|
+
repo,
|
|
247
|
+
'--body',
|
|
248
|
+
reply,
|
|
249
|
+
])
|
|
250
|
+
if (comment.exit_code !== 0) {
|
|
251
|
+
return {
|
|
252
|
+
ok: false,
|
|
253
|
+
output: fail(
|
|
254
|
+
`gh couldn't post \`${reply}\` on spec #${spec_number}: ${ghProblem({ result: comment })}.`
|
|
255
|
+
),
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const comment_url =
|
|
259
|
+
comment.stdout
|
|
260
|
+
.split('\n')
|
|
261
|
+
.map((line) => line.trim())
|
|
262
|
+
.find((line) => line.startsWith('https://')) ?? null
|
|
263
|
+
return { ok: true, comment_url }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Posts `input.reply` on the run's spec issue, if the run's board has
|
|
268
|
+
* `input.key` stuck and the word is one the engine takes for it, and
|
|
269
|
+
* the `gh` user owns the spec. The same reply for the same stuck item
|
|
270
|
+
* is posted once (`already_posted` after that, and while it is in
|
|
271
|
+
* flight); a different word for it is refused. A failed post can be
|
|
272
|
+
* tried again. Never throws.
|
|
273
|
+
*/
|
|
274
|
+
const postReply = async ({
|
|
275
|
+
run,
|
|
276
|
+
input,
|
|
277
|
+
}: {
|
|
278
|
+
run: ReplyRun | null
|
|
279
|
+
input: ReplyPostInput
|
|
280
|
+
}): Promise<ReplyPostOutput> => {
|
|
281
|
+
const { run_id, key, reply } = input
|
|
282
|
+
if (run === null) {
|
|
283
|
+
return refuse(`The board doesn't know run ${run_id}.`)
|
|
284
|
+
}
|
|
285
|
+
const { state } = run
|
|
286
|
+
if (state.run.demo) {
|
|
287
|
+
return refuse(
|
|
288
|
+
`Run ${run_id} is a demo: it has no spec issue to reply on.`
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
const spec_number = state.run.spec_number
|
|
292
|
+
if (spec_number === null) {
|
|
293
|
+
return refuse(`Run ${run_id} doesn't name its spec issue yet.`)
|
|
294
|
+
}
|
|
295
|
+
const item = state.needs_you.find((entry) => entry.key === key)
|
|
296
|
+
if (!item) {
|
|
297
|
+
return refuse(
|
|
298
|
+
`Nothing in run ${run_id} waits for a reply as "${key}" now, so the board didn't post \`${reply}\`.`
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
const allowed = allowedReplies(item)
|
|
302
|
+
if (!allowed.includes(reply)) {
|
|
303
|
+
return refuse(
|
|
304
|
+
`The engine doesn't take \`${reply}\` here. For "${item.subject}" it takes ${allowed.map((word) => `\`${word}\``).join(', ')}.`
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const id = `${run_id}\n${key}\n${item.since}`
|
|
309
|
+
const existing = posted.get(id)
|
|
310
|
+
if (existing) {
|
|
311
|
+
if (existing.reply !== reply) {
|
|
312
|
+
return refuse(
|
|
313
|
+
`\`${existing.reply}\` was already posted for this. The engine takes the first reply, so the board won't post \`${reply}\` too.`
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
ok: true,
|
|
318
|
+
status: 'already_posted',
|
|
319
|
+
message: existing.sending
|
|
320
|
+
? `\`${reply}\` is being posted now.`
|
|
321
|
+
: `\`${reply}\` was already posted on spec #${spec_number}.`,
|
|
322
|
+
posted: existing.sending ? null : toPosted({ entry: existing }),
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Kept before the first await, so a second tap sees it.
|
|
326
|
+
const entry: PostedEntry = {
|
|
327
|
+
key,
|
|
328
|
+
since: item.since,
|
|
329
|
+
reply,
|
|
330
|
+
posted_at: now().toISOString(),
|
|
331
|
+
comment_url: null,
|
|
332
|
+
taken_at: null,
|
|
333
|
+
run_id,
|
|
334
|
+
after_seq: run.next_seq,
|
|
335
|
+
sending: true,
|
|
336
|
+
}
|
|
337
|
+
posted.set(id, entry)
|
|
338
|
+
try {
|
|
339
|
+
const sent = await send({ run, spec_number, reply })
|
|
340
|
+
if (!sent.ok) {
|
|
341
|
+
posted.delete(id)
|
|
342
|
+
log(`[${run_id}] reply \`${reply}\`: ${sent.output.message}`)
|
|
343
|
+
return sent.output
|
|
344
|
+
}
|
|
345
|
+
entry.sending = false
|
|
346
|
+
entry.comment_url = sent.comment_url
|
|
347
|
+
log(
|
|
348
|
+
`[${run_id}] posted \`${reply}\` on spec #${spec_number}${sent.comment_url ? `: ${sent.comment_url}` : ''}`
|
|
349
|
+
)
|
|
350
|
+
return {
|
|
351
|
+
ok: true,
|
|
352
|
+
status: 'posted',
|
|
353
|
+
message: `Posted \`${reply}\` on spec #${spec_number}. The engine reads it within about a minute.`,
|
|
354
|
+
posted: toPosted({ entry }),
|
|
355
|
+
}
|
|
356
|
+
} catch (error) {
|
|
357
|
+
posted.delete(id)
|
|
358
|
+
return fail(
|
|
359
|
+
`Couldn't post \`${reply}\`: ${error instanceof Error ? error.message : String(error)}`
|
|
360
|
+
)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* The engine took a reply (`reply_received` at `seq`): marks the posted
|
|
366
|
+
* replies it matches as taken.
|
|
367
|
+
*/
|
|
368
|
+
const noteTaken = ({
|
|
369
|
+
run_id,
|
|
370
|
+
seq,
|
|
371
|
+
time,
|
|
372
|
+
word,
|
|
373
|
+
ticket,
|
|
374
|
+
}: {
|
|
375
|
+
run_id: string
|
|
376
|
+
seq: number
|
|
377
|
+
time: string
|
|
378
|
+
word: string
|
|
379
|
+
ticket: number | null
|
|
380
|
+
}) => {
|
|
381
|
+
for (const entry of posted.values()) {
|
|
382
|
+
if (entry.run_id !== run_id || entry.sending) continue
|
|
383
|
+
if (entry.taken_at !== null || seq < entry.after_seq) continue
|
|
384
|
+
const parts = replyParts({ reply: entry.reply })
|
|
385
|
+
if (parts?.word === word && parts.ticket === ticket) {
|
|
386
|
+
entry.taken_at = time
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** The replies posted for a run, oldest first. */
|
|
392
|
+
const postedOf = ({ run_id }: { run_id: string }): PostedReply[] =>
|
|
393
|
+
[...posted.values()]
|
|
394
|
+
.filter((entry) => entry.run_id === run_id && !entry.sending)
|
|
395
|
+
.map((entry) => toPosted({ entry }))
|
|
396
|
+
|
|
397
|
+
return { postReply, noteTaken, postedOf }
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Parses JSON, or `null` when it isn't. */
|
|
401
|
+
const safeJson = ({ text }: { text: string }): unknown => {
|
|
402
|
+
try {
|
|
403
|
+
return JSON.parse(text)
|
|
404
|
+
} catch {
|
|
405
|
+
return null
|
|
406
|
+
}
|
|
407
|
+
}
|
|
@@ -28,7 +28,10 @@ export const RunEntrySchema = z.object({
|
|
|
28
28
|
log_path: z.string(),
|
|
29
29
|
/** How the run's engine ended, once it did; `null` while it may run. */
|
|
30
30
|
ended: EngineEndedSchema.nullable().default(null),
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* How many times the plugin restarted the run by itself (#375); back to
|
|
33
|
+
* 0 when the owner resumes it (#493).
|
|
34
|
+
*/
|
|
32
35
|
restarts: z.number().int().min(0).default(0),
|
|
33
36
|
})
|
|
34
37
|
|
|
@@ -136,15 +139,16 @@ export const createRunRegistry = ({
|
|
|
136
139
|
write()
|
|
137
140
|
},
|
|
138
141
|
/**
|
|
139
|
-
* Changes a known run's `ended` or `
|
|
140
|
-
* Returns the changed entry, or
|
|
142
|
+
* Changes a known run's `ended`, `restarts`, or `token` (a resume
|
|
143
|
+
* gets a new one) and writes the file. Returns the changed entry, or
|
|
144
|
+
* `null` for an unknown run.
|
|
141
145
|
*/
|
|
142
146
|
update: ({
|
|
143
147
|
run_id,
|
|
144
148
|
change,
|
|
145
149
|
}: {
|
|
146
150
|
run_id: string
|
|
147
|
-
change: Partial<Pick<RunEntry, 'ended' | 'restarts'>>
|
|
151
|
+
change: Partial<Pick<RunEntry, 'ended' | 'restarts' | 'token'>>
|
|
148
152
|
}): RunEntry | null => {
|
|
149
153
|
reread()
|
|
150
154
|
const known = entries.find((entry) => entry.run_id === run_id)
|