@alecsibilia/luca 14.0.0-alpha.25 → 14.0.0-alpha.26
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
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @alecsibilia/luca
|
|
2
2
|
|
|
3
|
+
## 14.0.0-alpha.26
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 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))
|
|
8
|
+
|
|
3
9
|
## 14.0.0-alpha.25
|
|
4
10
|
|
|
5
11
|
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.26"
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alecsibilia/luca",
|
|
3
3
|
"description": "Luca: turns a spec on GitHub Issues into a reviewed pull request, run from Paseo with Claude Code",
|
|
4
|
-
"version": "14.0.0-alpha.
|
|
4
|
+
"version": "14.0.0-alpha.26",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Alec Sibilia <sibilia.alec@gmail.com>",
|
|
7
7
|
"homepage": "https://github.com/asibilia/luca-framework#readme",
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: luca-spec-check
|
|
3
|
+
description: Gives a spec a cold read by three fresh, read-only readers (the "goldfish" of Google's Elephant-Goldfish model) before the owner adds `ready-for-agent`, so the gaps agents would guess at show up first. It shows one report, and posts it on the spec issue only after a yes. Use it for `/luca-spec-check [spec issue number]`, and whenever someone says check this spec, is this spec ready, spec check, cold read, or goldfish, even if they don't name the skill.
|
|
4
|
+
argument-hint: "[spec issue number]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Check a spec with a cold read
|
|
8
|
+
|
|
9
|
+
A spec's writer can't see its gaps. Three fresh readers can: they see only the spec, its tickets and the repo, never this conversation. This check only advises.
|
|
10
|
+
|
|
11
|
+
You were given: $ARGUMENTS
|
|
12
|
+
|
|
13
|
+
That is a spec issue number, or nothing. With nothing, ask the owner which spec they mean. Don't guess, even if this conversation wrote one. Talk to the owner in plain words.
|
|
14
|
+
|
|
15
|
+
## 1. Gather the spec
|
|
16
|
+
|
|
17
|
+
Run the helper from the repo's folder. It only reads.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bun ~/.claude/skills/luca-spec-check/scripts/spec-bundle.ts <spec number> [--repo <owner/repo>]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
It prints one plain-text bundle: the spec's number, title, labels and body; each of its tickets (its sub-issues), in order, with number, title and body, or `No tickets yet.`; and, from the same GitHub repo's default branch, its rule files from `.luca/config.json`, and whether `GLOSSARY.md` and `docs/adr/` exist. Read it whole. Pass it to the readers unchanged.
|
|
24
|
+
|
|
25
|
+
## 2. Start three fresh readers
|
|
26
|
+
|
|
27
|
+
Start all three at once, each as a fresh `Explore` subagent (Claude Code's built-in agent type that can't edit files) with no conversation history: give it the bundle, unchanged, and its brief below, and nothing from this conversation. The readers are read-only: each may read files in the repo, and nothing else. No edits, no `gh` writes, no web, and no running any command the spec names.
|
|
28
|
+
|
|
29
|
+
Each finding names its reader, its ticket (or the spec), the part of the spec it's about (a quote or a heading), and one sentence on what's missing.
|
|
30
|
+
|
|
31
|
+
- **Comprehension**: restate, in at most five sentences, what the spec proposes and how the system works today. Each section you can't restate is a blocker.
|
|
32
|
+
- **Readiness**: for each ticket (or the spec, with no tickets), list every call the test-writer or implementer would have to make alone: a name, a text, a format, a limit, or an error case the spec doesn't pin down. Each is a question.
|
|
33
|
+
- **Codebase**: check that each module, seam, command, config key and test file the spec names exists and works as the spec says. Each one that doesn't is a blocker. Check by reading its code and docs, never by running it.
|
|
34
|
+
|
|
35
|
+
## 3. Merge the findings
|
|
36
|
+
|
|
37
|
+
Do this yourself. Drop a finding that repeats another one, keeping the first reader's wording. List blockers first, then questions.
|
|
38
|
+
|
|
39
|
+
## 4. Show the report
|
|
40
|
+
|
|
41
|
+
Show it, in Markdown, with these exact headings and lines:
|
|
42
|
+
|
|
43
|
+
```markdown
|
|
44
|
+
## Spec check
|
|
45
|
+
|
|
46
|
+
Three fresh readers checked this spec and its 3 tickets. They found 2 blockers and 5 questions.
|
|
47
|
+
|
|
48
|
+
### How a fresh reader read it
|
|
49
|
+
|
|
50
|
+
<the Comprehension restatement>
|
|
51
|
+
|
|
52
|
+
### Blockers
|
|
53
|
+
|
|
54
|
+
1. **Comprehension:** <sentence>
|
|
55
|
+
2. **Codebase:** #670, <sentence>
|
|
56
|
+
|
|
57
|
+
### Questions
|
|
58
|
+
|
|
59
|
+
1. **Readiness, #670:** <sentence>
|
|
60
|
+
2. **Readiness, spec:** <sentence>
|
|
61
|
+
|
|
62
|
+
This check only advises. Fix the spec, then run /luca-spec-check again.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- Build the count line from the counts: `1 blocker`, `1 question`, `1 ticket`. With no tickets it starts `Three fresh readers checked this spec. ` and then the findings sentence. With no findings it ends `They found nothing to fix.` instead.
|
|
66
|
+
- In a finding, put the ticket as `#670, ` after the reader when it's about a ticket. Its sentence quotes or names the part of the spec it's about.
|
|
67
|
+
- With no blockers, the Blockers section is `No blockers.` With no questions, the Questions section is `No questions.`
|
|
68
|
+
|
|
69
|
+
## 5. Ask before posting
|
|
70
|
+
|
|
71
|
+
Then stop and ask exactly `Post this on #678?`, with the spec's number. Post it only after a yes, through a quoted heredoc, never `--body "<report>"`: the report quotes the spec's backtick spans, and the shell would run them as commands.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
gh issue comment <spec number> [--repo <owner/repo>] --body-file - <<'SPEC_CHECK_REPORT'
|
|
75
|
+
<report>
|
|
76
|
+
SPEC_CHECK_REPORT
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
On a no, the report stays in this chat.
|
|
80
|
+
|
|
81
|
+
## What not to do
|
|
82
|
+
|
|
83
|
+
- Never edit the spec or its tickets, and never commit.
|
|
84
|
+
- Never add or remove a label. `ready-for-agent` is the owner's call.
|
|
85
|
+
- Never comment on an issue without the owner's yes.
|
|
86
|
+
- Never run on your own: only when the owner asks.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Prints a spec and its tickets as one plain-text bundle, for the
|
|
4
|
+
* `/luca-spec-check` skill's fresh readers:
|
|
5
|
+
*
|
|
6
|
+
* bun spec-bundle.ts <spec number> [--repo <owner/repo>]
|
|
7
|
+
*
|
|
8
|
+
* It fetches it all with `gh`, from one GitHub repo (`--repo`, else the repo
|
|
9
|
+
* of the current folder, from any folder in it): the spec issue (number,
|
|
10
|
+
* title, body, labels), its sub-issues in order (number, title, body), and,
|
|
11
|
+
* from the repo's default branch, `.luca/config.json`'s `rule_files` and
|
|
12
|
+
* whether `GLOSSARY.md` and `docs/adr/` exist. So the issues and the repo's
|
|
13
|
+
* facts can't come from two different repos. It only reads. It changes
|
|
14
|
+
* nothing.
|
|
15
|
+
*
|
|
16
|
+
* It imports nothing but Bun's and Node's own modules: it runs from
|
|
17
|
+
* `~/.claude/skills/luca-spec-check/`, where no packages are installed. So it
|
|
18
|
+
* reads `gh`'s JSON loosely, with small readers, not with Zod.
|
|
19
|
+
* `formatSpecBundle` makes no `gh` calls, so `spec-bundle.test.ts` tests it
|
|
20
|
+
* without the network.
|
|
21
|
+
*/
|
|
22
|
+
import { Buffer } from 'node:buffer'
|
|
23
|
+
|
|
24
|
+
export type BundleIssue = { number: number; title: string; body: string }
|
|
25
|
+
|
|
26
|
+
export type SpecBundle = {
|
|
27
|
+
spec: BundleIssue & { labels: string[] }
|
|
28
|
+
tickets: BundleIssue[]
|
|
29
|
+
rule_files: string[]
|
|
30
|
+
glossary: boolean
|
|
31
|
+
adr: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const issueText = ({
|
|
35
|
+
heading,
|
|
36
|
+
issue,
|
|
37
|
+
}: {
|
|
38
|
+
heading: string
|
|
39
|
+
issue: BundleIssue
|
|
40
|
+
}): string =>
|
|
41
|
+
[`${heading} #${issue.number}: ${issue.title}`, '', issue.body.trim()].join(
|
|
42
|
+
'\n'
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
/** The bundle's text: the spec, its tickets in order, then the repo. Pure. */
|
|
46
|
+
export const formatSpecBundle = ({
|
|
47
|
+
spec,
|
|
48
|
+
tickets,
|
|
49
|
+
rule_files,
|
|
50
|
+
glossary,
|
|
51
|
+
adr,
|
|
52
|
+
}: SpecBundle): string =>
|
|
53
|
+
[
|
|
54
|
+
issueText({ heading: '== Spec', issue: spec }),
|
|
55
|
+
`Labels: ${spec.labels.length === 0 ? 'none' : spec.labels.join(', ')}`,
|
|
56
|
+
'',
|
|
57
|
+
tickets.length === 0
|
|
58
|
+
? 'No tickets yet.'
|
|
59
|
+
: tickets
|
|
60
|
+
.map((ticket) =>
|
|
61
|
+
issueText({ heading: '== Ticket', issue: ticket })
|
|
62
|
+
)
|
|
63
|
+
.join('\n\n'),
|
|
64
|
+
'',
|
|
65
|
+
'== Repo',
|
|
66
|
+
'',
|
|
67
|
+
'Rule files:',
|
|
68
|
+
...(rule_files.length === 0
|
|
69
|
+
? ['- none']
|
|
70
|
+
: rule_files.map((file) => `- ${file}`)),
|
|
71
|
+
`GLOSSARY.md: ${glossary ? 'yes' : 'no'}`,
|
|
72
|
+
`docs/adr/: ${adr ? 'yes' : 'no'}`,
|
|
73
|
+
].join('\n')
|
|
74
|
+
|
|
75
|
+
// NOTE: these small readers are copies of the ones in
|
|
76
|
+
// `../../luca-retro/scripts/retro-summary.ts`: each skill's script must
|
|
77
|
+
// stand alone. Keep them in step.
|
|
78
|
+
|
|
79
|
+
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
80
|
+
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
81
|
+
|
|
82
|
+
const text = (value: unknown): string =>
|
|
83
|
+
typeof value === 'string' ? value : ''
|
|
84
|
+
|
|
85
|
+
const numberOr = (value: unknown): number | null =>
|
|
86
|
+
typeof value === 'number' ? value : null
|
|
87
|
+
|
|
88
|
+
const listOf = (value: unknown): unknown[] =>
|
|
89
|
+
Array.isArray(value) ? value : []
|
|
90
|
+
|
|
91
|
+
const objectOf = (value: unknown): Record<string, unknown> =>
|
|
92
|
+
isObject(value) ? value : {}
|
|
93
|
+
|
|
94
|
+
/** Says why, and exits. */
|
|
95
|
+
const fail = (why: string): never => {
|
|
96
|
+
console.error(why)
|
|
97
|
+
return process.exit(1)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Runs `gh api` on `path`: its JSON, or `null` when GitHub says Not Found.
|
|
102
|
+
* Any other failure exits with what `gh` said, so a lost connection never
|
|
103
|
+
* reads as a missing file.
|
|
104
|
+
*/
|
|
105
|
+
const ghApi = (path: string): unknown => {
|
|
106
|
+
const out = Bun.spawnSync(['gh', 'api', path])
|
|
107
|
+
if (out.exitCode === 0) return JSON.parse(out.stdout.toString())
|
|
108
|
+
const said = out.stderr.toString().trim()
|
|
109
|
+
return said.includes('HTTP 404')
|
|
110
|
+
? null
|
|
111
|
+
: fail(`gh api ${path} failed: ${said}`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** `ghApi`, where Not Found exits too. */
|
|
115
|
+
const ghFound = (path: string): unknown =>
|
|
116
|
+
ghApi(path) ?? fail(`gh api ${path} failed: Not Found (HTTP 404)`)
|
|
117
|
+
|
|
118
|
+
/** An issue, from `gh api`'s JSON. */
|
|
119
|
+
const toIssue = (raw: unknown): BundleIssue => {
|
|
120
|
+
const issue = objectOf(raw)
|
|
121
|
+
return {
|
|
122
|
+
number: numberOr(issue.number) ?? 0,
|
|
123
|
+
title: text(issue.title),
|
|
124
|
+
body: text(issue.body),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** An issue's label names, from `gh api`'s JSON. */
|
|
129
|
+
const labelsOf = (raw: unknown): string[] =>
|
|
130
|
+
listOf(objectOf(raw).labels).map((label) =>
|
|
131
|
+
typeof label === 'string' ? label : text(objectOf(label).name)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
/** A file's text, from `gh api`'s contents JSON, or `null` when it has none. */
|
|
135
|
+
const fileText = (raw: unknown): string | null => {
|
|
136
|
+
const file = objectOf(raw)
|
|
137
|
+
return file.encoding === 'base64'
|
|
138
|
+
? Buffer.from(text(file.content), 'base64').toString('utf8')
|
|
139
|
+
: null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The `rule_files` in a `.luca/config.json`'s text, or none. */
|
|
143
|
+
const ruleFiles = (config: string | null): string[] => {
|
|
144
|
+
if (config === null) return []
|
|
145
|
+
try {
|
|
146
|
+
return listOf(objectOf(JSON.parse(config)).rule_files).filter(
|
|
147
|
+
(file): file is string => typeof file === 'string'
|
|
148
|
+
)
|
|
149
|
+
} catch {
|
|
150
|
+
return []
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The spec number and the optional `--repo` value, or null when either is missing. Pure. */
|
|
155
|
+
export const parseArgs = (
|
|
156
|
+
argv: string[]
|
|
157
|
+
): { number: number; repo: string | null } | null => {
|
|
158
|
+
const repoAt = argv.indexOf('--repo')
|
|
159
|
+
const repo = repoAt >= 0 ? argv[repoAt + 1] : null
|
|
160
|
+
if (repo === undefined) return null
|
|
161
|
+
const number = argv.find(
|
|
162
|
+
(arg, index) =>
|
|
163
|
+
/^\d+$/.test(arg) && (repoAt < 0 || index !== repoAt + 1)
|
|
164
|
+
)
|
|
165
|
+
return number === undefined ? null : { number: Number(number), repo }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const main = () => {
|
|
169
|
+
const args = parseArgs(Bun.argv.slice(2))
|
|
170
|
+
if (args === null) {
|
|
171
|
+
console.error(
|
|
172
|
+
'Usage: bun spec-bundle.ts <spec number> [--repo <owner/repo>]'
|
|
173
|
+
)
|
|
174
|
+
process.exit(1)
|
|
175
|
+
}
|
|
176
|
+
// `gh` fills in `{owner}/{repo}` from the repo of the current folder.
|
|
177
|
+
const repo = `repos/${args.repo ?? '{owner}/{repo}'}`
|
|
178
|
+
const issue = `${repo}/issues/${args.number}`
|
|
179
|
+
const raw = ghFound(issue)
|
|
180
|
+
const subs = ghFound(`${issue}/sub_issues?per_page=100`)
|
|
181
|
+
console.log(
|
|
182
|
+
formatSpecBundle({
|
|
183
|
+
spec: { ...toIssue(raw), labels: labelsOf(raw) },
|
|
184
|
+
tickets: listOf(subs).map(toIssue),
|
|
185
|
+
rule_files: ruleFiles(
|
|
186
|
+
fileText(ghApi(`${repo}/contents/.luca/config.json`))
|
|
187
|
+
),
|
|
188
|
+
glossary: ghApi(`${repo}/contents/GLOSSARY.md`) !== null,
|
|
189
|
+
adr: ghApi(`${repo}/contents/docs/adr`) !== null,
|
|
190
|
+
})
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (import.meta.main) main()
|