@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,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the Claude launcher starts one agent session: checks the model, builds
|
|
3
|
+
* the role's locked-down options (guard hook, sandbox, clean environment),
|
|
4
|
+
* starts the SDK session, checks the account BEFORE the prompt is sent, then
|
|
5
|
+
* runs the first turn on the pool's open session.
|
|
6
|
+
*/
|
|
7
|
+
import { randomUUID } from 'node:crypto'
|
|
8
|
+
import { realpathSync } from 'node:fs'
|
|
9
|
+
import { homedir } from 'node:os'
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
EffortLevel,
|
|
13
|
+
Options,
|
|
14
|
+
SDKUserMessage,
|
|
15
|
+
} from '@anthropic-ai/claude-agent-sdk'
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
AgentSessionSchema,
|
|
19
|
+
type AgentLauncher,
|
|
20
|
+
type AgentSession,
|
|
21
|
+
type AgentTurn,
|
|
22
|
+
} from './agent-launcher'
|
|
23
|
+
import {
|
|
24
|
+
AccountSchema,
|
|
25
|
+
accountProblem,
|
|
26
|
+
errorText,
|
|
27
|
+
stop,
|
|
28
|
+
type Outcome,
|
|
29
|
+
} from './claude-messages'
|
|
30
|
+
import { agentEnv, agentOptions, checkLaunchModel } from './claude-options'
|
|
31
|
+
import { createInputChannel } from './input-channel'
|
|
32
|
+
import { commonGitDir, realClaudePath } from './launch-paths'
|
|
33
|
+
import { createDeliveryHook, createLucaServer } from './message-tool'
|
|
34
|
+
import {
|
|
35
|
+
nextOutcome,
|
|
36
|
+
pumpMessages,
|
|
37
|
+
type AgentQuerySession,
|
|
38
|
+
type OpenSession,
|
|
39
|
+
} from './open-session'
|
|
40
|
+
import type { SessionPool } from './session-pool'
|
|
41
|
+
import { createTurnProgress } from './turn-progress'
|
|
42
|
+
|
|
43
|
+
import { createGuardHook } from '../guards/guard-hook'
|
|
44
|
+
import { guardRoleOf } from '../guards/role-rules'
|
|
45
|
+
|
|
46
|
+
/** Starts an SDK session: the SDK's `query`, or a fake in tests. */
|
|
47
|
+
export type AgentQuery = (params: {
|
|
48
|
+
prompt: AsyncIterable<SDKUserMessage>
|
|
49
|
+
options: Options
|
|
50
|
+
}) => AgentQuerySession
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Makes the launcher's `launch`: it starts an SDK session with `start_query`
|
|
54
|
+
* and runs the session's first turn through the pool.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* const launch = createLaunch({ effort, start_query, claude_path, turn_timeout_ms, pool })
|
|
58
|
+
*/
|
|
59
|
+
export const createLaunch = ({
|
|
60
|
+
effort: useEffort,
|
|
61
|
+
start_query: startQuery,
|
|
62
|
+
claude_path,
|
|
63
|
+
turn_timeout_ms: timeoutMs,
|
|
64
|
+
pool,
|
|
65
|
+
}: {
|
|
66
|
+
effort: EffortLevel
|
|
67
|
+
start_query: AgentQuery
|
|
68
|
+
claude_path: string | undefined
|
|
69
|
+
turn_timeout_ms: number
|
|
70
|
+
pool: Pick<SessionPool, 'close' | 'runTurn'>
|
|
71
|
+
}): AgentLauncher['launch'] => {
|
|
72
|
+
const { close, runTurn } = pool
|
|
73
|
+
|
|
74
|
+
const launch: AgentLauncher['launch'] = async ({
|
|
75
|
+
role,
|
|
76
|
+
ticket,
|
|
77
|
+
prompt,
|
|
78
|
+
cwd,
|
|
79
|
+
may_edit_tests,
|
|
80
|
+
config,
|
|
81
|
+
messaging,
|
|
82
|
+
model,
|
|
83
|
+
on_progress,
|
|
84
|
+
}) => {
|
|
85
|
+
const refused = checkLaunchModel({ model })
|
|
86
|
+
if (refused !== null) return stop(refused)
|
|
87
|
+
const started = Date.now()
|
|
88
|
+
const fallback_id = `claude-launch-${randomUUID()}`
|
|
89
|
+
|
|
90
|
+
let claudePath: string
|
|
91
|
+
try {
|
|
92
|
+
claudePath = claude_path ?? realClaudePath()
|
|
93
|
+
} catch (error) {
|
|
94
|
+
return stop(errorText(error))
|
|
95
|
+
}
|
|
96
|
+
let worktree: string
|
|
97
|
+
let gitDir: string
|
|
98
|
+
try {
|
|
99
|
+
worktree = realpathSync(cwd)
|
|
100
|
+
gitDir = await commonGitDir({ cwd: worktree })
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return { ok: false, failure: 'engine', error: errorText(error) }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const summary: AgentSession = AgentSessionSchema.parse({})
|
|
106
|
+
const stderr: string[] = []
|
|
107
|
+
const helpers = new Set<string>()
|
|
108
|
+
const progress = createTurnProgress({
|
|
109
|
+
worktree,
|
|
110
|
+
readings: () => open.summary.rate_limit_events,
|
|
111
|
+
})
|
|
112
|
+
progress.start(on_progress)
|
|
113
|
+
const options = agentOptions({
|
|
114
|
+
role,
|
|
115
|
+
may_edit_tests,
|
|
116
|
+
cwd: worktree,
|
|
117
|
+
model,
|
|
118
|
+
effort: useEffort,
|
|
119
|
+
claude_path: claudePath,
|
|
120
|
+
config,
|
|
121
|
+
common_git_dir: gitDir,
|
|
122
|
+
home: realpathSync(homedir()),
|
|
123
|
+
env: agentEnv({ source: process.env }),
|
|
124
|
+
guard_hook: createGuardHook({
|
|
125
|
+
role: guardRoleOf({ role }),
|
|
126
|
+
may_edit_tests,
|
|
127
|
+
worktree,
|
|
128
|
+
config,
|
|
129
|
+
// The summary is swapped each turn, so this reads it late.
|
|
130
|
+
on_deny: (denial) => open.summary.guard_denials.push(denial),
|
|
131
|
+
helpers,
|
|
132
|
+
on_helper: () => {
|
|
133
|
+
open.summary.helpers_started += 1
|
|
134
|
+
},
|
|
135
|
+
on_allow: progress.opened,
|
|
136
|
+
}),
|
|
137
|
+
// Kept in the session's options, so its follow-ups have them too.
|
|
138
|
+
luca_server:
|
|
139
|
+
messaging === null ? null : createLucaServer({ messaging }),
|
|
140
|
+
delivery_hook:
|
|
141
|
+
messaging === null ? null : createDeliveryHook({ messaging }),
|
|
142
|
+
stderr: (data) => {
|
|
143
|
+
stderr.push(data)
|
|
144
|
+
if (stderr.length > 50) stderr.shift()
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const input = createInputChannel()
|
|
149
|
+
let running: AgentQuerySession
|
|
150
|
+
try {
|
|
151
|
+
running = startQuery({ prompt: input.iterable, options })
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
failure: 'engine',
|
|
156
|
+
error: `The SDK did not start: ${errorText(error)}`,
|
|
157
|
+
session: { ...summary, duration_ms: Date.now() - started },
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const open: OpenSession = {
|
|
161
|
+
role,
|
|
162
|
+
ticket,
|
|
163
|
+
cwd,
|
|
164
|
+
may_edit_tests,
|
|
165
|
+
running,
|
|
166
|
+
input,
|
|
167
|
+
summary,
|
|
168
|
+
helpers,
|
|
169
|
+
progress,
|
|
170
|
+
model_totals: {},
|
|
171
|
+
waiter: null,
|
|
172
|
+
ended: null,
|
|
173
|
+
pump: Promise.resolve(),
|
|
174
|
+
idle_timer: undefined,
|
|
175
|
+
stderr,
|
|
176
|
+
}
|
|
177
|
+
open.pump = pumpMessages({ open, on_end: () => void close(open) })
|
|
178
|
+
|
|
179
|
+
const ended = async (outcome: Outcome): Promise<AgentTurn> => {
|
|
180
|
+
await close(open)
|
|
181
|
+
return {
|
|
182
|
+
...outcome,
|
|
183
|
+
session_id: open.summary.session_id ?? fallback_id,
|
|
184
|
+
session: { ...open.summary, duration_ms: Date.now() - started },
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
189
|
+
const timedOut = new Promise<'timeout'>((resolve) => {
|
|
190
|
+
timer = setTimeout(() => resolve('timeout'), timeoutMs)
|
|
191
|
+
})
|
|
192
|
+
const account = await Promise.race([
|
|
193
|
+
running.accountInfo().then(
|
|
194
|
+
(info) => ({ info }),
|
|
195
|
+
(error: unknown) => ({ error })
|
|
196
|
+
),
|
|
197
|
+
nextOutcome(open).then((outcome) => ({ outcome })),
|
|
198
|
+
timedOut,
|
|
199
|
+
])
|
|
200
|
+
clearTimeout(timer)
|
|
201
|
+
open.waiter = null
|
|
202
|
+
if (account === 'timeout') {
|
|
203
|
+
return ended({
|
|
204
|
+
ok: false,
|
|
205
|
+
failure: 'engine',
|
|
206
|
+
error: 'accountInfo() did not answer in time.',
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
if ('outcome' in account) return ended(account.outcome)
|
|
210
|
+
if ('error' in account) {
|
|
211
|
+
return ended({
|
|
212
|
+
ok: false,
|
|
213
|
+
failure: 'engine',
|
|
214
|
+
error: `accountInfo() failed: ${errorText(account.error)}`,
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
const accountParsed = AccountSchema.safeParse(account.info)
|
|
218
|
+
summary.subscription_type = accountParsed.success
|
|
219
|
+
? (accountParsed.data.subscriptionType ?? null)
|
|
220
|
+
: null
|
|
221
|
+
const problem = accountProblem(account.info)
|
|
222
|
+
if (problem !== null) return ended(stop(`credential check: ${problem}`))
|
|
223
|
+
return runTurn({ open, message: prompt, started, fallback_id })
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return launch
|
|
227
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The learner's instructions (#370): its task, and its own rules in place of
|
|
3
|
+
* the shell and the rules for every agent.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const LEARNER = `# Your role: learner
|
|
7
|
+
|
|
8
|
+
You run once, at the end of a run, even a stuck or stopped one. Your prompt is a digest of the run's journal: failures, fix loops, failed tries, review findings, stuck points, assumptions, run notes, and the memories other agents were shown. You propose lessons for FUTURE runs, which the engine saves to memory.
|
|
9
|
+
|
|
10
|
+
- Propose only durable lessons: something a later run, maybe on another spec, would do better for knowing. Not a story of this run, not facts only this spec needs, and nothing a linter or the gates already enforce.
|
|
11
|
+
- Pick each one's type:
|
|
12
|
+
- "pattern": a way of building that worked and is worth repeating.
|
|
13
|
+
- "pitfall": a trap that cost a fix loop, a failed try, or a stuck ticket, and how to avoid it.
|
|
14
|
+
- "procedure": steps that reliably get something done.
|
|
15
|
+
- "decision": a choice made for THIS project that later runs on it must follow.
|
|
16
|
+
Any other type is refused.
|
|
17
|
+
- Mark each one's scope by asking "would this memory be useful in a completely different repo?":
|
|
18
|
+
- "repo": no, it only helps on this repo (its code, docs, or setup). It is saved to this project's memory.
|
|
19
|
+
- "anywhere": yes, it helps on any repo. It is saved to the shared memory.
|
|
20
|
+
A decision is always saved to this project's memory. A missing or other scope is refused.
|
|
21
|
+
- Keep each memory atomic: one lesson each, a short "concept" name in kebab-case (such as bun-junit-reporter-flags), the lesson in "content" (a few plain sentences), and a one-line "summary".
|
|
22
|
+
- At most 10. Fewer, better ones beat many. An empty list is a fine answer when the run taught nothing new.
|
|
23
|
+
- In "helped", list the ids of the shown memories that actually helped this run (the digest lists them with their ids). Leave out the ones that didn't matter or were wrong.
|
|
24
|
+
- You may read the repo (Read, Grep, Glob) to check a lesson. You have no shell, and you write nothing.
|
|
25
|
+
|
|
26
|
+
Your result (structured output): memories (each with type, scope, concept, content, summary) and helped (memory ids).`
|
|
27
|
+
|
|
28
|
+
export const LEARNER_RULES = `## Rules for the learner
|
|
29
|
+
- Plain code (the engine) drives this run and saves what you propose. Only the engine talks to memory; you have no memory tools.
|
|
30
|
+
- Never create, edit, or delete a file. The network is off.
|
|
31
|
+
- Finish by giving your structured result. A turn without one counts as failed.`
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One agent session the Claude launcher keeps open between turns, and how it
|
|
3
|
+
* is read: each SDK message goes into the session's summary, and an outcome
|
|
4
|
+
* (a result, a stop, a plan cut-off, an engine failure) goes to the turn
|
|
5
|
+
* waiting for it.
|
|
6
|
+
*/
|
|
7
|
+
import type { AgentSession, ModelTokens } from './agent-launcher'
|
|
8
|
+
import {
|
|
9
|
+
AssistantSchema,
|
|
10
|
+
errorText,
|
|
11
|
+
InitSchema,
|
|
12
|
+
initProblem,
|
|
13
|
+
planCut,
|
|
14
|
+
RateLimitSchema,
|
|
15
|
+
ResultSchema,
|
|
16
|
+
modelTotalsOf,
|
|
17
|
+
stop,
|
|
18
|
+
toolResultIds,
|
|
19
|
+
turnTokens,
|
|
20
|
+
type Outcome,
|
|
21
|
+
} from './claude-messages'
|
|
22
|
+
import type { createInputChannel } from './input-channel'
|
|
23
|
+
import type { AgentRole } from './role-results'
|
|
24
|
+
import type { TurnProgressKeeper } from './turn-progress'
|
|
25
|
+
|
|
26
|
+
import { planSignal } from '../limits/plan-signals'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* One running SDK session, as the launcher uses it. The SDK's own `Query`
|
|
30
|
+
* fits this; tests hand in a fake. Messages are read as `unknown` and parsed
|
|
31
|
+
* with the launcher's own schemas.
|
|
32
|
+
*/
|
|
33
|
+
export type AgentQuerySession = AsyncIterable<unknown> & {
|
|
34
|
+
accountInfo: () => Promise<unknown>
|
|
35
|
+
interrupt: () => Promise<unknown>
|
|
36
|
+
close: () => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* One agent session the launcher keeps open between turns, so a follow-up
|
|
41
|
+
* reaches the same agent. Its guard hook, sandbox, and options are the ones
|
|
42
|
+
* it was launched with.
|
|
43
|
+
*/
|
|
44
|
+
export type OpenSession = {
|
|
45
|
+
role: AgentRole
|
|
46
|
+
ticket: number
|
|
47
|
+
cwd: string
|
|
48
|
+
may_edit_tests: boolean
|
|
49
|
+
running: AgentQuerySession
|
|
50
|
+
input: ReturnType<typeof createInputChannel>
|
|
51
|
+
/** What the launcher has seen of the session; per-turn fields reset each turn. */
|
|
52
|
+
summary: AgentSession
|
|
53
|
+
/**
|
|
54
|
+
* The helpers running now, by the tool call that started each: the guard
|
|
55
|
+
* hook adds one, and its result (or the turn's end) takes it out.
|
|
56
|
+
*/
|
|
57
|
+
helpers: Set<string>
|
|
58
|
+
/** The turn's live progress (#628), told to the turn's `on_progress`. */
|
|
59
|
+
progress: TurnProgressKeeper
|
|
60
|
+
/** The SDK's running tokens per model, as of the session's last result. */
|
|
61
|
+
model_totals: Record<string, ModelTokens>
|
|
62
|
+
/** Resolves the turn waiting for the session's next outcome. */
|
|
63
|
+
waiter: ((outcome: Outcome) => void) | null
|
|
64
|
+
/** A stop or an engine failure: the session is over, with this outcome. */
|
|
65
|
+
ended: Outcome | null
|
|
66
|
+
/** Reads the session's messages until it ends. */
|
|
67
|
+
pump: Promise<void>
|
|
68
|
+
idle_timer: ReturnType<typeof setTimeout> | undefined
|
|
69
|
+
stderr: string[]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The session's next outcome: a result, a stop, or its end. */
|
|
73
|
+
export const nextOutcome = (open: OpenSession): Promise<Outcome> =>
|
|
74
|
+
open.ended !== null
|
|
75
|
+
? Promise.resolve(open.ended)
|
|
76
|
+
: new Promise<Outcome>((resolve) => {
|
|
77
|
+
open.waiter = resolve
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
export const deliver = ({
|
|
81
|
+
open,
|
|
82
|
+
outcome,
|
|
83
|
+
}: {
|
|
84
|
+
open: OpenSession
|
|
85
|
+
outcome: Outcome
|
|
86
|
+
}): void => {
|
|
87
|
+
if (!outcome.ok && outcome.failure !== 'agent') open.ended = outcome
|
|
88
|
+
const waiter = open.waiter
|
|
89
|
+
open.waiter = null
|
|
90
|
+
waiter?.(outcome)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const stderrTail = (open: OpenSession): string =>
|
|
94
|
+
open.stderr.length === 0 ? '' : `\n${open.stderr.join('').slice(-2000)}`
|
|
95
|
+
|
|
96
|
+
/** Reads one message into the session's summary; an outcome ends the turn. */
|
|
97
|
+
export const watch = ({
|
|
98
|
+
open,
|
|
99
|
+
raw,
|
|
100
|
+
}: {
|
|
101
|
+
open: OpenSession
|
|
102
|
+
raw: unknown
|
|
103
|
+
}): Outcome | null => {
|
|
104
|
+
const { summary } = open
|
|
105
|
+
const init = InitSchema.safeParse(raw)
|
|
106
|
+
if (init.success) {
|
|
107
|
+
summary.session_id = init.data.session_id
|
|
108
|
+
summary.model = init.data.model
|
|
109
|
+
summary.claude_code_version = init.data.claude_code_version
|
|
110
|
+
summary.api_key_source = init.data.apiKeySource
|
|
111
|
+
const problem = initProblem(init.data)
|
|
112
|
+
return problem === null ? null : stop(problem)
|
|
113
|
+
}
|
|
114
|
+
const rateLimit = RateLimitSchema.safeParse(raw)
|
|
115
|
+
if (rateLimit.success) {
|
|
116
|
+
const info = rateLimit.data.rate_limit_info
|
|
117
|
+
summary.rate_limit_events.push({
|
|
118
|
+
...info,
|
|
119
|
+
arrived_at: new Date().toISOString(),
|
|
120
|
+
})
|
|
121
|
+
const signal = planSignal({ info })
|
|
122
|
+
switch (signal.kind) {
|
|
123
|
+
case 'ok':
|
|
124
|
+
return null
|
|
125
|
+
case 'billing':
|
|
126
|
+
return planCut(signal.reason)
|
|
127
|
+
case 'limit':
|
|
128
|
+
return planCut(
|
|
129
|
+
`rate_limit_event status=rejected (${signal.rate_limit_type ?? 'unknown'})`
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const assistant = AssistantSchema.safeParse(raw)
|
|
134
|
+
if (assistant.success) {
|
|
135
|
+
if (assistant.data.error !== 'billing_error') return null
|
|
136
|
+
summary.billing_error = true
|
|
137
|
+
return planCut('assistant error billing_error')
|
|
138
|
+
}
|
|
139
|
+
for (const id of toolResultIds(raw)) open.helpers.delete(id)
|
|
140
|
+
const result = ResultSchema.safeParse(raw)
|
|
141
|
+
if (!result.success) return null
|
|
142
|
+
open.helpers.clear()
|
|
143
|
+
const done = result.data
|
|
144
|
+
summary.num_turns = done.num_turns
|
|
145
|
+
summary.usage = done.usage
|
|
146
|
+
const totals = modelTotalsOf(done.modelUsage)
|
|
147
|
+
summary.model_usage = turnTokens({
|
|
148
|
+
totals,
|
|
149
|
+
before: open.model_totals,
|
|
150
|
+
})
|
|
151
|
+
open.model_totals = { ...open.model_totals, ...totals }
|
|
152
|
+
summary.total_cost_usd = done.total_cost_usd
|
|
153
|
+
summary.permission_denials = done.permission_denials.map(
|
|
154
|
+
({ tool_name, tool_use_id }) => ({ tool_name, tool_use_id })
|
|
155
|
+
)
|
|
156
|
+
if (done.subtype === 'success' && !done.is_error) {
|
|
157
|
+
return { ok: true, structured_output: done.structured_output }
|
|
158
|
+
}
|
|
159
|
+
const detail = done.errors.length > 0 ? done.errors.join('; ') : done.result
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
failure: 'agent',
|
|
163
|
+
error: `The agent's turn ended with ${done.subtype}: ${detail}`,
|
|
164
|
+
...(done.subtype === 'error_max_turns' ? { out_of_turns: true } : {}),
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Reads a session's messages until it ends, handing each outcome to the turn
|
|
170
|
+
* waiting for it, then calls `on_end`.
|
|
171
|
+
*/
|
|
172
|
+
export const pumpMessages = async ({
|
|
173
|
+
open,
|
|
174
|
+
on_end,
|
|
175
|
+
}: {
|
|
176
|
+
open: OpenSession
|
|
177
|
+
on_end: () => void
|
|
178
|
+
}): Promise<void> => {
|
|
179
|
+
try {
|
|
180
|
+
for await (const raw of open.running) {
|
|
181
|
+
const outcome = watch({ open, raw })
|
|
182
|
+
open.progress.heard(raw)
|
|
183
|
+
if (outcome === null) continue
|
|
184
|
+
deliver({ open, outcome })
|
|
185
|
+
if (open.ended !== null) break
|
|
186
|
+
}
|
|
187
|
+
if (open.ended !== null) {
|
|
188
|
+
on_end()
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
deliver({
|
|
192
|
+
open,
|
|
193
|
+
outcome: {
|
|
194
|
+
ok: false,
|
|
195
|
+
failure: 'engine',
|
|
196
|
+
error: `The agent session ended with no result.${stderrTail(open)}`,
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
} catch (error) {
|
|
200
|
+
deliver({
|
|
201
|
+
open,
|
|
202
|
+
outcome: {
|
|
203
|
+
ok: false,
|
|
204
|
+
failure: 'engine',
|
|
205
|
+
error: `The agent session failed: ${errorText(error)}${stderrTail(open)}`,
|
|
206
|
+
},
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
on_end()
|
|
210
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sections the decision step adds to an agent's prompt: the notes earlier
|
|
3
|
+
* agents left, the prepare command, the feature map, and the changesets. Each
|
|
4
|
+
* is plain text.
|
|
5
|
+
*/
|
|
6
|
+
import type { AgentRole } from './role-results'
|
|
7
|
+
|
|
8
|
+
import { isFeatureMapFolder } from '../config/engine-config'
|
|
9
|
+
|
|
10
|
+
/** A run note as a later agent's prompt lists it. */
|
|
11
|
+
export type PromptRunNote = { ticket: number; role: AgentRole; note: string }
|
|
12
|
+
|
|
13
|
+
/** The notes earlier agents left, oldest first; nothing when there are none. */
|
|
14
|
+
export const runNotesSection = ({
|
|
15
|
+
run_notes,
|
|
16
|
+
}: {
|
|
17
|
+
run_notes: PromptRunNote[]
|
|
18
|
+
}): string[] =>
|
|
19
|
+
run_notes.length === 0
|
|
20
|
+
? []
|
|
21
|
+
: [
|
|
22
|
+
'## Run notes from earlier agents in this run',
|
|
23
|
+
run_notes
|
|
24
|
+
.map(
|
|
25
|
+
({ ticket, role, note }) =>
|
|
26
|
+
`- ${note} (${role}, #${ticket})`
|
|
27
|
+
)
|
|
28
|
+
.join('\n'),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The section a test-writer or implementer gets when the repo has a prepare
|
|
33
|
+
* command: the engine runs it before its test runs, so tests that need its
|
|
34
|
+
* outputs may fail when the agent runs them itself.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* prepareSection({ command: 'bun run build:rom' })
|
|
38
|
+
* // '## This repo has a prepare command\n\n...'
|
|
39
|
+
*/
|
|
40
|
+
export const prepareSection = ({ command }: { command: string }): string =>
|
|
41
|
+
[
|
|
42
|
+
'## This repo has a prepare command',
|
|
43
|
+
`The engine runs \`${command}\` in the worktree before each of its test runs (the red check and the gates), so tests that need its outputs find them up to date. ` +
|
|
44
|
+
"Never run it yourself. Tests that need its outputs may fail when you run them, and still pass at the engine's checks. " +
|
|
45
|
+
"If the engine's prepare command fails after your turn, its output comes back to you like a failed check.",
|
|
46
|
+
].join('\n\n')
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The section a test-writer gets when the repo has a feature map (#583): the
|
|
50
|
+
* one file besides test files it may edit. It comes after the task's "Edit
|
|
51
|
+
* test files only", so it reads as that line's exception. A folder map
|
|
52
|
+
* (#597) is its `*.md` pages, and a new test file goes on its area's page.
|
|
53
|
+
*
|
|
54
|
+
* @param path - The map's path, as `featureMapPath` gives it.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* featureMapSection({ path: 'docs/feature-map.md' })
|
|
58
|
+
* // '## The feature map\n\nBesides test files, you may edit the feature map, `docs/feature-map.md`. ...'
|
|
59
|
+
*/
|
|
60
|
+
export const featureMapSection = ({ path }: { path: string }): string =>
|
|
61
|
+
[
|
|
62
|
+
'## The feature map',
|
|
63
|
+
isFeatureMapFolder({ path })
|
|
64
|
+
? `Besides test files, you may edit the feature map's pages, the \`*.md\` files under \`${path}/\`. ` +
|
|
65
|
+
'Read the pages first: they list each feature and the test files that prove it. ' +
|
|
66
|
+
"Put new tests in a feature's existing test files when you can; " +
|
|
67
|
+
"a new test file must be listed under its feature on its area's page (add a page for a new area)."
|
|
68
|
+
: `Besides test files, you may edit the feature map, \`${path}\`. ` +
|
|
69
|
+
'Read it first: it lists each feature and the test files that prove it. ' +
|
|
70
|
+
"Put new tests in a feature's existing test files when you can; " +
|
|
71
|
+
'a new test file must be listed under its feature in the map.',
|
|
72
|
+
].join('\n\n')
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The section a test-writer or implementer (a ticket's or the final
|
|
76
|
+
* review's fixers) gets in a repo with changesets: the engine writes the
|
|
77
|
+
* run's one changeset, so an agent's own is extra.
|
|
78
|
+
*/
|
|
79
|
+
export const CHANGESETS_SECTION = [
|
|
80
|
+
'## The engine writes the changeset',
|
|
81
|
+
"Don't write changesets, even if the repo's rules ask for one: the engine writes the run's one changeset when it opens the PR. " +
|
|
82
|
+
'A new file you add in `.changeset/` is removed before the commit. ' +
|
|
83
|
+
'A finding that only asks for a changeset is answered by this: answer it "wont_fix" and say the engine adds it.',
|
|
84
|
+
].join('\n\n')
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The section every reviewer (the ticket-reviewer and each lens) gets in a
|
|
88
|
+
* repo with changesets, so a rule like "every PR needs a changeset" raises
|
|
89
|
+
* no finding.
|
|
90
|
+
*/
|
|
91
|
+
export const CHANGESETS_LENS_SECTION = [
|
|
92
|
+
'## The engine writes the changeset',
|
|
93
|
+
"The engine adds the run's one changeset when it opens the PR, so a rule that asks for a changeset is met. " +
|
|
94
|
+
'Raise no finding that only asks for a changeset.',
|
|
95
|
+
].join('\n\n')
|