doctrina-cli 0.18.0 → 0.21.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 +3 -2
- package/package.json +1 -1
- package/src/commands/adapter.js +84 -0
- package/src/commands/analyze.js +14 -3
- package/src/commands/change.js +164 -17
- package/src/commands/clarify.js +1 -204
- package/src/commands/close.js +18 -1
- package/src/commands/context.js +81 -9
- package/src/commands/contract.js +1 -0
- package/src/commands/delivery.js +356 -0
- package/src/commands/doctor.js +21 -1
- package/src/commands/handoff.js +11 -2
- package/src/commands/index-rebuild.js +23 -0
- package/src/commands/init.js +28 -1
- package/src/commands/intake.js +60 -3
- package/src/commands/metrics.js +43 -3
- package/src/commands/next.js +41 -6
- package/src/commands/prime.js +125 -7
- package/src/commands/review.js +39 -3
- package/src/commands/skill.js +2 -1
- package/src/commands/status.js +28 -0
- package/src/commands/upgrade.js +33 -0
- package/src/commands/validate.js +18 -0
- package/src/commands/verify.js +32 -4
- package/src/commands/work.js +126 -7
- package/src/commands/workspace.js +419 -0
- package/src/index.js +100 -66
- package/src/lib/actions.js +44 -4
- package/src/lib/adapters.js +143 -1
- package/src/lib/args.js +33 -1
- package/src/lib/clarify-scan.js +221 -0
- package/src/lib/code-inventory.js +770 -0
- package/src/lib/commands.js +52 -5
- package/src/lib/context-slice.js +149 -0
- package/src/lib/delivery.js +286 -0
- package/src/lib/docs-impact.js +36 -1
- package/src/lib/fs-ops.js +23 -0
- package/src/lib/intake-model.js +1 -1
- package/src/lib/json-out.js +9 -1
- package/src/lib/metrics-model.js +23 -1
- package/src/lib/playbook.js +1 -1
- package/src/lib/shell.js +45 -0
- package/src/lib/skill-export.js +95 -0
- package/src/lib/snapshot.js +43 -2
- package/src/lib/task-card.js +195 -0
- package/src/lib/views.js +52 -3
- package/src/lib/work-model.js +66 -2
- package/src/lib/workspace-agents.js +306 -0
- package/src/lib/workspace-guard.js +65 -0
- package/src/lib/workspace-locate.js +141 -0
- package/src/lib/workspace-tools.js +160 -0
- package/src/lib/workspace-view.js +391 -0
- package/src/lib/workspace.js +323 -0
- package/templates/AGENTS.md.template +17 -14
- package/templates/README.md +11 -1
- package/templates/adapters/claude/.claude/settings.json.template +18 -0
- package/templates/adapters/cursor/.cursor/hooks.json.template +8 -0
- package/templates/adapters/cursor/.cursor/rules/00-doctrina.mdc.template +14 -9
- package/templates/adapters/cursor/.cursor/{commands/doctrina-context.md.template → skills/doctrina-context/SKILL.md.template} +6 -0
- package/templates/adapters/cursor/.cursor/{commands/doctrina-next.md.template → skills/doctrina-next/SKILL.md.template} +6 -0
- package/templates/adapters/cursor/.cursor/{commands/doctrina-status.md.template → skills/doctrina-status/SKILL.md.template} +6 -0
- package/templates/adapters/cursor/.cursor/{commands/doctrina-why.md.template → skills/doctrina-why/SKILL.md.template} +6 -0
- package/templates/adapters/cursor/.cursor/{commands/doctrina-work.md.template → skills/doctrina-work/SKILL.md.template} +6 -0
- package/templates/delivery.md.template +39 -0
- package/templates/playbooks/absorb.md.template +63 -0
- package/templates/playbooks/bootstrap.md.template +18 -6
- package/templates/playbooks/delivery.md.template +44 -0
- package/templates/playbooks/work.md.template +1 -1
package/src/commands/clarify.js
CHANGED
|
@@ -5,112 +5,9 @@ import { readdirSync } from "node:fs";
|
|
|
5
5
|
import { exists, isDir, isFile, read, relPath, walk } from "../lib/fs-ops.js";
|
|
6
6
|
import { flagBool, flagString } from "../lib/args.js";
|
|
7
7
|
import { c } from "../lib/colors.js";
|
|
8
|
-
import {
|
|
9
|
-
import { loadConfig } from "../lib/config.js";
|
|
8
|
+
import { scanFile } from "../lib/clarify-scan.js";
|
|
10
9
|
import { EXIT, notADoctrinaProject } from "../lib/exit-codes.js";
|
|
11
10
|
|
|
12
|
-
// Two lexicons, one linter (0.11.0 field review item 3: an English-only
|
|
13
|
-
// clarify is permanently red on a PT-BR project — "some" is the verb *sumir*,
|
|
14
|
-
// "TODO o pensamento" is the pronoun *todo* — which teaches people to ignore
|
|
15
|
-
// the gate). The language comes from `.doctrina/config.json` ("language":
|
|
16
|
-
// "pt-BR" | "en"), else a per-file stopword heuristic. Portuguese mode swaps
|
|
17
|
-
// the ambiguous EN tokens for a PT lexicon; TODO only counts with a colon
|
|
18
|
-
// ("TODO:") since bare TODO is the pronoun.
|
|
19
|
-
const RULES_EN = [
|
|
20
|
-
{
|
|
21
|
-
// "may" is intentionally NOT in this list: EARS Optional grammar
|
|
22
|
-
// uses "the system may ..." legitimately, and a clarify pass that
|
|
23
|
-
// flags every EARS Optional is unusable on Doctrina-style specs.
|
|
24
|
-
// The remaining weasel terms are unambiguous.
|
|
25
|
-
name: "weasel",
|
|
26
|
-
re: /\b(might|could|should\s+probably|perhaps|maybe|approximately|roughly)\b/gi,
|
|
27
|
-
hint: "state explicitly with a definite verb",
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
name: "vague",
|
|
31
|
-
// Two exclusions, both structural rather than a vocabulary loosening.
|
|
32
|
-
// The lookahead was already here: `many 5` quantifies. The lookbehind is
|
|
33
|
-
// change 0079: `how many` is INTERROGATIVE, and a requirement that says
|
|
34
|
-
// "shall report how many contracts declared no rows" names exactly the
|
|
35
|
-
// number the command must print — the opposite of vague. Five of this
|
|
36
|
-
// repository's seventeen smells were that phrase, and the noise is what
|
|
37
|
-
// hid the one real finding among them ("name some of them", which said
|
|
38
|
-
// nothing about how many). A gate that cries wolf teaches people to
|
|
39
|
-
// ignore it.
|
|
40
|
-
//
|
|
41
|
-
// The quality adjectives sit here for the same reason the quantifiers do:
|
|
42
|
-
// "robust" names no threshold, so it cannot be met or missed. They were
|
|
43
|
-
// in the Portuguese lexicon and nowhere in this one, so the SAME sentence
|
|
44
|
-
// translated came back with three smells in one language and none in the
|
|
45
|
-
// other — and this project keeps its own pages in both. A smell test that
|
|
46
|
-
// answers differently per language is not testing the writing.
|
|
47
|
-
re: /(?<!\bhow\s+)\b(many|few|some|several|adequate|robust|scalable)\b(?!\s+\d)/gi,
|
|
48
|
-
hint: "quantify (use a number or a precise scope)",
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
name: "placeholder",
|
|
52
|
-
re: /\b(TBD|TODO|FIXME|XXX|\?\?\?)\b/g,
|
|
53
|
-
hint: "resolve before applying",
|
|
54
|
-
},
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
// How much of the previous line a rule may look back into. Long enough for
|
|
58
|
-
// a wrapped two-word phrase, short enough that the probe stays cheap.
|
|
59
|
-
const LOOKBEHIND_TAIL = 40;
|
|
60
|
-
|
|
61
|
-
const RULES_PT = [
|
|
62
|
-
{
|
|
63
|
-
name: "weasel",
|
|
64
|
-
re: /\b(talvez|possivelmente|provavelmente|aproximadamente|quem sabe|eventualmente)\b/gi,
|
|
65
|
-
hint: "afirme explicitamente, com um verbo definido",
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
// ASCII-folded variants included so "varios"/"vários" both match.
|
|
69
|
-
name: "vague",
|
|
70
|
-
re: /\b(muitos|muitas|poucos|poucas|v[aá]rios|v[aá]rias|alguns|algumas|adequado|adequada|robusto|robusta|escal[aá]vel)\b(?!\s+\d)/gi,
|
|
71
|
-
hint: "quantifique (um número ou um escopo preciso)",
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
// Bare TODO is the Portuguese pronoun; only TODO: is a marker here.
|
|
75
|
-
name: "placeholder",
|
|
76
|
-
re: /\b(TBD|FIXME|XXX|\?\?\?)\b|\bTODO:/g,
|
|
77
|
-
hint: "resolva antes de aplicar",
|
|
78
|
-
},
|
|
79
|
-
];
|
|
80
|
-
|
|
81
|
-
// Language for a file: the project-declared `.doctrina/config.json`
|
|
82
|
-
// ("language": "pt-BR" / "pt" / "en") wins; otherwise a deterministic
|
|
83
|
-
// stopword count over the file text decides. Never semantic (ADR 0005).
|
|
84
|
-
|
|
85
|
-
function projectLanguage(projectRoot) {
|
|
86
|
-
// One configuration reader (lib/config.js, change 0047): the key still
|
|
87
|
-
// lives in .doctrina/config.json, but `doctor` can now show what it
|
|
88
|
-
// resolved to, which is what a pt-BR project sitting red needed.
|
|
89
|
-
return loadConfig(projectRoot).language;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Which lexicon a document is written in — the shared detector (change
|
|
93
|
-
// 0040), so `clarify` and any future consumer count the same grammar words.
|
|
94
|
-
function detectLang(text) {
|
|
95
|
-
return detectLanguage(text);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function rulesFor(projectRoot, text, forcedLang = null) {
|
|
99
|
-
const lang = forcedLang ?? projectLanguage(projectRoot);
|
|
100
|
-
if (lang) return lang === "pt" ? RULES_PT : RULES_EN;
|
|
101
|
-
// Detected per file, the document is scanned with BOTH lexicons (change
|
|
102
|
-
// 0107). A bilingual spec — Portuguese purpose, English EARS, or the
|
|
103
|
-
// reverse — detected as one language hid every smell written in the
|
|
104
|
-
// other: `clarify --all` found nothing where `--lang pt` found two. The
|
|
105
|
-
// two vocabularies do not overlap, so a smell in either is a smell; the
|
|
106
|
-
// detected language only decides which hint text the reader gets first.
|
|
107
|
-
const detected = detectLang(text);
|
|
108
|
-
const [first, second] = detected === "pt" ? [RULES_PT, RULES_EN] : [RULES_EN, RULES_PT];
|
|
109
|
-
// The `placeholder` rule is language-neutral and lives in both lists:
|
|
110
|
-
// one copy, or TBD is reported twice.
|
|
111
|
-
return [...first, ...second.filter((r) => r.name !== "placeholder")];
|
|
112
|
-
}
|
|
113
|
-
|
|
114
11
|
// --lang pt|en wins over the project config and the per-file heuristic
|
|
115
12
|
// (operator review 2026-07-19 §4.8: on a PT-BR project with no config.json,
|
|
116
13
|
// a file whose stopword count tips English gets the EN lexicon and "some" —
|
|
@@ -221,106 +118,6 @@ function clarifyAll(projectRoot, lang = null) {
|
|
|
221
118
|
return 1;
|
|
222
119
|
}
|
|
223
120
|
|
|
224
|
-
function scanFile(fullPath, projectRoot = process.cwd(), lang = null) {
|
|
225
|
-
const text = read(fullPath);
|
|
226
|
-
const skippedRanges = collectSkippedRanges(text);
|
|
227
|
-
const rules = rulesFor(projectRoot, text, lang);
|
|
228
|
-
const smells = [];
|
|
229
|
-
|
|
230
|
-
// Per-line rules: regex pass with skip-range filter. A line carrying the
|
|
231
|
-
// inline suppression marker `<!-- clarify:ok -->` is deliberately accepted
|
|
232
|
-
// by the author — skip it entirely (the escape hatch for a false positive
|
|
233
|
-
// the lexicon cannot know about; "block, never imprison").
|
|
234
|
-
const lines = text.split("\n");
|
|
235
|
-
let cursor = 0;
|
|
236
|
-
for (let i = 0; i < lines.length; i++) {
|
|
237
|
-
const line = lines[i];
|
|
238
|
-
const lineStart = cursor;
|
|
239
|
-
const lineEnd = cursor + line.length;
|
|
240
|
-
if (line.includes("clarify:ok")) {
|
|
241
|
-
cursor = lineEnd + 1;
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
// A rule reads the previous line as CONTEXT, never as content: a match
|
|
245
|
-
// is only reported when it starts inside this line. Prose wraps, and
|
|
246
|
-
// "report per spec how / many criteria..." split the interrogative
|
|
247
|
-
// across a line break — which a line-at-a-time scanner cannot see, so
|
|
248
|
-
// the phrase came back as a smell on one line of the same document and
|
|
249
|
-
// not on the next (change 0079). The prefix is trimmed to the tail of
|
|
250
|
-
// the previous line: enough for a lookbehind, cheap on every line.
|
|
251
|
-
const prev = i > 0 ? lines[i - 1].slice(-LOOKBEHIND_TAIL) : "";
|
|
252
|
-
const probe = prev === "" ? line : `${prev}\n${line}`;
|
|
253
|
-
const offset = probe.length - line.length;
|
|
254
|
-
for (const rule of rules) {
|
|
255
|
-
rule.re.lastIndex = 0;
|
|
256
|
-
let m;
|
|
257
|
-
while ((m = rule.re.exec(probe))) {
|
|
258
|
-
if (m.index < offset) continue; // the match belongs to the previous line
|
|
259
|
-
const absoluteStart = lineStart + m.index - offset;
|
|
260
|
-
if (isInSkippedRange(absoluteStart, skippedRanges)) continue;
|
|
261
|
-
smells.push({
|
|
262
|
-
line: i + 1,
|
|
263
|
-
rule: rule.name,
|
|
264
|
-
token: m[0],
|
|
265
|
-
hint: rule.hint,
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
cursor = lineEnd + 1; // +1 for the newline
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Section rule: empty Acceptance criteria.
|
|
273
|
-
if (hasEmptyAcceptanceCriteria(text)) {
|
|
274
|
-
smells.push({
|
|
275
|
-
line: findAcceptanceLine(text),
|
|
276
|
-
rule: "empty-section",
|
|
277
|
-
token: "Acceptance criteria",
|
|
278
|
-
hint: "fill in observable, verifiable signals",
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
smells.sort((a, b) => a.line - b.line);
|
|
283
|
-
return smells;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Build ranges to skip: fenced code blocks, HTML comments, inline backticks.
|
|
287
|
-
function collectSkippedRanges(text) {
|
|
288
|
-
const ranges = [];
|
|
289
|
-
// Fenced code blocks (``` ... ```)
|
|
290
|
-
for (const m of text.matchAll(/```[\s\S]*?```/g)) ranges.push([m.index, m.index + m[0].length]);
|
|
291
|
-
// HTML comments (<!-- ... -->)
|
|
292
|
-
for (const m of text.matchAll(/<!--[\s\S]*?-->/g)) ranges.push([m.index, m.index + m[0].length]);
|
|
293
|
-
// Inline backtick spans (`...`)
|
|
294
|
-
for (const m of text.matchAll(/`[^`\n]+`/g)) ranges.push([m.index, m.index + m[0].length]);
|
|
295
|
-
return ranges;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function isInSkippedRange(offset, ranges) {
|
|
299
|
-
for (const [a, b] of ranges) if (offset >= a && offset < b) return true;
|
|
300
|
-
return false;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function hasEmptyAcceptanceCriteria(text) {
|
|
304
|
-
const headingRe = /^##\s+Acceptance criteria\s*$/m;
|
|
305
|
-
const m = headingRe.exec(text);
|
|
306
|
-
if (!m) return false;
|
|
307
|
-
const after = text.slice(m.index + m[0].length);
|
|
308
|
-
// Find the body between this heading and the next heading of equal level.
|
|
309
|
-
const nextHeadingMatch = after.match(/^##\s+/m);
|
|
310
|
-
const body = nextHeadingMatch ? after.slice(0, nextHeadingMatch.index) : after;
|
|
311
|
-
// Strip HTML comments before checking emptiness.
|
|
312
|
-
const stripped = body.replace(/<!--[\s\S]*?-->/g, "").trim();
|
|
313
|
-
return stripped.length === 0;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function findAcceptanceLine(text) {
|
|
317
|
-
const lines = text.split("\n");
|
|
318
|
-
for (let i = 0; i < lines.length; i++) {
|
|
319
|
-
if (/^##\s+Acceptance criteria\s*$/.test(lines[i])) return i + 1;
|
|
320
|
-
}
|
|
321
|
-
return 1;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
121
|
export const help = `
|
|
325
122
|
Usage: doctrina clarify <path> [--lang pt|en]
|
|
326
123
|
doctrina clarify --all [--lang pt|en]
|
package/src/commands/close.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
|
-
import { appendFileSync } from "node:fs";
|
|
4
|
+
import { appendFileSync, readdirSync } from "node:fs";
|
|
5
5
|
import { exists, isDir, isFile, read } from "../lib/fs-ops.js";
|
|
6
6
|
import { flagBool } from "../lib/args.js";
|
|
7
7
|
import { c } from "../lib/colors.js";
|
|
@@ -166,6 +166,8 @@ async function closeOne(projectRoot, id, flags) {
|
|
|
166
166
|
// archive), which is rendering, not sequencing.
|
|
167
167
|
const verifyConfigured = isFile(path.join(projectRoot, ".doctrina", "verify.json"));
|
|
168
168
|
const runners = {
|
|
169
|
+
// An applied change resumes here: analyze asks it only the integrity
|
|
170
|
+
// checks (change 0221).
|
|
169
171
|
analyze: { run: () => analyze.run([id], new Map()) },
|
|
170
172
|
|
|
171
173
|
// ADR checkpoint (operator review §4.6): the playbook's "record an ADR"
|
|
@@ -468,6 +470,13 @@ async function closeOne(projectRoot, id, flags) {
|
|
|
468
470
|
|
|
469
471
|
console.log("");
|
|
470
472
|
console.log(c.green(`✓ change ${id} closed`) + c.gray(closingClaim(ran, skippedSteps)));
|
|
473
|
+
// Where it went, and the sentence for the human (change 0214): the close
|
|
474
|
+
// moves the change out of every default read, and a field review found
|
|
475
|
+
// the person asking for "the doctrina work" of work already archived.
|
|
476
|
+
const archived = archivedPathOf(projectRoot, id);
|
|
477
|
+
if (archived) {
|
|
478
|
+
console.log(c.bold("Tell the human: ") + `change ${id} is closed — archived at ${archived}`);
|
|
479
|
+
}
|
|
471
480
|
console.log(c.gray("Next: ") + c.cyan("doctrina next"));
|
|
472
481
|
return 0;
|
|
473
482
|
}
|
|
@@ -500,6 +509,14 @@ function closingClaim(ran, skippedSteps) {
|
|
|
500
509
|
return `${claimed || " —"} ${skipped.length === 1 ? "Skipped" : "Skipped"}: ${skipped.join(", ")}.`;
|
|
501
510
|
}
|
|
502
511
|
|
|
512
|
+
// The archive folder a just-closed change landed in, newest first.
|
|
513
|
+
function archivedPathOf(projectRoot, id) {
|
|
514
|
+
const dir = path.join(projectRoot, ".doctrina", "changes", "archive");
|
|
515
|
+
if (!isDir(dir)) return null;
|
|
516
|
+
const name = readdirSync(dir).sort().reverse().find((n) => n.endsWith(`-${id}`) && /^\d{4}-\d{2}-\d{2}-/.test(n));
|
|
517
|
+
return name ? `.doctrina/changes/archive/${name}/` : null;
|
|
518
|
+
}
|
|
519
|
+
|
|
503
520
|
function missingRunner(step) {
|
|
504
521
|
console.error(c.red("error:") +
|
|
505
522
|
` "${step.label}" is declared in the close sequence but has no runner here`);
|
package/src/commands/context.js
CHANGED
|
@@ -14,6 +14,9 @@ import { GIT_STATE, historyState, changedFiles } from "../lib/git.js";
|
|
|
14
14
|
import { terms as queryTerms, relevance } from "../lib/lexicon.js";
|
|
15
15
|
import { notADoctrinaProject, EXIT } from "../lib/exit-codes.js";
|
|
16
16
|
import * as idx from "../lib/index-json.js";
|
|
17
|
+
import { adrDigest, sliceSpec, withoutComments } from "../lib/context-slice.js";
|
|
18
|
+
import { contextRouting, workspaceActions, workspaceFindings, workspaceHandoff, workspaceRootAt, workspaceSnapshot, workspaceStatus } from "../lib/workspace-view.js";
|
|
19
|
+
import { cdThen, qarg } from "../lib/shell.js";
|
|
17
20
|
|
|
18
21
|
// Materialise the AGENTS.md read order as a command: print the exact
|
|
19
22
|
// context pack for a task, in the order an agent should read it. This is
|
|
@@ -72,15 +75,33 @@ export const TIER = { CORE: 0, SPEC: 1, CHANGE: 2, DEPENDENCY: 3, DECISION: 4 };
|
|
|
72
75
|
// Flags this command accepts. Declared HERE, with the command, so
|
|
73
76
|
// adding a command never requires editing the entrypoint — the gap that
|
|
74
77
|
// let six flags ship undeclared and silently swallow a positional (C3).
|
|
75
|
-
export const flags = { boolean: ["json", "concat"], string: ["budget", "diff", "for"] };
|
|
78
|
+
export const flags = { boolean: ["json", "concat", "full"], string: ["budget", "diff", "for"] };
|
|
76
79
|
|
|
77
80
|
export async function run(positional, cmdFlags) {
|
|
78
81
|
const capability = positional[0] ?? null;
|
|
79
82
|
const concat = flagBool(cmdFlags, "concat", false);
|
|
83
|
+
// --full: the whole text of everything, as before change 0216 — AGENTS.md
|
|
84
|
+
// repeated, every spec and ADR whole. The lean pack is the default.
|
|
85
|
+
const full = flagBool(cmdFlags, "full", false);
|
|
80
86
|
const budgetRaw = flagString(cmdFlags, "budget");
|
|
81
87
|
const diffRef = flagString(cmdFlags, "diff");
|
|
82
88
|
const query = flagString(cmdFlags, "for");
|
|
83
89
|
const projectRoot = process.cwd();
|
|
90
|
+
// At a workspace root there is no one spec tree to read: the pack is per
|
|
91
|
+
// repository. Name which repositories the task is about, and the command
|
|
92
|
+
// that reads each one's pack (change 0230).
|
|
93
|
+
const wsRoot = workspaceRootAt(projectRoot);
|
|
94
|
+
if (wsRoot) {
|
|
95
|
+
const ranked = contextRouting(workspaceSnapshot(wsRoot), query ?? capability ?? "");
|
|
96
|
+
console.log(c.bold("The context pack is read inside a repository.") + c.gray(query ? ` Projects for "${query}":` : " Projects:"));
|
|
97
|
+
for (const r of ranked) {
|
|
98
|
+
const mark = r.state === "ready" ? "" : c.yellow(` (${r.state})`);
|
|
99
|
+
console.log(` ${r.name.padEnd(16)} ${query ? c.gray(`${r.score} shared term${r.score === 1 ? "" : "s"}${r.shared.length ? ` (${r.shared.slice(0, 5).join(", ")})` : ""}`) : ""}${mark}`);
|
|
100
|
+
console.log(` ${c.cyan(cdThen(r.path, `doctrina context ${capability ?? "<capability>"}${query ? ` --for ${qarg(query)}` : ""} --concat`))}`);
|
|
101
|
+
}
|
|
102
|
+
if (ranked.length === 0) console.log(c.gray(" none registered — `doctrina workspace add <path>`"));
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
84
105
|
if (!exists(path.join(projectRoot, ".doctrina"))) {
|
|
85
106
|
throw notADoctrinaProject();
|
|
86
107
|
}
|
|
@@ -162,6 +183,8 @@ export async function run(positional, cmdFlags) {
|
|
|
162
183
|
tier: TIER.CORE,
|
|
163
184
|
rank: [],
|
|
164
185
|
degraded: false,
|
|
186
|
+
/** @type {string | null} */ slice: null,
|
|
187
|
+
/** @type {string | null} */ digest: null,
|
|
165
188
|
...extra,
|
|
166
189
|
};
|
|
167
190
|
pack.push(item);
|
|
@@ -170,7 +193,10 @@ export async function run(positional, cmdFlags) {
|
|
|
170
193
|
|
|
171
194
|
// 1-2. Root rules and product truth — always first. Displayed paths are
|
|
172
195
|
// POSIX strings (path.join would emit backslashes on Windows).
|
|
173
|
-
|
|
196
|
+
// AGENTS.md is not repeated by default (change 0216): every agent this
|
|
197
|
+
// pack is for loads it already — natively, or through its adapter — and
|
|
198
|
+
// it cost ~2.5k tokens per pack to say it twice.
|
|
199
|
+
if (wanted("AGENTS.md") && full) pushFile("AGENTS.md", "root rules");
|
|
174
200
|
if (wanted(".doctrina/product.md")) pushFile(".doctrina/product.md", "product truth");
|
|
175
201
|
|
|
176
202
|
// 3. The capability spec(s) — the current truth. With a named capability,
|
|
@@ -241,6 +267,21 @@ export async function run(positional, cmdFlags) {
|
|
|
241
267
|
}
|
|
242
268
|
}
|
|
243
269
|
|
|
270
|
+
// A spec read for a task — the task's own, and the ones it depends on — is
|
|
271
|
+
// cut to what the task is about (change
|
|
272
|
+
// 0216): headers, Purpose, and the requirements and criteria the query
|
|
273
|
+
// matches, the rest named by count. No query, or nothing matched: whole.
|
|
274
|
+
if (!full && terms.length > 0) {
|
|
275
|
+
for (const item of pack) {
|
|
276
|
+
if ((item.tier !== TIER.CORE && item.tier !== TIER.DEPENDENCY) || !item.rel.startsWith(".doctrina/specs/")) continue;
|
|
277
|
+
const sliced = sliceSpec(read(path.join(projectRoot, item.rel)), terms, item.rel);
|
|
278
|
+
if (!sliced) continue;
|
|
279
|
+
item.slice = sliced.text;
|
|
280
|
+
item.tokens = estimateTokens(sliced.text);
|
|
281
|
+
item.note = `${item.note} — sliced: ${sliced.kept} of ${sliced.total} items match`;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
244
285
|
// 4. Open changes (their proposal, tasks, and deltas) — never
|
|
245
286
|
// diff-filtered: work in flight is the one thing a resuming agent
|
|
246
287
|
// cannot reconstruct from anywhere else, so it is always present.
|
|
@@ -295,7 +336,17 @@ export async function run(positional, cmdFlags) {
|
|
|
295
336
|
for (const ch of open) {
|
|
296
337
|
if (ch.id === focusId) {
|
|
297
338
|
// The change being worked on: every file, whole, in CORE.
|
|
298
|
-
for (const f of ch.files)
|
|
339
|
+
for (const f of ch.files) {
|
|
340
|
+
const item = pushFile(relPath(projectRoot, f), `open change: ${ch.id}`);
|
|
341
|
+
// Read, not written: the templates' authoring comments stay out
|
|
342
|
+
// unless --full (change 0228).
|
|
343
|
+
const bare = item && !full ? withoutComments(read(f)) : null;
|
|
344
|
+
if (item && bare !== null) {
|
|
345
|
+
item.slice = bare;
|
|
346
|
+
item.tokens = estimateTokens(bare);
|
|
347
|
+
item.note = `${item.note} — comments left out`;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
299
350
|
continue;
|
|
300
351
|
}
|
|
301
352
|
// A PARKED change is ONE entry in a queue, not three documents. It
|
|
@@ -347,7 +398,7 @@ export async function run(positional, cmdFlags) {
|
|
|
347
398
|
|
|
348
399
|
const id = path.basename(f).match(/^(\d{4})-/)?.[1] ?? "0000";
|
|
349
400
|
const title = getTitle(text) ?? path.basename(f, ".md");
|
|
350
|
-
pushFile(rel, global ? "accepted ADR" : `accepted ADR (${scope.join(", ")})`, {
|
|
401
|
+
const adrItem = pushFile(rel, global ? "accepted ADR" : `accepted ADR (${scope.join(", ")})`, {
|
|
351
402
|
tier: TIER.DECISION,
|
|
352
403
|
// Best-first, deterministic: an ADR that NAMES this capability outranks
|
|
353
404
|
// an unscoped one — which is DECLARED to belong in every pack — which in
|
|
@@ -360,6 +411,13 @@ export async function run(positional, cmdFlags) {
|
|
|
360
411
|
title,
|
|
361
412
|
summary: adrSummary(text),
|
|
362
413
|
});
|
|
414
|
+
// A decision as a digest by default (change 0216): its title and the
|
|
415
|
+
// opening of its Decision — what tells a reader whether it binds the
|
|
416
|
+
// task. Sixteen whole ADRs were ~22k tokens of the `gates` pack.
|
|
417
|
+
if (adrItem && !full) {
|
|
418
|
+
adrItem.digest = adrDigest(text, title, id);
|
|
419
|
+
adrItem.tokens = estimateTokens(adrItem.digest);
|
|
420
|
+
}
|
|
363
421
|
}
|
|
364
422
|
|
|
365
423
|
// 6. Skills are on-demand: list name + description + trigger, never the
|
|
@@ -406,7 +464,8 @@ export async function run(positional, cmdFlags) {
|
|
|
406
464
|
for (const item of kept) {
|
|
407
465
|
console.log(`===== ${item.rel} (${item.note}) =====`);
|
|
408
466
|
console.log("");
|
|
409
|
-
console.log(item.degraded ? degradedBody(item)
|
|
467
|
+
console.log(item.degraded ? degradedBody(item)
|
|
468
|
+
: item.slice ?? item.digest ?? read(path.join(projectRoot, item.rel)).trimEnd());
|
|
410
469
|
console.log("");
|
|
411
470
|
}
|
|
412
471
|
// Keep the pack pipeable: the budget verdict goes to stderr.
|
|
@@ -417,7 +476,7 @@ export async function run(positional, cmdFlags) {
|
|
|
417
476
|
console.log(c.bold("Context pack") + (capability ? c.gray(` (capability: ${capability})`) : "") + forNote + scope + c.gray(" — read in this order:"));
|
|
418
477
|
console.log("");
|
|
419
478
|
kept.forEach((item, i) => {
|
|
420
|
-
const mark = item.degraded ? c.yellow(" summary") : " ";
|
|
479
|
+
const mark = item.degraded ? c.yellow(" summary") : item.slice ? c.yellow(" sliced") : item.digest ? c.yellow(" digest") : " ";
|
|
421
480
|
console.log(`${String(i + 1).padStart(2)}. ${c.cyan(item.rel.padEnd(56))} ${String(item.lines).padStart(4)} lines ${c.gray(`~${String(item.tokens).padStart(5)} tok`)}${mark} ${c.gray(item.note)}`);
|
|
422
481
|
});
|
|
423
482
|
console.log("");
|
|
@@ -709,14 +768,25 @@ function changedPaths(projectRoot, ref) {
|
|
|
709
768
|
|
|
710
769
|
export const help = `
|
|
711
770
|
Usage: doctrina context [<capability>] [--for "<task>"] [--concat]
|
|
712
|
-
[--budget <tokens>] [--diff <ref>]
|
|
771
|
+
[--budget <tokens>] [--diff <ref>] [--full]
|
|
713
772
|
|
|
714
773
|
Print the exact context pack for a task, in the documented read
|
|
715
|
-
order:
|
|
716
|
-
|
|
774
|
+
order: product.md, the capability spec (when given — otherwise every
|
|
775
|
+
active spec), open changes, accepted ADRs. Skills are
|
|
717
776
|
listed name + description + when-trigger only — they are on-demand by
|
|
718
777
|
design. The change archive is excluded.
|
|
719
778
|
|
|
779
|
+
At a workspace root, where no one spec tree exists, it ranks the
|
|
780
|
+
registered projects against --for and prints the command that reads
|
|
781
|
+
each one's pack inside its repository.
|
|
782
|
+
|
|
783
|
+
The pack is LEAN by default: AGENTS.md is not repeated (your agent
|
|
784
|
+
loads it every turn), an accepted ADR is a digest — its title and the
|
|
785
|
+
opening of its Decision; \`doctrina show NNNN\` reads the rest — and
|
|
786
|
+
with --for the task's spec is sliced to its headers, Purpose and the
|
|
787
|
+
requirements and criteria the task matches, the others named by count.
|
|
788
|
+
--full restores every text whole.
|
|
789
|
+
|
|
720
790
|
Assembly is retrieval, not a dump. A token budget ALWAYS applies
|
|
721
791
|
(default ${DEFAULT_BUDGET}; set "context_budget": <n> in
|
|
722
792
|
.doctrina/config.json, or pass --budget). Over budget, accepted ADRs
|
|
@@ -731,6 +801,8 @@ Flags:
|
|
|
731
801
|
the list — ready to pipe into an agent. The budget
|
|
732
802
|
verdict goes to stderr, keeping stdout pure.
|
|
733
803
|
--budget <n> Token ceiling for this call, overriding the project's.
|
|
804
|
+
--full Every artifact whole: AGENTS.md included, specs not
|
|
805
|
+
sliced, ADRs not digested (the pre-0.19 pack).
|
|
734
806
|
--diff <ref> Scope the stable artifacts (AGENTS.md, product.md,
|
|
735
807
|
specs, ADRs) to those changed since the git ref; open
|
|
736
808
|
changes are always included. The resume-session read.
|
package/src/commands/contract.js
CHANGED
|
@@ -278,6 +278,7 @@ the port map, the environment contract, and API/WS/event interfaces.
|
|
|
278
278
|
|
|
279
279
|
Subcommands:
|
|
280
280
|
new <name> Scaffold .doctrina/contracts/<name>.md and index it
|
|
281
|
+
(--force overwrites an existing contract)
|
|
281
282
|
list One line per contract: id, status, last updated
|
|
282
283
|
check [<name>] Verify the mechanically checkable parts.
|
|
283
284
|
|