@alecsibilia/luca 14.0.0-alpha.25 → 14.0.0-alpha.27
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 +10 -0
- package/README.md +3 -3
- package/board/package.json +1 -1
- package/board/server/luca-version.ts +1 -1
- package/engine/agents/agent-launcher.ts +10 -1
- package/engine/agents/claude-messages.ts +2 -0
- package/engine/agents/claude-options.ts +7 -18
- package/engine/agents/launch-session.ts +2 -0
- package/engine/agents/refactor-update-prompts.ts +62 -0
- package/engine/agents/role-turns.ts +23 -0
- package/engine/agents/scripted-launcher.ts +4 -0
- package/engine/agents/session-pool.ts +1 -0
- package/engine/config/engine-config-schema.ts +80 -40
- package/engine/config/engine-config.ts +3 -0
- package/engine/config/test-commands.ts +35 -0
- package/engine/core/agent-turn.ts +13 -1
- package/engine/core/build-action.ts +17 -0
- package/engine/core/build-launch.ts +29 -9
- package/engine/core/decide-final-review.ts +30 -12
- package/engine/core/decide-red-green.ts +7 -5
- package/engine/core/decide-refactor-update.ts +109 -0
- package/engine/core/evidence-text.ts +13 -3
- package/engine/core/execute-build.ts +8 -0
- package/engine/core/execute-final-review.ts +2 -1
- package/engine/core/execute-red-check.ts +21 -0
- package/engine/core/execute-refactor-update.ts +107 -0
- package/engine/core/final-review-action.ts +11 -2
- package/engine/core/session-close.ts +10 -2
- package/engine/gates/gate-runner.ts +29 -4
- package/engine/gates/gate-schemas.ts +4 -1
- package/engine/gates/own-slow-check.ts +147 -0
- package/engine/gates/red-check.ts +44 -0
- package/engine/index.ts +1 -1
- package/engine/journal/final-review-replay.ts +17 -1
- package/engine/journal/final-review-state.ts +10 -0
- package/engine/journal/journal-entry-schema.ts +6 -0
- package/engine/journal/journal-kinds.ts +2 -0
- package/engine/journal/journal-record-schema.ts +6 -0
- package/engine/journal/refactor-update-entries.ts +45 -0
- package/engine/journal/ticket-progress.ts +32 -0
- package/engine/journal/ticket-replay.ts +18 -0
- package/engine/journal/ticket-result-replay.ts +36 -1
- package/engine/journal/ticket-retry-replay.ts +5 -2
- package/package.json +1 -1
- package/skills/luca-spec-check/SKILL.md +86 -0
- package/skills/luca-spec-check/scripts/spec-bundle.ts +194 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @alecsibilia/luca
|
|
2
2
|
|
|
3
|
+
## 14.0.0-alpha.27
|
|
4
|
+
|
|
5
|
+
No changes in this release.
|
|
6
|
+
|
|
7
|
+
## 14.0.0-alpha.26
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 075d3a4: Spec: /luca-spec-check, a cold read of a spec by three fresh readers before ready-for-agent ([#678](https://github.com/asibilia/luca-framework/issues/678))
|
|
12
|
+
|
|
3
13
|
## 14.0.0-alpha.25
|
|
4
14
|
|
|
5
15
|
No changes in this release.
|
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ This is Luca v14, in alpha. Old Luca (v13) is still on npm; see [Coming from v13
|
|
|
35
35
|
3. Check what `luca setup` wrote to `.luca/config.json`. It names your repo's GitHub name (like `tmnb`) as its memory vault in `muninn.vault`; change it if you want another.
|
|
36
36
|
|
|
37
37
|
4. In Claude Code, in your repo, run `/setup-matt-pocock-skills` so the planning skills know your repo.
|
|
38
|
-
5. Write a spec with `/to-spec`, then split it into tickets with `/to-tickets`.
|
|
38
|
+
5. Write a spec with `/to-spec`, then split it into tickets with `/to-tickets`. Before you add `ready-for-agent`, type `/luca-spec-check <spec number>` in Claude Code: three fresh readers give the spec a cold read and list what an agent would have to guess.
|
|
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
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.
|
|
@@ -46,7 +46,7 @@ After a run, type `/luca-retro` the same way to look back on it: it reads the ru
|
|
|
46
46
|
|
|
47
47
|
| Command | What it does |
|
|
48
48
|
| ------- | ------------ |
|
|
49
|
-
| `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` and `/luca-
|
|
49
|
+
| `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`, `/luca-retro` and `/luca-spec-check` skills. It asks before turning on Paseo's plugins, and leaves skills you already have alone (it only replaces its own). `--skip-skills` skips the planning skills. Inside a git repo, it then runs `luca setup` for that repo. |
|
|
50
50
|
| `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. |
|
|
51
51
|
| `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. |
|
|
52
52
|
| `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 the version it just installed finishes the job with its own code: it reloads the board, copies its skills (every one it ships, new ones included), and says when to run `/reload-skills`. |
|
|
@@ -92,7 +92,7 @@ This is the one package that goes to npm. It's TypeScript source with no build a
|
|
|
92
92
|
| ------ | ---------- |
|
|
93
93
|
| `engine/` | The engine's source, from [`packages/engine/src`](../engine/src). Both bins point into it. |
|
|
94
94
|
| `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. |
|
|
95
|
-
| `skills/` | Luca's own Claude Code skills, `luca-unstick` and `luca-
|
|
95
|
+
| `skills/` | Luca's own Claude Code skills, `luca-unstick`, `luca-retro` and `luca-spec-check`, from [`packages/engine/skills`](../engine/skills). `luca init` and `luca upgrade` copy every folder in it with a `SKILL.md` to `~/.claude/skills`. |
|
|
96
96
|
| `LICENSE` | The repo's license. |
|
|
97
97
|
|
|
98
98
|
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.
|
package/board/package.json
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The Luca version of this board, stamped when the package was packed. */
|
|
2
|
-
export const LUCA_VERSION: string = "14.0.0-alpha.
|
|
2
|
+
export const LUCA_VERSION: string = "14.0.0-alpha.27"
|
|
@@ -103,7 +103,9 @@ export type LauncherFailure = z.infer<typeof LauncherFailureSchema>
|
|
|
103
103
|
* of the model session, for the journal. `session_gone` marks a follow-up
|
|
104
104
|
* that found its session closed or unknown (#521): no agent ran, and the
|
|
105
105
|
* engine hands the follow-up's message to a fresh agent. `out_of_turns`
|
|
106
|
-
* marks a turn that reached its role's turn limit (#543).
|
|
106
|
+
* marks a turn that reached its role's turn limit (#543). `session_closed`
|
|
107
|
+
* marks a turn interrupted for running past its time limit (#685): the
|
|
108
|
+
* launcher closed its session, so no follow-up can reach it.
|
|
107
109
|
*/
|
|
108
110
|
export type AgentTurn =
|
|
109
111
|
| {
|
|
@@ -120,6 +122,7 @@ export type AgentTurn =
|
|
|
120
122
|
session?: AgentSession
|
|
121
123
|
session_gone?: boolean
|
|
122
124
|
out_of_turns?: boolean
|
|
125
|
+
session_closed?: boolean
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
/**
|
|
@@ -178,6 +181,12 @@ export type AgentLauncher = {
|
|
|
178
181
|
* session's follow-ups run at it too.
|
|
179
182
|
*/
|
|
180
183
|
effort: EffortLevel
|
|
184
|
+
/**
|
|
185
|
+
* The turn limit for this launch (#684), in place of its role's: a
|
|
186
|
+
* lens whose last try ran out of turns gets twice its role's. Left
|
|
187
|
+
* out, the role's limit.
|
|
188
|
+
*/
|
|
189
|
+
max_turns?: number
|
|
181
190
|
/**
|
|
182
191
|
* Hears the turn's live progress (#628) each time it changes, before
|
|
183
192
|
* the turn ends, for the engine's live file. Left out, nothing hears it.
|
|
@@ -105,6 +105,8 @@ export type Failed = {
|
|
|
105
105
|
error: string
|
|
106
106
|
/** The turn reached its role's turn limit (#543). */
|
|
107
107
|
out_of_turns?: boolean
|
|
108
|
+
/** The turn ran past its time limit, and its session was closed (#685). */
|
|
109
|
+
session_closed?: boolean
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
export type Outcome = { ok: true; structured_output: unknown } | Failed
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
TicketReviewResultSchema,
|
|
17
17
|
type AgentRole,
|
|
18
18
|
} from './role-results'
|
|
19
|
+
import { MAX_TURNS } from './role-turns'
|
|
19
20
|
|
|
20
21
|
import {
|
|
21
22
|
DEFAULT_MODEL,
|
|
@@ -27,29 +28,14 @@ import { HELPER_TYPE, SEARCHER } from '../guards/helper-rules'
|
|
|
27
28
|
import { guardRoleOf, permissionRules } from '../guards/role-rules'
|
|
28
29
|
import { sandboxSettings } from '../guards/sandbox-settings'
|
|
29
30
|
|
|
31
|
+
export { MAX_TURNS } from './role-turns'
|
|
32
|
+
|
|
30
33
|
/** The models an agent may launch on, by short name (`ModelNameSchema`). */
|
|
31
34
|
export const MODEL_NAMES = ModelNameSchema.options
|
|
32
35
|
|
|
33
36
|
/** The model reviewers run on, and every agent the decision model doesn't route (#603). */
|
|
34
37
|
export const CLAUDE_MODEL = DEFAULT_MODEL
|
|
35
38
|
|
|
36
|
-
/**
|
|
37
|
-
* How many model turns one agent gets before its try ends. The integration
|
|
38
|
-
* lens reads how every ticket fits together on the whole branch, so it gets
|
|
39
|
-
* more than the other lenses.
|
|
40
|
-
*/
|
|
41
|
-
export const MAX_TURNS: Record<AgentRole, number> = {
|
|
42
|
-
'test-writer': 80,
|
|
43
|
-
implementer: 150,
|
|
44
|
-
'ticket-reviewer': 60,
|
|
45
|
-
'architecture-lens': 60,
|
|
46
|
-
'simplification-lens': 60,
|
|
47
|
-
'security-lens': 60,
|
|
48
|
-
'integration-lens': 100,
|
|
49
|
-
'rules-lens': 60,
|
|
50
|
-
learner: 40,
|
|
51
|
-
}
|
|
52
|
-
|
|
53
39
|
/**
|
|
54
40
|
* Why a model may not launch an agent, or `null` if it may: exactly one of
|
|
55
41
|
* `MODEL_NAMES`. Every other short name is refused, since some (such as
|
|
@@ -181,6 +167,7 @@ export const agentOptions = ({
|
|
|
181
167
|
cwd,
|
|
182
168
|
model,
|
|
183
169
|
effort,
|
|
170
|
+
max_turns,
|
|
184
171
|
claude_path,
|
|
185
172
|
config,
|
|
186
173
|
common_git_dir,
|
|
@@ -199,6 +186,8 @@ export const agentOptions = ({
|
|
|
199
186
|
cwd: string
|
|
200
187
|
model: string
|
|
201
188
|
effort: EffortLevel
|
|
189
|
+
/** The launch's own turn limit (#684), in place of the role's; left out, the role's. */
|
|
190
|
+
max_turns?: number
|
|
202
191
|
/** The real path of the installed `claude` binary. */
|
|
203
192
|
claude_path: string
|
|
204
193
|
config: EngineConfig
|
|
@@ -264,7 +253,7 @@ export const agentOptions = ({
|
|
|
264
253
|
// Streamed chunks only feed the live file's heartbeat (#631); the
|
|
265
254
|
// launcher keeps nothing of them.
|
|
266
255
|
includePartialMessages: true,
|
|
267
|
-
maxTurns: MAX_TURNS[role],
|
|
256
|
+
maxTurns: max_turns ?? MAX_TURNS[role],
|
|
268
257
|
hooks: {
|
|
269
258
|
PreToolUse: [{ hooks: [guard_hook] }],
|
|
270
259
|
...(delivery_hook === null
|
|
@@ -80,6 +80,7 @@ export const createLaunch = ({
|
|
|
80
80
|
messaging,
|
|
81
81
|
model,
|
|
82
82
|
effort,
|
|
83
|
+
max_turns,
|
|
83
84
|
on_progress,
|
|
84
85
|
}) => {
|
|
85
86
|
const refused = checkLaunchModel({ model })
|
|
@@ -117,6 +118,7 @@ export const createLaunch = ({
|
|
|
117
118
|
model,
|
|
118
119
|
// Kept in the session's options, so its follow-ups run at it too.
|
|
119
120
|
effort,
|
|
121
|
+
max_turns,
|
|
120
122
|
claude_path: claudePath,
|
|
121
123
|
config,
|
|
122
124
|
common_git_dir: gitDir,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The texts of a refactor test update (#686): the section a fresh
|
|
3
|
+
* test-writer gets in place of the rejoin's test update section, the
|
|
4
|
+
* follow-up the implementer gets once it answered, and what the
|
|
5
|
+
* ticket-reviewer is told once it closed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The section a test-writer gets when a refactor ticket's implementer sent
|
|
10
|
+
* a test back as bad: why, and that it may change only that file.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* refactorUpdateSection({ file: 'src/rows.test.ts', reason: 'It pins the old order.' })
|
|
14
|
+
*/
|
|
15
|
+
export const refactorUpdateSection = ({
|
|
16
|
+
file,
|
|
17
|
+
reason,
|
|
18
|
+
}: {
|
|
19
|
+
file: string
|
|
20
|
+
reason: string
|
|
21
|
+
}): string =>
|
|
22
|
+
[
|
|
23
|
+
"## A refactor ticket's test was sent back as bad",
|
|
24
|
+
`The implementer says this test is wrong:\n\n- File: ${file}\n- Reason: ${reason || 'no reason given'}`,
|
|
25
|
+
`This is a refactor ticket. Change only ${file}, and only so it checks what the ticket's criteria say.`,
|
|
26
|
+
"The implementer's work is already in the worktree, so the test may pass now. That is fine here.",
|
|
27
|
+
].join('\n\n')
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The follow-up a refactor ticket's implementer gets once the test-writer
|
|
31
|
+
* fixed the test it sent back.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* refactorTestFixedMessage({ file: 'src/rows.test.ts' })
|
|
35
|
+
*/
|
|
36
|
+
export const refactorTestFixedMessage = ({ file }: { file: string }): string =>
|
|
37
|
+
`The test-writer changed ${file} so it checks what the ticket's criteria say. Carry on with the refactor. You may follow renames into test files, but don't change what a test checks.`
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The section a refactor ticket's reviewer gets once a refactor test update
|
|
41
|
+
* fixed a test: which file, why, and that only that change to what a test
|
|
42
|
+
* checks is expected.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* refactorFixedReviewSection({ fixed: [{ file: 'src/rows.test.ts', reason: 'It pins the old order.' }] })
|
|
46
|
+
*/
|
|
47
|
+
export const refactorFixedReviewSection = ({
|
|
48
|
+
fixed,
|
|
49
|
+
}: {
|
|
50
|
+
fixed: { file: string; reason: string }[]
|
|
51
|
+
}): string =>
|
|
52
|
+
[
|
|
53
|
+
'## Tests a test-writer changed on purpose',
|
|
54
|
+
"The implementer sent these tests back as bad, and a test-writer changed them so they check what the ticket's criteria say. The engine checked it changed no other test file.",
|
|
55
|
+
fixed
|
|
56
|
+
.map(
|
|
57
|
+
({ file, reason }) =>
|
|
58
|
+
`- File: ${file}\n Reason: ${reason || 'no reason given'}`
|
|
59
|
+
)
|
|
60
|
+
.join('\n'),
|
|
61
|
+
"This change to what these tests check is expected: judge it against the ticket's criteria, not as a behavior change. Any other change to what a test checks is still a blocker.",
|
|
62
|
+
].join('\n\n')
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure: how many model turns each role gets, whatever launcher runs it. A
|
|
3
|
+
* launch's own `max_turns` (#684) replaces its role's.
|
|
4
|
+
*/
|
|
5
|
+
import type { AgentRole } from './role-results'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* How many model turns one agent gets before its try ends. The integration
|
|
9
|
+
* lens reads how every ticket fits together on the whole branch, and the
|
|
10
|
+
* rules lens reads every rule file against it (#684), so they get more than
|
|
11
|
+
* the other lenses.
|
|
12
|
+
*/
|
|
13
|
+
export const MAX_TURNS: Record<AgentRole, number> = {
|
|
14
|
+
'test-writer': 80,
|
|
15
|
+
implementer: 150,
|
|
16
|
+
'ticket-reviewer': 60,
|
|
17
|
+
'architecture-lens': 60,
|
|
18
|
+
'simplification-lens': 60,
|
|
19
|
+
'security-lens': 60,
|
|
20
|
+
'integration-lens': 100,
|
|
21
|
+
'rules-lens': 100,
|
|
22
|
+
learner: 40,
|
|
23
|
+
}
|
|
@@ -78,6 +78,8 @@ export type ScriptedCall = {
|
|
|
78
78
|
* `null` for a session the launcher doesn't know.
|
|
79
79
|
*/
|
|
80
80
|
effort: EffortLevel | null
|
|
81
|
+
/** The turn limit a launch asked for (#684); left out when it asked for none. */
|
|
82
|
+
max_turns?: number
|
|
81
83
|
/** What each of the turn's tool calls handed over, in order. */
|
|
82
84
|
delivered: string[]
|
|
83
85
|
}
|
|
@@ -204,6 +206,7 @@ export const createScriptedLauncher = ({
|
|
|
204
206
|
messaging,
|
|
205
207
|
model,
|
|
206
208
|
effort,
|
|
209
|
+
max_turns,
|
|
207
210
|
}) => {
|
|
208
211
|
const session_id = `scripted-${role}-${ticket}-${calls.length + 1}`
|
|
209
212
|
sessions.set(session_id, { messaging, model, effort })
|
|
@@ -216,6 +219,7 @@ export const createScriptedLauncher = ({
|
|
|
216
219
|
may_edit_tests,
|
|
217
220
|
model,
|
|
218
221
|
effort,
|
|
222
|
+
...(max_turns === undefined ? {} : { max_turns }),
|
|
219
223
|
delivered: [],
|
|
220
224
|
}
|
|
221
225
|
calls.push(call)
|
|
@@ -51,46 +51,84 @@ const JunitPathSchema = z.string().min(1).refine(isFileInsideRepo, {
|
|
|
51
51
|
'must be a file inside the repo, relative to its root: not an absolute path, not one that leaves the repo with "..", and not ending in "/"',
|
|
52
52
|
})
|
|
53
53
|
|
|
54
|
+
/** The text a slow command's `run_files` must hold, where the files go (#683). */
|
|
55
|
+
export const RUN_FILES_PLACEHOLDER = '{files}'
|
|
56
|
+
|
|
54
57
|
/**
|
|
55
|
-
* One entry of a `checks.test` list: a command, or a
|
|
56
|
-
* A `junit` command names the JUnit files it writes in
|
|
57
|
-
* one path or a list; no other kind may have one.
|
|
58
|
+
* One entry of a `checks.test` or `checks.slow_test` list: a command, or a
|
|
59
|
+
* command and its kind. A `junit` command names the JUnit files it writes in
|
|
60
|
+
* `junit_path` (#668), one path or a list; no other kind may have one. A slow
|
|
61
|
+
* entry may also name the test files it owns (`file_patterns`) and how to run
|
|
62
|
+
* some of them (`run_files`, #683); they come together, and a `checks.test`
|
|
63
|
+
* entry may have neither.
|
|
58
64
|
*/
|
|
59
|
-
const
|
|
60
|
-
z.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
65
|
+
const testCommandEntrySchema = ({ slow }: { slow: boolean }) =>
|
|
66
|
+
z.union([
|
|
67
|
+
z.string().min(1),
|
|
68
|
+
z
|
|
69
|
+
.object({
|
|
70
|
+
run: z.string().min(1),
|
|
71
|
+
results: TestResultsSchema.optional(),
|
|
72
|
+
junit_path: z
|
|
73
|
+
.union([JunitPathSchema, z.array(JunitPathSchema).min(1)])
|
|
74
|
+
.optional(),
|
|
75
|
+
file_patterns: z.array(z.string().min(1)).min(1).optional(),
|
|
76
|
+
run_files: z.string().min(1).optional(),
|
|
77
|
+
})
|
|
78
|
+
.refine(
|
|
79
|
+
({ file_patterns, run_files }) =>
|
|
80
|
+
slow ||
|
|
81
|
+
(file_patterns === undefined && run_files === undefined),
|
|
82
|
+
{
|
|
83
|
+
message:
|
|
84
|
+
'checks.test: file_patterns and run_files belong only on checks.slow_test entries.',
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
.refine(
|
|
88
|
+
({ file_patterns, run_files }) =>
|
|
89
|
+
!slow ||
|
|
90
|
+
(file_patterns === undefined) === (run_files === undefined),
|
|
91
|
+
{
|
|
92
|
+
message:
|
|
93
|
+
'checks.slow_test: file_patterns and run_files come together; this entry has only one of them.',
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
.refine(
|
|
97
|
+
({ run_files }) =>
|
|
98
|
+
!slow ||
|
|
99
|
+
run_files === undefined ||
|
|
100
|
+
run_files.includes(RUN_FILES_PLACEHOLDER),
|
|
101
|
+
{
|
|
102
|
+
message: `checks.slow_test: run_files must contain ${RUN_FILES_PLACEHOLDER}.`,
|
|
103
|
+
path: ['run_files'],
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
.refine(
|
|
107
|
+
({ results, junit_path }) =>
|
|
108
|
+
results !== 'junit' || junit_path !== undefined,
|
|
109
|
+
{
|
|
110
|
+
message:
|
|
111
|
+
'a "junit" test command needs junit_path: the JUnit files it writes, relative to the repo root',
|
|
112
|
+
path: ['junit_path'],
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
.refine(
|
|
116
|
+
({ results, junit_path }) =>
|
|
117
|
+
results === 'junit' || junit_path === undefined,
|
|
118
|
+
{
|
|
119
|
+
message:
|
|
120
|
+
'junit_path is only for a test command with "results": "junit"',
|
|
121
|
+
path: ['junit_path'],
|
|
122
|
+
}
|
|
123
|
+
),
|
|
124
|
+
])
|
|
88
125
|
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
126
|
+
/**
|
|
127
|
+
* A test command setting: one command, or a list of them (see
|
|
128
|
+
* `testCommands`). Only a slow one's entries may own files (#683).
|
|
129
|
+
*/
|
|
130
|
+
const testCommandsSchema = ({ slow }: { slow: boolean }) =>
|
|
131
|
+
z.union([z.string().min(1), z.array(testCommandEntrySchema({ slow }))])
|
|
94
132
|
|
|
95
133
|
/** The config's `muninn` block. */
|
|
96
134
|
export const MuninnConfigSchema = z.object({
|
|
@@ -204,13 +242,15 @@ export type ModelsConfig = z.infer<typeof ModelsConfigSchema>
|
|
|
204
242
|
export const EngineConfigSchema = z.object({
|
|
205
243
|
checks: z
|
|
206
244
|
.object({
|
|
207
|
-
test:
|
|
245
|
+
test: testCommandsSchema({ slow: false }).optional(),
|
|
208
246
|
/**
|
|
209
247
|
* The slow tests (#563): the tests `test` leaves out, run only at
|
|
210
|
-
* the final review. Same shape as `test
|
|
248
|
+
* the final review. Same shape as `test`, and an entry may also
|
|
249
|
+
* own some test files and run them in a ticket's gates
|
|
250
|
+
* (`file_patterns`, `run_files`, #683). Left out, nothing
|
|
211
251
|
* changes.
|
|
212
252
|
*/
|
|
213
|
-
slow_test:
|
|
253
|
+
slow_test: testCommandsSchema({ slow: true }).optional(),
|
|
214
254
|
types: z.string().min(1).optional(),
|
|
215
255
|
lint: z.string().min(1).optional(),
|
|
216
256
|
})
|
|
@@ -37,6 +37,7 @@ export {
|
|
|
37
37
|
ModelsConfigSchema,
|
|
38
38
|
MuninnConfigSchema,
|
|
39
39
|
ROUTED_ROLES,
|
|
40
|
+
RUN_FILES_PLACEHOLDER,
|
|
40
41
|
TestResultsSchema,
|
|
41
42
|
} from './engine-config-schema'
|
|
42
43
|
export type {
|
|
@@ -55,8 +56,10 @@ export {
|
|
|
55
56
|
hasSlowTests,
|
|
56
57
|
isBunTestCommand,
|
|
57
58
|
junitCommands,
|
|
59
|
+
ownSlowCommands,
|
|
58
60
|
slowTestCommands,
|
|
59
61
|
testCommands,
|
|
62
|
+
type OwnSlowCommand,
|
|
60
63
|
type TestCommand,
|
|
61
64
|
} from './test-commands'
|
|
62
65
|
|
|
@@ -77,6 +77,41 @@ export const slowTestCommands = ({
|
|
|
77
77
|
config: EngineConfig
|
|
78
78
|
}): TestCommand[] => commandsOf(config.checks.slow_test)
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* A slow test command that runs some of its files (#683): the test files it
|
|
82
|
+
* owns, and the command with `{files}` where they go.
|
|
83
|
+
*/
|
|
84
|
+
export type OwnSlowCommand = { file_patterns: string[]; run_files: string }
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The slow test commands with `run_files` (#683), in config order: a
|
|
88
|
+
* ticket's gates run its own files that match `file_patterns` through them.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ownSlowCommands({ config: { ...config, checks: { test: 'bun test src', slow_test: [{ run: 'bun run ui', file_patterns: ['ui/**'], run_files: 'bun run ui-files {files}' }] } } })
|
|
92
|
+
* // [{ file_patterns: ['ui/**'], run_files: 'bun run ui-files {files}' }]
|
|
93
|
+
*/
|
|
94
|
+
export const ownSlowCommands = ({
|
|
95
|
+
config,
|
|
96
|
+
}: {
|
|
97
|
+
config: EngineConfig
|
|
98
|
+
}): OwnSlowCommand[] => {
|
|
99
|
+
const value = config.checks.slow_test
|
|
100
|
+
if (value === undefined || typeof value === 'string') return []
|
|
101
|
+
return value.flatMap((entry) =>
|
|
102
|
+
typeof entry === 'string' ||
|
|
103
|
+
entry.file_patterns === undefined ||
|
|
104
|
+
entry.run_files === undefined
|
|
105
|
+
? []
|
|
106
|
+
: [
|
|
107
|
+
{
|
|
108
|
+
file_patterns: entry.file_patterns,
|
|
109
|
+
run_files: entry.run_files,
|
|
110
|
+
},
|
|
111
|
+
]
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
80
115
|
/** Whether the config has a slow test command (#563). */
|
|
81
116
|
export const hasSlowTests = ({ config }: { config: EngineConfig }): boolean =>
|
|
82
117
|
slowTestCommands({ config }).length > 0
|
|
@@ -66,7 +66,9 @@ const failTurn = ({
|
|
|
66
66
|
* every launcher alike; what others changed in the shared `.git` meanwhile
|
|
67
67
|
* is journaled as `shared_git_changed`. Then the result is judged by its
|
|
68
68
|
* structured output. Each failed turn is journaled once as `agent_failed`
|
|
69
|
-
* with how it failed; the decision step picks what happens next. A
|
|
69
|
+
* with how it failed; the decision step picks what happens next. A turn the
|
|
70
|
+
* launcher interrupted for its time limit then journals its session closed
|
|
71
|
+
* (#685), since the launcher closed it. A writer's
|
|
70
72
|
* result then gets its chores done (`doChores`, #618); one the engine could
|
|
71
73
|
* not do fails the turn as a `chore` failure. A launcher stop journals
|
|
72
74
|
* `run_stopped` and ends the run. A turn the plan cut off (a rejected limit,
|
|
@@ -177,6 +179,16 @@ export const runTurn = async ({
|
|
|
177
179
|
session_gone,
|
|
178
180
|
out_of_turns,
|
|
179
181
|
})
|
|
182
|
+
// A timed-out turn's session was closed by the launcher (#685):
|
|
183
|
+
// journal the close, so the next try is a fresh agent.
|
|
184
|
+
if (!turn.ok && turn.session_closed === true && session_id !== null) {
|
|
185
|
+
context.journal.append({
|
|
186
|
+
kind: 'agent_session_closed',
|
|
187
|
+
ticket,
|
|
188
|
+
role,
|
|
189
|
+
content: { role, session_id },
|
|
190
|
+
})
|
|
191
|
+
}
|
|
180
192
|
return null
|
|
181
193
|
}
|
|
182
194
|
if (violations.length > 0) {
|
|
@@ -137,6 +137,23 @@ export type BuildAction =
|
|
|
137
137
|
from_sha: string
|
|
138
138
|
base_sha: string
|
|
139
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* A refactor ticket's first bad test (#686): keep the implementer's work,
|
|
142
|
+
* hash the worktree's test files, and journal it all as
|
|
143
|
+
* `refactor_test_update`, so a fresh test-writer fixes `file`.
|
|
144
|
+
*/
|
|
145
|
+
| {
|
|
146
|
+
type: 'open_refactor_update'
|
|
147
|
+
ticket: number
|
|
148
|
+
file: string
|
|
149
|
+
reason: string
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* After the refactor test update's test-writer answered: find the test
|
|
153
|
+
* files it changed since the update, journaled as
|
|
154
|
+
* `refactor_test_update_checked`.
|
|
155
|
+
*/
|
|
156
|
+
| { type: 'check_refactor_update'; ticket: number }
|
|
140
157
|
/** Remove these git worktrees at the end of the run; branches stay. */
|
|
141
158
|
| { type: 'remove_worktrees'; paths: string[] }
|
|
142
159
|
/** No ticket can move, and not every ticket has joined. */
|
|
@@ -16,6 +16,10 @@ import {
|
|
|
16
16
|
import { handoffSection, reviewFixSection, reviewSections } from './review-text'
|
|
17
17
|
import { retrySection } from './stuck-text'
|
|
18
18
|
|
|
19
|
+
import {
|
|
20
|
+
refactorFixedReviewSection,
|
|
21
|
+
refactorUpdateSection,
|
|
22
|
+
} from '../agents/refactor-update-prompts'
|
|
19
23
|
import {
|
|
20
24
|
CHANGESETS_LENS_SECTION,
|
|
21
25
|
CHANGESETS_SECTION,
|
|
@@ -95,9 +99,14 @@ const promptSections = ({
|
|
|
95
99
|
progress: TicketProgress
|
|
96
100
|
}): string[] => {
|
|
97
101
|
const sections = buildPromptSections({ role, progress })
|
|
98
|
-
const { retried, crashed_turn } = progress
|
|
102
|
+
const { retried, crashed_turn, refactor_update } = progress
|
|
99
103
|
return [
|
|
100
104
|
...sections,
|
|
105
|
+
...(role === 'test-writer' &&
|
|
106
|
+
refactor_update !== null &&
|
|
107
|
+
refactor_update.result === null
|
|
108
|
+
? [refactorUpdateSection(refactor_update)]
|
|
109
|
+
: []),
|
|
101
110
|
...(retried === null ? [] : [retrySection({ retried })]),
|
|
102
111
|
...(crashed_turn === role ? [CRASH_SECTION] : []),
|
|
103
112
|
]
|
|
@@ -152,8 +161,9 @@ const testUpdateSections = ({
|
|
|
152
161
|
|
|
153
162
|
/**
|
|
154
163
|
* The sections a launch adds for the ticket review: the reviewer's diff and
|
|
155
|
-
* gate results (a re-review's new changes and earlier findings),
|
|
156
|
-
*
|
|
164
|
+
* gate results (a re-review's new changes and earlier findings), and the
|
|
165
|
+
* tests a refactor test update changed on purpose (#686); or a review
|
|
166
|
+
* fixer's findings while a review fix round is open.
|
|
157
167
|
*/
|
|
158
168
|
const reviewPromptSections = ({
|
|
159
169
|
role,
|
|
@@ -164,10 +174,16 @@ const reviewPromptSections = ({
|
|
|
164
174
|
}): string[] => {
|
|
165
175
|
const fix = progress.review_fix
|
|
166
176
|
if (role === 'ticket-reviewer') {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
177
|
+
const { refactor_fixed } = progress
|
|
178
|
+
return [
|
|
179
|
+
...reviewSections({
|
|
180
|
+
progress,
|
|
181
|
+
base_sha: progress.worktree?.base_sha ?? 'HEAD',
|
|
182
|
+
}),
|
|
183
|
+
...(refactor_fixed.length === 0
|
|
184
|
+
? []
|
|
185
|
+
: [refactorFixedReviewSection({ fixed: refactor_fixed })]),
|
|
186
|
+
]
|
|
171
187
|
}
|
|
172
188
|
if (fix === null || progress.commits.green === null) return []
|
|
173
189
|
if (role === 'test-writer' && !fix.tests_answered) {
|
|
@@ -209,8 +225,12 @@ export const launch = ({
|
|
|
209
225
|
spec: snapshot.spec,
|
|
210
226
|
ticket,
|
|
211
227
|
refactor: isRefactorTicket({ ticket }),
|
|
212
|
-
// A test update names the bad test in its own
|
|
213
|
-
|
|
228
|
+
// A test update, or a refactor one, names the bad test in its own
|
|
229
|
+
// section.
|
|
230
|
+
bad_test:
|
|
231
|
+
progress.test_update === null && progress.refactor_update === null
|
|
232
|
+
? progress.bad_test
|
|
233
|
+
: null,
|
|
214
234
|
bad_test_by: progress.bad_test_source ?? undefined,
|
|
215
235
|
sections: [
|
|
216
236
|
...promptSections({ role, progress }),
|