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/handoff.js
CHANGED
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { exists } from "../lib/fs-ops.js";
|
|
5
|
-
import { collectStatus } from "./status.js";
|
|
6
|
-
import { computeActions } from "./next.js";
|
|
7
|
-
import { openChanges } from "./prime.js";
|
|
8
|
-
import { today } from "../lib/dates.js";
|
|
9
5
|
import { c } from "../lib/colors.js";
|
|
10
6
|
import { notADoctrinaProject } from "../lib/exit-codes.js";
|
|
7
|
+
import { collectSnapshot } from "../lib/snapshot.js";
|
|
8
|
+
import { renderView } from "../lib/views.js";
|
|
11
9
|
|
|
12
10
|
// Session handoff note: everything the NEXT session (a fresh agent, a
|
|
13
11
|
// teammate, tomorrow's you) needs to resume without re-reading the tree —
|
|
@@ -17,6 +15,10 @@ import { notADoctrinaProject } from "../lib/exit-codes.js";
|
|
|
17
15
|
// change. It is a VIEW derived from the tree at call time, deliberately not
|
|
18
16
|
// a stored file: a saved note goes stale the moment work continues, the
|
|
19
17
|
// tree never does (no new home for facts).
|
|
18
|
+
//
|
|
19
|
+
// Since change 0037 that word "view" is literal: the note is rendered by
|
|
20
|
+
// lib/views.js from the one snapshot lib/snapshot.js collects, and is equally
|
|
21
|
+
// reachable as `doctrina status --view handoff`.
|
|
20
22
|
|
|
21
23
|
// Flags this command accepts. Declared HERE, with the command, so
|
|
22
24
|
// adding a command never requires editing the entrypoint — the gap that
|
|
@@ -28,59 +30,7 @@ export async function run(_positional, _flags) {
|
|
|
28
30
|
if (!exists(path.join(projectRoot, ".doctrina"))) {
|
|
29
31
|
throw notADoctrinaProject();
|
|
30
32
|
}
|
|
31
|
-
|
|
32
|
-
const s = collectStatus(projectRoot);
|
|
33
|
-
const open = openChanges(projectRoot);
|
|
34
|
-
const actions = computeActions(projectRoot);
|
|
35
|
-
|
|
36
|
-
console.log(`# Doctrina handoff — ${s.project} (${today()})`);
|
|
37
|
-
console.log("");
|
|
38
|
-
|
|
39
|
-
// Gate digest — the red ones are what the resuming session fixes first.
|
|
40
|
-
console.log("## Where things stand");
|
|
41
|
-
console.log("");
|
|
42
|
-
console.log(`- index: ${s.indexState} · framework stamp: ${s.stamp ?? "—"} (CLI ${s.cli})`);
|
|
43
|
-
console.log(`- coverage: ${s.coverage.pct}% (${s.coverage.totalCovered}/${s.coverage.totalCriteria} criteria` +
|
|
44
|
-
(s.coverage.totalDangling ? `, ${s.coverage.totalDangling} dangling` : "") +
|
|
45
|
-
(s.coverage.totalConditional ? `, ${s.coverage.totalConditional} conditional` : "") + ")");
|
|
46
|
-
const tr = s.trace.anchors === 0 ? "no anchors declared" : `${s.trace.realized}/${s.trace.anchors} anchors realized`;
|
|
47
|
-
console.log(`- trace: ${tr}` + (s.trace.untraceable ? ` (${s.trace.untraceable} untraceable)` : ""));
|
|
48
|
-
console.log(`- verify: ${s.verify.configured ? `${s.verify.checks} checks declared — run \`doctrina verify\`` : "not configured"}`);
|
|
49
|
-
|
|
50
|
-
// The in-flight work, task by task.
|
|
51
|
-
console.log("");
|
|
52
|
-
if (open.length === 0) {
|
|
53
|
-
console.log("## Open work");
|
|
54
|
-
console.log("");
|
|
55
|
-
console.log("- none — the tree is at rest; start with `doctrina work \"<prompt>\"`");
|
|
56
|
-
} else {
|
|
57
|
-
for (const ch of open) {
|
|
58
|
-
console.log(`## Open change \`${ch.id}\`${ch.title ? ` — ${ch.title}` : ""}`);
|
|
59
|
-
console.log("");
|
|
60
|
-
console.log(`- proposal status: ${ch.status}`);
|
|
61
|
-
if (ch.tasksTotal > 0) {
|
|
62
|
-
console.log(`- tasks: ${ch.tasksDone}/${ch.tasksTotal} checked`);
|
|
63
|
-
for (const t of ch.unchecked.slice(0, 8)) console.log(` - [ ] ${t}`);
|
|
64
|
-
if (ch.unchecked.length > 8) console.log(` - … ${ch.unchecked.length - 8} more in tasks.md`);
|
|
65
|
-
} else {
|
|
66
|
-
console.log("- tasks: no tasks.md checklist found");
|
|
67
|
-
}
|
|
68
|
-
console.log(`- resume with: \`doctrina work --resume ${ch.id}\` · close with: \`doctrina close ${ch.id}\``);
|
|
69
|
-
console.log("");
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// What to do, in priority order.
|
|
74
|
-
console.log("## Next actions");
|
|
75
|
-
console.log("");
|
|
76
|
-
if (actions.length === 0) {
|
|
77
|
-
console.log("1. nothing pending — `doctrina next` will confirm; pick up new work");
|
|
78
|
-
} else {
|
|
79
|
-
actions.forEach((a, i) => console.log(`${i + 1}. ${a}`));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
console.log("");
|
|
83
|
-
console.log("*Generated read-only from the tree — regenerate anytime with `doctrina handoff`.*");
|
|
33
|
+
for (const line of renderView("handoff", collectSnapshot(projectRoot))) console.log(line);
|
|
84
34
|
if (process.stdout.isTTY) {
|
|
85
35
|
console.log("");
|
|
86
36
|
console.log(c.gray("tip: pipe it — `doctrina handoff > note.md` — or paste it as the first message of the next session"));
|
|
@@ -96,6 +46,9 @@ with its task-by-task progress and the exact resume command, and the
|
|
|
96
46
|
prioritised next actions. Made to be pasted as the first message of the
|
|
97
47
|
next agent session (or piped to a file).
|
|
98
48
|
|
|
49
|
+
The same view as \`doctrina status --view handoff\`; both render one
|
|
50
|
+
collection of the tree.
|
|
51
|
+
|
|
99
52
|
Deliberately a derived view, not a stored file — the tree is the truth
|
|
100
53
|
and never goes stale; regenerate on demand. Read-only; always exits 0.
|
|
101
54
|
`;
|
package/src/commands/hooks.js
CHANGED
|
@@ -3,10 +3,14 @@ import path from "node:path";
|
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { chmodSync } from "node:fs";
|
|
5
5
|
import { exists, mkdirp, read, relPath, write } from "../lib/fs-ops.js";
|
|
6
|
-
import { locateTemplatesDir } from "../lib/templates.js";
|
|
6
|
+
import { locateTemplatesDir, substitute } from "../lib/templates.js";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
7
10
|
import { flagBool } from "../lib/args.js";
|
|
8
11
|
import { c } from "../lib/colors.js";
|
|
9
12
|
import { suggest } from "../lib/suggest.js";
|
|
13
|
+
import { EXIT } from "../lib/exit-codes.js";
|
|
10
14
|
|
|
11
15
|
const SUBCOMMANDS = ["install"];
|
|
12
16
|
|
|
@@ -30,8 +34,12 @@ export async function run(positional, flags) {
|
|
|
30
34
|
const projectRoot = process.cwd();
|
|
31
35
|
|
|
32
36
|
if (!exists(path.join(projectRoot, ".git"))) {
|
|
37
|
+
// A PRECONDITION, not a gate: nothing was measured and found wanting,
|
|
38
|
+
// the place to put the hook simply does not exist yet. Class 1 told an
|
|
39
|
+
// agent to fix its work and retry the same command, which never clears.
|
|
33
40
|
console.error(c.red("error:") + " not a git repository (no .git/ in cwd)");
|
|
34
|
-
|
|
41
|
+
console.error(c.gray("hint: ") + "run `git init` here, then `doctrina hooks install`");
|
|
42
|
+
return EXIT.PRECONDITION;
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
const hooksDir = path.join(projectRoot, ".git", "hooks");
|
|
@@ -45,7 +53,13 @@ export async function run(positional, flags) {
|
|
|
45
53
|
|
|
46
54
|
const templatesDir = locateTemplatesDir();
|
|
47
55
|
const tplPath = path.join(templatesDir, "hooks", "pre-commit.sample");
|
|
48
|
-
|
|
56
|
+
// The hook pins the CLI that installed it (change 0116). `.git/hooks/` is
|
|
57
|
+
// local to the clone, so an absolute path is the right thing to write;
|
|
58
|
+
// a bare `doctrina` ran whichever release was on the PATH — a global
|
|
59
|
+
// 0.15.0 under a 0.15.1 tree rewrote the stamp backwards on every commit.
|
|
60
|
+
// `DOCTRINA=` in the environment overrides it.
|
|
61
|
+
const entry = path.resolve(here, "..", "index.js").replaceAll("\\", "/");
|
|
62
|
+
const body = substitute(read(tplPath), { DOCTRINA_BIN: `node \"${entry}\"` });
|
|
49
63
|
write(target, body, { force: true });
|
|
50
64
|
chmodSync(target, 0o755);
|
|
51
65
|
console.log(c.green("installed") + ` ${relPath(projectRoot, target)}`);
|
|
@@ -3,8 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { exists } from "../lib/fs-ops.js";
|
|
5
5
|
import * as idx from "../lib/index-json.js";
|
|
6
|
-
import {
|
|
7
|
-
import { cliVersion } from "../lib/version.js";
|
|
6
|
+
import { collectIndexDrift, collectStagedIndexDrift } from "../lib/scan.js";
|
|
8
7
|
import { today } from "../lib/dates.js";
|
|
9
8
|
import { flagBool } from "../lib/args.js";
|
|
10
9
|
import { c } from "../lib/colors.js";
|
|
@@ -16,7 +15,7 @@ const SUBCOMMANDS = ["rebuild"];
|
|
|
16
15
|
// Flags this command accepts. Declared HERE, with the command, so
|
|
17
16
|
// adding a command never requires editing the entrypoint — the gap that
|
|
18
17
|
// let six flags ship undeclared and silently swallow a positional (C3).
|
|
19
|
-
export const flags = { boolean: ["json", "check"], string: [] };
|
|
18
|
+
export const flags = { boolean: ["json", "check", "staged"], string: [] };
|
|
20
19
|
|
|
21
20
|
export async function run(positional, flags) {
|
|
22
21
|
const sub = positional[0];
|
|
@@ -33,44 +32,45 @@ export async function run(positional, flags) {
|
|
|
33
32
|
|
|
34
33
|
function rebuild(flags) {
|
|
35
34
|
const check = flagBool(flags, "check", false);
|
|
35
|
+
const staged = flagBool(flags, "staged", false);
|
|
36
36
|
const projectRoot = process.cwd();
|
|
37
37
|
if (!exists(path.join(projectRoot, ".doctrina"))) {
|
|
38
38
|
throw notADoctrinaProject();
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
if (staged) {
|
|
42
|
+
if (!check) {
|
|
43
|
+
console.error(c.red("error:") + " --staged reads the commit, so it can only check; it never writes");
|
|
44
|
+
console.error(c.gray("hint: ") + "run `doctrina index rebuild --check --staged`");
|
|
45
|
+
return 2;
|
|
46
|
+
}
|
|
47
|
+
return checkStaged(projectRoot);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const state = collectIndexDrift(projectRoot);
|
|
51
|
+
if (state.unreadable) {
|
|
45
52
|
if (check) {
|
|
46
|
-
console.error(c.red("error:") + ` ${
|
|
53
|
+
console.error(c.red("error:") + ` ${state.unreadable}`);
|
|
47
54
|
return 1;
|
|
48
55
|
}
|
|
49
|
-
console.log(c.yellow("warn:") + ` ${
|
|
56
|
+
console.log(c.yellow("warn:") + ` ${state.unreadable} — rebuilding from scratch`);
|
|
50
57
|
}
|
|
51
58
|
|
|
52
|
-
|
|
53
|
-
// Migrate the framework stamp to the running CLI (3.6). deriveIndex carries
|
|
54
|
-
// the old value over (so `next` does not nag on a version-only difference);
|
|
55
|
-
// overriding it here lets a stale stamp count as drift, so `index rebuild`
|
|
56
|
-
// both reports and fixes it instead of short-circuiting on "nothing to do".
|
|
57
|
-
derived.framework_version = cliVersion();
|
|
58
|
-
|
|
59
|
-
if (indexesMatch(derived, current)) {
|
|
59
|
+
if (state.ok) {
|
|
60
60
|
console.log(c.green("ok") + " index.json matches the tree (nothing to do)");
|
|
61
61
|
return 0;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
const drift
|
|
65
|
-
for (const line of drift) console.log((check ? c.yellow("drift: ") : c.gray("sync: ")) + line);
|
|
64
|
+
for (const line of state.drift) console.log((check ? c.yellow("drift: ") : c.gray("sync: ")) + line);
|
|
66
65
|
|
|
67
66
|
if (check) {
|
|
68
67
|
console.log("");
|
|
69
|
-
console.log(c.red("fail") + ` index.json has drifted from the tree (${drift.length} difference${drift.length === 1 ? "" : "s"})`);
|
|
68
|
+
console.log(c.red("fail") + ` index.json has drifted from the tree (${state.drift.length} difference${state.drift.length === 1 ? "" : "s"})`);
|
|
70
69
|
console.log(c.gray("hint: ") + "run `doctrina index rebuild` to regenerate it");
|
|
71
70
|
return 1;
|
|
72
71
|
}
|
|
73
72
|
|
|
73
|
+
const derived = state.derived;
|
|
74
74
|
derived.last_updated = today();
|
|
75
75
|
idx.save(projectRoot, derived);
|
|
76
76
|
const a = derived.artifacts;
|
|
@@ -80,33 +80,37 @@ function rebuild(flags) {
|
|
|
80
80
|
return 0;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
83
|
+
// The drift check, asked of what git will record. `index.json` is derived
|
|
84
|
+
// from the WHOLE tree, so it cannot describe a subset of it: stage part of
|
|
85
|
+
// `.doctrina/` and the index riding along names artifacts the commit does
|
|
86
|
+
// not carry. Every other gate reads the working tree, where those artifacts
|
|
87
|
+
// are still sitting, so none of them can see it.
|
|
88
|
+
function checkStaged(projectRoot) {
|
|
89
|
+
const state = collectStagedIndexDrift(projectRoot);
|
|
90
|
+
if (!state.applicable) {
|
|
91
|
+
console.log(c.green("ok") + ` nothing to check — ${state.reason}`);
|
|
92
|
+
return 0;
|
|
90
93
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
for (const id of cur.keys()) {
|
|
102
|
-
if (!der.has(id)) lines.push(`${cat}: "${id}" in index but not on disk`);
|
|
103
|
-
}
|
|
94
|
+
if (state.ok) {
|
|
95
|
+
console.log(c.green("ok") + " the staged index matches the staged tree");
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
if (state.reason) {
|
|
99
|
+
console.error(c.red("error:") + ` ${state.reason}`);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
for (const p of state.missing) {
|
|
103
|
+
console.log(c.yellow("staged: ") + `${p} — named by the staged index, absent from the commit`);
|
|
104
104
|
}
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
for (const p of state.unindexed) {
|
|
106
|
+
console.log(c.yellow("staged: ") + `${p} — in the commit, absent from the staged index`);
|
|
107
107
|
}
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
const n = state.missing.length + state.unindexed.length;
|
|
109
|
+
console.log("");
|
|
110
|
+
console.log(c.red("fail") + ` the staged index does not describe this commit (${n} difference${n === 1 ? "" : "s"})`);
|
|
111
|
+
console.log(c.gray("hint: ") + "stage the artifacts above with the index, or unstage the index — "
|
|
112
|
+
+ "index.json is derived from the whole tree, so a partial commit of .doctrina/ cannot carry a matching one");
|
|
113
|
+
return 1;
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
export const help = `
|
|
@@ -119,6 +123,13 @@ Subcommands:
|
|
|
119
123
|
source of truth; the index is derived.
|
|
120
124
|
rebuild --check Write nothing; exit 1 with a drift summary when the
|
|
121
125
|
index no longer matches the tree. CI-friendly.
|
|
126
|
+
rebuild --check --staged
|
|
127
|
+
Ask the same question of the COMMIT rather than the
|
|
128
|
+
disk: every artifact the staged index names must be in
|
|
129
|
+
the commit, and every staged artifact must be in the
|
|
130
|
+
index. A partial commit of .doctrina/ cannot satisfy
|
|
131
|
+
both, because the index is derived from the whole tree.
|
|
132
|
+
Exits 0 when nothing relevant is staged.
|
|
122
133
|
|
|
123
134
|
Fields with no on-disk source (project name, framework_version, product
|
|
124
135
|
metadata) are carried over from the existing index.
|
package/src/commands/init.js
CHANGED
|
@@ -7,13 +7,14 @@ import {
|
|
|
7
7
|
surfaceBlock, findSurfaceBlock, agentChangelogBlock, findAgentChangelogBlock,
|
|
8
8
|
} from "../lib/commands.js";
|
|
9
9
|
import * as idx from "../lib/index-json.js";
|
|
10
|
+
import { scaffoldConfig } from "../lib/config.js";
|
|
10
11
|
import { today } from "../lib/dates.js";
|
|
11
12
|
import { cliVersion } from "../lib/version.js";
|
|
12
|
-
import { flagBool, flagString } from "../lib/args.js";
|
|
13
|
+
import { flagBool, flagString, flagGivenWithoutValue } from "../lib/args.js";
|
|
13
14
|
import { c } from "../lib/colors.js";
|
|
14
15
|
import { EXIT } from "../lib/exit-codes.js";
|
|
15
16
|
import { ask, isInteractive } from "../lib/prompt.js";
|
|
16
|
-
import { writeIntakeFile, printBootstrapPlaybook, warnIfThinIntake } from "
|
|
17
|
+
import { looksLikePath, writeIntakeFile, printBootstrapPlaybook, warnIfThinIntake, nextStepAfterInit } from "../lib/intake-model.js";
|
|
17
18
|
|
|
18
19
|
const SUPPORTED_AGENTS = [
|
|
19
20
|
"claude",
|
|
@@ -32,7 +33,7 @@ const SUPPORTED_AGENTS = [
|
|
|
32
33
|
|
|
33
34
|
export const flags = {
|
|
34
35
|
boolean: ["json", "force", "non-interactive", "overwrite-content"],
|
|
35
|
-
string: ["project-name", "project-description", "agent", "intake", "from", "date"],
|
|
36
|
+
string: ["project-name", "project-description", "agent", "intake", "intake-text", "from", "date"],
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
export async function run(_positional, flags) {
|
|
@@ -45,17 +46,56 @@ export async function run(_positional, flags) {
|
|
|
45
46
|
// --intake hands over the FULL description up front (ADR 0005); the
|
|
46
47
|
// one-line summary is then derived instead of prompted for.
|
|
47
48
|
const intakeFile = flagString(flags, "intake");
|
|
49
|
+
const intakeText = flagString(flags, "intake-text");
|
|
48
50
|
let intakeBody = null;
|
|
51
|
+
let intakeSource = null;
|
|
52
|
+
// Onboarding is ONE moment, so it gets one command (change 0051). The
|
|
53
|
+
// separation between `init` and `intake` is an architectural fact — `init`
|
|
54
|
+
// refuses to read language, ADR 0005 — and it was being charged to the
|
|
55
|
+
// user as two steps of setup. Both commands stay: `intake` is still the
|
|
56
|
+
// door for converting a description in a project that already exists.
|
|
57
|
+
if (intakeFile && intakeText !== undefined) {
|
|
58
|
+
console.error(c.red("error:") + " pass --intake <file> or --intake-text \"<text>\", not both");
|
|
59
|
+
return EXIT.USAGE;
|
|
60
|
+
}
|
|
61
|
+
// A flag written without a value is a usage error, never a silent fallback
|
|
62
|
+
// to "no intake": the operator asked for one and would get a project
|
|
63
|
+
// without it, and nothing would say so.
|
|
64
|
+
for (const name of ["intake", "intake-text"]) {
|
|
65
|
+
if (flagGivenWithoutValue(flags, name)) {
|
|
66
|
+
console.error(c.red("error:") + ` --${name} needs a value`);
|
|
67
|
+
console.error(c.gray("hint: ") + (name === "intake"
|
|
68
|
+
? "doctrina init --intake ./description.md"
|
|
69
|
+
: "doctrina init --intake-text \"<the whole project description>\""));
|
|
70
|
+
return EXIT.USAGE;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (typeof intakeText === "string" && intakeText.trim() !== "") {
|
|
74
|
+
intakeBody = intakeText;
|
|
75
|
+
intakeSource = "inline (--intake-text)";
|
|
76
|
+
}
|
|
49
77
|
if (intakeFile) {
|
|
50
78
|
const intakeAbs = path.resolve(projectRoot, intakeFile);
|
|
51
|
-
if (
|
|
79
|
+
if (isFile(intakeAbs)) {
|
|
80
|
+
intakeBody = read(intakeAbs);
|
|
81
|
+
intakeSource = relPath(projectRoot, intakeAbs);
|
|
82
|
+
if (intakeBody.trim().length === 0) {
|
|
83
|
+
console.error(c.red("error:") + ` --intake file is empty: ${intakeFile}`);
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
} else if (looksLikePath(intakeFile)) {
|
|
52
87
|
console.error(c.red("error:") + ` --intake file not found: ${intakeFile}`);
|
|
88
|
+
console.error(c.gray("hint: ") +
|
|
89
|
+
'to pass the description itself, use `--intake-text "<description>"`');
|
|
53
90
|
return 1;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
91
|
+
} else {
|
|
92
|
+
// The same rule `intake` applies to its positional (change 0071): the
|
|
93
|
+
// two commands answer the same input shape the same way, so an agent
|
|
94
|
+
// that learned one is not surprised by the other.
|
|
95
|
+
intakeBody = intakeFile;
|
|
96
|
+
intakeSource = "inline";
|
|
97
|
+
console.error(c.gray("note: ") +
|
|
98
|
+
'read as the description itself, not as a file — `--intake-text "<description>"` says so explicitly');
|
|
59
99
|
}
|
|
60
100
|
}
|
|
61
101
|
|
|
@@ -67,9 +107,20 @@ export async function run(_positional, flags) {
|
|
|
67
107
|
// isTTY check the adapter prompt already had. A non-interactive caller
|
|
68
108
|
// that forgot --non-interactive read EOF, got an empty answer, and
|
|
69
109
|
// scaffolded a project with a blank description — silently (C9).
|
|
110
|
+
// At a terminal, ONE question, and its answer is the intake (change 0173):
|
|
111
|
+
// it was asked as a one-line description, written into product.md, and
|
|
112
|
+
// then `next` asked for the same thing again as an intake. A person
|
|
113
|
+
// describes the project once.
|
|
114
|
+
let intakeFromTty = false;
|
|
70
115
|
if (!description && !nonInteractive) {
|
|
71
116
|
if (isInteractive()) {
|
|
72
|
-
|
|
117
|
+
const answer = await ask("Describe the project — what it is, who it is for, what it must do:");
|
|
118
|
+
if (answer && answer.trim() !== "") {
|
|
119
|
+
intakeBody = answer.trim();
|
|
120
|
+
intakeSource = "typed at `doctrina init`";
|
|
121
|
+
intakeFromTty = true;
|
|
122
|
+
description = firstLineSummary(intakeBody);
|
|
123
|
+
}
|
|
73
124
|
}
|
|
74
125
|
if (!description) {
|
|
75
126
|
console.error(c.red("error:") + " no project description given and no terminal to ask on");
|
|
@@ -214,6 +265,20 @@ export async function run(_positional, flags) {
|
|
|
214
265
|
console.log(c.green("created") + ` ${relPath(projectRoot, written)}`);
|
|
215
266
|
}
|
|
216
267
|
|
|
268
|
+
// config.json, written from the module that READS it (lib/config.js) for
|
|
269
|
+
// the same reason index.json is written from its schema: a template copy
|
|
270
|
+
// is how a project is born already needing a scaffold update. Before this
|
|
271
|
+
// the file was created by nothing, named in no surface block and reported
|
|
272
|
+
// by no command — it existed only for someone who had read the source, and
|
|
273
|
+
// a pt-BR project sat permanently red under `clarify` with no clue why.
|
|
274
|
+
{
|
|
275
|
+
const dest = path.join(doctrinaDir, "config.json");
|
|
276
|
+
if (!isFile(dest) || force) {
|
|
277
|
+
write(dest, scaffoldConfig(), { force });
|
|
278
|
+
console.log(c.green("created") + ` ${relPath(projectRoot, dest)}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
217
282
|
// Adapters
|
|
218
283
|
if (agentSelector) {
|
|
219
284
|
const agents = agentSelector === "all" ? SUPPORTED_AGENTS : [agentSelector];
|
|
@@ -229,7 +294,7 @@ export async function run(_positional, flags) {
|
|
|
229
294
|
if (intakeBody) {
|
|
230
295
|
const intakePath = writeIntakeFile(projectRoot, {
|
|
231
296
|
body: intakeBody,
|
|
232
|
-
source:
|
|
297
|
+
source: intakeSource,
|
|
233
298
|
projectName,
|
|
234
299
|
date,
|
|
235
300
|
force,
|
|
@@ -239,7 +304,11 @@ export async function run(_positional, flags) {
|
|
|
239
304
|
|
|
240
305
|
console.log("");
|
|
241
306
|
console.log(c.bold("Doctrina initialised."));
|
|
242
|
-
if (
|
|
307
|
+
if (intakeFromTty) {
|
|
308
|
+
// A person at a terminal gets the one instruction, not the agent's
|
|
309
|
+
// playbook: the agent reprints it with `doctrina intake`.
|
|
310
|
+
for (const line of nextStepAfterInit("tty")) console.log(line);
|
|
311
|
+
} else if (intakeBody) {
|
|
243
312
|
console.log(`Next: execute the bootstrap playbook below — it converts the intake into`);
|
|
244
313
|
console.log(`${c.cyan(".doctrina/product.md")} and capability specs. Any AGENTS.md-aware agent`);
|
|
245
314
|
console.log(`runs it on its own; reprint anytime with ${c.cyan("doctrina intake")}.`);
|
|
@@ -247,7 +316,7 @@ export async function run(_positional, flags) {
|
|
|
247
316
|
warnIfThinIntake(intakeBody);
|
|
248
317
|
printBootstrapPlaybook(projectRoot);
|
|
249
318
|
} else {
|
|
250
|
-
|
|
319
|
+
for (const line of nextStepAfterInit(null)) console.log(line);
|
|
251
320
|
}
|
|
252
321
|
return 0;
|
|
253
322
|
}
|
|
@@ -361,9 +430,12 @@ Options:
|
|
|
361
430
|
--project-description <text> One-sentence description
|
|
362
431
|
--agent <name> Install adapter for one agent
|
|
363
432
|
(claude|codex|cursor|copilot|gemini|aider|windsurf|continue|amp|devin|factory|jules|all)
|
|
364
|
-
--intake <file> Full project description; stored verbatim at .doctrina/intake.md
|
|
365
|
-
|
|
366
|
-
|
|
433
|
+
--intake <file> Full project description; stored verbatim at .doctrina/intake.md,
|
|
434
|
+
used to derive the one-line description when absent, and the
|
|
435
|
+
bootstrap playbook is printed inline — no second command
|
|
436
|
+
--intake-text "<text>" The same, inline, with no file to write first. Mutually
|
|
437
|
+
exclusive with --intake; either one written without a value
|
|
438
|
+
is a usage error, not a project scaffolded without an intake
|
|
367
439
|
--from <path> Local conventions directory; if it contains AGENTS.md and/or
|
|
368
440
|
.doctrina/product.md, the content is folded into the new project
|
|
369
441
|
--date <YYYY-MM-DD> Override the system date
|