@alecsibilia/luca 14.0.0-alpha.35 → 14.0.0-alpha.37
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 +8 -0
- package/README.md +5 -1
- package/board/package.json +1 -1
- package/board/server/describe-record.ts +19 -3
- package/board/server/luca-version.ts +1 -1
- package/board/server/reduce-board-agents.ts +6 -1
- package/board/server/reduce-board-update.ts +6 -0
- package/board/server/reduce-board.ts +1 -1
- package/engine/agents/claude-options.ts +2 -0
- package/engine/agents/role-effort.ts +2 -0
- package/engine/agents/role-instructions.ts +17 -2
- package/engine/agents/role-prompts.ts +2 -0
- package/engine/agents/role-results.ts +22 -3
- package/engine/agents/role-turns.ts +2 -0
- package/engine/agents/story-writer-instructions.ts +69 -0
- package/engine/config/config-fields.ts +10 -0
- package/engine/config/config-reload.ts +17 -1
- package/engine/config/engine-config-schema.ts +6 -0
- package/engine/config/engine-config.ts +1 -0
- package/engine/core/decide-limits-helpers.ts +37 -0
- package/engine/core/decide-many-tickets-helpers.ts +84 -0
- package/engine/core/decide-story.ts +98 -0
- package/engine/core/decide-usage-line.ts +1 -0
- package/engine/core/decide.ts +9 -1
- package/engine/core/execute-action.ts +13 -0
- package/engine/core/execute-story.ts +398 -0
- package/engine/core/final-review-text.ts +1 -1
- package/engine/core/pull-request-text.ts +82 -19
- package/engine/core/schedule-rules.ts +8 -2
- package/engine/core/session-close.ts +5 -1
- package/engine/core/story-asks.ts +100 -0
- package/engine/core/story-diff.ts +137 -0
- package/engine/core/story-prompt.ts +107 -0
- package/engine/core/story-serves.ts +82 -0
- package/engine/git/git-adapter.ts +58 -0
- package/engine/git/git-diff.ts +230 -0
- package/engine/guards/guard-roles.ts +2 -0
- package/engine/index.ts +55 -0
- package/engine/jev/jev-jobs.ts +8 -2
- package/engine/journal/agent-entries.ts +12 -0
- package/engine/journal/journal-entry-schema.ts +10 -0
- package/engine/journal/journal-kinds.ts +4 -0
- package/engine/journal/journal-record-schema.ts +10 -0
- package/engine/journal/replay.ts +21 -0
- package/engine/journal/run-state.ts +4 -0
- package/engine/journal/story-entries.ts +77 -0
- package/engine/journal/story-replay.ts +181 -0
- package/engine/journal/ticket-replay.ts +4 -0
- package/engine/shell/run-command.ts +4 -1
- package/engine/story/pr-story-main.ts +71 -0
- package/engine/story/story-cli.ts +274 -0
- package/engine/story/story-comments.ts +243 -0
- package/engine/story/story-github.ts +284 -0
- package/engine/story/story-home.ts +10 -0
- package/engine/story/story-parts.ts +220 -0
- package/engine/story/story-pick.ts +139 -0
- package/engine/story/story-render.ts +100 -0
- package/engine/story/story-save.ts +94 -0
- package/engine/story/story-schema.ts +257 -0
- package/engine/story/story-shift-cli.ts +136 -0
- package/engine/story/story-shift.ts +300 -0
- package/engine/story/story-store.ts +141 -0
- package/engine/story/testing/story-fixtures.ts +234 -0
- package/engine/testing/agent-turn-fixtures.ts +1 -0
- package/engine/testing/auto-retry-fixtures.ts +2 -0
- package/engine/testing/build-fixtures.ts +1 -0
- package/engine/testing/practice-repo.ts +13 -0
- package/engine/testing/practice-run.ts +3 -1
- package/engine/testing/run-fixtures.ts +12 -1
- package/engine/testing/story-run-fixtures.ts +306 -0
- package/engine/tracker/gh-retry.ts +12 -5
- package/engine/tracker/github-tracker-story.ts +85 -0
- package/engine/tracker/github-tracker.ts +8 -1
- package/engine/tracker/in-memory-tracker.ts +55 -0
- package/engine/tracker/tracker.ts +46 -0
- package/package.json +1 -1
- package/skills/luca-pr-address/SKILL.md +258 -0
- package/skills/luca-pr-address/references/judgment-calls.md +49 -0
- package/skills/luca-pr-address/references/plan-file.md +26 -0
- package/skills/luca-pr-address/references/story-refresh.md +34 -0
- package/skills/luca-pr-address/references/summary-comment.md +49 -0
- package/skills/luca-pr-address/references/workflow-script.md +437 -0
- package/skills/luca-pr-address/scripts/fetch-findings.ts +436 -0
- package/skills/luca-pr-address/scripts/pr-findings.ts +404 -0
- package/skills/luca-pr-address/scripts/summary-cover.ts +177 -0
- package/skills/luca-pr-address/scripts/testing-fixtures.ts +148 -0
- package/skills/luca-pr-prep/SKILL.md +150 -0
- package/skills/luca-pr-prep/references/story-example.json +201 -0
- package/skills/luca-pr-prep/references/story-format.md +98 -0
- package/skills/luca-pr-prep/scripts/pr-story.js +87 -0
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -42,6 +42,10 @@ If a ticket gets stuck, Luca comments on the spec and waits for your one-word re
|
|
|
42
42
|
|
|
43
43
|
After a run, type `/luca-retro` the same way to look back on it: it reads the run's journal, ranks what went wrong, and suggests a check, a rule, or a deletion for each, not a code change. It opens an issue for each one you pick, once you say yes.
|
|
44
44
|
|
|
45
|
+
To get any pull request ready for review, in any repo, type `/luca-pr-prep [PR number or URL]`: it writes the PR's story, a guide that walks a reviewer through the change in call order, with the why, the proof, and the blast radius of each changed region. In your own repos it posts the story on the PR and adds the `review-ready` label; in anyone else's it saves the story on your computer and posts nothing.
|
|
46
|
+
|
|
47
|
+
Once a PR has been reviewed, type `/luca-pr-address [PR number or URL]`: it reads every review comment and check alert, fixes what was asked or asks you a question, replies on each thread, runs the repo's checks, pushes, and brings the story up to date. It never declines one of your own comments, and it never merges.
|
|
48
|
+
|
|
45
49
|
## Commands
|
|
46
50
|
|
|
47
51
|
| Command | What it does |
|
|
@@ -92,7 +96,7 @@ This is the one package that goes to npm. It's TypeScript source with no build a
|
|
|
92
96
|
| ------ | ---------- |
|
|
93
97
|
| `engine/` | The engine's source, from [`packages/engine/src`](../engine/src). Both bins point into it. |
|
|
94
98
|
| `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`, `luca-retro` and `luca-
|
|
99
|
+
| `skills/` | Luca's own Claude Code skills, such as `luca-unstick`, `luca-retro`, `luca-spec-check`, `luca-pr-prep` and `luca-pr-address`, from [`packages/engine/skills`](../engine/skills). `luca init` and `luca upgrade` copy every folder in it with a `SKILL.md` to `~/.claude/skills`. |
|
|
96
100
|
| `LICENSE` | The repo's license. |
|
|
97
101
|
|
|
98
102
|
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
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
runReasonText,
|
|
27
27
|
roleWords,
|
|
28
28
|
shortSha,
|
|
29
|
+
STORY_WRITER,
|
|
29
30
|
testsSentBackText,
|
|
30
31
|
} from './reduce-board'
|
|
31
32
|
|
|
@@ -149,6 +150,13 @@ export const describeRecord = ({
|
|
|
149
150
|
tone: 'info',
|
|
150
151
|
})
|
|
151
152
|
}
|
|
153
|
+
// The story-writer (#743) is no final review agent.
|
|
154
|
+
if (record.content.role === STORY_WRITER) {
|
|
155
|
+
return event({
|
|
156
|
+
text: "The story-writer writes the pull request's review story.",
|
|
157
|
+
tone: 'info',
|
|
158
|
+
})
|
|
159
|
+
}
|
|
152
160
|
return record.ticket === null
|
|
153
161
|
? finalStartedText({ record })
|
|
154
162
|
: startedText({ before, after, record })
|
|
@@ -156,6 +164,12 @@ export const describeRecord = ({
|
|
|
156
164
|
if (record.content.role === LEARNER) {
|
|
157
165
|
return learnerFinishedText({ record })
|
|
158
166
|
}
|
|
167
|
+
if (record.content.role === STORY_WRITER) {
|
|
168
|
+
return event({
|
|
169
|
+
text: 'The story-writer answered; the engine checks and saves its story.',
|
|
170
|
+
tone: 'info',
|
|
171
|
+
})
|
|
172
|
+
}
|
|
159
173
|
return record.ticket === null
|
|
160
174
|
? finalFinishedText({ record })
|
|
161
175
|
: finishedText({ before, record })
|
|
@@ -164,9 +178,11 @@ export const describeRecord = ({
|
|
|
164
178
|
const who =
|
|
165
179
|
role === LEARNER
|
|
166
180
|
? 'The learner'
|
|
167
|
-
:
|
|
168
|
-
?
|
|
169
|
-
:
|
|
181
|
+
: role === STORY_WRITER
|
|
182
|
+
? 'The story-writer'
|
|
183
|
+
: record.ticket === null
|
|
184
|
+
? `Final review: the ${roleWords({ role })}`
|
|
185
|
+
: `${at}the ${role}`
|
|
170
186
|
return event({
|
|
171
187
|
text: `${who} ${failureText({ failure })}: ${error.split('\n')[0] ?? ''}`,
|
|
172
188
|
tone: failure === 'engine' ? 'warning' : 'danger',
|
|
@@ -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.37"
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* An agent's turn on a ticket or the final review: its start, how it
|
|
3
|
-
* finished, and how it failed.
|
|
3
|
+
* finished, and how it failed. The learner's and the story-writer's turns
|
|
4
|
+
* are neither.
|
|
4
5
|
*/
|
|
5
6
|
import type { BoardRecord } from './board-vocabulary'
|
|
6
7
|
import { agentStarted } from './reduce-board-agent-start'
|
|
7
8
|
import { countFindings, failureText, roleWords } from './reduce-board-text'
|
|
8
9
|
import {
|
|
9
10
|
LEARNER,
|
|
11
|
+
STORY_WRITER,
|
|
10
12
|
finalTried,
|
|
11
13
|
updateTicket,
|
|
12
14
|
withLearner,
|
|
@@ -118,6 +120,9 @@ export const applyAgentKind = ({
|
|
|
118
120
|
record: AgentRecord
|
|
119
121
|
}): BoardState => {
|
|
120
122
|
const { ticket } = record
|
|
123
|
+
// The story-writer (#743) has no ticket and is no final review agent;
|
|
124
|
+
// the board doesn't show the PR's story, so its turns change nothing.
|
|
125
|
+
if (record.content.role === STORY_WRITER) return state
|
|
121
126
|
switch (record.kind) {
|
|
122
127
|
case 'agent_started':
|
|
123
128
|
// The learner (#370) has no ticket and is no final review agent.
|
|
@@ -21,6 +21,12 @@ export const DETAIL_MAX = 2000
|
|
|
21
21
|
/** The learner's role (#370): its records have no ticket. */
|
|
22
22
|
export const LEARNER = 'learner'
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The story-writer's role (#743): it writes the opened PR's story. Its
|
|
26
|
+
* records have no ticket, and are never the final review's.
|
|
27
|
+
*/
|
|
28
|
+
export const STORY_WRITER = 'story-writer'
|
|
29
|
+
|
|
24
30
|
export const withLearner = ({
|
|
25
31
|
state,
|
|
26
32
|
learner,
|
|
@@ -71,7 +71,7 @@ export {
|
|
|
71
71
|
shortSha,
|
|
72
72
|
testsSentBackText,
|
|
73
73
|
} from './reduce-board-text'
|
|
74
|
-
export { LEARNER } from './reduce-board-update'
|
|
74
|
+
export { LEARNER, STORY_WRITER } from './reduce-board-update'
|
|
75
75
|
|
|
76
76
|
/** Record kinds that don't mean the engine moved on after a stop. */
|
|
77
77
|
const QUIET_KINDS = new Set([
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
IntegrationLensResultSchema,
|
|
13
13
|
LearnerResultSchema,
|
|
14
14
|
LensReviewResultSchema,
|
|
15
|
+
StoryWriterResultSchema,
|
|
15
16
|
TestWriterResultSchema,
|
|
16
17
|
TicketReviewResultSchema,
|
|
17
18
|
type AgentRole,
|
|
@@ -133,6 +134,7 @@ const RESULT_SCHEMAS: Record<AgentRole, z.ZodType> = {
|
|
|
133
134
|
'integration-lens': IntegrationLensResultSchema,
|
|
134
135
|
'rules-lens': LensReviewResultSchema,
|
|
135
136
|
learner: LearnerResultSchema,
|
|
137
|
+
'story-writer': StoryWriterResultSchema,
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
/** A role's result schema as JSON Schema, for the SDK's structured output. */
|
|
@@ -13,6 +13,11 @@ import {
|
|
|
13
13
|
import { LEARNER, LEARNER_RULES } from './learner-instructions'
|
|
14
14
|
import { LENS, REVIEWER } from './reviewer-instructions'
|
|
15
15
|
import { lensOf, type AgentRole } from './role-results'
|
|
16
|
+
import {
|
|
17
|
+
STORY_WRITER,
|
|
18
|
+
STORY_WRITER_RULES,
|
|
19
|
+
storyFormat,
|
|
20
|
+
} from './story-writer-instructions'
|
|
16
21
|
import { IMPLEMENTER, TEST_WRITER } from './writer-instructions'
|
|
17
22
|
|
|
18
23
|
import type { EngineConfig } from '../config/engine-config'
|
|
@@ -27,8 +32,8 @@ import { canMessage } from '../messages/agent-messages'
|
|
|
27
32
|
* and implementers with messaging are told how agent messages work;
|
|
28
33
|
* reviewers aren't. Each final review lens gets its own focus; the
|
|
29
34
|
* integration lens also gives the PR's Summary picture and Merge Danger
|
|
30
|
-
* (#513). The learner (#370)
|
|
31
|
-
* nothing written. Every role is told about its read-only helpers (#627),
|
|
35
|
+
* (#513). The learner (#370) and the story-writer (#743) get their own
|
|
36
|
+
* task and rules: no shell, nothing written. Every role is told about its read-only helpers (#627),
|
|
32
37
|
* and what its sandbox blocks (#540). The writers, as final review fixers
|
|
33
38
|
* too, are told where the repo's rule files are (#646).
|
|
34
39
|
*
|
|
@@ -53,6 +58,16 @@ export const roleInstructions = ({
|
|
|
53
58
|
// The learner has no shell and no agent messages, and its rules differ.
|
|
54
59
|
if (role === 'learner')
|
|
55
60
|
return [LEARNER, HELPERS, SANDBOX, LEARNER_RULES].join('\n\n')
|
|
61
|
+
// The story-writer (#743) has no shell either, carries the skill's
|
|
62
|
+
// field guide word for word, and has rules of its own.
|
|
63
|
+
if (role === 'story-writer')
|
|
64
|
+
return [
|
|
65
|
+
STORY_WRITER,
|
|
66
|
+
storyFormat().trim(),
|
|
67
|
+
HELPERS,
|
|
68
|
+
SANDBOX,
|
|
69
|
+
STORY_WRITER_RULES,
|
|
70
|
+
].join('\n\n')
|
|
56
71
|
const lens = lensOf({ role })
|
|
57
72
|
const task =
|
|
58
73
|
lens !== null
|
|
@@ -75,6 +75,8 @@ const ROLE_TASKS: Record<AgentRole, string> = {
|
|
|
75
75
|
learner:
|
|
76
76
|
"Read this run's journal digest below and propose only durable lessons that will help future runs. " +
|
|
77
77
|
"Pick each one's type, and list the ids of the shown memories that actually helped. Write nothing.",
|
|
78
|
+
'story-writer':
|
|
79
|
+
"Write the review story of this run's pull request: its summary, its map, and its walk, from the code at its head. Write nothing.",
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
/**
|
|
@@ -30,9 +30,9 @@ export type LensRole = (typeof LENS_ROLES)[number]
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* The roles an agent can play: the ticket roles while a ticket is built, one
|
|
33
|
-
* reviewer role per lens in the final review,
|
|
34
|
-
* a run (#370)
|
|
35
|
-
* implementer.
|
|
33
|
+
* reviewer role per lens in the final review, the learner at the end of
|
|
34
|
+
* a run (#370), and the story-writer once the run's PR is open (#743). The
|
|
35
|
+
* final review's fixers are the test-writer and the implementer.
|
|
36
36
|
*/
|
|
37
37
|
export const AgentRoleSchema = z.enum([
|
|
38
38
|
'test-writer',
|
|
@@ -40,6 +40,7 @@ export const AgentRoleSchema = z.enum([
|
|
|
40
40
|
'ticket-reviewer',
|
|
41
41
|
...LENS_ROLES,
|
|
42
42
|
'learner',
|
|
43
|
+
'story-writer',
|
|
43
44
|
])
|
|
44
45
|
|
|
45
46
|
export type AgentRole = z.infer<typeof AgentRoleSchema>
|
|
@@ -370,6 +371,20 @@ export const LearnerResultSchema = z.object({
|
|
|
370
371
|
|
|
371
372
|
export type LearnerResult = z.infer<typeof LearnerResultSchema>
|
|
372
373
|
|
|
374
|
+
/**
|
|
375
|
+
* The story-writer's result (#743): the parts of a PR's story only a reader
|
|
376
|
+
* of the code writes. Loose on purpose, so a wrong field inside is named by
|
|
377
|
+
* the story schema's own reader (`readStory`), not lost in a failed turn.
|
|
378
|
+
* Any other top-level key is dropped.
|
|
379
|
+
*/
|
|
380
|
+
export const StoryWriterResultSchema = z.object({
|
|
381
|
+
summary: z.record(z.string(), z.unknown()),
|
|
382
|
+
map: z.record(z.string(), z.unknown()),
|
|
383
|
+
stops: z.array(z.unknown()),
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
export type StoryWriterResult = z.infer<typeof StoryWriterResultSchema>
|
|
387
|
+
|
|
373
388
|
const lensResult = <Role extends LensRole>(role: Role) =>
|
|
374
389
|
z.object({ role: z.literal(role), result: LensReviewResultSchema })
|
|
375
390
|
|
|
@@ -396,6 +411,10 @@ export const RoleResultSchema = z.discriminatedUnion('role', [
|
|
|
396
411
|
}),
|
|
397
412
|
lensResult('rules-lens'),
|
|
398
413
|
z.object({ role: z.literal('learner'), result: LearnerResultSchema }),
|
|
414
|
+
z.object({
|
|
415
|
+
role: z.literal('story-writer'),
|
|
416
|
+
result: StoryWriterResultSchema,
|
|
417
|
+
}),
|
|
399
418
|
])
|
|
400
419
|
|
|
401
420
|
export type RoleResult = z.infer<typeof RoleResultSchema>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The story-writer's instructions (#743): a short opening for a writer with
|
|
3
|
+
* no shell, then the `/luca-pr-prep` skill's field guide word for word,
|
|
4
|
+
* the one source of what each story field holds.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from 'node:fs'
|
|
7
|
+
import { join } from 'node:path'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The skill's field guide, next to the engine's source as it ships (see
|
|
11
|
+
* `skillsDir`): `skills/luca-pr-prep/references/story-format.md`.
|
|
12
|
+
*/
|
|
13
|
+
export const STORY_FORMAT_PATH = join(
|
|
14
|
+
import.meta.dir,
|
|
15
|
+
'..',
|
|
16
|
+
'..',
|
|
17
|
+
'skills',
|
|
18
|
+
'luca-pr-prep',
|
|
19
|
+
'references',
|
|
20
|
+
'story-format.md'
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
/** The fields the engine fills in itself; the story-writer leaves them out. */
|
|
24
|
+
export const ENGINE_STORY_FIELDS = [
|
|
25
|
+
'schema_version',
|
|
26
|
+
'repo',
|
|
27
|
+
'pr_number',
|
|
28
|
+
'pr_url',
|
|
29
|
+
'title',
|
|
30
|
+
'base_ref',
|
|
31
|
+
'head_ref',
|
|
32
|
+
'head_sha',
|
|
33
|
+
'base_sha',
|
|
34
|
+
'written_at',
|
|
35
|
+
'written_by',
|
|
36
|
+
'summary.counts',
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
export const STORY_WRITER = `# Your role: story-writer
|
|
40
|
+
|
|
41
|
+
You run once, after the run's pull request opened. You write its review story: the guide that walks a reviewer through the change in the order the code runs. You read the code in your folder, the run branch at the pull request's head, with Read, Grep and Glob. You have no shell. You write nothing.
|
|
42
|
+
|
|
43
|
+
Your prompt holds the pull request, its changed files, its diff (a file left out for size says so: read the file instead), the run's asks (its spec, and each ticket it built with its acceptance criteria), what the run was unsure about, and the repo's rule files, for flags.
|
|
44
|
+
|
|
45
|
+
The field guide below was written for a writer with a shell. Where this part and the guide disagree, this part wins:
|
|
46
|
+
- You have no shell, so you can't run \`gh\` or \`git\`. Everything you need from git is in your prompt.
|
|
47
|
+
- The engine fills in ${ENGINE_STORY_FIELDS.map((field) => `\`${field}\``).join(', ')}. Leave them out.
|
|
48
|
+
- You can't open the example file or run the helper's \`check\` command the guide points at. Skip those lines. The engine checks your story, and sends every problem back to you.
|
|
49
|
+
- \`summary.feedback_wanted.source\` is \`author\` when its text comes from your prompt's \`What the run was unsure about\` section, and \`guessed\` otherwise. This rule replaces the guide's.
|
|
50
|
+
- A serves ref points only at your prompt's \`The run's asks\`. Its \`label\` is exactly one of two forms, lower case, with nothing before or after: \`spec #12\` for the run's spec, or \`ticket #14, AC2\` for one acceptance criterion of a ticket the run built (the numbers are the issue's and the criterion's). Its \`url\` is that issue's URL, exactly as your prompt gives it. For a ticket ref, \`quote\` is that criterion's exact text; for a spec ref, it is a sentence copied from the spec's body, or null. A change that serves nothing the spec or tickets asked for has \`found: false\`, no refs, and a note that says so.
|
|
51
|
+
|
|
52
|
+
Your result (structured output): \`summary\` (without \`counts\`), \`map\` and \`stops\`, as the guide describes them.`
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The story-writer's own rules, in place of the writers' and reviewers'
|
|
56
|
+
* (whose results it doesn't share), as the learner has its own.
|
|
57
|
+
*/
|
|
58
|
+
export const STORY_WRITER_RULES = `## Rules for the story-writer
|
|
59
|
+
- Plain code (the engine) drives this run. It fills in the fields it knows, checks your story, and saves it.
|
|
60
|
+
- Never create, edit, or delete a file. The network is off.
|
|
61
|
+
- Finish by giving your structured result. A turn without one counts as failed.`
|
|
62
|
+
|
|
63
|
+
let format: string | null = null
|
|
64
|
+
|
|
65
|
+
/** The skill's field guide, read once. */
|
|
66
|
+
export const storyFormat = (): string => {
|
|
67
|
+
format ??= readFileSync(STORY_FORMAT_PATH, 'utf8')
|
|
68
|
+
return format
|
|
69
|
+
}
|
|
@@ -104,6 +104,16 @@ export const modelsOf = ({ config }: { config: EngineConfig }): ModelsConfig =>
|
|
|
104
104
|
export const autoRetryOf = ({ config }: { config: EngineConfig }): boolean =>
|
|
105
105
|
config.auto_retry ?? true
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Whether a run that opens its pull request writes the PR's story (#743):
|
|
109
|
+
* the config's `pr_story`, else `true`.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* prStoryOf({ config: { ...config, pr_story: false } }) // false
|
|
113
|
+
*/
|
|
114
|
+
export const prStoryOf = ({ config }: { config: EngineConfig }): boolean =>
|
|
115
|
+
config.pr_story ?? true
|
|
116
|
+
|
|
107
117
|
/**
|
|
108
118
|
* The repo's build-cache paths and globs (#721): the config's
|
|
109
119
|
* `build_cache_paths`, else none.
|
|
@@ -3,7 +3,7 @@ import omit from 'lodash/omit'
|
|
|
3
3
|
import reduce from 'lodash/reduce'
|
|
4
4
|
import { z } from 'zod'
|
|
5
5
|
|
|
6
|
-
import { autoRetryOf, buildCachePathsOf } from './config-fields'
|
|
6
|
+
import { autoRetryOf, buildCachePathsOf, prStoryOf } from './config-fields'
|
|
7
7
|
import type { EngineConfig } from './engine-config'
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -35,6 +35,8 @@ import type { EngineConfig } from './engine-config'
|
|
|
35
35
|
* - `build_cache_paths`: the build caches cloned into each new ticket
|
|
36
36
|
* worktree (#721). Each new worktree reads it then, so a new value counts
|
|
37
37
|
* from the next ticket started.
|
|
38
|
+
* - `pr_story`: whether the run writes its PR's story (#743). Turned off
|
|
39
|
+
* on a resume after the story step began, the step just stops.
|
|
38
40
|
*/
|
|
39
41
|
export const RELOADED_FIELDS = [
|
|
40
42
|
'prepare',
|
|
@@ -46,6 +48,7 @@ export const RELOADED_FIELDS = [
|
|
|
46
48
|
'leftover_allow',
|
|
47
49
|
'auto_retry',
|
|
48
50
|
'build_cache_paths',
|
|
51
|
+
'pr_story',
|
|
49
52
|
] as const
|
|
50
53
|
|
|
51
54
|
/**
|
|
@@ -151,6 +154,12 @@ export const ConfigChangeSchema = z.discriminatedUnion('field', [
|
|
|
151
154
|
from: z.array(z.string().min(1)),
|
|
152
155
|
to: z.array(z.string().min(1)),
|
|
153
156
|
}),
|
|
157
|
+
// Never left out: a config without it reads as `true`.
|
|
158
|
+
z.object({
|
|
159
|
+
field: z.literal('pr_story'),
|
|
160
|
+
from: z.boolean(),
|
|
161
|
+
to: z.boolean(),
|
|
162
|
+
}),
|
|
154
163
|
])
|
|
155
164
|
|
|
156
165
|
export type ConfigChange = z.infer<typeof ConfigChangeSchema>
|
|
@@ -217,6 +226,11 @@ export const configChanges = ({
|
|
|
217
226
|
from: buildCachePathsOf({ config: current }),
|
|
218
227
|
to: buildCachePathsOf({ config: next }),
|
|
219
228
|
},
|
|
229
|
+
{
|
|
230
|
+
field: 'pr_story',
|
|
231
|
+
from: prStoryOf({ config: current }),
|
|
232
|
+
to: prStoryOf({ config: next }),
|
|
233
|
+
},
|
|
220
234
|
]
|
|
221
235
|
return all.filter(({ from, to }) => !isEqual(from, to))
|
|
222
236
|
}
|
|
@@ -261,6 +275,8 @@ const applyChange = ({
|
|
|
261
275
|
return { ...config, auto_retry: change.to }
|
|
262
276
|
case 'build_cache_paths':
|
|
263
277
|
return { ...config, build_cache_paths: change.to }
|
|
278
|
+
case 'pr_story':
|
|
279
|
+
return { ...config, pr_story: change.to }
|
|
264
280
|
}
|
|
265
281
|
}
|
|
266
282
|
|
|
@@ -417,6 +417,12 @@ export const EngineConfigSchema = z.object({
|
|
|
417
417
|
* written before it may lack it (`buildCachePathsOf`).
|
|
418
418
|
*/
|
|
419
419
|
build_cache_paths: z.array(BuildCachePathSchema).default([]).optional(),
|
|
420
|
+
/**
|
|
421
|
+
* Whether a run that opens its pull request writes the PR's story
|
|
422
|
+
* (#743). Left out, `true`; a config written before it may lack it
|
|
423
|
+
* (`prStoryOf`).
|
|
424
|
+
*/
|
|
425
|
+
pr_story: z.boolean().default(true).optional(),
|
|
420
426
|
})
|
|
421
427
|
|
|
422
428
|
export type EngineConfig = z.infer<typeof EngineConfigSchema>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { decide } from './decide'
|
|
2
|
+
|
|
3
|
+
import type { JournalEntry } from '../journal/journal-record'
|
|
4
|
+
import {
|
|
5
|
+
agentStarted,
|
|
6
|
+
intakePassed,
|
|
7
|
+
practiceTicket,
|
|
8
|
+
runBranchCreated,
|
|
9
|
+
ticketBuilt,
|
|
10
|
+
withInstalls,
|
|
11
|
+
} from '../testing/build-fixtures'
|
|
12
|
+
import { recordsFrom } from '../testing/intake-fixtures'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Helpers shared by the tests of the decision step around plan limits and
|
|
16
|
+
* usage (`decide-limits.test.ts` and `decide-limits-usage.test.ts`).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const TICKET = practiceTicket({ number: 11 })
|
|
20
|
+
|
|
21
|
+
/** Decide on a run with one ticket (#11) and these entries after intake. */
|
|
22
|
+
export const decideAfter = (entries: JournalEntry[]) =>
|
|
23
|
+
decide({
|
|
24
|
+
records: recordsFrom({
|
|
25
|
+
entries: [
|
|
26
|
+
...intakePassed({ tickets: [TICKET] }),
|
|
27
|
+
...withInstalls({ entries }),
|
|
28
|
+
],
|
|
29
|
+
}),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
/** Ticket #11 up to its test-writer's launch, which has not finished. */
|
|
33
|
+
export const testWriterRunning = (): JournalEntry[] => [
|
|
34
|
+
runBranchCreated(),
|
|
35
|
+
...ticketBuilt({ ticket: 11 }).slice(0, 2),
|
|
36
|
+
agentStarted({ ticket: 11, role: 'test-writer' }),
|
|
37
|
+
]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { decideSteps } from './decide'
|
|
2
|
+
|
|
3
|
+
import type { TicketSnapshot } from '../intake/intake-schemas'
|
|
4
|
+
import type { JournalEntry } from '../journal/journal-record'
|
|
5
|
+
import {
|
|
6
|
+
agentStarted,
|
|
7
|
+
commitMade,
|
|
8
|
+
gatesRun,
|
|
9
|
+
implemented,
|
|
10
|
+
intakePassed,
|
|
11
|
+
joinClashed,
|
|
12
|
+
leftoverScan,
|
|
13
|
+
practiceTicket,
|
|
14
|
+
reviewed,
|
|
15
|
+
runBranchCreated,
|
|
16
|
+
SESSIONS,
|
|
17
|
+
ticketApproved,
|
|
18
|
+
ticketRebased,
|
|
19
|
+
withInstalls,
|
|
20
|
+
} from '../testing/build-fixtures'
|
|
21
|
+
import { recordsFrom } from '../testing/intake-fixtures'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Helpers shared by the tests of the decision step for many tickets in one
|
|
25
|
+
* run (`decide-many-tickets.test.ts` and `decide-many-tickets-clash.test.ts`).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export const SUM = practiceTicket({ number: 11, title: 'Add sum' })
|
|
29
|
+
export const PRODUCT = practiceTicket({ number: 12, title: 'Add product' })
|
|
30
|
+
export const AVERAGE = practiceTicket({
|
|
31
|
+
number: 13,
|
|
32
|
+
title: 'Add average',
|
|
33
|
+
blockers: [11, 12],
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
/** Every action that can run now, on a run with these tickets and entries. */
|
|
37
|
+
export const stepsAfter = ({
|
|
38
|
+
tickets,
|
|
39
|
+
entries,
|
|
40
|
+
}: {
|
|
41
|
+
tickets: TicketSnapshot[]
|
|
42
|
+
entries: JournalEntry[]
|
|
43
|
+
}) =>
|
|
44
|
+
decideSteps({
|
|
45
|
+
records: recordsFrom({
|
|
46
|
+
entries: [
|
|
47
|
+
...intakePassed({ tickets }),
|
|
48
|
+
...withInstalls({ entries }),
|
|
49
|
+
],
|
|
50
|
+
}),
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
/** The same, on a run with #11 and #12 only. */
|
|
54
|
+
export const twoSteps = (entries: JournalEntry[]) =>
|
|
55
|
+
stepsAfter({ tickets: [SUM, PRODUCT], entries })
|
|
56
|
+
|
|
57
|
+
/** The same, on a run with #11 only. */
|
|
58
|
+
export const oneSteps = (entries: JournalEntry[]) =>
|
|
59
|
+
stepsAfter({ tickets: [SUM], entries })
|
|
60
|
+
|
|
61
|
+
/** #11 approved, then its join clashed. */
|
|
62
|
+
export const clashed = (): JournalEntry[] => [
|
|
63
|
+
runBranchCreated(),
|
|
64
|
+
...ticketApproved({ ticket: 11 }),
|
|
65
|
+
joinClashed({ ticket: 11 }),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
/** #11 rebased after `src/index.ts` clashed. */
|
|
69
|
+
export const codeClash = (): JournalEntry =>
|
|
70
|
+
ticketRebased({ ticket: 11, cause: 'clash', code: ['src/index.ts'] })
|
|
71
|
+
|
|
72
|
+
/** After a rebase, the ticket fixed and approved again, up to its green commit. */
|
|
73
|
+
export const fixedOnRunBranch = (): JournalEntry[] => [
|
|
74
|
+
agentStarted({
|
|
75
|
+
ticket: 11,
|
|
76
|
+
role: 'implementer',
|
|
77
|
+
follow_up_of: SESSIONS.implementer,
|
|
78
|
+
}),
|
|
79
|
+
implemented({ ticket: 11 }),
|
|
80
|
+
gatesRun({ ticket: 11, target: 'ticket', ok: true }),
|
|
81
|
+
leftoverScan({ ticket: 11, stage: 'green' }),
|
|
82
|
+
commitMade({ ticket: 11, stage: 'green' }),
|
|
83
|
+
reviewed({ ticket: 11 }),
|
|
84
|
+
]
|