doctrina-cli 0.21.0 → 0.22.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/README.md +6 -6
- package/package.json +1 -1
- package/src/commands/change.js +111 -14
- package/src/commands/clarify.js +2 -2
- package/src/commands/close.js +1 -1
- package/src/commands/coverage.js +69 -6
- package/src/commands/doctor.js +62 -4
- package/src/commands/hooks.js +56 -11
- package/src/commands/index-rebuild.js +47 -1
- package/src/commands/init.js +51 -7
- package/src/commands/metrics.js +1 -1
- package/src/commands/onboard.js +113 -0
- package/src/commands/review.js +1 -1
- package/src/commands/search.js +2 -2
- package/src/commands/templates.js +2 -1
- package/src/commands/triage.js +16 -1
- package/src/commands/ui.js +512 -0
- package/src/commands/upgrade.js +46 -6
- package/src/commands/verify.js +3 -3
- package/src/commands/why.js +1 -1
- package/src/commands/work.js +53 -40
- package/src/commands/workspace.js +16 -3
- package/src/index.js +9 -0
- package/src/lib/actions.js +19 -0
- package/src/lib/adapters.js +23 -3
- package/src/lib/agent-bridge.js +400 -0
- package/src/lib/applied-deltas.js +82 -0
- package/src/lib/args.js +1 -1
- package/src/lib/change-numbers.js +115 -0
- package/src/lib/change-ops.js +2 -2
- package/src/lib/clarify-scan.js +3 -3
- package/src/lib/code-inventory.js +22 -5
- package/src/lib/commands.js +50 -4
- package/src/lib/config.js +27 -8
- package/src/lib/conflict-markers.js +61 -0
- package/src/lib/console-model.js +668 -0
- package/src/lib/console-prefs.js +67 -0
- package/src/lib/console-room.js +201 -0
- package/src/lib/console-state.js +150 -0
- package/src/lib/console-view.js +557 -0
- package/src/lib/coverage-model.js +29 -6
- package/src/lib/delivery.js +2 -2
- package/src/lib/docs-impact.js +1 -1
- package/src/lib/fs-ops.js +80 -12
- package/src/lib/gates.js +7 -1
- package/src/lib/git.js +18 -0
- package/src/lib/index-json.js +10 -1
- package/src/lib/index-merge.js +90 -0
- package/src/lib/install.js +80 -0
- package/src/lib/intake-model.js +4 -1
- package/src/lib/ledger.js +2 -3
- package/src/lib/lock.js +58 -0
- package/src/lib/merge-setup.js +55 -0
- package/src/lib/metrics-model.js +2 -2
- package/src/lib/onboard.js +197 -0
- package/src/lib/preset.js +177 -0
- package/src/lib/prompt.js +1 -1
- package/src/lib/properties.js +149 -0
- package/src/lib/runtime.js +19 -7
- package/src/lib/scan.js +13 -8
- package/src/lib/spec-ops.js +2 -2
- package/src/lib/task-card.js +1 -1
- package/src/lib/templates-model.js +3 -2
- package/src/lib/triage-model.js +75 -0
- package/src/lib/validation-model.js +23 -5
- package/src/lib/views.js +2 -1
- package/src/lib/workspace-agents.js +2 -2
- package/src/lib/workspace-tools.js +41 -1
- package/src/lib/workspace.js +1 -1
- package/templates/AGENTS.md.template +8 -4
- package/templates/README.md +2 -1
- package/templates/adapters/claude/.claude/commands/doctrina-onboard.md.template +15 -0
- package/templates/adapters/cursor/.cursor/rules/00-doctrina.mdc.template +2 -0
- package/templates/adapters/cursor/.cursor/skills/doctrina-onboard/SKILL.md.template +18 -0
- package/templates/adapters/kiro/README.md +22 -0
|
@@ -10,8 +10,10 @@ import { c } from "../lib/colors.js";
|
|
|
10
10
|
import { suggest } from "../lib/suggest.js";
|
|
11
11
|
import { notADoctrinaProject } from "../lib/exit-codes.js";
|
|
12
12
|
import { planSkillExport, applySkillExport } from "../lib/skill-export.js";
|
|
13
|
+
import { mergeIndex } from "../lib/index-merge.js";
|
|
14
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
13
15
|
|
|
14
|
-
const SUBCOMMANDS = ["rebuild"];
|
|
16
|
+
const SUBCOMMANDS = ["rebuild", "merge"];
|
|
15
17
|
|
|
16
18
|
// Flags this command accepts. Declared HERE, with the command, so
|
|
17
19
|
// adding a command never requires editing the entrypoint — the gap that
|
|
@@ -20,6 +22,7 @@ export const flags = { boolean: ["json", "check", "staged"], string: [] };
|
|
|
20
22
|
|
|
21
23
|
export async function run(positional, flags) {
|
|
22
24
|
const sub = positional[0];
|
|
25
|
+
if (sub === "merge") return merge(positional.slice(1));
|
|
23
26
|
if (sub !== "rebuild") {
|
|
24
27
|
console.error(c.red("error:") + ` unknown index subcommand "${sub ?? ""}"`);
|
|
25
28
|
const guess = suggest(sub, SUBCOMMANDS);
|
|
@@ -98,6 +101,43 @@ function rebuild(flags) {
|
|
|
98
101
|
return 0;
|
|
99
102
|
}
|
|
100
103
|
|
|
104
|
+
// The merge driver git calls for `.doctrina/index.json` (change 0242):
|
|
105
|
+
// `doctrina index merge %O %A %B`. It writes the merged index over <ours>
|
|
106
|
+
// and exits 0, which is how a driver tells git there is nothing to resolve.
|
|
107
|
+
// A side that is not JSON is the one case it leaves to git, with exit 1.
|
|
108
|
+
function merge(args) {
|
|
109
|
+
const [basePath, oursPath, theirsPath] = args;
|
|
110
|
+
if (!basePath || !oursPath || !theirsPath) {
|
|
111
|
+
console.error(c.red("error:") + " index merge takes three files: <base> <ours> <theirs>");
|
|
112
|
+
console.error(c.gray("hint: ") + "git calls it as a merge driver — `doctrina hooks install` sets it up");
|
|
113
|
+
return 2;
|
|
114
|
+
}
|
|
115
|
+
const load = (/** @type {string} */ file, /** @type {string} */ side) => {
|
|
116
|
+
let text = "";
|
|
117
|
+
try {
|
|
118
|
+
text = readFileSync(file, "utf8");
|
|
119
|
+
} catch {
|
|
120
|
+
if (side === "base") return {};
|
|
121
|
+
throw new Error(`cannot read the ${side} index at ${file}`);
|
|
122
|
+
}
|
|
123
|
+
if (text.trim() === "") return {};
|
|
124
|
+
try {
|
|
125
|
+
return JSON.parse(text);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
throw new Error(`the ${side} index is not valid JSON (${err.message})`);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
let merged;
|
|
131
|
+
try {
|
|
132
|
+
merged = mergeIndex(load(basePath, "base"), load(oursPath, "ours"), load(theirsPath, "theirs"));
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.error(c.red("error:") + ` ${err.message} — left for a manual merge; \`doctrina index rebuild\` derives it from the tree afterwards`);
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
writeFileSync(oursPath, JSON.stringify(merged, null, 2) + "\n");
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
101
141
|
// The drift check, asked of what git will record. `index.json` is derived
|
|
102
142
|
// from the WHOLE tree, so it cannot describe a subset of it: stage part of
|
|
103
143
|
// `.doctrina/` and the index riding along names artifacts the commit does
|
|
@@ -135,6 +175,12 @@ export const help = `
|
|
|
135
175
|
Usage: doctrina index <subcommand>
|
|
136
176
|
|
|
137
177
|
Subcommands:
|
|
178
|
+
merge <base> <ours> <theirs>
|
|
179
|
+
The merge driver git runs for index.json once
|
|
180
|
+
\`doctrina hooks install\` has set it up: two branches
|
|
181
|
+
that each closed a change merge as lists of artifacts,
|
|
182
|
+
so the file never stops a merge with conflict markers.
|
|
183
|
+
Writes the result over <ours>.
|
|
138
184
|
rebuild Regenerate .doctrina/index.json from the artifacts on
|
|
139
185
|
disk (spec headers, ADR headers, change proposals,
|
|
140
186
|
archive folders, skill frontmatter). The files are the
|
package/src/commands/init.js
CHANGED
|
@@ -5,10 +5,12 @@ import process from "node:process";
|
|
|
5
5
|
import { exists, isDir, isFile, read, relPath, write } from "../lib/fs-ops.js";
|
|
6
6
|
import { locateTemplatesDir, loadTemplateTree, materialiseEntry, substitute } from "../lib/templates.js";
|
|
7
7
|
import {
|
|
8
|
-
surfaceBlock, findSurfaceBlock, agentChangelogBlock, findAgentChangelogBlock,
|
|
8
|
+
surfaceBlock, findSurfaceBlock, agentChangelogBlock, findAgentChangelogBlock, SURFACE_PROFILES,
|
|
9
9
|
} from "../lib/commands.js";
|
|
10
10
|
import * as idx from "../lib/index-json.js";
|
|
11
11
|
import { scaffoldConfig } from "../lib/config.js";
|
|
12
|
+
import { applyPreset, describePreset, planPreset, readPreset } from "../lib/preset.js";
|
|
13
|
+
import { deriveIndex } from "../lib/scan.js";
|
|
12
14
|
import { today } from "../lib/dates.js";
|
|
13
15
|
import { cliVersion } from "../lib/version.js";
|
|
14
16
|
import { flagBool, flagString, flagGivenWithoutValue } from "../lib/args.js";
|
|
@@ -31,11 +33,12 @@ const SUPPORTED_AGENTS = [
|
|
|
31
33
|
"devin",
|
|
32
34
|
"factory",
|
|
33
35
|
"jules",
|
|
36
|
+
"kiro",
|
|
34
37
|
];
|
|
35
38
|
|
|
36
39
|
export const flags = {
|
|
37
40
|
boolean: ["json", "force", "non-interactive", "overwrite-content"],
|
|
38
|
-
string: ["project-name", "project-description", "agent", "intake", "intake-text", "from", "date"],
|
|
41
|
+
string: ["project-name", "project-description", "agent", "intake", "intake-text", "from", "date", "surface", "preset"],
|
|
39
42
|
};
|
|
40
43
|
|
|
41
44
|
export async function run(_positional, flags) {
|
|
@@ -142,6 +145,30 @@ export async function run(_positional, flags) {
|
|
|
142
145
|
}
|
|
143
146
|
}
|
|
144
147
|
|
|
148
|
+
// Which commands AGENTS.md lists (change 0251). A new project is born with
|
|
149
|
+
// the session loop only — `core` — and the rest one `--help` away; the
|
|
150
|
+
// choice is written into config.json, where `upgrade` reads it.
|
|
151
|
+
// The organisation's preset (change 0253), read BEFORE anything is
|
|
152
|
+
// written: a folder that is not one stops the init with nothing scaffolded.
|
|
153
|
+
let preset = null;
|
|
154
|
+
if (flags.has("preset")) {
|
|
155
|
+
try {
|
|
156
|
+
const dir = flagString(flags, "preset");
|
|
157
|
+
if (!dir) throw new Error("--preset needs the preset's folder");
|
|
158
|
+
preset = readPreset(dir);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.error(c.red("error:") + ` ${/** @type {Error} */ (err).message}`);
|
|
161
|
+
return 2;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// The preset's surface is the default when the flag names none: the block
|
|
165
|
+
// is generated once, from the profile the project will keep.
|
|
166
|
+
const surface = flagString(flags, "surface") ?? preset?.config.surface ?? "core";
|
|
167
|
+
if (!SURFACE_PROFILES.includes(surface) || flagGivenWithoutValue(flags, "surface")) {
|
|
168
|
+
console.error(c.red("error:") + ` --surface takes ${SURFACE_PROFILES.join(" or ")}${flagGivenWithoutValue(flags, "surface") ? "" : ` (got "${surface}")`}`);
|
|
169
|
+
return 2;
|
|
170
|
+
}
|
|
171
|
+
|
|
145
172
|
let agentSelector = flagString(flags, "agent");
|
|
146
173
|
// Interactive wizard step: when no --agent was given and we are on a real
|
|
147
174
|
// terminal, offer the adapter install instead of silently skipping it —
|
|
@@ -202,7 +229,7 @@ export async function run(_positional, flags) {
|
|
|
202
229
|
// if the static template lags a release (ADR 0015).
|
|
203
230
|
const agentsTemplatePath = path.join(templatesDir, "AGENTS.md.template");
|
|
204
231
|
const agentsTemplateBody = read(agentsTemplatePath);
|
|
205
|
-
const projectAgents = refreshSurfaceBlock(substitute(agentsTemplateBody, tokens));
|
|
232
|
+
const projectAgents = refreshSurfaceBlock(substitute(agentsTemplateBody, tokens), surface);
|
|
206
233
|
|
|
207
234
|
// --force used to mean "overwrite everything", which made it a content
|
|
208
235
|
// shredder: the only way to add an adapter to an existing project was
|
|
@@ -285,11 +312,21 @@ export async function run(_positional, flags) {
|
|
|
285
312
|
{
|
|
286
313
|
const dest = path.join(doctrinaDir, "config.json");
|
|
287
314
|
if (!isFile(dest) || force) {
|
|
288
|
-
write(dest, scaffoldConfig(), { force });
|
|
315
|
+
write(dest, scaffoldConfig({ surface }), { force });
|
|
289
316
|
console.log(c.green("created") + ` ${relPath(projectRoot, dest)}`);
|
|
290
317
|
}
|
|
291
318
|
}
|
|
292
319
|
|
|
320
|
+
// The preset, over the scaffold: what it adds is indexed with the rest.
|
|
321
|
+
if (preset) {
|
|
322
|
+
const plan = planPreset(projectRoot, preset);
|
|
323
|
+
applyPreset(projectRoot, preset, plan);
|
|
324
|
+
for (const line of describePreset(preset, plan, true)) console.log(line);
|
|
325
|
+
const derived = deriveIndex(projectRoot, idx.load(projectRoot));
|
|
326
|
+
derived.last_updated = date;
|
|
327
|
+
idx.save(projectRoot, derived);
|
|
328
|
+
}
|
|
329
|
+
|
|
293
330
|
// Adapters
|
|
294
331
|
if (agentSelector) {
|
|
295
332
|
const agents = agentSelector === "all" ? SUPPORTED_AGENTS : [agentSelector];
|
|
@@ -384,10 +421,10 @@ function normaliseScaffold(text) {
|
|
|
384
421
|
// Replace the template's doctrina:surface span with the one generated from
|
|
385
422
|
// the running CLI's catalog; pass the text through untouched when the
|
|
386
423
|
// template carries no markers (older custom templates).
|
|
387
|
-
function refreshSurfaceBlock(text) {
|
|
424
|
+
function refreshSurfaceBlock(text, profile = "full") {
|
|
388
425
|
const block = findSurfaceBlock(text);
|
|
389
426
|
if (!block) return text;
|
|
390
|
-
let out = text.slice(0, block.start) + surfaceBlock() + text.slice(block.end);
|
|
427
|
+
let out = text.slice(0, block.start) + surfaceBlock(profile) + text.slice(block.end);
|
|
391
428
|
|
|
392
429
|
// The agent-facing changelog is version-specific, so it is GENERATED at
|
|
393
430
|
// init like the surface block — not carried in the template. Writing it
|
|
@@ -453,8 +490,15 @@ Scaffold AGENTS.md and the .doctrina/ skeleton in the current directory.
|
|
|
453
490
|
Options:
|
|
454
491
|
--project-name <name> Override the project name (default: basename of cwd)
|
|
455
492
|
--project-description <text> One-sentence description
|
|
493
|
+
--surface core|full Which commands AGENTS.md lists: core (default) is the session
|
|
494
|
+
loop, with the rest deferred to \`doctrina --help\`; full lists
|
|
495
|
+
every command. Stored in .doctrina/config.json.
|
|
496
|
+
--preset <dir> Lay an organisation's preset over the scaffold: a folder with
|
|
497
|
+
preset.json (name, config) and optional templates/, decisions/,
|
|
498
|
+
skills/ and verify.json. Additive — see \`doctrina upgrade --preset\`
|
|
499
|
+
for a project that already exists.
|
|
456
500
|
--agent <name> Install adapter for one agent
|
|
457
|
-
(claude|codex|cursor|copilot|gemini|aider|windsurf|continue|amp|devin|factory|jules|all)
|
|
501
|
+
(claude|codex|cursor|copilot|gemini|aider|windsurf|continue|amp|devin|factory|jules|kiro|all)
|
|
458
502
|
--intake <file> Full project description; stored verbatim at .doctrina/intake.md,
|
|
459
503
|
used to derive the one-line description when absent, and the
|
|
460
504
|
bootstrap playbook is printed inline — no second command
|
package/src/commands/metrics.js
CHANGED
|
@@ -54,7 +54,7 @@ export async function run(_positional, flags) {
|
|
|
54
54
|
if (flagBool(flags, "trend", false)) return printTrend(projectRoot);
|
|
55
55
|
// The window is the invocation's: refused the same with or without history.
|
|
56
56
|
const sinceRaw = flagString(flags, "since", "90");
|
|
57
|
-
const since = sinceWindow(sinceRaw);
|
|
57
|
+
const since = sinceWindow(sinceRaw ?? "90");
|
|
58
58
|
if (since === null) {
|
|
59
59
|
console.error(c.red("error:") + ` --since expects a day count, a date (YYYY-MM-DD) or "<n> <unit>s ago", got "${sinceRaw}"`);
|
|
60
60
|
console.error(c.gray("hint: ") + "examples: --since 30 · --since 2026-01-01 · --since \"3 months ago\"");
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { flagBool } from "../lib/args.js";
|
|
4
|
+
import { c } from "../lib/colors.js";
|
|
5
|
+
import { emitJson } from "../lib/json-out.js";
|
|
6
|
+
import { EXIT } from "../lib/exit-codes.js";
|
|
7
|
+
import { TRACKS } from "../lib/triage-model.js";
|
|
8
|
+
import { firstTaskCandidates, onboardState, onboardSteps, tourLines } from "../lib/onboard.js";
|
|
9
|
+
|
|
10
|
+
// `onboard` walks the whole cycle once, on this repository (change 0256).
|
|
11
|
+
// It reads where the project stands and prints the walk; the agent is the
|
|
12
|
+
// guide and the human the one being shown. It writes nothing and works
|
|
13
|
+
// before `init`, because the first step is the install.
|
|
14
|
+
|
|
15
|
+
export const jsonNative = true;
|
|
16
|
+
|
|
17
|
+
export const flags = { boolean: ["json"], string: [] };
|
|
18
|
+
|
|
19
|
+
export async function run(_positional, cmdFlags) {
|
|
20
|
+
const projectRoot = process.cwd();
|
|
21
|
+
const state = onboardState(projectRoot);
|
|
22
|
+
const { steps, current } = onboardSteps(state);
|
|
23
|
+
const wantsCandidates = current === "first-task" && state.installed;
|
|
24
|
+
const candidates = wantsCandidates ? firstTaskCandidates(projectRoot) : [];
|
|
25
|
+
|
|
26
|
+
if (flagBool(cmdFlags, "json", false)) {
|
|
27
|
+
emitJson("onboard", {
|
|
28
|
+
current,
|
|
29
|
+
steps: steps.map((s) => ({ id: s.id, title: s.title, done: s.done, run: s.run, ask: s.ask || null })),
|
|
30
|
+
candidates,
|
|
31
|
+
closed: state.closed,
|
|
32
|
+
});
|
|
33
|
+
return EXIT.OK;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(c.bold("Doctrina onboarding") + c.gray(" — the whole cycle once, on this repository"));
|
|
37
|
+
console.log("");
|
|
38
|
+
console.log("You are the guide. Do the step marked →, say in one sentence what it did,");
|
|
39
|
+
console.log("stop where it says ASK, then run `doctrina onboard` again: it reads the tree.");
|
|
40
|
+
console.log("");
|
|
41
|
+
steps.forEach((s, i) => {
|
|
42
|
+
const mark = s.done ? c.green("✓") : (s.id === current ? c.cyan("→") : " ");
|
|
43
|
+
console.log(` ${mark} ${i + 1}. ${s.done ? s.title.padEnd(31) + c.gray(s.evidence) : s.title}`);
|
|
44
|
+
});
|
|
45
|
+
console.log("");
|
|
46
|
+
|
|
47
|
+
if (current === null) {
|
|
48
|
+
console.log(c.bold("The cycle has run here") + c.gray(` — ${state.closed} change${state.closed === 1 ? "" : "s"} closed. For someone new to the project:`));
|
|
49
|
+
console.log("");
|
|
50
|
+
for (const line of tourLines(state)) console.log(` ${line}`);
|
|
51
|
+
console.log("");
|
|
52
|
+
printTracks();
|
|
53
|
+
return EXIT.OK;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const at = steps.findIndex((s) => s.id === current);
|
|
57
|
+
const step = steps[at];
|
|
58
|
+
console.log(c.bold(`Step ${at + 1} — ${step.title}`));
|
|
59
|
+
console.log(` ${c.gray("Tell the human:")} ${step.say}`);
|
|
60
|
+
if (wantsCandidates) {
|
|
61
|
+
console.log("");
|
|
62
|
+
if (candidates.length === 0) {
|
|
63
|
+
console.log(c.gray(" No candidate stands out in this repository (every criterion cites proof, no TODO in the code)."));
|
|
64
|
+
} else {
|
|
65
|
+
console.log(" Candidates read from this repository:");
|
|
66
|
+
for (const cand of candidates) console.log(` - ${cand.where.padEnd(24)} ${cand.text}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const after_ = "askAfter" in step && step.askAfter === true;
|
|
70
|
+
if (step.ask && !after_) console.log(` ${c.yellow("ASK")} the human ${step.ask}`);
|
|
71
|
+
console.log(step.ask && !after_ ? " Then run:" : " Run:");
|
|
72
|
+
for (const cmd of step.run) console.log(` ${c.cyan(cmd)}`);
|
|
73
|
+
if (step.ask && after_) console.log(` Then ${c.yellow("ASK")} the human ${step.ask}`);
|
|
74
|
+
if (current === "first-task") {
|
|
75
|
+
console.log("");
|
|
76
|
+
printTracks();
|
|
77
|
+
}
|
|
78
|
+
const after = steps[at + 1];
|
|
79
|
+
if (after) console.log(c.gray(`\nAfter it: step ${at + 2}, ${after.title.toLowerCase()} — \`doctrina onboard\` shows it.`));
|
|
80
|
+
return EXIT.OK;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function printTracks() {
|
|
84
|
+
console.log(c.gray("Not every edit is a change — the tracks, by what a request changes:"));
|
|
85
|
+
for (const t of TRACKS) console.log(c.gray(` ${t.id.padEnd(7)} ${t.changes}`));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const help = `
|
|
89
|
+
Usage: doctrina onboard [--json]
|
|
90
|
+
|
|
91
|
+
Walk the whole Doctrina cycle once, on this repository. Prints the six
|
|
92
|
+
steps — install, describe the project, pick a first task and open it,
|
|
93
|
+
write the delta and the tasks, implement and prove, close — each marked
|
|
94
|
+
done when the tree says so, and for the first one that is not: what to
|
|
95
|
+
tell the human, where to stop and ask, and the commands to run. Run it
|
|
96
|
+
again after each step; it stores nothing and reads the tree every time.
|
|
97
|
+
|
|
98
|
+
At "pick a first task" it lists candidates read from the repository: an
|
|
99
|
+
acceptance criterion that cites no proof (or a proof that is missing),
|
|
100
|
+
then a TODO or FIXME left in the code. They are suggestions; the human
|
|
101
|
+
chooses, or names a task of their own.
|
|
102
|
+
|
|
103
|
+
In a project that already closed a change, every step is done and it
|
|
104
|
+
prints the tour for someone new to the project instead: the commands to
|
|
105
|
+
read the project with, and the last closed change as a worked example.
|
|
106
|
+
|
|
107
|
+
It works before \`doctrina init\` (the first step is the install) and
|
|
108
|
+
writes nothing.
|
|
109
|
+
|
|
110
|
+
Options:
|
|
111
|
+
--json Machine-readable: the steps with their state, the current
|
|
112
|
+
step, and the first-task candidates
|
|
113
|
+
`;
|
package/src/commands/review.js
CHANGED
|
@@ -116,7 +116,7 @@ export async function run(_positional, flags) {
|
|
|
116
116
|
// capability whose spec.md itself changed.
|
|
117
117
|
const specChanged = new Set(
|
|
118
118
|
all.filter((f) => /(^|\/)\.doctrina\/specs\/[^/]+\/spec\.md$/.test(f.replace(/\\/g, "/")))
|
|
119
|
-
.map((f) => f.replace(/\\/g, "/").match(/\.doctrina\/specs\/([^/]+)\/spec\.md$/)[1]),
|
|
119
|
+
.map((f) => f.replace(/\\/g, "/").match(/\.doctrina\/specs\/([^/]+)\/spec\.md$/)?.[1] ?? ""),
|
|
120
120
|
);
|
|
121
121
|
const ranked = rankCapabilitiesByDiff(projectRoot, sourceFiles, { limit: Infinity, pending });
|
|
122
122
|
const touched = new Set([...specChanged, ...ranked.map((m) => m.id)]);
|
package/src/commands/search.js
CHANGED
|
@@ -59,8 +59,8 @@ export async function run(positional, flags) {
|
|
|
59
59
|
for (const cat of categories) {
|
|
60
60
|
const files = cat.file
|
|
61
61
|
? [path.join(projectRoot, cat.file)].filter(exists)
|
|
62
|
-
: isDir(path.join(projectRoot, cat.dir))
|
|
63
|
-
? walk(path.join(projectRoot, cat.dir)).filter((f) => f.endsWith(".md"))
|
|
62
|
+
: isDir(path.join(projectRoot, cat.dir ?? ""))
|
|
63
|
+
? walk(path.join(projectRoot, cat.dir ?? "")).filter((f) => f.endsWith(".md"))
|
|
64
64
|
: [];
|
|
65
65
|
|
|
66
66
|
const findings = [];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { loadConfig } from "../lib/config.js";
|
|
3
4
|
import process from "node:process";
|
|
4
5
|
import { exists, isDir, isFile, lineCount, read, relPath, walk, write } from "../lib/fs-ops.js";
|
|
5
6
|
import { locateTemplatesDir, listResolvedTemplates } from "../lib/templates.js";
|
|
@@ -71,7 +72,7 @@ function updateTemplates(flags) {
|
|
|
71
72
|
if (isFile(agentsMdPath)) {
|
|
72
73
|
const text = read(agentsMdPath);
|
|
73
74
|
const block = findSurfaceBlock(text);
|
|
74
|
-
const fresh = surfaceBlock();
|
|
75
|
+
const fresh = surfaceBlock(loadConfig(projectRoot).surface);
|
|
75
76
|
if (block) {
|
|
76
77
|
if (normalizeBlock(text.slice(block.start, block.end)) !== normalizeBlock(fresh)) {
|
|
77
78
|
plan.push({
|
package/src/commands/triage.js
CHANGED
|
@@ -4,7 +4,7 @@ import process from "node:process";
|
|
|
4
4
|
import { exists } from "../lib/fs-ops.js";
|
|
5
5
|
import { flagBool } from "../lib/args.js";
|
|
6
6
|
import { c } from "../lib/colors.js";
|
|
7
|
-
import { classify } from "../lib/triage-model.js";
|
|
7
|
+
import { classify, directSignals, trackOf } from "../lib/triage-model.js";
|
|
8
8
|
|
|
9
9
|
// The classifier is shared with `work`; see lib/triage-model.js.
|
|
10
10
|
export { classify } from "../lib/triage-model.js";
|
|
@@ -67,6 +67,7 @@ export async function run(positional, cmdFlags) {
|
|
|
67
67
|
emitJson("triage", {
|
|
68
68
|
lane: verdict?.lane ?? null,
|
|
69
69
|
confident: verdict?.confident ?? null,
|
|
70
|
+
track: verdict ? trackOf(prompt, verdict) : null,
|
|
70
71
|
contracts: runtime.contracts,
|
|
71
72
|
declared: runtime.declared,
|
|
72
73
|
findings,
|
|
@@ -87,6 +88,20 @@ function renderLane(prompt, verdict) {
|
|
|
87
88
|
runtime: c.yellow("RUNTIME"),
|
|
88
89
|
chore: c.cyan("CHORE"),
|
|
89
90
|
};
|
|
91
|
+
// The track first (change 0255): an edit that changes nothing a spec says
|
|
92
|
+
// needs neither a lane's playbook nor a change.
|
|
93
|
+
const direct = directSignals(prompt, verdict);
|
|
94
|
+
if (direct.length > 0) {
|
|
95
|
+
console.log(c.bold("Track") + c.gray(" — deterministic term match; a hint, not a decision"));
|
|
96
|
+
console.log("");
|
|
97
|
+
console.log(` ${c.green("DIRECT")}${c.gray(" nothing a spec says changes")}`);
|
|
98
|
+
console.log(c.gray(` signals: ${[...new Set(direct)].slice(0, 6).join(", ")}`));
|
|
99
|
+
console.log("");
|
|
100
|
+
console.log(`Edit it, run ${c.cyan("doctrina verify")}, and commit: no change is opened.`);
|
|
101
|
+
console.log(c.gray(" If it does change behaviour: ") + c.cyan(`doctrina work --force "${truncate(prompt)}"`));
|
|
102
|
+
console.log("");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
90
105
|
console.log(c.bold("Lane") + c.gray(" — deterministic term match; a hint, not a decision"));
|
|
91
106
|
console.log("");
|
|
92
107
|
console.log(` ${LABEL[verdict.lane]}${verdict.confident ? "" : c.gray(" (weak signal — read the playbook and override if it is wrong)")}`);
|