do-better 1.0.0
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/LICENSE +21 -0
- package/README.md +265 -0
- package/bin/cli.js +262 -0
- package/do-better/SKILL.md +417 -0
- package/do-better/references/refute-charter.md +170 -0
- package/do-better/references/scoring.md +98 -0
- package/do-better/references/taxonomy.md +136 -0
- package/do-better/references/templates/charter-template.md +71 -0
- package/do-better/references/templates/finding-template.md +56 -0
- package/do-better/references/templates/rail-template.md +74 -0
- package/do-better/references/templates/roadmap-template.md +67 -0
- package/do-better/references/templates/ticket-template.md +78 -0
- package/do-better/references/verification.md +129 -0
- package/package.json +20 -0
- package/src/adlc.js +331 -0
- package/src/artifacts.js +580 -0
- package/src/charter.js +657 -0
- package/src/comprehend.js +553 -0
- package/src/identify.js +1119 -0
- package/src/llm.js +530 -0
- package/src/rail.js +533 -0
- package/src/refresh.js +256 -0
- package/src/roadmap.js +630 -0
- package/src/scan.js +313 -0
- package/src/state.js +260 -0
- package/src/utils.js +449 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chris Williams
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# do-better
|
|
2
|
+
|
|
3
|
+
> Point an agent at an existing codebase and ask: *what should be better here,
|
|
4
|
+
> in what order, and how do we prove we didn't break anything?* Then produce a
|
|
5
|
+
> defensible technical roadmap — not vibes, **verified findings**.
|
|
6
|
+
|
|
7
|
+
**do-better** is comprehension → fault/improvement identification → technical
|
|
8
|
+
roadmap, for existing codebases — the "just do better" work, not new-feature
|
|
9
|
+
build. It is a sibling to
|
|
10
|
+
[skill-mining](https://github.com/voodootikigod/skill-mining) (extracts latent
|
|
11
|
+
*knowledge* from a repo) and [aidlc](https://github.com/voodootikigod/aidlc)
|
|
12
|
+
(executes builds): do-better produces *judgment + plan*, and is the
|
|
13
|
+
**brownfield front-end to the ADLC**. It never executes fixes — each roadmap
|
|
14
|
+
item hands off to ADLC P3/P4 as a cold-start-tested ticket.
|
|
15
|
+
|
|
16
|
+
What makes the output defensible:
|
|
17
|
+
|
|
18
|
+
- Every claim carries a `file:line@sha` citation, verified deterministically
|
|
19
|
+
and pinned to a commit SHA. Stale claims are flagged, never trusted.
|
|
20
|
+
- Every finding survived **reproduce-or-kill** adversarial verification —
|
|
21
|
+
unverified findings never reach output.
|
|
22
|
+
- Sampling is **declared, never silent** — the coverage manifest states what
|
|
23
|
+
was deep-read, scanned, and skipped.
|
|
24
|
+
- The roadmap lists what was **declined** and the risk of inaction, not just
|
|
25
|
+
what made the cut.
|
|
26
|
+
|
|
27
|
+
Dual mode, like skill-mining: a cross-harness skill
|
|
28
|
+
([`do-better/SKILL.md`](do-better/SKILL.md)) any agent harness can run
|
|
29
|
+
manually, plus an `npx do-better` CLI that automates the same lifecycle with
|
|
30
|
+
the same gates.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Zero-install (recommended)
|
|
36
|
+
npx do-better run
|
|
37
|
+
|
|
38
|
+
# Or as a cross-harness agent skill
|
|
39
|
+
npx skills add voodootikigod/do-better
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Set an LLM key (Anthropic default; Gemini, OpenAI, and local endpoints supported):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
export ANTHROPIC_API_KEY="..." # default, detected first
|
|
46
|
+
# or: export GEMINI_API_KEY="..." # with --provider gemini
|
|
47
|
+
# or: export OPENAI_API_KEY="..." # with --provider openai
|
|
48
|
+
# or (data-governance / air-gapped): any OpenAI-compatible local server
|
|
49
|
+
# export DOBETTER_LOCAL_BASE_URL="http://localhost:11434/v1" # e.g. Ollama
|
|
50
|
+
# export DOBETTER_LOCAL_MODEL="qwen2.5-coder" # with --provider local
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
No key at all? `--offline` degrades to static analysis and structure-only
|
|
54
|
+
artifacts — every degradation declared, never silent.
|
|
55
|
+
|
|
56
|
+
Requires Node >= 18 and a **git repository** as the target (claims are
|
|
57
|
+
SHA-pinned; no git, no run). Zero runtime dependencies.
|
|
58
|
+
|
|
59
|
+
## The loop
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
D-1 Scan ──▶ D0 Charter ──▶ D1 Comprehend ──▶ D2 Identify ──▶ D3 Roadmap ──▶ D4 Rail ──▶ Handoff
|
|
63
|
+
cheap facts interview + 7 artifacts, refute-chartered score/sequence/ pin behavior tickets →
|
|
64
|
+
+ codemap weights parallax-checked finders + verify ticket with rails ADLC P3/P4
|
|
65
|
+
[HUMAN GATE 1] divergence gate dry + 0-unverified coldstart gate + rails green +
|
|
66
|
+
[HUMAN GATE 2] hollow audit
|
|
67
|
+
|
|
68
|
+
↻ refresh — idempotent living-document re-run (only changed files)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Exactly **two human gates**: charter approval and roadmap approval. Everything
|
|
72
|
+
else is deterministic or threshold-based. Enterprise engagements pause for days
|
|
73
|
+
at human gates; `state.json` makes every phase resumable, so a pause costs
|
|
74
|
+
nothing.
|
|
75
|
+
|
|
76
|
+
## Usage
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npx do-better scan # D-1 — cheap repo scan: facts, incantations, codemap draft
|
|
80
|
+
npx do-better charter # D0 — interactive stakeholder interview, seeded by scan facts
|
|
81
|
+
npx do-better charter --approve # approve the (possibly edited) charter [HUMAN GATE 1]
|
|
82
|
+
npx do-better audit # D1+D2 — comprehend (7 artifacts) + identify (verified findings)
|
|
83
|
+
npx do-better roadmap # D3 — score, sequence, tickets, coldstart gate
|
|
84
|
+
npx do-better roadmap --approve # approve the (possibly edited) roadmap [HUMAN GATE 2]
|
|
85
|
+
npx do-better rail # D4 — characterization rails + hollow-test audit
|
|
86
|
+
npx do-better run # full pipeline; stops cleanly at human gates, resumes after --approve
|
|
87
|
+
npx do-better refresh # idempotent re-run; diffs vs pinned SHA; only changed files
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Flags
|
|
91
|
+
|
|
92
|
+
| Flag | Applies to | Meaning |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `--provider anthropic\|gemini\|openai\|local` | all | Force the LLM provider. Default: env autodetect, Anthropic first (a configured `DOBETTER_LOCAL_BASE_URL` is detected last). `local` targets any OpenAI-compatible endpoint (`DOBETTER_LOCAL_BASE_URL` + `DOBETTER_LOCAL_MODEL`). A named provider without its key/URL is an error — never a silent fallback. |
|
|
95
|
+
| `--budget <usd>` | all | Hard USD ceiling. A call that would exceed it refuses and stops with resume instructions; per-phase spend lives in `state.json`. |
|
|
96
|
+
| `--offline` | all | No LLM calls — static analysis + structure-only artifacts, every degradation declared. |
|
|
97
|
+
| `--model-cheap <id>` / `--model-mid <id>` / `--model-frontier <id>` | all | Override the model for one tier (see Model tiering). |
|
|
98
|
+
| `--target <dir>` | all | Target repo (also the second positional). Default `.`. |
|
|
99
|
+
| `--approve` | `charter`, `roadmap` | Approve the human-gated artifact as it stands on disk (post-edit hashes are recorded, not rejected). |
|
|
100
|
+
| `--n <N>` / `--threshold <t>` | `audit` | D1 parallax fan width (default 3) **and** the D2 finder-pool ceiling / divergence threshold (default 0.25). In D2, `--n` caps a charter-weighted pool of distinct-lens finders (table below); unset, the D2 pool ceiling is 1 (pooling opt-in). |
|
|
101
|
+
| `--yes` | `rail`, others | Skip confirmations (e.g. the rails commit). Never skips the two human gates. |
|
|
102
|
+
| `--json` | all | Machine-readable summary on stdout. |
|
|
103
|
+
| `-h`, `--help` | all | Help. |
|
|
104
|
+
|
|
105
|
+
**D2 charter-weighted finder-pool width.** `--n` is the ceiling; each pass fans
|
|
106
|
+
that many finders, each under a distinct lens, so "dry" means the codebase is
|
|
107
|
+
exhausted rather than that a single context converged:
|
|
108
|
+
|
|
109
|
+
| Charter weight | Effective pool width |
|
|
110
|
+
|---|---|
|
|
111
|
+
| 4–5 | `--n` (full width) |
|
|
112
|
+
| 2–3 | `max(1, floor(--n / 2))` |
|
|
113
|
+
| 1 | `1` (no pooling) |
|
|
114
|
+
|
|
115
|
+
`--n 1` reproduces the pre-pool single-finder call counts exactly (every
|
|
116
|
+
dimension is width 1). Unset, the D2 ceiling is 1; lens rotation across passes
|
|
117
|
+
is always on, while the within-pass fan-out is opt-in via `--n`.
|
|
118
|
+
|
|
119
|
+
### Exit codes
|
|
120
|
+
|
|
121
|
+
The adlc-universal contract:
|
|
122
|
+
|
|
123
|
+
| Code | Meaning |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `0` | Success — **including a clean human-gate pause** with printed resume instructions. |
|
|
126
|
+
| `1` | Operational error: bad input, missing file, no provider/key, network or LLM failure after retries, budget exceeded. |
|
|
127
|
+
| `2` | Deterministic gate failure: divergence over threshold, a `(dimension × packet)` cell not dry / unverified findings, coldstart gaps unrepaired, rails red, hollow-audit survivor. |
|
|
128
|
+
|
|
129
|
+
### D2 coverage & sizing `--budget`
|
|
130
|
+
|
|
131
|
+
D2 no longer rotates a single ≤30 KB window over the deep-read set — it
|
|
132
|
+
**partitions the whole set into packets** (every deep-read file lands in exactly
|
|
133
|
+
one packet; an oversized file becomes its own truncated singleton) and loops
|
|
134
|
+
each `(dimension × packet)` cell until dry. That guarantees every deep-read byte
|
|
135
|
+
is actually shown to a finder, but it also means finder calls scale with the
|
|
136
|
+
number of packets. After a run, `.dobetter/comprehension/coverage-manifest.md`
|
|
137
|
+
gains a **`## D2 finder coverage`** section recording, per dimension, the files
|
|
138
|
+
examined, packet count, total passes, and any truncated slices (unreadable
|
|
139
|
+
files land under `### Unexamined`).
|
|
140
|
+
|
|
141
|
+
Finder calls scale as **`dimensions × packets × passes × N`**, so size
|
|
142
|
+
`--budget` before a run. For a reference repo of ~8 dimensions and ~5 packets,
|
|
143
|
+
with the dry loop settling near its `K_DRY = 2` floor (best case) and its
|
|
144
|
+
`MAX_PASSES = 8` cap (worst case):
|
|
145
|
+
|
|
146
|
+
| Term | Value | Notes |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| dimensions | 8 | taxonomy floor (+ any charter extras) |
|
|
149
|
+
| packets | ~5 | reference shape; grows with deep-read size / file size |
|
|
150
|
+
| passes per cell | 2 – 8 | `K_DRY = 2` when a cell settles immediately; `MAX_PASSES = 8` cap |
|
|
151
|
+
| N | `poolN` | finder pool width per pass — charter-weighted (see the pooling table above); `--n 1` (or unset) reproduces single-finder counts |
|
|
152
|
+
| **finder calls** | **80·N – 320·N** | `8 × 5 × {2‥8} × N`; verification adds ≤1 repro + ≤1 verdict per surviving candidate |
|
|
153
|
+
|
|
154
|
+
`--budget` is the hard ceiling: a call that would exceed it refuses and stops
|
|
155
|
+
with resume instructions, and every finding verified so far is already on disk
|
|
156
|
+
(findings are written per candidate, not batched). A same-sha resume skips the
|
|
157
|
+
packets already recorded dry — zero re-issued finder calls for them.
|
|
158
|
+
|
|
159
|
+
### Environment variables
|
|
160
|
+
|
|
161
|
+
| Var | Purpose |
|
|
162
|
+
|---|---|
|
|
163
|
+
| `ANTHROPIC_API_KEY` / `GEMINI_API_KEY` / `OPENAI_API_KEY` | Provider credentials (autodetected in that order). |
|
|
164
|
+
| `DOBETTER_LOCAL_BASE_URL` | Base URL of an OpenAI-compatible local endpoint (e.g. `http://localhost:11434/v1`) for `--provider local`. Autodetected last, after the hosted keys above. |
|
|
165
|
+
| `DOBETTER_LOCAL_MODEL` | Model name to request from the local endpoint (e.g. `qwen2.5-coder`). |
|
|
166
|
+
| `DOBETTER_LOCAL_API_KEY` | Optional bearer token for the local endpoint (many local servers ignore it). |
|
|
167
|
+
| `DOBETTER_ADLC_DIR` | Path to an aidlc checkout for the composed tools (else sibling-dir probe, else `npx @adlc/<tool>`). |
|
|
168
|
+
| `DOBETTER_SKILL_MINING_DIR` | Path to a skill-mining checkout (else sibling probe, else `npx skill-mining`). |
|
|
169
|
+
| `DOBETTER_ANSWERS` | Path to a JSON `string[]` of scripted charter answers (non-interactive runs, CI, tests). |
|
|
170
|
+
| `DOBETTER_FAKE_LLM` | Test seam: path to a module default-exporting an async `fake({prompt,system,tier,label,jsonMode})` — bypasses all providers/keys and no network path is reachable. |
|
|
171
|
+
| `DOBETTER_DEBUG` | Print stack traces on error. |
|
|
172
|
+
|
|
173
|
+
## Artifact layout
|
|
174
|
+
|
|
175
|
+
Everything is committed to the target repo under `.dobetter/`:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
.dobetter/
|
|
179
|
+
charter.md # D0 output, human-approved; dimension weights
|
|
180
|
+
comprehension/
|
|
181
|
+
codemap.md
|
|
182
|
+
architecture.md # intended design vs actual drift
|
|
183
|
+
behavior-inventory.md # KEYSTONE — observable behaviors: routes, jobs, CLIs, events
|
|
184
|
+
dependencies.md # versions, EOL/CVE, coupling hotspots
|
|
185
|
+
rails-map.md # covered vs load-bearing-but-untested
|
|
186
|
+
glossary.md # business terms ↔ code terms
|
|
187
|
+
coverage-manifest.md # deep-read X%, scanned Y% — declared, never silent
|
|
188
|
+
findings/ # one file per VERIFIED finding; file:line + commit-SHA evidence,
|
|
189
|
+
# dimension, severity, reproduction record
|
|
190
|
+
ROADMAP.md # executive deliverable (Now/Next/Later, declined, risk-of-inaction)
|
|
191
|
+
backlog/ # ADLC-shaped tickets; tickets.json is coldstart-consumable as-is
|
|
192
|
+
rails/manifest.md # pointers — actual tests live in the repo's test tree
|
|
193
|
+
state.json # run history, SHA pins, per-phase spend, prior-roadmap hashes
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The **behavior inventory is the keystone**: it is the denominator for "retain
|
|
197
|
+
existing functionality," the scope source for rails, and the baseline for
|
|
198
|
+
regression detection on refresh.
|
|
199
|
+
|
|
200
|
+
## Composition contracts
|
|
201
|
+
|
|
202
|
+
- **skill-mining** — invoked as a D1 comprehension sub-step; mined skills are
|
|
203
|
+
comprehension artifacts and ride along for the execution phase.
|
|
204
|
+
- **aidlc** — `backlog/tickets.json` conforms to the ADLC P2 ticket schema
|
|
205
|
+
(atomic, fresh-agent executable, explicit contracts, coldstart-tested) and is
|
|
206
|
+
consumable by `coldstart --tickets .dobetter/backlog/tickets.json`
|
|
207
|
+
unmodified. Rails conform to P3 doctrine (separate context, frozen,
|
|
208
|
+
hollow-audited). Reused packages: `parallax` (D1 divergence gate),
|
|
209
|
+
`coldstart` (D3 gate), `hollow-test` (D4 audit), `preflight` (D4 env check),
|
|
210
|
+
`behavior-diff` (refresh regression detection).
|
|
211
|
+
- **grill-me** — the D0 charter interview is grill-me with a codebase-check
|
|
212
|
+
clause, seeded by D-1 facts: questions cite real scan facts, and anything the
|
|
213
|
+
codebase already answers is established, not asked.
|
|
214
|
+
|
|
215
|
+
Every composed tool **degrades gracefully and loudly** when absent: parallax →
|
|
216
|
+
declared single-reading mode + mandatory human skim; coldstart → native
|
|
217
|
+
cheap-tier probe; hollow-test → deletion spot-check; preflight → basic env
|
|
218
|
+
probe; behavior-diff → SHA/file-diff staleness only; skill-mining → sub-step
|
|
219
|
+
skipped. Each degradation is recorded in `coverage-manifest.md` and the gate
|
|
220
|
+
records — weaker is acceptable, silent is not.
|
|
221
|
+
|
|
222
|
+
## Model tiering
|
|
223
|
+
|
|
224
|
+
Tiered by **cost of detecting error**, not prestige (override per tier with
|
|
225
|
+
`--model-cheap/--model-mid/--model-frontier`):
|
|
226
|
+
|
|
227
|
+
| Tier | Phases | Why safe |
|
|
228
|
+
|---|---|---|
|
|
229
|
+
| cheap (Haiku-class) | D-1 scan, codemap draft, dependency inventory, coldstart probes | Mechanical; errors caught instantly and deterministically |
|
|
230
|
+
| mid (Sonnet-class) | D1 readers, D2 finders, D4 rail drafting | Adversarial verification + hollow-test catch their errors |
|
|
231
|
+
| frontier | D0 charter synthesis, D2 finding verdicts, D3 roadmap judgment | Errors here sail through every gate undetected |
|
|
232
|
+
|
|
233
|
+
## Success metrics
|
|
234
|
+
|
|
235
|
+
1. **Ticket survival rate** — % of backlog items accepted into execution
|
|
236
|
+
without reworking the ticket itself.
|
|
237
|
+
2. **Zero retained-functionality regressions** traced to rails gaps.
|
|
238
|
+
|
|
239
|
+
**Refused vanity metrics**: findings count, roadmap length, artifact volume.
|
|
240
|
+
A hundred plausible findings are worth less than ten verified ones; do-better's
|
|
241
|
+
verification stage exists to kill, and the kill count is diagnostic, not
|
|
242
|
+
shameful. If a do-better report impresses by sheer volume, it has failed.
|
|
243
|
+
|
|
244
|
+
## What's in here
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
do-better/
|
|
248
|
+
├── bin/cli.js # the CLI (npx do-better)
|
|
249
|
+
├── src/ # phase modules: scan, charter, comprehend, identify,
|
|
250
|
+
│ # roadmap, rail, refresh + llm/adlc/state/artifacts
|
|
251
|
+
├── do-better/
|
|
252
|
+
│ ├── SKILL.md # the cross-harness skill (the lifecycle itself)
|
|
253
|
+
│ └── references/
|
|
254
|
+
│ ├── taxonomy.md # the 8-dimension fixed floor + per-dimension finder charters
|
|
255
|
+
│ ├── refute-charter.md # D2 finder doctrine: adversarial refutation
|
|
256
|
+
│ ├── verification.md # reproduce-or-kill protocol + citation rules
|
|
257
|
+
│ ├── scoring.md # impact × confidence ÷ effort, sequencing rules
|
|
258
|
+
│ └── templates/ # charter, finding, roadmap, ticket, rail shapes
|
|
259
|
+
└── test/ # node --test; no network, fake LLM + fake adlc fixtures
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## License
|
|
263
|
+
|
|
264
|
+
MIT © 2026 Chris Williams ([@voodootikigod](https://github.com/voodootikigod)).
|
|
265
|
+
See [LICENSE](LICENSE).
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// do-better CLI — parse → configure → dispatch → exit codes.
|
|
3
|
+
// Exit codes: 0 success or clean human-gate pause; 1 operational error;
|
|
4
|
+
// 2 deterministic gate failure.
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import {
|
|
10
|
+
GateError,
|
|
11
|
+
HELP_TEXT,
|
|
12
|
+
OpError,
|
|
13
|
+
colors,
|
|
14
|
+
gitHeadSha,
|
|
15
|
+
log,
|
|
16
|
+
stderrLog,
|
|
17
|
+
makeExec,
|
|
18
|
+
nowIso,
|
|
19
|
+
parseArgs,
|
|
20
|
+
} from "../src/utils.js";
|
|
21
|
+
import {
|
|
22
|
+
beginRun,
|
|
23
|
+
defaultState,
|
|
24
|
+
finishRun,
|
|
25
|
+
loadState,
|
|
26
|
+
nextIncompletePhase,
|
|
27
|
+
saveState,
|
|
28
|
+
} from "../src/state.js";
|
|
29
|
+
|
|
30
|
+
const VERSION = "0.1.0";
|
|
31
|
+
|
|
32
|
+
// Printed when a human gate pauses the pipeline (D8) and the phase module did
|
|
33
|
+
// not supply its own instruction text in gate.detail.
|
|
34
|
+
const HUMAN_GATE_INSTRUCTIONS = {
|
|
35
|
+
charter:
|
|
36
|
+
"Charter drafted at .dobetter/charter.md — review/edit, then run: do-better charter --approve",
|
|
37
|
+
roadmap:
|
|
38
|
+
"Roadmap drafted — review .dobetter/ROADMAP.md + backlog/, then run: do-better roadmap --approve",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function importPhase(name) {
|
|
42
|
+
return import(new URL(`../src/${name}.js`, import.meta.url));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Machine-readable run stats for the --json envelope (H17): spend and the
|
|
46
|
+
// identify verified/killed counts, all already sitting in state at emit time.
|
|
47
|
+
function stateStats(state) {
|
|
48
|
+
const identify = state?.phases?.identify ?? {};
|
|
49
|
+
return {
|
|
50
|
+
spendUSD: Number.isFinite(state?.budget?.spentUSD) ? state.budget.spentUSD : 0,
|
|
51
|
+
verified: Number.isInteger(identify.verified) ? identify.verified : null,
|
|
52
|
+
killed: Number.isInteger(identify.killed) ? identify.killed : null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function makeAsk(env) {
|
|
57
|
+
if (env.DOBETTER_ANSWERS) {
|
|
58
|
+
const answersPath = path.resolve(env.DOBETTER_ANSWERS);
|
|
59
|
+
if (!fs.existsSync(answersPath)) {
|
|
60
|
+
throw new OpError(`DOBETTER_ANSWERS file not found: ${answersPath}`);
|
|
61
|
+
}
|
|
62
|
+
let answers;
|
|
63
|
+
try {
|
|
64
|
+
answers = JSON.parse(fs.readFileSync(answersPath, "utf8"));
|
|
65
|
+
} catch (e) {
|
|
66
|
+
throw new OpError(`DOBETTER_ANSWERS is not valid JSON: ${e.message}`);
|
|
67
|
+
}
|
|
68
|
+
if (!Array.isArray(answers) || answers.some((a) => typeof a !== "string")) {
|
|
69
|
+
throw new OpError("DOBETTER_ANSWERS must be a JSON array of strings");
|
|
70
|
+
}
|
|
71
|
+
let i = 0;
|
|
72
|
+
// Exhausted answers → empty string (accept the recommendation).
|
|
73
|
+
return { ask: async () => answers[i++] ?? "", close: () => {} };
|
|
74
|
+
}
|
|
75
|
+
if (process.stdin.isTTY) {
|
|
76
|
+
let rl = null;
|
|
77
|
+
const getRl = async () => {
|
|
78
|
+
if (!rl) {
|
|
79
|
+
const { createInterface } = await import("node:readline/promises");
|
|
80
|
+
rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
81
|
+
}
|
|
82
|
+
return rl;
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
ask: async (question) => (await getRl()).question(question),
|
|
86
|
+
close: () => rl?.close(),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return { ask: null, close: () => {} };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function main() {
|
|
93
|
+
const flags = parseArgs(process.argv.slice(2));
|
|
94
|
+
|
|
95
|
+
if (flags.help) {
|
|
96
|
+
console.log(HELP_TEXT);
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
if (!flags.command) {
|
|
100
|
+
if (flags.target !== ".") log.error(`Unknown command: ${flags.target}`);
|
|
101
|
+
console.log(HELP_TEXT);
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const root = path.resolve(flags.target);
|
|
106
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
|
|
107
|
+
throw new OpError(`Target directory not found: ${root}`);
|
|
108
|
+
}
|
|
109
|
+
const dotdir = path.join(root, ".dobetter");
|
|
110
|
+
const exec = makeExec();
|
|
111
|
+
|
|
112
|
+
let headSha = null;
|
|
113
|
+
try {
|
|
114
|
+
headSha = gitHeadSha(root, exec);
|
|
115
|
+
} catch {
|
|
116
|
+
headSha = null; // scan enforces the git-repo requirement with a clear message
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let { state } = loadState(dotdir);
|
|
120
|
+
if (!state) state = defaultState({ headSha, now: nowIso() });
|
|
121
|
+
|
|
122
|
+
// Service layers (dynamic so --help / usage errors never need them).
|
|
123
|
+
const { createLLM } = await importPhase("llm");
|
|
124
|
+
const { locateAdlc } = await importPhase("adlc");
|
|
125
|
+
const llm = createLLM({ flags, state, env: process.env });
|
|
126
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
127
|
+
const adlc = locateAdlc({ env: process.env, packageRoot, targetRoot: root });
|
|
128
|
+
const { ask, close: closeAsk } = makeAsk(process.env);
|
|
129
|
+
|
|
130
|
+
if (!flags.json) console.log(colors.dim(`do-better v${VERSION} · ${flags.command} · ${root}`));
|
|
131
|
+
|
|
132
|
+
// Under --json, stdout must carry ONLY the final JSON envelope — route all
|
|
133
|
+
// phase-module progress (ctx.log.phase/step/…) to stderr (H17).
|
|
134
|
+
const activeLog = flags.json ? stderrLog : log;
|
|
135
|
+
let ctx = { root, dotdir, state, llm, adlc, flags, log: activeLog, now: nowIso, exec, ask };
|
|
136
|
+
let runId = null;
|
|
137
|
+
let paused = false;
|
|
138
|
+
let lastSummary = "";
|
|
139
|
+
|
|
140
|
+
const runPhase = async (name, fnName = "run") => {
|
|
141
|
+
const mod = await importPhase(name);
|
|
142
|
+
const fn = mod[fnName];
|
|
143
|
+
if (typeof fn !== "function") {
|
|
144
|
+
throw new OpError(`Phase module "${name}" does not export ${fnName}()`);
|
|
145
|
+
}
|
|
146
|
+
const res = await fn(ctx);
|
|
147
|
+
state = res.state;
|
|
148
|
+
ctx = { ...ctx, state };
|
|
149
|
+
saveState(dotdir, state);
|
|
150
|
+
if (res.summary) {
|
|
151
|
+
lastSummary = res.summary;
|
|
152
|
+
if (!flags.json) log.info(res.summary);
|
|
153
|
+
}
|
|
154
|
+
if (res.gate && res.gate.human && !res.gate.passed) {
|
|
155
|
+
paused = true;
|
|
156
|
+
const instructions = res.gate.detail || HUMAN_GATE_INSTRUCTIONS[res.gate.name] || "";
|
|
157
|
+
if (!flags.json && instructions) log.warn(instructions);
|
|
158
|
+
}
|
|
159
|
+
return res;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
({ state, runId } = beginRun(state, {
|
|
164
|
+
command: flags.command,
|
|
165
|
+
provider: llm.provider ?? null,
|
|
166
|
+
headSha,
|
|
167
|
+
now: nowIso(),
|
|
168
|
+
}));
|
|
169
|
+
ctx = { ...ctx, state };
|
|
170
|
+
|
|
171
|
+
switch (flags.command) {
|
|
172
|
+
case "scan":
|
|
173
|
+
await runPhase("scan");
|
|
174
|
+
break;
|
|
175
|
+
case "charter":
|
|
176
|
+
await runPhase("charter", flags.approve ? "approve" : "run");
|
|
177
|
+
break;
|
|
178
|
+
case "audit":
|
|
179
|
+
await runPhase("comprehend");
|
|
180
|
+
if (!paused) await runPhase("identify");
|
|
181
|
+
break;
|
|
182
|
+
case "roadmap":
|
|
183
|
+
await runPhase("roadmap", flags.approve ? "approve" : "run");
|
|
184
|
+
break;
|
|
185
|
+
case "rail":
|
|
186
|
+
await runPhase("rail");
|
|
187
|
+
break;
|
|
188
|
+
case "refresh":
|
|
189
|
+
await runPhase("refresh");
|
|
190
|
+
break;
|
|
191
|
+
case "run": {
|
|
192
|
+
let next;
|
|
193
|
+
while (!paused && (next = nextIncompletePhase(state)) !== null) {
|
|
194
|
+
await runPhase(next);
|
|
195
|
+
if (paused) break;
|
|
196
|
+
if (nextIncompletePhase(state) === next) {
|
|
197
|
+
throw new OpError(`Phase "${next}" did not complete; cannot continue the pipeline.`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (!paused && nextIncompletePhase(state) === null && !flags.json) {
|
|
201
|
+
log.success(
|
|
202
|
+
"All phases complete. Each ticket in .dobetter/backlog/ is ready for ADLC P3/P4 intake.",
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
default:
|
|
208
|
+
throw new OpError(`Unknown command: ${flags.command}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
state = finishRun(state, runId, { now: nowIso(), ok: true });
|
|
212
|
+
saveState(dotdir, state);
|
|
213
|
+
if (flags.json) {
|
|
214
|
+
console.log(JSON.stringify({
|
|
215
|
+
command: flags.command, ok: true, paused, summary: lastSummary,
|
|
216
|
+
artifactsDir: path.relative(process.cwd(), dotdir) || ".dobetter",
|
|
217
|
+
...stateStats(state),
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
return 0;
|
|
221
|
+
} catch (err) {
|
|
222
|
+
if (err && err.state) state = err.state;
|
|
223
|
+
if (runId) {
|
|
224
|
+
try {
|
|
225
|
+
state = finishRun(state, runId, { now: nowIso(), ok: false });
|
|
226
|
+
} catch {
|
|
227
|
+
/* keep the original error */
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
saveState(dotdir, state);
|
|
232
|
+
} catch {
|
|
233
|
+
/* never mask the original error with a save failure */
|
|
234
|
+
}
|
|
235
|
+
// A CI wrapper gets a parseable failure envelope on stdout (H17); the human
|
|
236
|
+
// message still prints to stderr via the top-level handler. Exit code is set
|
|
237
|
+
// there too, so the two stay consistent.
|
|
238
|
+
if (flags.json) {
|
|
239
|
+
const error = err instanceof GateError
|
|
240
|
+
? { kind: "gate", gate: err.gate, detail: err.detail }
|
|
241
|
+
: { kind: err instanceof OpError ? "operational" : "error", message: err instanceof Error ? err.message : String(err) };
|
|
242
|
+
console.log(JSON.stringify({ command: flags.command, ok: false, error, ...stateStats(state) }));
|
|
243
|
+
}
|
|
244
|
+
throw err;
|
|
245
|
+
} finally {
|
|
246
|
+
closeAsk();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
process.exitCode = await main();
|
|
252
|
+
} catch (err) {
|
|
253
|
+
if (err instanceof GateError) {
|
|
254
|
+
log.error(`Gate failed: ${err.gate} — ${err.detail}`);
|
|
255
|
+
if (process.env.DOBETTER_DEBUG) log.errorTrace(err);
|
|
256
|
+
process.exitCode = 2;
|
|
257
|
+
} else {
|
|
258
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
259
|
+
if (process.env.DOBETTER_DEBUG) log.errorTrace(err);
|
|
260
|
+
process.exitCode = typeof err?.exitCode === "number" ? err.exitCode : 1;
|
|
261
|
+
}
|
|
262
|
+
}
|