doctrina-cli 0.15.1 → 0.17.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 +27 -72
- package/package.json +3 -3
- package/src/commands/adapter.js +27 -3
- package/src/commands/analyze.js +16 -212
- package/src/commands/change.js +144 -267
- package/src/commands/ci.js +182 -0
- package/src/commands/clarify.js +65 -21
- package/src/commands/close.js +349 -45
- package/src/commands/completion.js +8 -2
- package/src/commands/context.js +284 -111
- package/src/commands/contract.js +95 -75
- package/src/commands/coverage.js +58 -273
- package/src/commands/decision.js +66 -7
- package/src/commands/doctor.js +293 -130
- package/src/commands/handoff.js +10 -57
- package/src/commands/hooks.js +17 -3
- package/src/commands/index-rebuild.js +55 -44
- package/src/commands/init.js +88 -16
- package/src/commands/intake.js +85 -99
- package/src/commands/intent.js +14 -1
- package/src/commands/metrics.js +65 -88
- package/src/commands/next.js +105 -147
- package/src/commands/prime.js +23 -91
- package/src/commands/report.js +40 -109
- package/src/commands/review.js +68 -36
- package/src/commands/search.js +18 -4
- package/src/commands/show.js +24 -9
- package/src/commands/skill.js +23 -39
- package/src/commands/spec.js +38 -11
- package/src/commands/status.js +69 -171
- package/src/commands/templates.js +35 -148
- package/src/commands/trace.js +36 -71
- package/src/commands/triage.js +4 -82
- package/src/commands/upgrade.js +31 -6
- package/src/commands/validate.js +44 -877
- package/src/commands/verify.js +60 -138
- package/src/commands/why.js +2 -2
- package/src/commands/work.js +243 -267
- package/src/index.js +139 -12
- package/src/lib/actions.js +490 -0
- package/src/lib/adapters.js +0 -11
- package/src/lib/adr-guard.js +19 -0
- package/src/lib/agent-changelog.js +133 -0
- package/src/lib/analysis.js +264 -0
- package/src/lib/args.js +14 -0
- package/src/lib/change-ops.js +133 -0
- package/src/lib/commands.js +135 -10
- package/src/lib/config.js +206 -0
- package/src/lib/constitution-model.js +102 -0
- package/src/lib/coverage-model.js +431 -0
- package/src/lib/criteria.js +23 -0
- package/src/lib/doc-model.js +370 -3
- package/src/lib/docs-impact.js +270 -27
- package/src/lib/ears.js +24 -1
- package/src/lib/flag-catalog.js +0 -16
- package/src/lib/fs-ops.js +58 -1
- package/src/lib/gates.js +231 -29
- package/src/lib/git.js +132 -0
- package/src/lib/index-json.js +42 -24
- package/src/lib/intake-model.js +155 -0
- package/src/lib/json-out.js +100 -2
- package/src/lib/ledger.js +192 -0
- package/src/lib/lexicon.js +228 -0
- package/src/lib/metrics-model.js +189 -0
- package/src/lib/names.js +42 -0
- package/src/lib/playbook.js +97 -0
- package/src/lib/project.js +105 -0
- package/src/lib/reproducibility.js +137 -0
- package/src/lib/runtime.js +191 -5
- package/src/lib/scan.js +328 -25
- package/src/lib/signoff.js +147 -0
- package/src/lib/snapshot.js +231 -0
- package/src/lib/spec-ops.js +72 -8
- package/src/lib/templates-model.js +289 -0
- package/src/lib/trace-model.js +95 -0
- package/src/lib/triage-model.js +121 -0
- package/src/lib/usage.js +6 -4
- package/src/lib/validation-model.js +1381 -0
- package/src/lib/version.js +22 -0
- package/src/lib/views.js +413 -0
- package/src/lib/work-model.js +102 -0
- package/templates/AGENTS.md.template +21 -28
- package/templates/README.md +35 -0
- package/templates/adapters/claude/.claude/commands/doctrina-work.md.template +6 -3
- package/templates/adapters/cursor/.cursor/commands/doctrina-work.md.template +4 -3
- package/templates/change/proposal.md.template +16 -0
- package/templates/change/tasks.md.template +3 -8
- package/templates/contract.md.template +2 -2
- package/templates/hooks/pre-commit.sample +16 -2
- package/templates/playbooks/bootstrap.md.template +47 -0
- package/templates/playbooks/chore.md.template +24 -0
- package/templates/playbooks/work.md.template +83 -0
- package/src/commands/constitution.js +0 -145
package/src/commands/work.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { readdirSync } from "node:fs";
|
|
5
|
-
import { spawnSync } from "node:child_process";
|
|
6
5
|
import { exists, isDir, isFile, mkdirp, read, relPath, write } from "../lib/fs-ops.js";
|
|
7
6
|
import { flagString, flagBool } from "../lib/args.js";
|
|
8
7
|
import * as idx from "../lib/index-json.js";
|
|
@@ -10,8 +9,14 @@ import { c } from "../lib/colors.js";
|
|
|
10
9
|
import { assessBrief } from "../lib/clarity.js";
|
|
11
10
|
import { locateTemplatesDir, substitute } from "../lib/templates.js";
|
|
12
11
|
import { EXIT, notADoctrinaProject } from "../lib/exit-codes.js";
|
|
13
|
-
import { changeNew } from "
|
|
14
|
-
import { classify } from "
|
|
12
|
+
import { changeNew, reindexChange } from "../lib/change-ops.js";
|
|
13
|
+
import { classify } from "../lib/triage-model.js";
|
|
14
|
+
import { printPlaybookTemplate } from "../lib/playbook.js";
|
|
15
|
+
import { changedFiles } from "../lib/git.js";
|
|
16
|
+
import { slugFromPrompt, fold, CONFIDENT_MARGIN } from "../lib/lexicon.js";
|
|
17
|
+
import { rankCapabilities, rankCapabilitiesByDiff } from "../lib/work-model.js";
|
|
18
|
+
import { isChangeId, refuseChangeRef } from "../lib/project.js";
|
|
19
|
+
export { rankCapabilities, rankCapabilitiesByDiff } from "../lib/work-model.js";
|
|
15
20
|
|
|
16
21
|
// `work` is the second half of the no-ceremony path (ADR 0005): a brief
|
|
17
22
|
// prompt ("add login") becomes a fully scaffolded change plus a playbook
|
|
@@ -24,7 +29,7 @@ import { classify } from "./triage.js";
|
|
|
24
29
|
// Flags this command accepts. Declared HERE, with the command, so
|
|
25
30
|
// adding a command never requires editing the entrypoint — the gap that
|
|
26
31
|
// let six flags ship undeclared and silently swallow a positional (C3).
|
|
27
|
-
export const flags = { boolean: ["json", "chore", "force", "from-diff", "no-spec", "quiet"], string: ["capability", "id", "resume", "title"] };
|
|
32
|
+
export const flags = { boolean: ["json", "chore", "design", "force", "from-diff", "no-spec", "quiet"], string: ["capability", "id", "resume", "title"] };
|
|
28
33
|
|
|
29
34
|
export async function run(positional, flags) {
|
|
30
35
|
const prompt = positional.join(" ").trim();
|
|
@@ -46,7 +51,7 @@ export async function run(positional, flags) {
|
|
|
46
51
|
const chore = flagBool(flags, "chore", false) || flagBool(flags, "no-spec", false);
|
|
47
52
|
let files = [];
|
|
48
53
|
if (fromDiff) {
|
|
49
|
-
files = changedFiles(projectRoot);
|
|
54
|
+
files = changedFiles(projectRoot).files;
|
|
50
55
|
if (files.length === 0) {
|
|
51
56
|
console.error(c.red("error:") + " --from-diff found no working-tree changes to backfill from");
|
|
52
57
|
console.error(c.gray("hint: ") + "make (or stage) the code changes first, then run `doctrina work --from-diff`");
|
|
@@ -80,9 +85,16 @@ export async function run(positional, flags) {
|
|
|
80
85
|
// prompt is stopped here, once, with the diagnosis path named. It is a
|
|
81
86
|
// deterministic term match and it can be wrong, so --force proceeds and
|
|
82
87
|
// the message says so.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
// The verdict is computed for EVERY prompt, not only the ones the hold
|
|
89
|
+
// applies to (change 0042). It used to be calculated, printed and thrown
|
|
90
|
+
// away, so an archived proposal never recorded which lane the change was
|
|
91
|
+
// born in — no report could say what kind of work the team does, and the
|
|
92
|
+
// classifier had no set of right and wrong answers to be calibrated
|
|
93
|
+
// against. It is recorded as HISTORY: a gate never reads it to decide.
|
|
94
|
+
const verdict = prompt ? classify(prompt) : null;
|
|
95
|
+
const forced = flagBool(flags, "force", false);
|
|
96
|
+
if (!fromDiff && !chore && !forced) {
|
|
97
|
+
if (verdict && verdict.lane === "runtime" && verdict.confident) {
|
|
86
98
|
console.error(c.yellow("hold:") + " this reads as a RUNTIME problem, not a change of behaviour");
|
|
87
99
|
console.error(c.gray(`signals: ${[...new Set(verdict.scores.runtime.hits)].slice(0, 6).join(", ")}`));
|
|
88
100
|
console.error("");
|
|
@@ -109,9 +121,17 @@ export async function run(positional, flags) {
|
|
|
109
121
|
// drives the slug and the proposal H1; the whole prompt still lands, intact,
|
|
110
122
|
// under ## Why. Without it a long prompt used to become a 900-char H1.
|
|
111
123
|
const title = flagString(flags, "title") ?? effPrompt;
|
|
112
|
-
|
|
124
|
+
// The id is what a person types and what sorts a backlog, so it stays short;
|
|
125
|
+
// the title is what a person reads, so it stays whole (change 0070). With
|
|
126
|
+
// `--title` the author has already made that split, so the slug follows the
|
|
127
|
+
// title they chose; without it, the slug is the prompt's first content words
|
|
128
|
+
// and the H1's title half is the prompt, so the two halves stop being the
|
|
129
|
+
// same sentence twice.
|
|
130
|
+
const slug = flags.has("title")
|
|
131
|
+
? slugify(title)
|
|
132
|
+
: (prompt ? slugFromPrompt(prompt) : "backfill");
|
|
113
133
|
const id = flagString(flags, "id") ?? `${nextChangeNumber(projectRoot)}-${slug}`;
|
|
114
|
-
if (
|
|
134
|
+
if (!isChangeId(id)) {
|
|
115
135
|
console.error(c.red("error:") + ` invalid change id "${id}" (lowercase letters, digits, hyphens)`);
|
|
116
136
|
return 2;
|
|
117
137
|
}
|
|
@@ -119,6 +139,21 @@ export async function run(positional, flags) {
|
|
|
119
139
|
const code = changeNew([id, title], flags);
|
|
120
140
|
if (code !== 0) return code;
|
|
121
141
|
|
|
142
|
+
// The CLI reduces a prompt to an id deterministically; it cannot write a
|
|
143
|
+
// good short name, and ADR 0005 says it must not try. So it says how to get
|
|
144
|
+
// one, once, where the author is already looking.
|
|
145
|
+
if (!flags.has("title") && !flags.has("id") && prompt) {
|
|
146
|
+
console.error(c.gray("note: ") +
|
|
147
|
+
`id derived from the prompt — \`--title "<short name>"\` gives a shorter one ` +
|
|
148
|
+
"and keeps the full prompt under ## Why");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Stamp the lane the classifier read, and — when the operator went a
|
|
152
|
+
// different way — what they did instead. Recording only the agreements
|
|
153
|
+
// would make the calibration set exactly the one that needs no
|
|
154
|
+
// calibrating; the disagreements are the data.
|
|
155
|
+
writeLane(projectRoot, id, verdict, { forced, chore, fromDiff });
|
|
156
|
+
|
|
122
157
|
// Record the prompt verbatim as the change's Why — the raw intent has
|
|
123
158
|
// one home, and it is the proposal, not the playbook output. For --from-diff,
|
|
124
159
|
// also record the changed files so the backfill's input is in the artifact.
|
|
@@ -140,34 +175,43 @@ export async function run(positional, flags) {
|
|
|
140
175
|
if (updated !== txt) write(proposalPath, updated, { force: true });
|
|
141
176
|
}
|
|
142
177
|
|
|
143
|
-
// A pinned capability is an explicit statement of intent, so the delta —
|
|
144
|
-
// historically the only 100% hand-authored file in the flow, and the one
|
|
145
|
-
// whose missing **Operation:** header exploded days later at analyze
|
|
146
|
-
// (operator review 2026-07-19 §3.2) — is scaffolded from the template with
|
|
147
|
-
// the Operation prefilled: MODIFIED when the spec exists, ADDED when it
|
|
148
|
-
// does not. Never scaffolded from a ranked GUESS (that would put a wrong
|
|
149
|
-
// capability's delta in the change); only from --capability.
|
|
150
|
-
if (pinned && !chore) {
|
|
151
|
-
const deltaPath = path.join(projectRoot, ".doctrina", "changes", id, "specs", pinned, "delta.md");
|
|
152
|
-
if (!exists(deltaPath)) {
|
|
153
|
-
const tpl = read(path.join(locateTemplatesDir(), "change", "spec-delta.md.template"));
|
|
154
|
-
const op = isFile(path.join(projectRoot, ".doctrina", "specs", pinned, "spec.md")) ? "MODIFIED" : "ADDED";
|
|
155
|
-
const body = substitute(tpl, { CAPABILITY: pinned })
|
|
156
|
-
.replace(/^\*\*Operation:\*\*.*$/m, `**Operation:** ${op}`);
|
|
157
|
-
mkdirp(path.dirname(deltaPath));
|
|
158
|
-
write(deltaPath, body);
|
|
159
|
-
console.log(c.green("created") + ` ${relPath(projectRoot, deltaPath)}` + c.gray(` (Operation: ${op} prefilled)`));
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
178
|
// Capability hint: term overlap for a prompt, changed-file overlap for a diff
|
|
164
179
|
// (review F10 — rank by what the working tree touched, not just prompt words).
|
|
165
|
-
const allChanged = fromDiff ? files : changedFiles(projectRoot);
|
|
180
|
+
const allChanged = fromDiff ? files : changedFiles(projectRoot).files;
|
|
166
181
|
const diffMatches = pinned ? [] : rankCapabilitiesByDiff(projectRoot, allChanged);
|
|
167
|
-
const
|
|
182
|
+
const promptMatches = pinned || fromDiff ? [] : rankCapabilities(projectRoot, effPrompt);
|
|
183
|
+
const matches = pinned ? [] : (fromDiff ? diffMatches : promptMatches);
|
|
168
184
|
const capability = pinned ?? matches[0]?.id ?? null;
|
|
169
185
|
const clarity = assessBrief(effPrompt, { kind: "prompt" });
|
|
170
186
|
|
|
187
|
+
// The delta is scaffolded whenever the CLI can NAME the capability — from
|
|
188
|
+
// --capability, or from a prompt ranking whose winner leads the runner-up
|
|
189
|
+
// by a real margin (change 0044). It was historically the only 100%
|
|
190
|
+
// hand-authored file in the flow, and the one whose missing **Operation:**
|
|
191
|
+
// header exploded days later at analyze (operator review 2026-07-19 §3.2);
|
|
192
|
+
// leaving the default path hand-authoring it left the failure in place for
|
|
193
|
+
// every run that did not pin. A wrong, obvious file costs less than an
|
|
194
|
+
// absent, silent one — so the guess is MARKED as one, and below the margin
|
|
195
|
+
// nothing is written at all: a weak guess in the wrong folder is worse
|
|
196
|
+
// than no file.
|
|
197
|
+
//
|
|
198
|
+
// Only the PROMPT ranking scaffolds. The diff ranker scores on its own
|
|
199
|
+
// scale (path/citation points, no density term), so CONFIDENT_MARGIN does
|
|
200
|
+
// not transfer to it, and a backfill normally touches several capabilities
|
|
201
|
+
// at once — one scaffolded winner would be the wrong shape there.
|
|
202
|
+
const winner = promptMatches[0];
|
|
203
|
+
const guess = winner && winner.margin >= CONFIDENT_MARGIN ? winner : null;
|
|
204
|
+
const scaffolded = chore || !capability || (!pinned && !guess)
|
|
205
|
+
? null
|
|
206
|
+
: scaffoldDelta(projectRoot, { id, capability, guess, runnerUp: promptMatches[1] ?? null });
|
|
207
|
+
|
|
208
|
+
// The proposal is written in two passes — `changeNew` scaffolds it, the
|
|
209
|
+
// lines above stamp the lane and the pinned specs — so its index entry is
|
|
210
|
+
// re-derived HERE, from the finished file. Indexing at the end of the first
|
|
211
|
+
// pass is what made every `doctrina work` leave the tree failing `validate`
|
|
212
|
+
// on the very next command (change 0076).
|
|
213
|
+
reindexChange(projectRoot, id);
|
|
214
|
+
|
|
171
215
|
// --quiet: registering backlog, not starting now (operator review §3.7 — 19
|
|
172
216
|
// works printed 19 identical 50-line playbooks). One line per change; the
|
|
173
217
|
// playbook is reprintable on demand with --resume.
|
|
@@ -183,27 +227,91 @@ export async function run(positional, flags) {
|
|
|
183
227
|
printChorePlaybook(projectRoot, { id, prompt: effPrompt });
|
|
184
228
|
} else {
|
|
185
229
|
printPlaybook(projectRoot, {
|
|
186
|
-
id, prompt: effPrompt, pinned, matches, capability, clarity,
|
|
230
|
+
id, prompt: effPrompt, pinned, matches, capability, clarity, scaffolded,
|
|
187
231
|
fromDiff, diffMatches: fromDiff ? [] : diffMatches,
|
|
188
232
|
});
|
|
189
233
|
}
|
|
190
234
|
return 0;
|
|
191
235
|
}
|
|
192
236
|
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
237
|
+
// Write the change's spec delta from the template with **Operation:**
|
|
238
|
+
// prefilled — MODIFIED when the target spec exists, ADDED when it does not.
|
|
239
|
+
//
|
|
240
|
+
// A GUESS (the ranked winner rather than a pinned capability) can only ever
|
|
241
|
+
// be MODIFIED: the ranker scores existing specs, so it cannot name one that
|
|
242
|
+
// is not there. It carries a comment saying it is a guess and how to correct
|
|
243
|
+
// it, because the whole trade this makes is that a wrong file the agent can
|
|
244
|
+
// see beats a missing file nothing reports.
|
|
245
|
+
//
|
|
246
|
+
// Returns what was written, or null when the file already exists.
|
|
247
|
+
function scaffoldDelta(projectRoot, { id, capability, guess = null, runnerUp = null }) {
|
|
248
|
+
const deltaPath = path.join(projectRoot, ".doctrina", "changes", id, "specs", capability, "delta.md");
|
|
249
|
+
if (exists(deltaPath)) return null;
|
|
250
|
+
const tpl = read(path.join(locateTemplatesDir(), "change", "spec-delta.md.template"));
|
|
251
|
+
const op = isFile(path.join(projectRoot, ".doctrina", "specs", capability, "spec.md")) ? "MODIFIED" : "ADDED";
|
|
252
|
+
let body = substitute(tpl, { CAPABILITY: capability })
|
|
253
|
+
.replace(/^\*\*Operation:\*\*.*$/m, `**Operation:** ${op}`);
|
|
254
|
+
if (guess) body = body.replace(/(\*\*Target spec on apply:\*\*.*\r?\n)/, `$1\n${guessNote(id, guess, runnerUp)}\n`);
|
|
255
|
+
mkdirp(path.dirname(deltaPath));
|
|
256
|
+
write(deltaPath, body);
|
|
257
|
+
console.log(c.green("created") + ` ${relPath(projectRoot, deltaPath)}` +
|
|
258
|
+
c.gray(guess
|
|
259
|
+
? ` (Operation: ${op} prefilled — ranked guess, correct it if wrong)`
|
|
260
|
+
: ` (Operation: ${op} prefilled)`));
|
|
261
|
+
return { capability, op, guess: Boolean(guess) };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// The words that make a guessed delta self-describing on disk: --resume
|
|
265
|
+
// prints nothing it did not write itself, so the file is the only place a
|
|
266
|
+
// later session can learn the capability was ranked rather than chosen.
|
|
267
|
+
const GUESS_MARK = "RANKED GUESS";
|
|
268
|
+
|
|
269
|
+
// The mark a guessed delta carries. It names the evidence (the score and the
|
|
270
|
+
// runner-up it beat) and the exact correction, so the agent can overrule the
|
|
271
|
+
// ranking in one step instead of inheriting it silently.
|
|
272
|
+
function guessNote(id, guess, runnerUp) {
|
|
273
|
+
const beat = runnerUp
|
|
274
|
+
? `beating \`${runnerUp.id}\` (${runnerUp.score})`
|
|
275
|
+
: "the only spec the prompt matched";
|
|
276
|
+
return [
|
|
277
|
+
`<!-- ${GUESS_MARK} — no --capability was given. \`doctrina work\` picked`,
|
|
278
|
+
` \`${guess.id}\` by deterministic term overlap (score ${guess.score}, ${beat}).`,
|
|
279
|
+
" It is a hint, never a decision (ADR 0005). If it is the wrong",
|
|
280
|
+
" capability, delete this folder and write the right one instead:",
|
|
281
|
+
` rm -r .doctrina/changes/${id}/specs/${guess.id}`,
|
|
282
|
+
` .doctrina/changes/${id}/specs/<capability>/delta.md`,
|
|
283
|
+
" Delete this comment once the capability is confirmed. -->",
|
|
284
|
+
].join("\n");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Render the Lane header: the classifier's verdict, how sure it was, the
|
|
288
|
+
// signals that decided it, and any operator override. One line, so the
|
|
289
|
+
// proposal header stays a header.
|
|
290
|
+
function laneRecord(verdict, { forced, chore, fromDiff }) {
|
|
291
|
+
const override = chore ? "chore" : fromDiff ? "backfill" : null;
|
|
292
|
+
if (!verdict) return override ? `${override} (no prompt to classify)` : "";
|
|
293
|
+
const hits = [...new Set(verdict.scores[verdict.lane]?.hits ?? [])].slice(0, 6);
|
|
294
|
+
const detail = hits.length > 0 ? `; signals: ${hits.join(", ")}` : "";
|
|
295
|
+
let line = `${verdict.lane} (${verdict.confident ? "confident" : "uncertain"}${detail})`;
|
|
296
|
+
// The operator disagreed with the classifier, or overrode its hold. That is
|
|
297
|
+
// the row calibration actually needs: recording only the agreements would
|
|
298
|
+
// make the set exactly the one that needs no calibrating.
|
|
299
|
+
if (override) line += ` — opened as ${override}`;
|
|
300
|
+
else if (forced && verdict.lane === "runtime") line += " — opened anyway (--force)";
|
|
301
|
+
return line;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function writeLane(projectRoot, id, verdict, opts) {
|
|
305
|
+
const line = laneRecord(verdict, opts);
|
|
306
|
+
if (!line) return;
|
|
307
|
+
const proposalPath = path.join(projectRoot, ".doctrina", "changes", id, "proposal.md");
|
|
308
|
+
if (!isFile(proposalPath)) return;
|
|
309
|
+
const text = read(proposalPath);
|
|
310
|
+
// CRLF-safe: the scaffolded proposals are CRLF, and a `.*$` pattern
|
|
311
|
+
// silently matches nothing against them — the `patch-doctrina-files-as-crlf`
|
|
312
|
+
// skill exists for exactly this mistake.
|
|
313
|
+
const updated = text.replace(/^(-[ \t]+\*\*Lane:\*\*)[^\r\n]*/m, `$1 ${line}`);
|
|
314
|
+
if (updated !== text) write(proposalPath, updated, { force: true });
|
|
207
315
|
}
|
|
208
316
|
|
|
209
317
|
// Next sequential NNNN across open changes and the archive, so work-driven
|
|
@@ -279,6 +387,8 @@ function resumeChange(projectRoot, resumeId) {
|
|
|
279
387
|
if (open.length) console.error(c.gray("open: ") + open.map((ch) => ch.id).join(", "));
|
|
280
388
|
return 2;
|
|
281
389
|
}
|
|
390
|
+
const refused = refuseChangeRef(resumeId);
|
|
391
|
+
if (refused !== null) return refused;
|
|
282
392
|
const entry = open.find((ch) => ch.id === resumeId);
|
|
283
393
|
if (!entry && !isDir(path.join(projectRoot, ".doctrina", "changes", resumeId))) {
|
|
284
394
|
console.error(c.red("error:") + ` no open change "${resumeId}"`);
|
|
@@ -289,9 +399,17 @@ function resumeChange(projectRoot, resumeId) {
|
|
|
289
399
|
const matches = rankCapabilities(projectRoot, prompt);
|
|
290
400
|
const capability = matches[0]?.id ?? null;
|
|
291
401
|
const clarity = assessBrief(prompt, { kind: "prompt" });
|
|
402
|
+
// --resume creates nothing, but it must not tell the agent to write a delta
|
|
403
|
+
// the first run already scaffolded (pinned or guessed): it reads the folder.
|
|
404
|
+
const deltaPath = capability
|
|
405
|
+
? path.join(projectRoot, ".doctrina", "changes", resumeId, "specs", capability, "delta.md")
|
|
406
|
+
: null;
|
|
407
|
+
const existing = deltaPath && isFile(deltaPath)
|
|
408
|
+
? { capability, op: null, guess: read(deltaPath).includes(GUESS_MARK) }
|
|
409
|
+
: null;
|
|
292
410
|
console.log(c.bold(`Resuming change ${resumeId}`) + c.gray(" — agent-executed (ADR 0005)."));
|
|
293
411
|
console.log("");
|
|
294
|
-
printPlaybook(projectRoot, { id: resumeId, prompt, pinned: null, matches, capability, clarity });
|
|
412
|
+
printPlaybook(projectRoot, { id: resumeId, prompt, pinned: null, matches, capability, clarity, scaffolded: existing });
|
|
295
413
|
return 0;
|
|
296
414
|
}
|
|
297
415
|
|
|
@@ -314,229 +432,80 @@ export function slugify(text) {
|
|
|
314
432
|
return slug.length > 0 ? slug : "task";
|
|
315
433
|
}
|
|
316
434
|
|
|
317
|
-
// Deterministic term overlap: fold prompt and spec text to ASCII lowercase,
|
|
318
|
-
// drop short/stop words, score name hits heavily and body hits lightly.
|
|
319
|
-
// This is a hint for the agent, never a decision (ADR 0005).
|
|
320
|
-
const STOPWORDS = new Set([
|
|
321
|
-
// en
|
|
322
|
-
"the", "and", "for", "with", "from", "that", "this", "into", "when",
|
|
323
|
-
"then", "shall", "should", "must", "can", "will", "make", "add", "new",
|
|
324
|
-
"use", "create", "implement", "feature", "system", "user", "users",
|
|
325
|
-
// pt (ASCII-folded)
|
|
326
|
-
"uma", "umas", "uns", "dos", "das", "nos", "nas", "por", "para", "com",
|
|
327
|
-
"que", "sem", "aos", "faca", "fazer", "criar", "crie", "novo", "nova",
|
|
328
|
-
"adicionar", "adicione", "implementar", "implemente", "funcionalidade",
|
|
329
|
-
"sistema", "usuario", "usuarios",
|
|
330
|
-
]);
|
|
331
|
-
|
|
332
|
-
function fold(text) {
|
|
333
|
-
return text.normalize("NFD").replace(/\p{M}+/gu, "").toLowerCase();
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function promptTerms(prompt) {
|
|
337
|
-
return [...new Set(
|
|
338
|
-
fold(prompt)
|
|
339
|
-
.split(/[^a-z0-9]+/)
|
|
340
|
-
.filter((t) => t.length >= 3 && !STOPWORDS.has(t)),
|
|
341
|
-
)];
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
export function rankCapabilities(projectRoot, prompt) {
|
|
345
|
-
const specsDir = path.join(projectRoot, ".doctrina", "specs");
|
|
346
|
-
const terms = promptTerms(prompt);
|
|
347
|
-
if (terms.length === 0 || !isDir(specsDir)) return [];
|
|
348
|
-
|
|
349
|
-
const ranked = [];
|
|
350
|
-
for (const cap of readdirSync(specsDir).sort()) {
|
|
351
|
-
const specPath = path.join(specsDir, cap, "spec.md");
|
|
352
|
-
if (!isFile(specPath)) continue;
|
|
353
|
-
const body = fold(read(specPath));
|
|
354
|
-
const nameTokens = cap.split("-");
|
|
355
|
-
let score = 0;
|
|
356
|
-
for (const term of terms) {
|
|
357
|
-
if (nameTokens.includes(term)) score += 5;
|
|
358
|
-
const hits = body.split(term).length - 1;
|
|
359
|
-
score += Math.min(hits, 5);
|
|
360
|
-
}
|
|
361
|
-
if (score > 0) ranked.push({ id: cap, score, path: `.doctrina/specs/${cap}/spec.md` });
|
|
362
|
-
}
|
|
363
|
-
return ranked.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id)).slice(0, 3);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Rank capabilities by the working tree, not the prompt (review F10): a changed
|
|
367
|
-
// file scores its capability when the file sits under a path segment named for
|
|
368
|
-
// it, or when the spec cites the file as evidence. A deterministic overlap
|
|
369
|
-
// hint for the agent — never a decision (ADR 0005).
|
|
370
|
-
export function rankCapabilitiesByDiff(projectRoot, files, { limit = 3 } = {}) {
|
|
371
|
-
const specsDir = path.join(projectRoot, ".doctrina", "specs");
|
|
372
|
-
if (!files || files.length === 0 || !isDir(specsDir)) return [];
|
|
373
|
-
const norm = files.map((f) => f.replace(/\\/g, "/"));
|
|
374
|
-
|
|
375
|
-
const ranked = [];
|
|
376
|
-
for (const cap of readdirSync(specsDir).sort()) {
|
|
377
|
-
const specPath = path.join(specsDir, cap, "spec.md");
|
|
378
|
-
if (!isFile(specPath)) continue;
|
|
379
|
-
const specText = read(specPath);
|
|
380
|
-
let score = 0;
|
|
381
|
-
for (const f of norm) {
|
|
382
|
-
if (f.split("/").includes(cap)) score += 3; // src/<cap>/... etc.
|
|
383
|
-
if (specText.includes(f)) score += 5; // file cited in the spec
|
|
384
|
-
else if (specText.includes(path.basename(f))) score += 2; // filename cited
|
|
385
|
-
}
|
|
386
|
-
if (score > 0) ranked.push({ id: cap, score, path: `.doctrina/specs/${cap}/spec.md` });
|
|
387
|
-
}
|
|
388
|
-
// `limit` keeps the work-playbook hint short (top 3); review passes
|
|
389
|
-
// Infinity — truncating there is how it missed 5 of 8 touched capabilities
|
|
390
|
-
// in the 0.11.0 field session.
|
|
391
|
-
return ranked.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id)).slice(0, limit);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
435
|
// A chore is a spec-less change (review F9): the playbook drops the spec-delta
|
|
395
436
|
// steps and goes straight to implement → verify → archive → validate, so the
|
|
396
437
|
// agent is not prompted to invent a delta for infra/docs/build work.
|
|
438
|
+
// Both playbooks are rendered from templates (lib/playbook.js), resolved
|
|
439
|
+
// project-over-bundled like every other scaffold. What stays here is the only
|
|
440
|
+
// part that is not prose: computing the variable blocks. Each is PRE-RENDERED
|
|
441
|
+
// into a token, so `substitute` stays a plain string map and the template
|
|
442
|
+
// stays a document rather than a language with conditionals in it.
|
|
397
443
|
function printChorePlaybook(projectRoot, { id, prompt }) {
|
|
398
|
-
|
|
399
|
-
console.log("");
|
|
400
|
-
console.log(`Prompt: "${prompt}"`);
|
|
401
|
-
console.log("");
|
|
402
|
-
console.log("Execute in order, in a single linear pass:");
|
|
403
|
-
console.log("");
|
|
404
|
-
console.log(`1. Replace the placeholder tasks in .doctrina/changes/${id}/tasks.md`);
|
|
405
|
-
console.log(" with small, checkable steps; implement them, checking each box.");
|
|
406
|
-
console.log(c.gray(" (Do it before implementing — analyze/close refuse leftover scaffold"));
|
|
407
|
-
console.log(c.gray(" placeholders, and `change tick` will not tick an empty box.)"));
|
|
408
|
-
console.log("2. Prove it (archive refuses unchecked boxes):");
|
|
409
|
-
console.log(` ${c.cyan("doctrina verify")} — the project's typecheck/test/build gate`);
|
|
410
|
-
console.log(` ${c.cyan("doctrina verify --clean")} — clean-checkout reproducibility lint`);
|
|
411
|
-
console.log(" Then check the proposal's ## Verification boxes and every task.");
|
|
412
|
-
console.log("3. This is a chore: no spec changes. If you find yourself needing a");
|
|
413
|
-
console.log(` spec delta, it is not a chore — reopen with ${c.cyan("doctrina work \"<prompt>\"")}.`);
|
|
414
|
-
console.log(`4. ${c.cyan(`doctrina change apply ${id}`)} (zero deltas → flips to applied).`);
|
|
415
|
-
console.log(`5. ${c.cyan(`doctrina change archive ${id}`)}, then ${c.cyan("doctrina validate")}.`);
|
|
416
|
-
console.log(c.gray(" If this fix taught a lesson worth not relearning, capture it: ") +
|
|
417
|
-
c.cyan("doctrina skill new <slug>") + c.gray("."));
|
|
444
|
+
printPlaybookTemplate(projectRoot, "chore", { CHANGE_ID: id, PROMPT: prompt });
|
|
418
445
|
}
|
|
419
446
|
|
|
420
|
-
function printPlaybook(projectRoot, { id, prompt, pinned, matches, capability, clarity, fromDiff = false, diffMatches = [] }) {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
447
|
+
function printPlaybook(projectRoot, { id, prompt, pinned, matches, capability, clarity, scaffolded = null, fromDiff = false, diffMatches = [] }) {
|
|
448
|
+
printPlaybookTemplate(projectRoot, "work", {
|
|
449
|
+
TITLE: fromDiff ? "Backfill playbook" : "Work playbook",
|
|
450
|
+
CHANGE_ID: id,
|
|
451
|
+
PROMPT: prompt,
|
|
452
|
+
CAPABILITY: capability ?? "<capability>",
|
|
453
|
+
FROM_DIFF_NOTE: fromDiff
|
|
454
|
+
? c.gray("Code-first: write the spec that describes what the working tree already does.")
|
|
455
|
+
: "",
|
|
456
|
+
// Clarification gate (review Topic A): a thin prompt is the moment to ask
|
|
457
|
+
// the user, not to invent a spec. Advisory — the change is still
|
|
458
|
+
// scaffolded (it is a draft), but the agent is told to resolve the gaps
|
|
459
|
+
// first. The leading blank line belongs to the block, so an absent
|
|
460
|
+
// warning leaves no gap behind.
|
|
461
|
+
THIN_WARNING: clarity?.thin && !fromDiff
|
|
462
|
+
? ["", c.yellow("⚠ thin prompt — clarify with the user before writing spec deltas:"),
|
|
463
|
+
...clarity.reasons.map((r) => ` - ${r}`)].join("\n")
|
|
464
|
+
: "",
|
|
465
|
+
CAPABILITY_BLOCK: capabilityBlock(projectRoot, { pinned, matches, fromDiff }),
|
|
466
|
+
// Extra signal (review F10): even for a prompt-driven change, show which
|
|
467
|
+
// capabilities the working tree touched — often the truer hint.
|
|
468
|
+
DIFF_MATCHES: !fromDiff && diffMatches.length > 0
|
|
469
|
+
? c.gray("Also touched by your working tree: ") + diffMatches.map((m) => c.cyan(m.id)).join(", ")
|
|
470
|
+
: "",
|
|
471
|
+
STEP3_INTRO: scaffolded
|
|
472
|
+
? "3. A delta is already scaffolded (Operation prefilled) at\n" +
|
|
473
|
+
` .doctrina/changes/${id}/specs/${scaffolded.capability}/delta.md — fill its body.\n` +
|
|
474
|
+
(scaffolded.guess
|
|
475
|
+
// Coloured per line: a span left open across a newline survives a
|
|
476
|
+
// terminal but not every pager the output gets piped into.
|
|
477
|
+
? c.yellow(" It was RANKED, not pinned — confirm the capability first; the file") + "\n" +
|
|
478
|
+
c.yellow(" says how to correct it if the ranking got it wrong.") + "\n"
|
|
479
|
+
: "") +
|
|
480
|
+
" Add one more delta per additional affected capability:"
|
|
481
|
+
: "3. Write one delta per affected capability at\n" +
|
|
482
|
+
` .doctrina/changes/${id}/specs/<capability>/delta.md:`,
|
|
483
|
+
FROM_DIFF_DELTA_NOTE: fromDiff
|
|
484
|
+
? [c.gray(" --from-diff: the code already exists — describe its CURRENT behaviour, and"),
|
|
485
|
+
c.gray(" mark each criterion [unverified] until a test proves it (don't assume the"),
|
|
486
|
+
c.gray(" diff is tested). The changed files are listed in the proposal's ## Why.")].join("\n")
|
|
487
|
+
: "",
|
|
488
|
+
});
|
|
489
|
+
}
|
|
438
490
|
|
|
491
|
+
// Which capability the change is about, as far as the CLI can tell: pinned by
|
|
492
|
+
// the operator, ranked by term overlap, or nothing — a hint in every case,
|
|
493
|
+
// never a decision (ADR 0005).
|
|
494
|
+
function capabilityBlock(projectRoot, { pinned, matches, fromDiff }) {
|
|
439
495
|
if (pinned) {
|
|
440
496
|
const hasSpec = isFile(path.join(projectRoot, ".doctrina", "specs", pinned, "spec.md"));
|
|
441
|
-
|
|
442
|
-
(hasSpec ? "" : c.yellow(" — no spec yet; create it in step 2"))
|
|
443
|
-
} else if (matches.length > 0) {
|
|
444
|
-
const how = fromDiff ? "touched by your working tree" : "deterministic term match";
|
|
445
|
-
console.log(`Likely capabilities (${how} — a hint, not a decision):`);
|
|
446
|
-
for (const m of matches) {
|
|
447
|
-
console.log(` ${c.cyan(m.id.padEnd(20))} score ${String(m.score).padStart(3)} ${c.gray(m.path)}`);
|
|
448
|
-
}
|
|
449
|
-
} else {
|
|
450
|
-
console.log(c.gray(fromDiff
|
|
451
|
-
? "No existing spec matches the changed files — likely a new capability."
|
|
452
|
-
: "No existing spec matches the prompt — likely a new capability."));
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Extra signal (review F10): even for a prompt-driven change, show which
|
|
456
|
-
// capabilities the working tree touched — often the truer hint.
|
|
457
|
-
if (!fromDiff && diffMatches.length > 0) {
|
|
458
|
-
console.log(c.gray("Also touched by your working tree: ") +
|
|
459
|
-
diffMatches.map((m) => c.cyan(m.id)).join(", "));
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
console.log("");
|
|
463
|
-
console.log("Execute in order, in a single linear pass:");
|
|
464
|
-
console.log("");
|
|
465
|
-
console.log("1. Read the context pack and confirm (or correct) the capability:");
|
|
466
|
-
console.log(` ${c.cyan(`doctrina context ${capToken} --concat`)}`);
|
|
467
|
-
console.log("");
|
|
468
|
-
console.log("2. If the capability has no spec yet:");
|
|
469
|
-
console.log(` ${c.cyan("doctrina spec new <capability>")}`);
|
|
470
|
-
console.log(c.gray(" Trace it to product intent: tag the product.md success-criteria bullet"));
|
|
471
|
-
console.log(c.gray(" it serves with an anchor (\"- [SC1] ...\") and set the spec's"));
|
|
472
|
-
console.log(c.gray(" \"**Realizes:** SC1\" header (or \"n/a — <why>\"). `doctrina validate` warns"));
|
|
473
|
-
console.log(c.gray(" on an active spec with no Realizes; `doctrina trace` reports the link."));
|
|
474
|
-
console.log("");
|
|
475
|
-
if (pinned) {
|
|
476
|
-
console.log(`3. A delta is already scaffolded (Operation prefilled) at`);
|
|
477
|
-
console.log(` .doctrina/changes/${id}/specs/${pinned}/delta.md — fill its body.`);
|
|
478
|
-
console.log(" Add one more delta per additional affected capability:");
|
|
479
|
-
} else {
|
|
480
|
-
console.log("3. Write one delta per affected capability at");
|
|
481
|
-
console.log(` .doctrina/changes/${id}/specs/<capability>/delta.md:`);
|
|
497
|
+
return `Capability (pinned): ${c.cyan(pinned)}` +
|
|
498
|
+
(hasSpec ? "" : c.yellow(" — no spec yet; create it in step 2"));
|
|
482
499
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
console.log(c.gray(" keep aspiration under ## Maturity → Future, and cite the proof per"));
|
|
489
|
-
console.log(c.gray(" criterion: \"1. [unverified] <signal> — verified by `path/to/test`\">"));
|
|
490
|
-
console.log(c.gray(" New capability? `spec new` then an ADDED delta with the FULL body — apply"));
|
|
491
|
-
console.log(c.gray(" replaces the untouched scaffold. Bookkeeping edits? MODIFIED with an ops"));
|
|
492
|
-
console.log(c.gray(" block, applied mechanically (all ops or none):"));
|
|
493
|
-
console.log(c.gray(" ```ops"));
|
|
494
|
-
console.log(c.gray(" set-header Implementation: verified — `src/x.js`"));
|
|
495
|
-
console.log(c.gray(" bump-version minor"));
|
|
496
|
-
console.log(c.gray(" set-criterion 1: verified"));
|
|
497
|
-
console.log(c.gray(" append-criterion [unverified] new signal — verified by `test/y.test.js`"));
|
|
498
|
-
console.log(c.gray(" append-requirement event: When <trigger>, the system shall <action>."));
|
|
499
|
-
console.log(c.gray(" replace-requirement ubiquitous 2: The system shall <action>."));
|
|
500
|
-
console.log(c.gray(" ```"));
|
|
501
|
-
console.log(c.gray(" append-* ops number/position at APPLY time, so concurrent changes"));
|
|
502
|
-
console.log(c.gray(" appending to the same spec never collide. Only free-prose rewrites"));
|
|
503
|
-
console.log(c.gray(" (Purpose, Maturity, ...) stay a by-hand merge."));
|
|
504
|
-
if (fromDiff) {
|
|
505
|
-
console.log(c.gray(" --from-diff: the code already exists — describe its CURRENT behaviour, and"));
|
|
506
|
-
console.log(c.gray(" mark each criterion [unverified] until a test proves it (don't assume the"));
|
|
507
|
-
console.log(c.gray(" diff is tested). The changed files are listed in the proposal's ## Why."));
|
|
500
|
+
if (matches.length > 0) {
|
|
501
|
+
const how = fromDiff ? "touched by your working tree" : "deterministic term match";
|
|
502
|
+
return [`Likely capabilities (${how} — a hint, not a decision):`,
|
|
503
|
+
...matches.map((m) => ` ${c.cyan(m.id.padEnd(20))} score ${String(m.score).padStart(3)} ${c.gray(m.path)}`),
|
|
504
|
+
].join("\n");
|
|
508
505
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
console.log(" and record the change's What/Scope in its proposal.md. Do this BEFORE");
|
|
513
|
-
console.log(" implementing — analyze and close refuse a change whose scaffold");
|
|
514
|
-
console.log(" placeholders were never replaced, and `change tick` will not tick them.");
|
|
515
|
-
console.log("");
|
|
516
|
-
console.log("5. Implement task by task, checking each box as it lands. Advance the");
|
|
517
|
-
console.log(" spec's Implementation: planned → partial → implemented as code lands.");
|
|
518
|
-
console.log(" If the prompt is genuinely ambiguous, ask the user before assuming.");
|
|
519
|
-
console.log("");
|
|
520
|
-
console.log(`6. ADR checkpoint — does this change decide something structural`);
|
|
521
|
-
console.log(" (an architecture, a boundary, a trade-off a future session must");
|
|
522
|
-
console.log(` not relitigate)? If yes: ${c.cyan("doctrina decision new \"<title>\"")} now,`);
|
|
523
|
-
console.log(" before closing — the definition of done requires it recorded.");
|
|
524
|
-
console.log(c.gray(" (close re-checks this: it warns when the change touches capabilities"));
|
|
525
|
-
console.log(c.gray(" cited by an accepted ADR — amend via decision supersede, not silence.)"));
|
|
526
|
-
console.log(" Touching an integration surface (ports, env vars, public endpoints)?");
|
|
527
|
-
console.log(` Own it in a contract: ${c.cyan("doctrina contract new <id>")} · ${c.cyan("doctrina contract check")}.`);
|
|
528
|
-
console.log("");
|
|
529
|
-
console.log(`7. Close in one attested pass (preferred — runs every gate and stops`);
|
|
530
|
-
console.log(" at the first failure with the exact rerun command):");
|
|
531
|
-
console.log(` ${c.cyan(`doctrina close ${id}`)}`);
|
|
532
|
-
console.log(c.gray(" (equivalent, step by step: ") +
|
|
533
|
-
c.gray(`analyze → change apply → verify → coverage → trace → change archive → validate)`));
|
|
534
|
-
console.log(" Before it: check the proposal's ## Verification boxes and every task,");
|
|
535
|
-
console.log(" closing steps included, and bump Implementation to verified.");
|
|
536
|
-
console.log("");
|
|
537
|
-
console.log(`8. ${c.cyan("doctrina next")} for the follow-up.`);
|
|
538
|
-
console.log(c.gray(" If this change taught a reusable lesson (a fix you'd hate to relearn,"));
|
|
539
|
-
console.log(c.gray(" a recurring convention), capture it: ") + c.cyan("doctrina skill new <slug>") + c.gray("."));
|
|
506
|
+
return c.gray(fromDiff
|
|
507
|
+
? "No existing spec matches the changed files — likely a new capability."
|
|
508
|
+
: "No existing spec matches the prompt — likely a new capability.");
|
|
540
509
|
}
|
|
541
510
|
|
|
542
511
|
export const help = `
|
|
@@ -548,9 +517,16 @@ change id (NNNN-<slug>) from the prompt, opens the change folder,
|
|
|
548
517
|
records the prompt under the proposal's "## Why", ranks existing specs
|
|
549
518
|
by term overlap as a capability hint, flags a thin/under-specified prompt
|
|
550
519
|
so you clarify before writing deltas (advisory), and prints the steps:
|
|
551
|
-
context
|
|
552
|
-
|
|
553
|
-
|
|
520
|
+
context, spec delta, tasks, implement, then \`doctrina change check <id>\`
|
|
521
|
+
to preview the close and \`doctrina close <id>\` to finish, which runs the
|
|
522
|
+
whole closing sequence in one pass. No natural-language interpretation
|
|
523
|
+
happens in the CLI.
|
|
524
|
+
|
|
525
|
+
The change's spec delta is scaffolded with **Operation:** prefilled
|
|
526
|
+
whenever the CLI can name the capability: from --capability, or from the
|
|
527
|
+
ranking when the winner leads the runner-up by a whole matched term. A
|
|
528
|
+
ranked delta is always MODIFIED and says in the file that it is a guess,
|
|
529
|
+
with the command that corrects it. Below that margin nothing is written.
|
|
554
530
|
|
|
555
531
|
When the prompt is a bare "continue"/"prossiga"/"next" and a change is
|
|
556
532
|
already open, work suggests resuming it instead of opening a junk change
|
|
@@ -559,10 +535,8 @@ named after that word (review G1). Use --resume to do so directly.
|
|
|
559
535
|
Options:
|
|
560
536
|
--title "<short>" Short display title: drives the slug and the proposal
|
|
561
537
|
H1; the full prompt still lands under ## Why
|
|
562
|
-
--capability <cap> Pin the capability instead of ranking matches
|
|
563
|
-
|
|
564
|
-
Operation header prefilled (MODIFIED when the spec
|
|
565
|
-
exists, ADDED when it does not)
|
|
538
|
+
--capability <cap> Pin the capability instead of ranking matches, and
|
|
539
|
+
drop the guess comment from the scaffolded delta
|
|
566
540
|
--quiet Register the change and print one line — no playbook
|
|
567
541
|
(backlog entry; reprint later with --resume <id>)
|
|
568
542
|
--id <id> Override the derived change id
|
|
@@ -571,6 +545,8 @@ Options:
|
|
|
571
545
|
prompt needed) and print a code-first playbook (F8)
|
|
572
546
|
--chore, --no-spec Open a spec-less chore change (infra/docs/build) with a
|
|
573
547
|
playbook that skips the spec-delta steps (F9)
|
|
548
|
+
--design Also scaffold design.md, for a change with non-trivial
|
|
549
|
+
choices (opt-in, as with \`change new --design\`)
|
|
574
550
|
--force Open a new change even when one is open, overwrite an
|
|
575
551
|
existing change folder, and proceed past the lane hold
|
|
576
552
|
below
|