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 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
+ }