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/trace.js
CHANGED
|
@@ -8,6 +8,11 @@ import { flagBool } from "../lib/args.js";
|
|
|
8
8
|
import { c } from "../lib/colors.js";
|
|
9
9
|
import { emitJson } from "../lib/json-out.js";
|
|
10
10
|
import { notADoctrinaProject } from "../lib/exit-codes.js";
|
|
11
|
+
import { summarize, collectAnchors, collectAnchorDuplicates, collectSpecs } from "../lib/trace-model.js";
|
|
12
|
+
|
|
13
|
+
// The provenance arithmetic lives in lib/trace-model.js, which `status`,
|
|
14
|
+
// `review` and the project snapshot read too (audit finding F7).
|
|
15
|
+
export { summarize } from "../lib/trace-model.js";
|
|
11
16
|
|
|
12
17
|
// Intent-provenance report (ADR 0006). `coverage` proves a criterion has a
|
|
13
18
|
// test; `trace` proves a capability traces to a stated intent. Together they
|
|
@@ -48,9 +53,23 @@ export async function run(_positional, flags) {
|
|
|
48
53
|
|
|
49
54
|
const anchors = collectAnchors(projectRoot); // [{ id }], in document order
|
|
50
55
|
const specs = collectSpecs(projectRoot); // [{ cap, status, realizes: [id] }]
|
|
56
|
+
// An id declared twice is two intents under one name (change 0108): the
|
|
57
|
+
// collector keeps the first, so the second is invisible everywhere unless
|
|
58
|
+
// named here. A gap, like a dropped anchor — `validate` reports it as an
|
|
59
|
+
// error as well.
|
|
60
|
+
const duplicates = collectAnchorDuplicates(projectRoot); // [{ id, lines }]
|
|
51
61
|
|
|
52
62
|
const anchorIds = new Set(anchors.map((a) => a.id));
|
|
53
|
-
|
|
63
|
+
// A spec OPTED IN when it cites at least one anchor id. The header alone
|
|
64
|
+
// is not opting in: `spec new` scaffolds `**Realizes:** n/a — <reason>`,
|
|
65
|
+
// which parses to an empty id list and used to count as participation.
|
|
66
|
+
// One scaffolded spec was therefore enough to take a project out of the
|
|
67
|
+
// never-opted-in branch and into the normal report, where zero anchors
|
|
68
|
+
// rendered as `ok 0 of 0 intent anchors realized` — a green verdict over
|
|
69
|
+
// nothing, on the very first read a new project gets about itself, while
|
|
70
|
+
// `doctor` read the same collection and warned (change 0083). Absence is
|
|
71
|
+
// not approval — the half change 0057 fixed in `coverage` and not here.
|
|
72
|
+
const anyRealizes = specs.some((s) => s.realizes !== null && s.realizes.length > 0);
|
|
54
73
|
|
|
55
74
|
// The feature is unused: do not nag a project that never opted in.
|
|
56
75
|
if (anchors.length === 0 && !anyRealizes) {
|
|
@@ -88,8 +107,8 @@ export async function run(_positional, flags) {
|
|
|
88
107
|
|
|
89
108
|
if (json) {
|
|
90
109
|
const rows = anchors.map((a) => ({ id: a.id, realizedBy: (realizedBy.get(a.id) ?? []).sort() }));
|
|
91
|
-
const clean = rows.every((r) => r.realizedBy.length > 0) && dangling.length === 0 && untraceable.length === 0;
|
|
92
|
-
emitJson("trace", { anchors: rows, dangling, untraceable, summary: summarize(projectRoot) });
|
|
110
|
+
const clean = rows.every((r) => r.realizedBy.length > 0) && dangling.length === 0 && untraceable.length === 0 && duplicates.length === 0;
|
|
111
|
+
emitJson("trace", { anchors: rows, dangling, untraceable, duplicates, summary: summarize(projectRoot) });
|
|
93
112
|
return clean ? 0 : strict ? 1 : 0;
|
|
94
113
|
}
|
|
95
114
|
|
|
@@ -107,7 +126,10 @@ export async function run(_positional, flags) {
|
|
|
107
126
|
}
|
|
108
127
|
}
|
|
109
128
|
|
|
110
|
-
if (dangling.length > 0 || untraceable.length > 0) console.log("");
|
|
129
|
+
if (dangling.length > 0 || untraceable.length > 0 || duplicates.length > 0) console.log("");
|
|
130
|
+
for (const d of duplicates) {
|
|
131
|
+
console.log(` ${c.red("✗")} duplicate anchor [${d.id}] — declared at product.md lines ${d.lines.join(" and ")}; only the first is read, give the second its own id`);
|
|
132
|
+
}
|
|
111
133
|
for (const d of dangling) {
|
|
112
134
|
console.log(` ${c.yellow("!")} dangling: spec "${d.cap}" realizes ${d.id} (no such anchor in product.md)`);
|
|
113
135
|
}
|
|
@@ -117,10 +139,16 @@ export async function run(_positional, flags) {
|
|
|
117
139
|
|
|
118
140
|
const dropped = anchors.length - realized;
|
|
119
141
|
console.log("");
|
|
120
|
-
const summary =
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
142
|
+
const summary = anchors.length === 0
|
|
143
|
+
? `no intent anchors declared in product.md` +
|
|
144
|
+
`; ${dangling.length} dangling, ${untraceable.length} untraceable`
|
|
145
|
+
: `${realized} of ${anchors.length} intent anchor${anchors.length === 1 ? "" : "s"} realized` +
|
|
146
|
+
`; ${dropped} dropped, ${dangling.length} dangling, ${untraceable.length} untraceable` +
|
|
147
|
+
(duplicates.length > 0 ? `, ${duplicates.length} duplicate` : "");
|
|
148
|
+
// Reaching the report with no anchors at all is never clean: there is
|
|
149
|
+
// nothing to have realized, and a ratio over zero says nothing true.
|
|
150
|
+
const clean = anchors.length > 0
|
|
151
|
+
&& dropped === 0 && dangling.length === 0 && untraceable.length === 0 && duplicates.length === 0;
|
|
124
152
|
if (clean) {
|
|
125
153
|
console.log(c.green("ok") + " " + summary);
|
|
126
154
|
return 0;
|
|
@@ -130,69 +158,6 @@ export async function run(_positional, flags) {
|
|
|
130
158
|
return strict ? 1 : 0;
|
|
131
159
|
}
|
|
132
160
|
|
|
133
|
-
// Pure summary of intent provenance, for other commands (`status`, `review`)
|
|
134
|
-
// that need the numbers without the report output.
|
|
135
|
-
export function summarize(projectRoot) {
|
|
136
|
-
const anchors = collectAnchors(projectRoot);
|
|
137
|
-
const specs = collectSpecs(projectRoot);
|
|
138
|
-
const anchorIds = new Set(anchors.map((a) => a.id));
|
|
139
|
-
const realizedBy = new Map();
|
|
140
|
-
let dangling = 0;
|
|
141
|
-
for (const s of specs) {
|
|
142
|
-
if (s.realizes === null) continue;
|
|
143
|
-
for (const id of s.realizes) {
|
|
144
|
-
if (anchorIds.has(id)) {
|
|
145
|
-
if (!realizedBy.has(id)) realizedBy.set(id, []);
|
|
146
|
-
realizedBy.get(id).push(s.cap);
|
|
147
|
-
} else {
|
|
148
|
-
dangling += 1;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
const untraceable = specs.filter((s) => s.realizes === null && s.status === "active").length;
|
|
153
|
-
let realized = 0;
|
|
154
|
-
for (const a of anchors) if ((realizedBy.get(a.id) ?? []).length > 0) realized += 1;
|
|
155
|
-
return {
|
|
156
|
-
anchors: anchors.length,
|
|
157
|
-
realized,
|
|
158
|
-
dropped: anchors.length - realized,
|
|
159
|
-
dangling,
|
|
160
|
-
untraceable,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Every "[A-Z]+\d+" tag at the head of a bullet in product.md is an intent
|
|
165
|
-
// anchor. Section-agnostic so Success-criteria and In-scope bullets both work.
|
|
166
|
-
function collectAnchors(projectRoot) {
|
|
167
|
-
const productPath = path.join(projectRoot, ".doctrina", "product.md");
|
|
168
|
-
if (!isFile(productPath)) return [];
|
|
169
|
-
const out = [];
|
|
170
|
-
const seen = new Set();
|
|
171
|
-
for (const line of read(productPath).split(/\r?\n/)) {
|
|
172
|
-
const m = line.match(/^\s*[-*]\s+\[([A-Z]+\d+)\]\s+/);
|
|
173
|
-
if (m && !seen.has(m[1])) {
|
|
174
|
-
seen.add(m[1]);
|
|
175
|
-
out.push({ id: m[1] });
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return out;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function collectSpecs(projectRoot) {
|
|
182
|
-
const specsDir = path.join(projectRoot, ".doctrina", "specs");
|
|
183
|
-
if (!isDir(specsDir)) return [];
|
|
184
|
-
const out = [];
|
|
185
|
-
for (const cap of readdirSync(specsDir).sort()) {
|
|
186
|
-
const specPath = path.join(specsDir, cap, "spec.md");
|
|
187
|
-
if (!isFile(specPath)) continue;
|
|
188
|
-
const text = read(specPath);
|
|
189
|
-
const realizesRaw = specHeader(text, "Realizes");
|
|
190
|
-
const realizes = realizesRaw === null ? null : (realizesRaw.match(ANCHOR_RE) ?? []);
|
|
191
|
-
const status = (specHeader(text, "Status") ?? "active").trim().toLowerCase();
|
|
192
|
-
out.push({ cap, status, realizes });
|
|
193
|
-
}
|
|
194
|
-
return out;
|
|
195
|
-
}
|
|
196
161
|
|
|
197
162
|
export const help = `
|
|
198
163
|
Usage: doctrina trace [--strict]
|
package/src/commands/triage.js
CHANGED
|
@@ -4,6 +4,10 @@ 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";
|
|
8
|
+
|
|
9
|
+
// The classifier is shared with `work`; see lib/triage-model.js.
|
|
10
|
+
export { classify } from "../lib/triage-model.js";
|
|
7
11
|
import { emitJson } from "../lib/json-out.js";
|
|
8
12
|
import { collectRuntimeFindings, checkLocalEnv } from "../lib/runtime.js";
|
|
9
13
|
import { notADoctrinaProject } from "../lib/exit-codes.js";
|
|
@@ -35,96 +39,14 @@ export const jsonNative = true;
|
|
|
35
39
|
|
|
36
40
|
export const flags = { boolean: ["json", "env"], string: [] };
|
|
37
41
|
|
|
38
|
-
// Signals that a request is about the running system rather than about what
|
|
39
|
-
// the system should do. Weighted: a phrase that can only mean runtime counts
|
|
40
|
-
// for more than a word that merely leans that way.
|
|
41
|
-
const RUNTIME_SIGNALS = [
|
|
42
|
-
[3, /\b(workflow|pipeline|ci|github actions|actions run|job|runner)\b/i],
|
|
43
|
-
[3, /\b(env var|environment variable|dotenv|\.env|secret|secrets|vars)\b/i],
|
|
44
|
-
[3, /\b(0 (tests?|scenarios?|cases?)|no (tests?|scenarios?) ran|ran nothing|empty (run|suite|summary))\b/i],
|
|
45
|
-
[2, /\b(exit code|stack ?trace|traceback|logs?|step summary|artifact missing)\b/i],
|
|
46
|
-
[2, /\b(green but|passes but|silently|never (fires|applies|reaches|ran))\b/i],
|
|
47
|
-
[2, /\b(flaky|timeout|timed out|hangs?|crash(es|ed)?|broken build|failing build)\b/i],
|
|
48
|
-
[2, /\b(tag|selector|filter|dispatch)\b/i],
|
|
49
|
-
[1, /\b(config|configuration|wiring|wired|deploy(ment)?|docker|compose)\b/i],
|
|
50
|
-
[1, /\b(debug|diagnos(e|is)|investigate|why (is|does|did)|not working|doesn't work)\b/i],
|
|
51
|
-
];
|
|
52
42
|
|
|
53
|
-
const CHORE_SIGNALS = [
|
|
54
|
-
[3, /\b(bump|upgrade|pin|lockfile|dependency|dependencies|devdependenc)\b/i],
|
|
55
|
-
[2, /\b(rename|move|reorganis|reorganiz|refactor|tidy|cleanup|clean up|format(ting)?|lint)\b/i],
|
|
56
|
-
[2, /\b(typo|comment|readme|changelog|docs? only)\b/i],
|
|
57
|
-
[2, /\b(already (specified|in the spec)|wiring only|no behaviour change|no behavior change)\b/i],
|
|
58
|
-
];
|
|
59
43
|
|
|
60
|
-
const PRODUCT_SIGNALS = [
|
|
61
|
-
[3, /\b(add|introduce|support|allow|enable|implement|build)\b/i],
|
|
62
|
-
[3, /\b(should|shall|must|requirement|acceptance|criteri)\b/i],
|
|
63
|
-
// AUTHORING VERBS. Both lanes above match domain NOUNS, which turned out
|
|
64
|
-
// to misread work ON the machinery as an incident IN it: "declare the
|
|
65
|
-
// release workflow wiring in a contract" scored RUNTIME on "workflow" and
|
|
66
|
-
// "wiring", and "so an intentional rename stops warning" scored CHORE on
|
|
67
|
-
// "rename" — a noun, not the act. Neither prompt was a diagnosis; both
|
|
68
|
-
// were someone sitting down to write an artifact.
|
|
69
|
-
//
|
|
70
|
-
// These are the verbs of authoring. A prompt whose verb is "declare" or
|
|
71
|
-
// "document" is describing work on the tree, whatever nouns surround it.
|
|
72
|
-
[3, /\b(declare|declaring|document|documenting|record|recording|specify|specifying|define|defining)\b/i],
|
|
73
|
-
[2, /\b(new (feature|capability|command|endpoint|screen|flow))\b/i],
|
|
74
|
-
[2, /\b(users? (can|should|want)|so that|in order to)\b/i],
|
|
75
|
-
[1, /\b(change|behaviour|behavior|spec)\b/i],
|
|
76
|
-
];
|
|
77
44
|
|
|
78
45
|
/**
|
|
79
46
|
* Score a prompt against each lane. Deterministic, explainable, and cheap:
|
|
80
47
|
* the caller gets the winning lane AND the signals that produced it, so a
|
|
81
48
|
* wrong guess is arguable rather than mysterious.
|
|
82
49
|
*/
|
|
83
|
-
export function classify(prompt) {
|
|
84
|
-
const text = String(prompt ?? "");
|
|
85
|
-
const score = (signals) => {
|
|
86
|
-
let total = 0;
|
|
87
|
-
const hits = [];
|
|
88
|
-
for (const [weight, re] of signals) {
|
|
89
|
-
const m = text.match(re);
|
|
90
|
-
if (m) {
|
|
91
|
-
total += weight;
|
|
92
|
-
hits.push(m[0].toLowerCase());
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return { total, hits };
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const lanes = {
|
|
99
|
-
runtime: score(RUNTIME_SIGNALS),
|
|
100
|
-
chore: score(CHORE_SIGNALS),
|
|
101
|
-
product: score(PRODUCT_SIGNALS),
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
// Product is the default: when nothing distinguishes the request, the
|
|
105
|
-
// ceremony that writes a spec delta is the safe one to fall into. Runtime
|
|
106
|
-
// must BEAT product to win, so an ordinary feature request mentioning a
|
|
107
|
-
// "job" does not get diverted into a diagnosis.
|
|
108
|
-
let lane = "product";
|
|
109
|
-
let best = lanes.product.total;
|
|
110
|
-
for (const name of ["runtime", "chore"]) {
|
|
111
|
-
if (lanes[name].total > best) {
|
|
112
|
-
lane = name;
|
|
113
|
-
best = lanes[name].total;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (best === 0) lane = "product";
|
|
117
|
-
|
|
118
|
-
// Confidence is the MARGIN over the runner-up, not the winner's own score:
|
|
119
|
-
// a prompt that scores 5 for runtime and 4 for product is a coin toss the
|
|
120
|
-
// agent should see as one, and a clear product request must be able to read
|
|
121
|
-
// as confident too (comparing a lane against itself never could).
|
|
122
|
-
const runnerUp = Object.entries(lanes)
|
|
123
|
-
.filter(([name]) => name !== lane)
|
|
124
|
-
.reduce((max, [, v]) => Math.max(max, v.total), 0);
|
|
125
|
-
|
|
126
|
-
return { lane, scores: lanes, confident: best > 0 && best - runnerUp >= 2 };
|
|
127
|
-
}
|
|
128
50
|
|
|
129
51
|
export async function run(positional, cmdFlags) {
|
|
130
52
|
const projectRoot = process.cwd();
|
package/src/commands/upgrade.js
CHANGED
|
@@ -9,6 +9,7 @@ import { flagBool } from "../lib/args.js";
|
|
|
9
9
|
import { c } from "../lib/colors.js";
|
|
10
10
|
import { today } from "../lib/dates.js";
|
|
11
11
|
import { notADoctrinaProject } from "../lib/exit-codes.js";
|
|
12
|
+
import { collectFindings } from "../lib/templates-model.js";
|
|
12
13
|
import * as templates from "./templates.js";
|
|
13
14
|
import * as validate from "./validate.js";
|
|
14
15
|
|
|
@@ -56,11 +57,23 @@ export async function run(_positional, flags) {
|
|
|
56
57
|
// 2. Additive template updates (missing recommended sections in AGENTS.md /
|
|
57
58
|
// product.md, missing index.json fields). Reuses `templates update`
|
|
58
59
|
// verbatim: preview without --write, apply with it.
|
|
59
|
-
console.log(c.gray("──── 1/3 scaffold shape
|
|
60
|
+
console.log(c.gray("──── 1/3 scaffold shape"));
|
|
60
61
|
const tplFlags = writeMode ? new Map([["write", true]]) : new Map();
|
|
61
62
|
const tplCode = await templates.run(["update"], tplFlags);
|
|
62
63
|
if (tplCode !== 0 && !writeMode) pending += 1; // preview exits 1 when updates are pending
|
|
63
64
|
|
|
65
|
+
// What the additive update cannot fix — a drifted adapter, a broken
|
|
66
|
+
// playbook, an index schema it does not know — used to be visible only to
|
|
67
|
+
// `templates check`. Printed here, each with its own remedy, so the one
|
|
68
|
+
// command that brings a project up to date also says what is left for a
|
|
69
|
+
// person (change 0178: the check and the update became this step).
|
|
70
|
+
const manual = collectFindings(projectRoot).findings
|
|
71
|
+
.filter((f) => !/upgrade --write|index rebuild/.test(f.remedy ?? ""));
|
|
72
|
+
for (const f of manual) {
|
|
73
|
+
console.log(c.yellow("✗ ") + f.message);
|
|
74
|
+
console.log(c.gray(" fix: ") + (f.remedy ? c.cyan(f.remedy) : c.gray("manual repair — no command can fix this")));
|
|
75
|
+
}
|
|
76
|
+
|
|
64
77
|
// 3. Index: rebuild from the tree, which also migrates the framework stamp.
|
|
65
78
|
console.log("");
|
|
66
79
|
console.log(c.gray("──── 2/3 index + framework stamp"));
|
|
@@ -101,14 +114,22 @@ export async function run(_positional, flags) {
|
|
|
101
114
|
const vCode = await validate.run([], vFlags);
|
|
102
115
|
|
|
103
116
|
console.log("");
|
|
117
|
+
if (manual.length > 0) {
|
|
118
|
+
console.log(c.yellow("manual") + ` ${manual.length} finding${manual.length === 1 ? "" : "s"} the upgrade cannot repair — see step 1`);
|
|
119
|
+
}
|
|
104
120
|
if (!writeMode) {
|
|
105
|
-
if (pending > 0) {
|
|
121
|
+
if (pending > 0 || manual.length > 0) {
|
|
122
|
+
if (pending === 0) return 1;
|
|
106
123
|
console.log(c.yellow("pending") + ` upgrade steps found — apply them: ${c.cyan("doctrina upgrade --write")}`);
|
|
107
124
|
return 1;
|
|
108
125
|
}
|
|
109
126
|
console.log(c.green("ok") + " nothing to upgrade — the project matches the installed CLI");
|
|
110
127
|
return vCode;
|
|
111
128
|
}
|
|
129
|
+
if (vCode === 0 && manual.length > 0) {
|
|
130
|
+
console.log(c.yellow(`upgraded to ${running} with manual repairs left`) + " — each finding in step 1 names its fix.");
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
112
133
|
if (vCode === 0) {
|
|
113
134
|
console.log(c.green(`✓ project upgraded to ${running}`) + c.gray(" — review any warnings above; new commands: `doctrina --help`."));
|
|
114
135
|
console.log(c.gray("AGENTS.md's doctrina:surface block was regenerated from the installed catalog, so agents reading the hub now discover the current commands."));
|
|
@@ -125,16 +146,20 @@ Bring an existing project up to the installed CLI after an npm update.
|
|
|
125
146
|
The project keeps the scaffold of the version that init-ed it; this is
|
|
126
147
|
the one command that closes the gap, orchestrating the existing pieces:
|
|
127
148
|
|
|
128
|
-
1.
|
|
149
|
+
1. scaffold shape — regenerate the AGENTS.md doctrina:surface block
|
|
129
150
|
from the installed command catalog (the block is
|
|
130
151
|
CLI-owned, ADR 0015 — this is how agents reading
|
|
131
|
-
the hub discover commands added since init),
|
|
152
|
+
the hub discover commands added since init),
|
|
132
153
|
append missing recommended sections / index.json
|
|
133
|
-
fields (additive-only outside the block)
|
|
154
|
+
fields (additive-only outside the block), and
|
|
155
|
+
list what no command can repair (a drifted
|
|
156
|
+
adapter, a broken playbook), each with its fix
|
|
134
157
|
2. index rebuild — regenerate index.json from the tree and migrate
|
|
135
158
|
the framework_version stamp to the running CLI
|
|
136
159
|
3. validate (--fix) — surface anything the upgrade cannot fix
|
|
137
160
|
(hand-authored drift, new validate checks)
|
|
138
161
|
|
|
139
|
-
Preview by default (exits 1 when steps are pending
|
|
162
|
+
Preview by default (exits 1 when steps are pending or a manual repair is
|
|
163
|
+
left); --write applies. The preview is what \`templates check\` reported
|
|
164
|
+
and --write what \`templates update --write\` did — both deprecated.
|
|
140
165
|
`;
|