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/metrics.js
CHANGED
|
@@ -12,6 +12,8 @@ import { notADoctrinaProject } from "../lib/exit-codes.js";
|
|
|
12
12
|
import { USAGE_ENV, summarise } from "../lib/usage.js";
|
|
13
13
|
import { OPERATIONS } from "../lib/commands.js";
|
|
14
14
|
import { REEDIT_WINDOW_DAYS, sinceWindow, computeSnapshot, parseLog, readSeries, latestSnapshot, trend, round } from "../lib/metrics-model.js";
|
|
15
|
+
import { workspaceRootAt, workspaceSnapshot } from "../lib/workspace-view.js";
|
|
16
|
+
import { cdThen } from "../lib/shell.js";
|
|
15
17
|
|
|
16
18
|
// Local-only adoption metrics derived from git history. No network calls,
|
|
17
19
|
// no telemetry: the numbers stay in the repository, versioned like any
|
|
@@ -24,6 +26,19 @@ export const flags = { boolean: ["json", "save", "commands", "trend"], string: [
|
|
|
24
26
|
|
|
25
27
|
export async function run(_positional, flags) {
|
|
26
28
|
const projectRoot = process.cwd();
|
|
29
|
+
// At the workspace root (change 0235): adoption is measured per repository;
|
|
30
|
+
// the deliveries' own lead time is `delivery list`.
|
|
31
|
+
const wsRoot = workspaceRootAt(projectRoot);
|
|
32
|
+
if (wsRoot && !flagBool(flags, "commands", false)) {
|
|
33
|
+
const snap = workspaceSnapshot(wsRoot);
|
|
34
|
+
console.log(c.bold("Doctrina metrics") + c.gray(` — workspace "${snap.ws.name}"`));
|
|
35
|
+
console.log("");
|
|
36
|
+
console.log(`Deliveries ${snap.open.length} open · ${snap.deliveries.length - snap.open.length} closed — lead time per delivery: ${c.cyan("doctrina delivery list")}`);
|
|
37
|
+
console.log("");
|
|
38
|
+
console.log("Adoption is measured in each repository:");
|
|
39
|
+
for (const p of snap.projects.filter((x) => x.state !== "missing")) console.log(" " + c.cyan(cdThen(p.project.path, "doctrina metrics")));
|
|
40
|
+
return EXIT.OK;
|
|
41
|
+
}
|
|
27
42
|
if (!exists(path.join(projectRoot, ".doctrina"))) {
|
|
28
43
|
throw notADoctrinaProject();
|
|
29
44
|
}
|
|
@@ -98,6 +113,9 @@ export async function run(_positional, flags) {
|
|
|
98
113
|
printDelta("revert rate", previous.revert_rate, snapshot.revert_rate);
|
|
99
114
|
printDelta("fix share", previous.fix_share, snapshot.fix_share);
|
|
100
115
|
printDelta(`${REEDIT_WINDOW_DAYS}-day re-edit rate`, previous.reedit_rate, snapshot.reedit_rate);
|
|
116
|
+
if (typeof previous.change_adherence === "number" && typeof snapshot.change_adherence === "number") {
|
|
117
|
+
printDelta("commits with a change", previous.change_adherence, snapshot.change_adherence);
|
|
118
|
+
}
|
|
101
119
|
}
|
|
102
120
|
}
|
|
103
121
|
return 0;
|
|
@@ -140,6 +158,7 @@ const LABELS = {
|
|
|
140
158
|
revert_rate: "revert rate",
|
|
141
159
|
fix_share: "fix share",
|
|
142
160
|
reedit_rate: `${REEDIT_WINDOW_DAYS}-day re-edit rate`,
|
|
161
|
+
change_adherence: "commits with a change",
|
|
143
162
|
};
|
|
144
163
|
|
|
145
164
|
function printReport(s) {
|
|
@@ -148,6 +167,10 @@ function printReport(s) {
|
|
|
148
167
|
console.log(`fix-typed commits ${pct(s.fix_share)} of commits (Conventional Commits "fix")`);
|
|
149
168
|
console.log(`${s.reedit_window_days}-day re-edit rate ${pct(s.reedit_rate)} of commits touch a file edited in the prior ${s.reedit_window_days} days`);
|
|
150
169
|
console.log(c.gray(" (proxy for rework — iterative work also counts)"));
|
|
170
|
+
if (s.change_adherence !== null && s.change_adherence !== undefined) {
|
|
171
|
+
console.log(`commits with a change ${pct(s.change_adherence)} of ${s.code_commits} code commits touched .doctrina/changes/ or named a change`);
|
|
172
|
+
console.log(c.gray(" (workflow adherence — a one-line fix may rightly need none)"));
|
|
173
|
+
}
|
|
151
174
|
if (s.top_churn.length > 0) {
|
|
152
175
|
console.log("");
|
|
153
176
|
console.log("top churn:");
|
|
@@ -200,6 +223,20 @@ function commandUsage() {
|
|
|
200
223
|
for (const [op, n] of used) {
|
|
201
224
|
console.log(` ${String(n).padStart(5)} ${c.cyan(op)}`);
|
|
202
225
|
}
|
|
226
|
+
// The cycle, read off the sample (change 0218): did a change get its read
|
|
227
|
+
// and its close? A field review found `prime` and `work` used, `context`
|
|
228
|
+
// and `triage` skipped. Ratios, not verdicts — one context can serve two
|
|
229
|
+
// works, and a close can be retried.
|
|
230
|
+
const count = (op) => used.find(([o]) => o === op)?.[1] ?? 0;
|
|
231
|
+
const works = count("work");
|
|
232
|
+
if (works > 0) {
|
|
233
|
+
console.log("");
|
|
234
|
+
console.log(c.bold("Workflow") + c.gray(` — per \`work\` (${works})`));
|
|
235
|
+
console.log(` context ${(count("context") / works).toFixed(1)} close ${(count("close") / works).toFixed(1)} prime ${(count("prime") / works).toFixed(1)}`);
|
|
236
|
+
if (count("context") < works) {
|
|
237
|
+
console.log(c.gray(" fewer reads than changes: the pack is the spec, `prime` is only the state"));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
203
240
|
console.log("");
|
|
204
241
|
console.log(c.gray(`${unused.length} of ${catalog.length} operations never invoked in this sample:`));
|
|
205
242
|
for (let i = 0; i < unused.length; i += 4) {
|
|
@@ -216,12 +253,15 @@ export const help = `
|
|
|
216
253
|
Usage: doctrina metrics [--since <days|date>] [--save] [--commands]
|
|
217
254
|
|
|
218
255
|
Derive adoption metrics from LOCAL git history: commit count, revert
|
|
219
|
-
count and rate, Conventional-Commit fix share, top-churn files,
|
|
220
|
-
${REEDIT_WINDOW_DAYS}-day re-edit proxy rate
|
|
256
|
+
count and rate, Conventional-Commit fix share, top-churn files, a
|
|
257
|
+
${REEDIT_WINDOW_DAYS}-day re-edit proxy rate, and workflow adherence — the share of
|
|
258
|
+
code commits that carried a change (touched .doctrina/changes/ or named
|
|
259
|
+
one). No network calls; nothing leaves the repo.
|
|
221
260
|
|
|
222
261
|
Flags:
|
|
223
262
|
--commands Report which operations were actually invoked, from the
|
|
224
|
-
opt-in usage log
|
|
263
|
+
opt-in usage log, and how many context, close and prime
|
|
264
|
+
runs there were per work. Recording is OFF unless DOCTRINA_USAGE_LOG
|
|
225
265
|
names a file; only the operation is recorded, never
|
|
226
266
|
arguments, and nothing leaves the machine.
|
|
227
267
|
--since <n|date> Window: a day count (default 90), a calendar date
|
package/src/commands/next.js
CHANGED
|
@@ -8,6 +8,7 @@ import { c } from "../lib/colors.js";
|
|
|
8
8
|
import { emitJson } from "../lib/json-out.js";
|
|
9
9
|
import { EXIT } from "../lib/exit-codes.js";
|
|
10
10
|
import { action, computeActions, invocation } from "../lib/actions.js";
|
|
11
|
+
import { attachCommand, contextRouting, shadowedCloneAt, workspaceActions, workspaceFindings, workspaceHandoff, workspaceRootAt, workspaceSnapshot, workspaceStatus } from "../lib/workspace-view.js";
|
|
11
12
|
|
|
12
13
|
// `next` answers "what now?". The ANSWER lives in `lib/actions.js` as
|
|
13
14
|
// records; this module renders them and, under `--run`, executes one
|
|
@@ -22,18 +23,48 @@ import { action, computeActions, invocation } from "../lib/actions.js";
|
|
|
22
23
|
// wrap it in the generic envelope.
|
|
23
24
|
export const jsonNative = true;
|
|
24
25
|
|
|
26
|
+
// How many actions `next` prints at a workspace root.
|
|
27
|
+
const ROOT_ACTIONS_SHOWN = 10;
|
|
28
|
+
|
|
25
29
|
export const flags = { boolean: ["json", "run"], string: [] };
|
|
26
30
|
|
|
27
31
|
export async function run(_positional, flags) {
|
|
28
32
|
const projectRoot = process.cwd();
|
|
29
33
|
const json = flagBool(flags, "json", false);
|
|
34
|
+
// At a workspace root the planner's next step is the workspace's — never
|
|
35
|
+
// `init`, which would create a project inside the workspace (change 0230).
|
|
36
|
+
const wsRoot = workspaceRootAt(projectRoot);
|
|
37
|
+
if (wsRoot) {
|
|
38
|
+
const actions = workspaceActions(workspaceSnapshot(wsRoot));
|
|
39
|
+
if (json) emitJson("next", { workspace: wsRoot, actions });
|
|
40
|
+
else {
|
|
41
|
+
console.log(c.bold("Next actions in the workspace") + c.gray(" (in priority order):"));
|
|
42
|
+
console.log("");
|
|
43
|
+
// The first actions, not all of them (fifth audit, change 0236): forty
|
|
44
|
+
// open deliveries printed 162 lines into the planner's context. The
|
|
45
|
+
// order already puts what unblocks most first; --json keeps every one.
|
|
46
|
+
actions.slice(0, ROOT_ACTIONS_SHOWN).forEach((a, i) => console.log(`${i + 1}. ${a.text}\n ${c.cyan(a.command)}`));
|
|
47
|
+
if (actions.length > ROOT_ACTIONS_SHOWN) {
|
|
48
|
+
console.log("");
|
|
49
|
+
console.log(c.gray(`… and ${actions.length - ROOT_ACTIONS_SHOWN} more — every delivery's state: \`doctrina delivery list\`; every action: \`doctrina next --json\``));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
30
54
|
if (!exists(path.join(projectRoot, ".doctrina"))) {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
55
|
+
const clone = shadowedCloneAt(projectRoot);
|
|
56
|
+
const first = clone
|
|
57
|
+
? action({
|
|
58
|
+
id: "shadowed-clone",
|
|
59
|
+
why: `this repository is the workspace's shadowed "${clone.name}" at another path — link its context here, never init it`,
|
|
60
|
+
text: `${attachCommand(clone, projectRoot)} — this repository is the workspace's shadowed "${clone.name}"${clone.same ? " and its links are missing" : ` (registered at ${clone.path})`}; never \`doctrina init\` here`,
|
|
61
|
+
})
|
|
62
|
+
: action({
|
|
63
|
+
id: "not-a-project",
|
|
64
|
+
command: "init",
|
|
65
|
+
runnable: false,
|
|
66
|
+
why: "this directory is not a Doctrina project yet",
|
|
67
|
+
});
|
|
37
68
|
if (json) emitJson("next", { actions: [first] });
|
|
38
69
|
else console.log(`1. ${first.text}`);
|
|
39
70
|
return 0;
|
|
@@ -154,6 +185,10 @@ proposed status, accepted ADRs with nothing proving them (cite Evidence
|
|
|
154
185
|
or run "decision land"), a skill-capture nudge when a past fix went
|
|
155
186
|
uncaptured, and index drift last.
|
|
156
187
|
|
|
188
|
+
At a workspace root it lists the workspace's next actions instead —
|
|
189
|
+
absorb, route, open, hand the next card, close, clear a delivery's
|
|
190
|
+
record, close the delivery — and never names init there.
|
|
191
|
+
|
|
157
192
|
Intended use: agents and humans run it to resume work without
|
|
158
193
|
re-reading the whole tree.
|
|
159
194
|
|
package/src/commands/prime.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
|
-
import { exists } from "../lib/fs-ops.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
4
|
+
import { exists, isFile, read } from "../lib/fs-ops.js";
|
|
5
|
+
import { findWorkspace, readWorkspace } from "../lib/workspace.js";
|
|
6
|
+
import { listDeliveries } from "../lib/delivery.js";
|
|
7
|
+
import { workspacePrimer } from "../lib/workspace-agents.js";
|
|
8
|
+
import { projectWorkspaceLine } from "../lib/workspace-view.js";
|
|
9
|
+
import { flagBool, flagString } from "../lib/args.js";
|
|
10
|
+
import { c } from "../lib/colors.js";
|
|
11
|
+
import { EXIT, notADoctrinaProject } from "../lib/exit-codes.js";
|
|
12
|
+
import { collectSnapshot, openChanges, recentlyClosed } from "../lib/snapshot.js";
|
|
13
|
+
import { renderView, pulse } from "../lib/views.js";
|
|
9
14
|
|
|
10
15
|
// Session primer: the ~40-line read that orients an agent at the start of a
|
|
11
16
|
// session — where things stand, what the standing rules are, what work is
|
|
@@ -22,23 +27,125 @@ import { renderView } from "../lib/views.js";
|
|
|
22
27
|
// Flags this command accepts. Declared HERE, with the command, so
|
|
23
28
|
// adding a command never requires editing the entrypoint — the gap that
|
|
24
29
|
// let six flags ship undeclared and silently swallow a positional (C3).
|
|
25
|
-
export const flags = { boolean: ["json", "rules"], string: [] };
|
|
30
|
+
export const flags = { boolean: ["json", "rules", "pulse"], string: ["hook"] };
|
|
31
|
+
|
|
32
|
+
// What an agent hook accepts on stdout (change 0211). Claude Code adds the
|
|
33
|
+
// plain text of a SessionStart hook to the conversation; Cursor's
|
|
34
|
+
// sessionStart reads `additional_context` from a JSON object. Both cap what
|
|
35
|
+
// they keep — Claude at 10,000 characters — so the primer is cut short of
|
|
36
|
+
// that rather than replaced by a file path the agent is never asked to read.
|
|
37
|
+
const HOOK_AGENTS = ["claude", "cursor"];
|
|
38
|
+
const HOOK_MAX_CHARS = 9000;
|
|
26
39
|
|
|
27
40
|
export async function run(_positional, flags) {
|
|
28
41
|
const projectRoot = process.cwd();
|
|
29
42
|
if (!exists(path.join(projectRoot, ".doctrina"))) {
|
|
43
|
+
// At a workspace root the session belongs to the orchestrator (change
|
|
44
|
+
// 0223): the primer is the workspace's.
|
|
45
|
+
const found = findWorkspace(projectRoot);
|
|
46
|
+
if (found && found.via !== "pointer") return primeWorkspace(found.dir, flags);
|
|
30
47
|
throw notADoctrinaProject();
|
|
31
48
|
}
|
|
32
49
|
// --rules: the standing rules in full, which is what the removed
|
|
33
50
|
// `constitution` printed (changes 0049, 0175). The primer names the ADRs and counts the non-goals; this
|
|
34
51
|
// prints both lists, from the same collection, so the two cannot disagree.
|
|
52
|
+
// --pulse: one line for a per-prompt hook — the open changes only, read
|
|
53
|
+
// without collecting the gates, because it runs before every prompt.
|
|
54
|
+
if (flagBool(flags, "pulse", false)) {
|
|
55
|
+
// Run by Cursor as the Claude adapter's UserPromptSubmit (sixth audit,
|
|
56
|
+
// change 0237): Cursor imports `.claude/settings.json` hooks and runs it
|
|
57
|
+
// as beforeSubmitPrompt, whose answer is `{"continue": …}` and cannot add
|
|
58
|
+
// context. The plain-text line Claude Code reads is not that.
|
|
59
|
+
if (underCursor()) {
|
|
60
|
+
console.log(JSON.stringify({ continue: true }));
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
for (const line of pulse(openChanges(projectRoot), recentlyClosed(projectRoot, { limit: 1 }))) console.log(line);
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
if (flags.has("hook")) {
|
|
67
|
+
const agent = flagString(flags, "hook");
|
|
68
|
+
if (!agent || !HOOK_AGENTS.includes(agent)) {
|
|
69
|
+
console.error(c.red("error:") + ` --hook needs one of ${HOOK_AGENTS.join(", ")} (got "${agent ?? ""}")`);
|
|
70
|
+
return EXIT.USAGE;
|
|
71
|
+
}
|
|
72
|
+
const as = hookFlavour(agent, projectRoot);
|
|
73
|
+
if (as === null) return 0;
|
|
74
|
+
const text = ["Doctrina session primer (`doctrina prime`) — the project's gates, rules and open work:", "",
|
|
75
|
+
...renderView("prime", collectSnapshot(projectRoot)), ...workspaceLines(projectRoot), "", ...pulse(openChanges(projectRoot), recentlyClosed(projectRoot, { limit: 1 }))]
|
|
76
|
+
.join("\n").replace(/\x1b\[[0-9;]*m/g, "").slice(0, HOOK_MAX_CHARS);
|
|
77
|
+
console.log(as === "cursor" ? JSON.stringify({ additional_context: text }) : text);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
35
80
|
const view = flagBool(flags, "rules", false) ? "rules" : "prime";
|
|
36
81
|
for (const line of renderView(view, collectSnapshot(projectRoot))) console.log(line);
|
|
82
|
+
if (view === "prime") for (const line of workspaceLines(projectRoot)) console.log(line);
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Cursor runs the hooks Claude Code's `.claude/settings.json` declares too
|
|
87
|
+
// (its "third-party" import, on by default), and sets CURSOR_VERSION and
|
|
88
|
+
// CURSOR_PROJECT_DIR for every hook it runs (sixth audit, change 0237).
|
|
89
|
+
function underCursor(env = process.env) {
|
|
90
|
+
return Boolean(env.CURSOR_VERSION && env.CURSOR_PROJECT_DIR);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The format a session hook answers in, or null to answer nothing. The
|
|
95
|
+
* Claude adapter's SessionStart, run by Cursor, printed plain text where
|
|
96
|
+
* Cursor reads JSON — and beside the Cursor adapter's own sessionStart it
|
|
97
|
+
* put the primer in twice. Under Cursor it yields to that hook when the
|
|
98
|
+
* folder has one, and speaks Cursor's format when it has not.
|
|
99
|
+
* @returns {"claude" | "cursor" | null}
|
|
100
|
+
*/
|
|
101
|
+
function hookFlavour(agent, dir) {
|
|
102
|
+
if (agent !== "claude" || !underCursor()) return /** @type {"claude" | "cursor"} */ (agent);
|
|
103
|
+
const own = path.join(dir, ".cursor", "hooks.json");
|
|
104
|
+
return isFile(own) && /prime --hook cursor/.test(read(own)) ? null : "cursor";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// A project inside a workspace says so (change 0230): a session opened in
|
|
108
|
+
// the repository — an executor's, or the human's — otherwise never learns a
|
|
109
|
+
// delivery is open on it, or where its planner works.
|
|
110
|
+
function workspaceLines(projectRoot) {
|
|
111
|
+
const w = projectWorkspaceLine(projectRoot);
|
|
112
|
+
if (!w) return [];
|
|
113
|
+
const out = ["", `Workspace ${w.name} (${w.dir}) — this repository is "${w.project}"`];
|
|
114
|
+
for (const d of w.deliveries) {
|
|
115
|
+
out.push(` open delivery ${d.id} — ${d.title}: ${d.changes.length ? `this project's change ${d.changes.join(", ")}` : "no change here yet"}`);
|
|
116
|
+
}
|
|
117
|
+
if (w.deliveries.length === 0) out.push(" no open delivery routes here");
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function primeWorkspace(wsDir, flags) {
|
|
122
|
+
const ws = readWorkspace(wsDir);
|
|
123
|
+
const open = listDeliveries(wsDir).filter((d) => d.status !== "closed");
|
|
124
|
+
if (flagBool(flags, "pulse", false)) {
|
|
125
|
+
console.log(open.length === 0
|
|
126
|
+
? "Doctrina workspace: no delivery is open — an item of work across repositories opens one with `doctrina delivery new \"<title>\" --ref <id>`."
|
|
127
|
+
: open.map((d) => `open delivery: ${d.id} — "${d.title}" (${d.projects.join(", ") || "not routed"}). Name it in your first sentence to the human; \`doctrina delivery status ${d.id}\`.`).join(" "));
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
const lines = workspacePrimer(ws, wsDir, open);
|
|
131
|
+
if (flags.has("hook")) {
|
|
132
|
+
const agent = flagString(flags, "hook");
|
|
133
|
+
if (!agent || !HOOK_AGENTS.includes(agent)) {
|
|
134
|
+
console.error(c.red("error:") + ` --hook needs one of ${HOOK_AGENTS.join(", ")} (got "${agent ?? ""}")`);
|
|
135
|
+
return EXIT.USAGE;
|
|
136
|
+
}
|
|
137
|
+
const as = hookFlavour(agent, wsDir);
|
|
138
|
+
if (as === null) return 0;
|
|
139
|
+
const text = [...lines, "", read(path.join(wsDir, "AGENTS.md"))].join("\n").replace(/\x1b\[[0-9;]*m/g, "").slice(0, HOOK_MAX_CHARS);
|
|
140
|
+
console.log(as === "cursor" ? JSON.stringify({ additional_context: text }) : text);
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
for (const line of lines) console.log(line);
|
|
37
144
|
return 0;
|
|
38
145
|
}
|
|
39
146
|
|
|
40
147
|
export const help = `
|
|
41
|
-
Usage: doctrina prime
|
|
148
|
+
Usage: doctrina prime [--rules | --pulse | --hook claude|cursor]
|
|
42
149
|
|
|
43
150
|
Print the session primer: gate digest, standing rules (accepted ADR
|
|
44
151
|
titles + non-goal count), open work with task progress, and the top
|
|
@@ -51,6 +158,17 @@ collection of the tree, so they cannot disagree.
|
|
|
51
158
|
--rules Print the standing rules in full instead of the primer: every
|
|
52
159
|
accepted ADR and every declared non-goal — what the removed
|
|
53
160
|
\`doctrina constitution\` printed, from the same collection.
|
|
161
|
+
--pulse One line for a per-prompt agent hook: the open change(s), or
|
|
162
|
+
that a request changing behaviour opens one with \`work\`.
|
|
163
|
+
--hook <agent>
|
|
164
|
+
The primer plus the pulse, as the agent's session-start hook
|
|
165
|
+
reads it: plain text for claude, {"additional_context"} JSON
|
|
166
|
+
for cursor; cut at 9,000 characters. \`doctrina adapter add
|
|
167
|
+
claude|cursor\` installs the hooks that call these.
|
|
168
|
+
|
|
169
|
+
At a workspace root (no .doctrina/, a workspace.json) it is the
|
|
170
|
+
orchestrator's primer: the projects and their state, the open deliveries,
|
|
171
|
+
the tools — and with --hook, the workspace's AGENTS.md after it.
|
|
54
172
|
|
|
55
173
|
Read-only; always exits 0. Deeper reads: \`doctrina context\`,
|
|
56
174
|
\`doctrina why <cap>\`, \`doctrina show <ref>\`.
|
package/src/commands/review.js
CHANGED
|
@@ -6,6 +6,7 @@ import { exists, isDir, isFile, read } from "../lib/fs-ops.js";
|
|
|
6
6
|
import { flagBool, flagString } from "../lib/args.js";
|
|
7
7
|
import { c } from "../lib/colors.js";
|
|
8
8
|
import { rankCapabilitiesByDiff } from "../lib/work-model.js";
|
|
9
|
+
import { applyOps, extractOps } from "../lib/spec-ops.js";
|
|
9
10
|
import { readLedger, churnByCapability } from "../lib/ledger.js";
|
|
10
11
|
import { changedFiles, windowCutoff, historyState, refExists } from "../lib/git.js";
|
|
11
12
|
import { dependentsOf, deltaCapabilities } from "../lib/scan.js";
|
|
@@ -67,6 +68,8 @@ export async function run(_positional, flags) {
|
|
|
67
68
|
const changeId = flagString(flags, "change");
|
|
68
69
|
/** @type {Set<string> | null} */
|
|
69
70
|
let changeCaps = null;
|
|
71
|
+
/** @type {Map<string, string>} the change's delta text per capability */
|
|
72
|
+
const pending = new Map();
|
|
70
73
|
if (changeId !== undefined) {
|
|
71
74
|
const refused = refuseChangeRef(changeId);
|
|
72
75
|
if (refused !== null) return refused;
|
|
@@ -76,6 +79,19 @@ export async function run(_positional, flags) {
|
|
|
76
79
|
return EXIT.USAGE;
|
|
77
80
|
}
|
|
78
81
|
changeCaps = new Set(deltaCapabilities(changeDir));
|
|
82
|
+
// The specs as this change leaves them: an ops delta applied in memory,
|
|
83
|
+
// as `change check` dry-runs it; a delta without ops (a manual merge)
|
|
84
|
+
// read beside the spec, since nothing can apply it.
|
|
85
|
+
for (const cap of changeCaps) {
|
|
86
|
+
const delta = path.join(changeDir, "specs", cap, "delta.md");
|
|
87
|
+
const spec = path.join(projectRoot, ".doctrina", "specs", cap, "spec.md");
|
|
88
|
+
if (!isFile(delta)) continue;
|
|
89
|
+
const deltaText = read(delta);
|
|
90
|
+
const current = isFile(spec) ? read(spec) : "";
|
|
91
|
+
const ops = extractOps(deltaText);
|
|
92
|
+
const after = ops.length > 0 ? applyOps(current, ops) : null;
|
|
93
|
+
pending.set(cap, after && after.errors.length === 0 ? after.text : `${current}\n${deltaText}`);
|
|
94
|
+
}
|
|
79
95
|
}
|
|
80
96
|
|
|
81
97
|
const { all, sourceFiles } = reviewScope(projectRoot, against);
|
|
@@ -102,11 +118,19 @@ export async function run(_positional, flags) {
|
|
|
102
118
|
all.filter((f) => /(^|\/)\.doctrina\/specs\/[^/]+\/spec\.md$/.test(f.replace(/\\/g, "/")))
|
|
103
119
|
.map((f) => f.replace(/\\/g, "/").match(/\.doctrina\/specs\/([^/]+)\/spec\.md$/)[1]),
|
|
104
120
|
);
|
|
105
|
-
const ranked = rankCapabilitiesByDiff(projectRoot, sourceFiles, { limit: Infinity });
|
|
121
|
+
const ranked = rankCapabilitiesByDiff(projectRoot, sourceFiles, { limit: Infinity, pending });
|
|
106
122
|
const touched = new Set([...specChanged, ...ranked.map((m) => m.id)]);
|
|
107
123
|
const stale = ranked.map((m) => m.id).filter((id) => !specChanged.has(id) && !changeCaps?.has(id));
|
|
124
|
+
// Without --change, an OPEN change that carries a delta for the capability
|
|
125
|
+
// updates its spec at close (fifth audit, change 0236): the bare review
|
|
126
|
+
// reported "its spec was not updated" for exactly the work a planner had
|
|
127
|
+
// in flight — the real agent read it as a break and went to fix the spec.
|
|
128
|
+
const inFlight = changeCaps === null ? openDeltaOwners(projectRoot) : new Map();
|
|
108
129
|
if (changeCaps === null) {
|
|
109
|
-
for (const id of stale) {
|
|
130
|
+
for (const id of stale.filter((x) => inFlight.has(x))) {
|
|
131
|
+
notes.push(`code under capability "${id}" changed; its spec is updated when ${inFlight.get(id)?.join(", ")} closes (\`doctrina review --change <id>\` reviews it as that close does)`);
|
|
132
|
+
}
|
|
133
|
+
for (const id of stale.filter((x) => !inFlight.has(x))) {
|
|
110
134
|
breaks.push(`code under capability "${id}" changed, but its spec was not updated — confirm \`.doctrina/specs/${id}/spec.md\` still describes the code (update it, or note why not)`);
|
|
111
135
|
}
|
|
112
136
|
} else if (stale.length > 0) {
|
|
@@ -151,7 +175,7 @@ export async function run(_positional, flags) {
|
|
|
151
175
|
// file in the change. A review of an adapter change reported
|
|
152
176
|
// "Capabilities touched: docs" and said nothing about the adapter.
|
|
153
177
|
const orphans = sourceFiles.filter(
|
|
154
|
-
(f) => rankCapabilitiesByDiff(projectRoot, [f], { limit: 1 }).length === 0,
|
|
178
|
+
(f) => rankCapabilitiesByDiff(projectRoot, [f], { limit: 1, pending }).length === 0,
|
|
155
179
|
);
|
|
156
180
|
if (orphans.length > 0) {
|
|
157
181
|
const shown = orphans.slice(0, ORPHAN_LIST_LIMIT).map((f) => `\`${f}\``).join(", ");
|
|
@@ -274,3 +298,15 @@ from specs that declare no Source.
|
|
|
274
298
|
Read-only. Exits 0 as a report; with --strict, exits 1 when any hard break
|
|
275
299
|
exists (CI gate). Notes (advisory) never fail the gate.
|
|
276
300
|
`;
|
|
301
|
+
|
|
302
|
+
/** Capability → the open changes that carry a delta for it. */
|
|
303
|
+
function openDeltaOwners(projectRoot) {
|
|
304
|
+
const out = new Map();
|
|
305
|
+
const changes = path.join(projectRoot, ".doctrina", "changes");
|
|
306
|
+
if (!isDir(changes)) return out;
|
|
307
|
+
for (const id of readdirSync(changes).sort()) {
|
|
308
|
+
if (id === "archive" || id.startsWith(".") || !isDir(path.join(changes, id))) continue;
|
|
309
|
+
for (const cap of deltaCapabilities(path.join(changes, id))) out.set(cap, [...(out.get(cap) ?? []), id]);
|
|
310
|
+
}
|
|
311
|
+
return out;
|
|
312
|
+
}
|
package/src/commands/skill.js
CHANGED
|
@@ -455,7 +455,8 @@ knowledge loaded only when relevant.
|
|
|
455
455
|
|
|
456
456
|
Subcommands:
|
|
457
457
|
new <name> Scaffold a new skill from the template and index
|
|
458
|
-
it. Name must match [a-z][a-z0-9-]*.
|
|
458
|
+
it. Name must match [a-z][a-z0-9-]*. --force
|
|
459
|
+
overwrites an existing skill.
|
|
459
460
|
list Print one line per skill with name + description.
|
|
460
461
|
Read-only.
|
|
461
462
|
sync Copy each skill's frontmatter description into
|
package/src/commands/status.js
CHANGED
|
@@ -11,6 +11,7 @@ import { renderView, VIEWS } from "../lib/views.js";
|
|
|
11
11
|
import { draftAgentChangelog, renderDraft } from "../lib/agent-changelog.js";
|
|
12
12
|
import { cliVersion } from "../lib/version.js";
|
|
13
13
|
import { suggest } from "../lib/suggest.js";
|
|
14
|
+
import { contextRouting, workspaceActions, workspaceFindings, workspaceHandoff, workspaceRootAt, workspaceSnapshot, workspaceStatus } from "../lib/workspace-view.js";
|
|
14
15
|
|
|
15
16
|
// One-glance project health (review 2026-06-27 passive-user feature #1): a
|
|
16
17
|
// dashboard that answers "where do things stand?" in a single read, so neither
|
|
@@ -38,6 +39,8 @@ const STATUS_VIEWS = [...VIEWS, "agent-changelog"];
|
|
|
38
39
|
|
|
39
40
|
export async function run(_positional, flags) {
|
|
40
41
|
const projectRoot = process.cwd();
|
|
42
|
+
const wsRoot = workspaceRootAt(projectRoot);
|
|
43
|
+
if (wsRoot) return workspaceStatusView(wsRoot, flags);
|
|
41
44
|
if (!exists(path.join(projectRoot, ".doctrina"))) {
|
|
42
45
|
throw notADoctrinaProject();
|
|
43
46
|
}
|
|
@@ -90,6 +93,9 @@ signals (index drift, framework stamp, coverage %, trace anchors, whether
|
|
|
90
93
|
verify is configured) and the artifact counts (specs by implementation
|
|
91
94
|
state, open changes, decisions, skills). Read-only; always exits 0.
|
|
92
95
|
|
|
96
|
+
At a workspace root it prints the workspace instead: projects ready,
|
|
97
|
+
pending and missing, open changes, deliveries open and closed, tools.
|
|
98
|
+
|
|
93
99
|
Flags:
|
|
94
100
|
--view <name> Render a different shape of the same snapshot:
|
|
95
101
|
dashboard the default one-glance health board
|
|
@@ -116,3 +122,25 @@ It is a fast summary, not the authoritative gate: run \`doctrina validate\`
|
|
|
116
122
|
and \`doctrina verify\` for the full structural and build checks, and
|
|
117
123
|
\`doctrina next\` for the recommended next action.
|
|
118
124
|
`;
|
|
125
|
+
|
|
126
|
+
// At a workspace root: one glance at the workspace (change 0230).
|
|
127
|
+
function workspaceStatusView(wsRoot, flags) {
|
|
128
|
+
const snap = workspaceSnapshot(wsRoot);
|
|
129
|
+
const st = workspaceStatus(snap);
|
|
130
|
+
if (flagBool(flags, "json", false)) {
|
|
131
|
+
emitJson("status", { workspace: wsRoot, ...st });
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
console.log(c.bold(`Doctrina workspace "${st.name}"`) + c.gray(` — ${wsRoot}`));
|
|
135
|
+
console.log("");
|
|
136
|
+
console.log(`Projects ${st.ready}/${st.projects} ready` + (st.pending.length ? c.yellow(` · pending: ${st.pending.join(", ")}`) : "")
|
|
137
|
+
+ (st.missing.length ? c.red(` · missing: ${st.missing.join(", ")}`) : "") + ` · ${st.openChanges} open change${st.openChanges === 1 ? "" : "s"}`);
|
|
138
|
+
console.log(`Deliveries ${st.deliveries.open} open · ${st.deliveries.closed} closed`);
|
|
139
|
+
console.log(`Tools ${st.tools} declared`);
|
|
140
|
+
const first = workspaceActions(snap)[0];
|
|
141
|
+
if (first) {
|
|
142
|
+
console.log("");
|
|
143
|
+
console.log(c.gray("next: ") + `${first.text} — ` + c.cyan(first.command));
|
|
144
|
+
}
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
package/src/commands/upgrade.js
CHANGED
|
@@ -9,6 +9,10 @@ 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 { planSkillExport, applySkillExport } from "../lib/skill-export.js";
|
|
13
|
+
import { applyAdapterMigration, planAdapterMigration } from "../lib/adapters.js";
|
|
14
|
+
import { projectName } from "../lib/project.js";
|
|
15
|
+
import { relinkShadow, shadowRootOf } from "../lib/workspace.js";
|
|
12
16
|
import { collectFindings } from "../lib/templates-model.js";
|
|
13
17
|
import * as templates from "./templates.js";
|
|
14
18
|
import * as validate from "./validate.js";
|
|
@@ -74,6 +78,13 @@ export async function run(_positional, flags) {
|
|
|
74
78
|
console.log(c.gray(" fix: ") + (f.remedy ? c.cyan(f.remedy) : c.gray("manual repair — no command can fix this")));
|
|
75
79
|
}
|
|
76
80
|
|
|
81
|
+
// The skill exports (change 0212) follow the index: regenerated by the
|
|
82
|
+
// write, reported by the preview.
|
|
83
|
+
const exportPlan = planSkillExport(projectRoot);
|
|
84
|
+
// Adapter files that moved (change 0238): Cursor's slash commands became skills.
|
|
85
|
+
const movePlan = planAdapterMigration(projectRoot, { PROJECT_NAME: projectName(projectRoot), PROJECT_DESCRIPTION: "", DATE: today() }, shadowRootOf(projectRoot) ?? projectRoot);
|
|
86
|
+
const exportChanges = exportPlan.writes.length + exportPlan.removes.length;
|
|
87
|
+
|
|
77
88
|
// 3. Index: rebuild from the tree, which also migrates the framework stamp.
|
|
78
89
|
console.log("");
|
|
79
90
|
console.log(c.gray("──── 2/3 index + framework stamp"));
|
|
@@ -81,6 +92,28 @@ export async function run(_positional, flags) {
|
|
|
81
92
|
const current = idx.load(projectRoot);
|
|
82
93
|
const drifted = !indexesMatch(deriveIndex(projectRoot, current), current);
|
|
83
94
|
const staleStamp = (current.framework_version ?? null) !== running;
|
|
95
|
+
if (exportChanges > 0) {
|
|
96
|
+
if (writeMode) {
|
|
97
|
+
applySkillExport(projectRoot, exportPlan);
|
|
98
|
+
console.log(c.green("export ") + ` ${exportChanges} skill file${exportChanges === 1 ? "" : "s"} where the agents discover skills`);
|
|
99
|
+
} else {
|
|
100
|
+
console.log(c.yellow("would ") + `export ${exportChanges} skill file${exportChanges === 1 ? "" : "s"} where the agents discover skills`);
|
|
101
|
+
pending += 1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const moves = movePlan.writes.length + movePlan.removes.length;
|
|
105
|
+
if (moves > 0) {
|
|
106
|
+
if (writeMode) {
|
|
107
|
+
const gone = applyAdapterMigration(shadowRootOf(projectRoot) ?? projectRoot, movePlan);
|
|
108
|
+
relinkShadow(projectRoot, gone);
|
|
109
|
+
console.log(c.green("moved ") + ` ${movePlan.writes.length} adapter file${movePlan.writes.length === 1 ? "" : "s"} to the skills the agent reads now` +
|
|
110
|
+
(movePlan.removes.length ? `, ${movePlan.removes.length} retired cop${movePlan.removes.length === 1 ? "y" : "ies"} removed` : ""));
|
|
111
|
+
} else {
|
|
112
|
+
console.log(c.yellow("would ") + `move ${movePlan.removes.length || movePlan.writes.length} Cursor command${(movePlan.removes.length || movePlan.writes.length) === 1 ? "" : "s"} to .cursor/skills/ (Cursor reads skills now)`);
|
|
113
|
+
pending += 1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
for (const rel of movePlan.kept) console.log(c.yellow("kept ") + ` ${rel} — edited since install; its skill replaces it, so move your edits and remove it`);
|
|
84
117
|
if (!drifted && !staleStamp) {
|
|
85
118
|
console.log(c.green("ok") + " index.json matches the tree and the stamp is current");
|
|
86
119
|
} else if (writeMode) {
|
package/src/commands/validate.js
CHANGED
|
@@ -7,6 +7,7 @@ import { c } from "../lib/colors.js";
|
|
|
7
7
|
import { emitJson } from "../lib/json-out.js";
|
|
8
8
|
import { notADoctrinaProject } from "../lib/exit-codes.js";
|
|
9
9
|
import { collectValidation } from "../lib/validation-model.js";
|
|
10
|
+
import { contextRouting, workspaceActions, workspaceFindings, workspaceHandoff, workspaceRootAt, workspaceSnapshot, workspaceStatus } from "../lib/workspace-view.js";
|
|
10
11
|
|
|
11
12
|
// The structural gate. The checks themselves live in lib/validation-model.js
|
|
12
13
|
// so a driver can ASK for them instead of spawning this binary and parsing
|
|
@@ -27,6 +28,19 @@ export async function run(_positional, flags) {
|
|
|
27
28
|
// Outside a Doctrina project this is a PRECONDITION, not a pile of
|
|
28
29
|
// gate failures about missing files: the remedy is `doctrina init`,
|
|
29
30
|
// not editing artifacts that do not exist yet (C7).
|
|
31
|
+
// At a workspace root: the workspace's own integrity — registry, shadow
|
|
32
|
+
// links, deliveries (change 0230). Each project validates inside itself.
|
|
33
|
+
const wsRoot = workspaceRootAt(projectRoot);
|
|
34
|
+
if (wsRoot) {
|
|
35
|
+
const { errors, warnings } = workspaceFindings(workspaceSnapshot(wsRoot));
|
|
36
|
+
for (const e of errors) console.log(c.red("error: ") + e);
|
|
37
|
+
for (const w of warnings) console.log(c.yellow("warn: ") + w);
|
|
38
|
+
console.log("");
|
|
39
|
+
console.log(errors.length === 0
|
|
40
|
+
? c.green("ok") + ` the workspace is consistent${warnings.length ? ` (${warnings.length} warning${warnings.length === 1 ? "" : "s"})` : ""} — each project validates inside itself: \`cd <path> && doctrina validate\``
|
|
41
|
+
: c.red("fail") + ` ${errors.length} workspace error${errors.length === 1 ? "" : "s"}`);
|
|
42
|
+
return errors.length > 0 || (flagBool(flags, "strict", false) && warnings.length > 0) ? 1 : 0;
|
|
43
|
+
}
|
|
30
44
|
if (!isDir(path.join(projectRoot, ".doctrina"))) throw notADoctrinaProject();
|
|
31
45
|
|
|
32
46
|
const { errors, warnings, fixes } = collectValidation(projectRoot, {
|
|
@@ -82,6 +96,10 @@ Run schema, artifact-existence, and structural checks against the
|
|
|
82
96
|
.doctrina/ tree in the current working directory. Exits 0 if no errors
|
|
83
97
|
(warnings allowed), 1 otherwise. With --strict, a warning fails it too.
|
|
84
98
|
|
|
99
|
+
At a workspace root it checks the workspace instead — registered
|
|
100
|
+
projects and their dependencies, shadow links, deliveries — and leaves
|
|
101
|
+
each project's validation to \`cd <path> && doctrina validate\`.
|
|
102
|
+
|
|
85
103
|
Two structural checks worth naming:
|
|
86
104
|
|
|
87
105
|
Pipeline a spec's optional "### Pipeline" block declares ordered
|
package/src/commands/verify.js
CHANGED
|
@@ -5,6 +5,7 @@ import { readdirSync } from "node:fs";
|
|
|
5
5
|
import { wantsJson } from "../lib/json-out.js";
|
|
6
6
|
import { spawn, spawnSync } from "node:child_process";
|
|
7
7
|
import { exists, isDir, isFile, read, write, relPath } from "../lib/fs-ops.js";
|
|
8
|
+
import { inventory } from "../lib/code-inventory.js";
|
|
8
9
|
import { flagBool, flagString } from "../lib/args.js";
|
|
9
10
|
import { today } from "../lib/dates.js";
|
|
10
11
|
import { c } from "../lib/colors.js";
|
|
@@ -38,6 +39,16 @@ const STARTER = {
|
|
|
38
39
|
],
|
|
39
40
|
};
|
|
40
41
|
|
|
42
|
+
const SEEDED_COMMENT =
|
|
43
|
+
"doctrina verify runs each check below, in order, through your shell. " +
|
|
44
|
+
"These were seeded from the project's own manifests by the code " +
|
|
45
|
+
"inventory — confirm each one runs what its name says, and remove or add " +
|
|
46
|
+
"checks until this file is the project's executable definition of 'the " +
|
|
47
|
+
"code actually works'. A non-zero exit fails the gate. A check may add an " +
|
|
48
|
+
"\"expect\" block — {\"fail_if_output_matches\": \"<regex>\"} and/or " +
|
|
49
|
+
"{\"require_output_matches\": \"<regex>\"} — so that a run which exits 0 " +
|
|
50
|
+
"having executed NOTHING fails the gate instead of passing it.";
|
|
51
|
+
|
|
41
52
|
// Flags this command accepts. Declared HERE, with the command, so
|
|
42
53
|
// adding a command never requires editing the entrypoint — the gap that
|
|
43
54
|
// let six flags ship undeclared and silently swallow a positional (C3).
|
|
@@ -69,9 +80,24 @@ export async function run(_positional, flags) {
|
|
|
69
80
|
console.error(c.red("error:") + ` ${CONFIG_REL} already exists (pass --force to overwrite)`);
|
|
70
81
|
return 1;
|
|
71
82
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
83
|
+
// Seeded from the code inventory when it found the project's own build
|
|
84
|
+
// and test commands (change 0228): the absorb playbook says "declare the
|
|
85
|
+
// build gate from the inventory's candidate commands", and the inventory
|
|
86
|
+
// lists them under that heading — while --init wrote `exit 1` anyway,
|
|
87
|
+
// leaving the agent to copy by hand what the CLI had just read. With no
|
|
88
|
+
// candidate the starter stays fail-closed: a gate nobody declared must
|
|
89
|
+
// not pass.
|
|
90
|
+
// A check that belongs to one unit of a monorepo runs there (`cwd`).
|
|
91
|
+
const seeded = inventory(projectRoot).checks.map((ch) => ({ name: ch.name, run: ch.command, ...(ch.cwd ? { cwd: ch.cwd } : {}) }));
|
|
92
|
+
const config = seeded.length > 0
|
|
93
|
+
? { ...STARTER, $comment: SEEDED_COMMENT, checks: seeded }
|
|
94
|
+
: STARTER;
|
|
95
|
+
write(configPath, JSON.stringify(config, null, 2) + "\n", { force: true });
|
|
96
|
+
console.log(c.green("created") + ` ${CONFIG_REL}` +
|
|
97
|
+
(seeded.length > 0 ? c.gray(` — seeded from the code inventory: ${seeded.map((s) => s.cwd ? `${s.run} (in ${s.cwd}/)` : s.run).join(", ")}`) : ""));
|
|
98
|
+
console.log(c.gray(seeded.length > 0
|
|
99
|
+
? "Confirm each check runs what its name says (add an \"expect\" block where a run could execute nothing), then run `doctrina verify`."
|
|
100
|
+
: "Edit the `checks` array with your real commands, then run `doctrina verify`."));
|
|
75
101
|
return 0;
|
|
76
102
|
}
|
|
77
103
|
|
|
@@ -488,7 +514,9 @@ copy accumulates for the match. An "expect" pattern that is not a valid
|
|
|
488
514
|
regular expression fails at config time, not silently.
|
|
489
515
|
|
|
490
516
|
Flags:
|
|
491
|
-
--init Scaffold a starter ${CONFIG_REL} (refuses to overwrite without --force)
|
|
517
|
+
--init Scaffold a starter ${CONFIG_REL} (refuses to overwrite without --force):
|
|
518
|
+
the build and test commands the code inventory finds in the
|
|
519
|
+
project's manifests, or a fail-closed example when it finds none
|
|
492
520
|
--list Print the configured checks without running them
|
|
493
521
|
--clean Don't run checks; lint the project's package.json files for
|
|
494
522
|
reproducibility footguns (entry points in a build dir with no
|