doctrina-cli 0.20.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 +2 -2
- package/package.json +1 -1
- package/src/commands/adapter.js +10 -0
- package/src/commands/change.js +92 -14
- package/src/commands/clarify.js +1 -204
- package/src/commands/context.js +33 -2
- package/src/commands/contract.js +1 -0
- package/src/commands/delivery.js +135 -17
- package/src/commands/doctor.js +21 -1
- package/src/commands/handoff.js +11 -2
- package/src/commands/init.js +13 -1
- package/src/commands/intake.js +16 -5
- package/src/commands/metrics.js +15 -0
- package/src/commands/next.js +41 -6
- package/src/commands/prime.js +53 -4
- 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 +18 -0
- package/src/commands/validate.js +18 -0
- package/src/commands/verify.js +32 -4
- package/src/commands/work.js +43 -8
- package/src/commands/workspace.js +73 -63
- package/src/index.js +98 -62
- package/src/lib/actions.js +43 -3
- package/src/lib/adapters.js +63 -1
- package/src/lib/args.js +33 -1
- package/src/lib/clarify-scan.js +221 -0
- package/src/lib/code-inventory.js +576 -103
- package/src/lib/commands.js +8 -0
- package/src/lib/context-slice.js +34 -3
- package/src/lib/delivery.js +88 -10
- 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/shell.js +45 -0
- package/src/lib/snapshot.js +5 -1
- package/src/lib/task-card.js +100 -7
- package/src/lib/views.js +3 -3
- package/src/lib/work-model.js +8 -2
- package/src/lib/workspace-agents.js +65 -18
- package/src/lib/workspace-guard.js +65 -0
- package/src/lib/workspace-locate.js +141 -0
- package/src/lib/workspace-tools.js +63 -16
- package/src/lib/workspace-view.js +391 -0
- package/src/lib/workspace.js +82 -130
- package/templates/AGENTS.md.template +1 -1
- package/templates/README.md +2 -1
- 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/playbooks/absorb.md.template +39 -16
- package/templates/playbooks/bootstrap.md.template +18 -6
- package/templates/playbooks/delivery.md.template +12 -6
package/src/commands/delivery.js
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import process from "node:process";
|
|
3
|
-
import { relPath, write } from "../lib/fs-ops.js";
|
|
4
|
-
import { flagString } from "../lib/args.js";
|
|
3
|
+
import { eolOf, read, relPath, withEol, write } from "../lib/fs-ops.js";
|
|
4
|
+
import { flagBool, flagString } from "../lib/args.js";
|
|
5
5
|
import { c } from "../lib/colors.js";
|
|
6
|
+
import { emitJson } from "../lib/json-out.js";
|
|
6
7
|
import { suggest } from "../lib/suggest.js";
|
|
7
8
|
import { today } from "../lib/dates.js";
|
|
8
9
|
import { EXIT, PreconditionError } from "../lib/exit-codes.js";
|
|
9
10
|
import { readTemplate, substitute } from "../lib/templates.js";
|
|
10
11
|
import { renderPlaybook } from "../lib/playbook.js";
|
|
11
|
-
import { findWorkspace, readWorkspace } from "../lib/workspace.js";
|
|
12
|
+
import { absorbCommand, findWorkspace, readWorkspace } from "../lib/workspace.js";
|
|
12
13
|
import {
|
|
13
14
|
deliveryBlockers, deliveryChanges, deliveryPath, dependencyOrder, listDeliveries, nextDeliveryId,
|
|
14
15
|
rankProjects, readDelivery, resolveDeliveryId, setDeliveryHeader, daysBetween, recordBlockers,
|
|
16
|
+
releaseBoxes, tickRelease, unroutable, otherWriters,
|
|
15
17
|
} from "../lib/delivery.js";
|
|
18
|
+
import { cdThen, qarg } from "../lib/shell.js";
|
|
16
19
|
|
|
17
20
|
// `doctrina delivery` — one item of work across repositories (ADR 0030).
|
|
18
21
|
// Decisions in lib/delivery.js; this module renders them.
|
|
19
22
|
|
|
20
|
-
const SUBCOMMANDS = ["new", "route", "list", "status", "check", "close"];
|
|
23
|
+
const SUBCOMMANDS = ["new", "route", "list", "status", "tick", "check", "close"];
|
|
21
24
|
|
|
22
|
-
export const flags = { boolean: ["json"], string: ["ref", "projects", "date"] };
|
|
25
|
+
export const flags = { boolean: ["json", "all", "force"], string: ["ref", "projects", "date"] };
|
|
26
|
+
// `delivery status` builds its own payload (change 0234); the rest answer in the envelope.
|
|
27
|
+
export const jsonNative = (/** @type {string[]} */ args) => args[0] === "status";
|
|
23
28
|
|
|
24
29
|
export async function run(positional, flags) {
|
|
25
30
|
const sub = positional[0];
|
|
@@ -43,8 +48,9 @@ export async function run(positional, flags) {
|
|
|
43
48
|
console.error(c.gray("hint: ") + "`doctrina delivery list` shows them");
|
|
44
49
|
return EXIT.USAGE;
|
|
45
50
|
}
|
|
46
|
-
if (sub === "route") return deliveryRoute(wsDir, ws, id, positional[2] ?? flagString(flags, "projects"));
|
|
47
|
-
if (sub === "
|
|
51
|
+
if (sub === "route") return deliveryRoute(wsDir, ws, id, positional[2] ?? flagString(flags, "projects"), flags);
|
|
52
|
+
if (sub === "tick") return deliveryTick(wsDir, id, positional.slice(2), flags);
|
|
53
|
+
if (sub === "status") return deliveryStatus(wsDir, ws, id, flagBool(flags, "json", false));
|
|
48
54
|
if (sub === "check") return deliveryCheck(wsDir, ws, id);
|
|
49
55
|
return deliveryClose(wsDir, ws, id, flags);
|
|
50
56
|
}
|
|
@@ -55,6 +61,20 @@ function parseProjects(ws, raw) {
|
|
|
55
61
|
return { names, unknown };
|
|
56
62
|
}
|
|
57
63
|
|
|
64
|
+
// A project with no spec, or whose repository is gone, cannot take a change
|
|
65
|
+
// for the delivery yet (change 0226). Refused unless --force, naming the fix.
|
|
66
|
+
function refuseUnroutable(wsDir, ws, names, flags) {
|
|
67
|
+
const blocked = unroutable(wsDir, ws, names);
|
|
68
|
+
if (blocked.length === 0 || flagBool(flags, "force", false)) return false;
|
|
69
|
+
for (const b of blocked) {
|
|
70
|
+
console.error(c.red("error:") + ` ${b.name} is ${b.state} (${b.reason}) — a change there would have no spec to change`);
|
|
71
|
+
console.error(c.gray("hint: ") + (b.state === "missing"
|
|
72
|
+
? `point the workspace at the clone: \`doctrina workspace attach ${b.name} --path <dir>\``
|
|
73
|
+
: `absorb it first: \`${absorbCommand(b, b.reason)}\` — or route anyway with --force`));
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
58
78
|
function deliveryNew(wsDir, ws, title, flags) {
|
|
59
79
|
if (!title) {
|
|
60
80
|
console.error(c.red("error:") + " delivery new needs a title");
|
|
@@ -69,6 +89,7 @@ function deliveryNew(wsDir, ws, title, flags) {
|
|
|
69
89
|
console.error(c.red("error:") + ` --projects names no registered project: ${unknown.join(", ")}`);
|
|
70
90
|
return EXIT.USAGE;
|
|
71
91
|
}
|
|
92
|
+
if (refuseUnroutable(wsDir, ws, names, flags)) return EXIT.PRECONDITION;
|
|
72
93
|
const id = nextDeliveryId(wsDir, title);
|
|
73
94
|
const ref = flagString(flags, "ref") ?? "—";
|
|
74
95
|
const ordered = dependencyOrder(ws, names);
|
|
@@ -95,6 +116,7 @@ function playbookTokens(wsDir, ws, delivery) {
|
|
|
95
116
|
return {
|
|
96
117
|
DELIVERY_ID: delivery.id,
|
|
97
118
|
DELIVERY_TITLE: delivery.title,
|
|
119
|
+
DELIVERY_TITLE_ARG: qarg(delivery.title),
|
|
98
120
|
DELIVERY_REF: delivery.ref === "—" ? "(no --ref given: ask the human which item this is)" : delivery.ref,
|
|
99
121
|
DELIVERY_FILE: relPath(wsDir, deliveryPath(wsDir, delivery.id)),
|
|
100
122
|
ROUTING: routing,
|
|
@@ -103,7 +125,7 @@ function playbookTokens(wsDir, ws, delivery) {
|
|
|
103
125
|
};
|
|
104
126
|
}
|
|
105
127
|
|
|
106
|
-
function deliveryRoute(wsDir, ws, id, raw) {
|
|
128
|
+
function deliveryRoute(wsDir, ws, id, raw, flags) {
|
|
107
129
|
const { names, unknown } = parseProjects(ws, raw);
|
|
108
130
|
if (names.length === 0) {
|
|
109
131
|
console.error(c.red("error:") + " delivery route needs the projects, comma-separated");
|
|
@@ -114,15 +136,35 @@ function deliveryRoute(wsDir, ws, id, raw) {
|
|
|
114
136
|
console.error(c.red("error:") + ` no registered project named ${unknown.join(", ")}`);
|
|
115
137
|
return EXIT.USAGE;
|
|
116
138
|
}
|
|
139
|
+
if (refuseUnroutable(wsDir, ws, names, flags)) return EXIT.PRECONDITION;
|
|
140
|
+
// Routing a project OUT while it carries an open change for the delivery
|
|
141
|
+
// (third audit, change 0234) orphaned the change: it kept its Delivery
|
|
142
|
+
// header, the delivery stopped seeing it, and the delivery could close
|
|
143
|
+
// around work still in flight.
|
|
144
|
+
const before = readDelivery(wsDir, id);
|
|
145
|
+
const dropped = before.projects.filter((n) => !names.includes(n));
|
|
146
|
+
const carrying = deliveryChanges(wsDir, ws, { ...before, projects: dropped })
|
|
147
|
+
.flatMap((row) => row.changes.filter((ch) => !ch.archived).map((ch) => ({ project: row.project, id: ch.id })));
|
|
148
|
+
if (carrying.length > 0 && !flagBool(flags, "force", false)) {
|
|
149
|
+
for (const x of carrying) {
|
|
150
|
+
console.error(c.red("error:") + ` ${x.project} carries ${x.id} for ${id} — routing it out would orphan the change`);
|
|
151
|
+
}
|
|
152
|
+
console.error(c.gray("hint: ") + `close or abandon it there first (\`doctrina change abandon <id> --reason "<why>" --force\`), or pass --force to route it out anyway`);
|
|
153
|
+
return EXIT.PRECONDITION;
|
|
154
|
+
}
|
|
117
155
|
const ordered = dependencyOrder(ws, names);
|
|
118
156
|
setDeliveryHeader(wsDir, id, "Projects", ordered.join(", "));
|
|
119
157
|
const d = readDelivery(wsDir, id);
|
|
120
158
|
console.log(c.green("routed") + ` ${id} → ${ordered.join(" → ")}` + c.gray(" (dependencies first)"));
|
|
159
|
+
for (const x of carrying) console.log(c.yellow("warn") + ` ${x.project}'s ${x.id} still names ${id} in its Delivery header — routed out with --force`);
|
|
121
160
|
console.log("");
|
|
161
|
+
const rows = deliveryChanges(wsDir, ws, d);
|
|
122
162
|
console.log("Open one change in each, in this order:");
|
|
123
163
|
for (const name of ordered) {
|
|
124
164
|
const p = ws.projects.find((x) => x.name === name);
|
|
125
|
-
|
|
165
|
+
const has = rows.find((r) => r.project === name)?.changes ?? [];
|
|
166
|
+
if (has.length > 0) console.log(" " + c.gray(`${name} already carries ${has.map((ch) => ch.id).join(", ")}`));
|
|
167
|
+
else console.log(" " + c.cyan(cdThen(p?.path ?? "", `doctrina work ${qarg(d.title)} --delivery ${id}`)));
|
|
126
168
|
}
|
|
127
169
|
return EXIT.OK;
|
|
128
170
|
}
|
|
@@ -151,19 +193,47 @@ function deliveryList(wsDir, ws) {
|
|
|
151
193
|
return EXIT.OK;
|
|
152
194
|
}
|
|
153
195
|
|
|
154
|
-
function deliveryStatus(wsDir, ws, id) {
|
|
196
|
+
function deliveryStatus(wsDir, ws, id, json = false) {
|
|
155
197
|
const d = readDelivery(wsDir, id);
|
|
198
|
+
// A structured payload for the planner's loop (third audit, change 0234):
|
|
199
|
+
// the envelope's text lines were all an agent got to branch on.
|
|
200
|
+
if (json) {
|
|
201
|
+
emitJson("delivery status", {
|
|
202
|
+
id: d.id, title: d.title, ref: d.ref, status: d.status, date: d.date ?? null, projects: d.projects,
|
|
203
|
+
rows: deliveryChanges(wsDir, ws, d).map((r) => ({
|
|
204
|
+
project: r.project, path: r.path ?? null, missing: r.missing ?? null, absent: r.absent ?? false,
|
|
205
|
+
changes: r.changes, abandoned: r.abandoned ?? [],
|
|
206
|
+
})),
|
|
207
|
+
otherWriters: otherWriters(wsDir, ws, d),
|
|
208
|
+
release: releaseBoxes(d.text).map(({ ordinal, checked, text }) => ({ ordinal, checked, text })),
|
|
209
|
+
blockers: deliveryBlockers(wsDir, ws, d),
|
|
210
|
+
});
|
|
211
|
+
return EXIT.OK;
|
|
212
|
+
}
|
|
156
213
|
console.log(c.bold(`Delivery ${d.id}`) + ` — ${d.title}` + c.gray(` (${d.status}, ref ${d.ref}, opened ${d.date ?? "?"})`));
|
|
157
214
|
console.log("");
|
|
158
215
|
for (const row of deliveryChanges(wsDir, ws, d)) {
|
|
159
216
|
console.log(` ${c.cyan(row.project)}${row.path ? c.gray(" " + row.path) : ""}`);
|
|
160
217
|
if (row.missing) console.log(c.red(` ${row.missing}`));
|
|
161
|
-
|
|
218
|
+
// A repository that moved: a `cd` into its old path fails (change 0234).
|
|
219
|
+
else if (row.absent) console.log(c.red(" repository not at its path") + c.gray(` — doctrina workspace attach ${row.project} --path <dir>`));
|
|
220
|
+
else if (row.changes.length === 0 && row.abandoned?.length) console.log(c.yellow(` abandoned ${row.abandoned.map((a) => a.id).join(", ")}`) + c.gray(` — re-route without ${row.project}, or ${cdThen(row.path, `doctrina work ${qarg(d.title)} --delivery ${d.id}`)}`));
|
|
221
|
+
else if (row.changes.length === 0) console.log(c.yellow(" no change yet") + c.gray(` — ${cdThen(row.path, `doctrina work ${qarg(d.title)} --delivery ${d.id}`)}`));
|
|
162
222
|
for (const ch of row.changes) {
|
|
163
223
|
const colour = ch.archived ? c.green : c.yellow;
|
|
164
224
|
console.log(` ${colour(ch.status.padEnd(9))} ${ch.id}` + c.gray(` tasks ${ch.tasksDone}/${ch.tasksTotal}`));
|
|
225
|
+
// The card is read from inside the repository (change 0226): from the
|
|
226
|
+
// workspace root there is no .doctrina/ to read it from.
|
|
227
|
+
if (!ch.archived && ch.placeholders > 0) console.log(c.gray(` tasks not written yet (placeholders): ${cdThen(row.path, `doctrina work --resume ${ch.id}`)}`));
|
|
228
|
+
else if (!ch.archived && ch.tasksDone < ch.tasksTotal) console.log(c.gray(` next card: ${cdThen(row.path, `doctrina change card ${ch.id}`)}`));
|
|
165
229
|
}
|
|
166
230
|
}
|
|
231
|
+
const others = otherWriters(wsDir, ws, d);
|
|
232
|
+
if (others.length > 0) {
|
|
233
|
+
console.log("");
|
|
234
|
+
console.log(c.yellow(" one writer per repository:") + c.gray(" these changes are open in the same repositories — sequence them, never two writers at once"));
|
|
235
|
+
for (const o of others) console.log(c.gray(` ${o.project}: ${o.id}${o.delivery ? ` (delivery ${o.delivery})` : " (no delivery)"}`));
|
|
236
|
+
}
|
|
167
237
|
const record = recordBlockers(d);
|
|
168
238
|
if (record.length > 0) {
|
|
169
239
|
console.log(` ${c.cyan("record")}`);
|
|
@@ -177,6 +247,43 @@ function deliveryStatus(wsDir, ws, id) {
|
|
|
177
247
|
return EXIT.OK;
|
|
178
248
|
}
|
|
179
249
|
|
|
250
|
+
// The Release claims are ticked through the CLI, like a change's boxes
|
|
251
|
+
// (change 0226): Story, Sources, Plan and Observability are prose the planner
|
|
252
|
+
// writes; a Release box is a claim `delivery check` holds it to.
|
|
253
|
+
function deliveryTick(wsDir, id, args, flags) {
|
|
254
|
+
const file = deliveryPath(wsDir, id);
|
|
255
|
+
const text = read(file);
|
|
256
|
+
const boxes = releaseBoxes(text);
|
|
257
|
+
if (boxes.length === 0) {
|
|
258
|
+
console.log(c.gray(`${id} has no box under ## Release`));
|
|
259
|
+
return EXIT.OK;
|
|
260
|
+
}
|
|
261
|
+
const all = flagBool(flags, "all", false);
|
|
262
|
+
if (!all && args.length === 0) {
|
|
263
|
+
console.log(c.bold(`Release boxes in ${id}`) + c.gray(` (${boxes.filter((b) => !b.checked).length} of ${boxes.length} unticked):`));
|
|
264
|
+
for (const b of boxes) console.log(` ${String(b.ordinal).padStart(2)}. [${b.checked ? "x" : " "}] ${b.text}`);
|
|
265
|
+
console.log(c.gray("tick: ") + c.cyan(`doctrina delivery tick ${id} <n...>`) + c.gray(" · all: ") + c.cyan(`doctrina delivery tick ${id} --all`));
|
|
266
|
+
return EXIT.OK;
|
|
267
|
+
}
|
|
268
|
+
const ordinals = all ? "all" : args.map(Number);
|
|
269
|
+
if (ordinals !== "all") {
|
|
270
|
+
const bad = args.filter((a, i) => !Number.isInteger(ordinals[i]) || ordinals[i] < 1 || ordinals[i] > boxes.length);
|
|
271
|
+
if (bad.length > 0) {
|
|
272
|
+
console.error(c.red("error:") + ` no Release box ${bad.join(", ")} (1..${boxes.length} — \`doctrina delivery tick ${id}\` lists them)`);
|
|
273
|
+
return EXIT.USAGE;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Count what this call changes: a box already ticked, or named twice, is
|
|
277
|
+
// not news, and "ticked 2" for one new tick is a claim the file contradicts.
|
|
278
|
+
const fresh = boxes.filter((b) => !b.checked && (ordinals === "all" || ordinals.includes(b.ordinal)));
|
|
279
|
+
if (fresh.length > 0) write(file, withEol(tickRelease(text, ordinals), eolOf(text)), { force: true });
|
|
280
|
+
const n = fresh.length;
|
|
281
|
+
console.log(n === 0
|
|
282
|
+
? c.gray(`nothing to tick in ${id} — every named Release box is already ticked`)
|
|
283
|
+
: c.green("ticked") + ` ${n} Release box${n === 1 ? "" : "es"} in ${id}`);
|
|
284
|
+
return EXIT.OK;
|
|
285
|
+
}
|
|
286
|
+
|
|
180
287
|
function deliveryCheck(wsDir, ws, id) {
|
|
181
288
|
const d = readDelivery(wsDir, id);
|
|
182
289
|
const blockers = deliveryBlockers(wsDir, ws, d);
|
|
@@ -206,7 +313,7 @@ function deliveryClose(wsDir, ws, id, flags) {
|
|
|
206
313
|
}
|
|
207
314
|
|
|
208
315
|
export const help = `
|
|
209
|
-
Usage: doctrina delivery <new|route|list|status|check|close> [options]
|
|
316
|
+
Usage: doctrina delivery <new|route|list|status|tick|check|close> [options]
|
|
210
317
|
|
|
211
318
|
One item of work across repositories (ADR 0030) — a story, a screen, a fix.
|
|
212
319
|
It lives in the workspace's deliveries/ and holds what no single repository
|
|
@@ -214,24 +321,35 @@ owns. Each project it touches gets ONE change, opened there with
|
|
|
214
321
|
\`doctrina work "<title>" --delivery <id>\`; the delivery's state is read
|
|
215
322
|
from those changes, never copied into it.
|
|
216
323
|
|
|
217
|
-
new "<title>" [--ref <tracker-id>] [--projects <a,b>]
|
|
324
|
+
new "<title>" [--ref <tracker-id>] [--projects <a,b>] [--force]
|
|
218
325
|
Create deliveries/<NNNN-slug>/delivery.md and print the orchestration
|
|
219
326
|
playbook: read the story, route, open one change per repository,
|
|
220
327
|
delegate the task cards, close each change, release, watch, close.
|
|
221
328
|
Without --projects it ranks the workspace's projects against the title.
|
|
222
|
-
route <id> <a,b>
|
|
329
|
+
route <id> <a,b> [--force]
|
|
223
330
|
Set the projects (dependencies first) and print the \`work\` line for each.
|
|
331
|
+
A project still pending (no spec) or missing is refused — absorb it with
|
|
332
|
+
\`doctrina intake --from-code\` first, or pass --force.
|
|
333
|
+
Dropping a project that carries an open change for the delivery is
|
|
334
|
+
refused too — close or abandon the change there first, or --force.
|
|
224
335
|
list
|
|
225
336
|
Every delivery with its status, ref, closed/total changes and age —
|
|
226
337
|
lead time once closed, with the median over the closed ones.
|
|
227
338
|
status <id>
|
|
228
|
-
Per project: the changes carrying this delivery, their state and tasks
|
|
339
|
+
Per project: the changes carrying this delivery, their state and tasks,
|
|
340
|
+
the command that reads the next task card, and any other open change
|
|
341
|
+
in the same repository — one writer per repository at a time.
|
|
342
|
+
--json: the same as a payload (rows, release boxes, blockers).
|
|
343
|
+
tick <id> [<n>... | --all]
|
|
344
|
+
List the ## Release boxes, or tick them: they are claims the check
|
|
345
|
+
holds you to, so they are ticked through the CLI.
|
|
229
346
|
check <id>
|
|
230
347
|
Exit 1 while a routed project has no change for it or one still open,
|
|
231
348
|
while ## Story is empty, a ## Release box is unticked, or
|
|
232
349
|
## Observability records nothing.
|
|
233
|
-
close <id>
|
|
234
|
-
The check, then Status: closed and the Closed date
|
|
350
|
+
close <id> [--date <YYYY-MM-DD>]
|
|
351
|
+
The check, then Status: closed and the Closed date (--date overrides
|
|
352
|
+
today, as it does for the Date that \`new\` writes).
|
|
235
353
|
|
|
236
354
|
<id> is the folder name or its number (\`1\`, \`0001\`).
|
|
237
355
|
`;
|
package/src/commands/doctor.js
CHANGED
|
@@ -8,7 +8,7 @@ import { c } from "../lib/colors.js";
|
|
|
8
8
|
import { flagBool } from "../lib/args.js";
|
|
9
9
|
import { collectRuntimeFindings, checkLocalEnv } from "../lib/runtime.js";
|
|
10
10
|
import { sequence, stepRerun } from "../lib/gates.js";
|
|
11
|
-
import { notADoctrinaProject } from "../lib/exit-codes.js";
|
|
11
|
+
import { EXIT, notADoctrinaProject } from "../lib/exit-codes.js";
|
|
12
12
|
import { agentsMdBudget, collectValidation } from "../lib/validation-model.js";
|
|
13
13
|
import { collectIndexDrift } from "../lib/scan.js";
|
|
14
14
|
import { collectReproducibility } from "../lib/reproducibility.js";
|
|
@@ -17,6 +17,8 @@ import { USAGE_ENV, summarise } from "../lib/usage.js";
|
|
|
17
17
|
import { OPERATIONS } from "../lib/commands.js";
|
|
18
18
|
|
|
19
19
|
import { coverageLabel } from "../lib/views.js";
|
|
20
|
+
import { workspaceFindings, workspaceRootAt, workspaceSnapshot } from "../lib/workspace-view.js";
|
|
21
|
+
import { cdThen } from "../lib/shell.js";
|
|
20
22
|
|
|
21
23
|
// How many never-invoked operations the usage row names before pointing at
|
|
22
24
|
// `metrics --commands` for the rest. The spec states this number, so it is
|
|
@@ -43,6 +45,24 @@ export const flags = { boolean: ["json", "env"], string: [] };
|
|
|
43
45
|
|
|
44
46
|
export async function run(_positional, _flags) {
|
|
45
47
|
const projectRoot = process.cwd();
|
|
48
|
+
// At the workspace root (fourth audit, change 0235): the planner reaches
|
|
49
|
+
// for doctor when something looks wrong, and was told to `doctrina init`
|
|
50
|
+
// — a project inside the workspace. The root's diagnosis is the
|
|
51
|
+
// workspace's own integrity; each repository diagnoses itself.
|
|
52
|
+
const wsRoot = workspaceRootAt(projectRoot);
|
|
53
|
+
if (wsRoot) {
|
|
54
|
+
const snap = workspaceSnapshot(wsRoot);
|
|
55
|
+
const { errors, warnings } = workspaceFindings(snap);
|
|
56
|
+
console.log(c.bold("Doctrina doctor") + c.gray(` — workspace "${snap.ws.name}" (read-only):`));
|
|
57
|
+
console.log("");
|
|
58
|
+
for (const e of errors) console.log(` ${c.red("error")} ${e}`);
|
|
59
|
+
for (const w of warnings) console.log(` ${c.yellow("warn ")} ${w}`);
|
|
60
|
+
if (errors.length + warnings.length === 0) console.log(` ${c.green("ok ")} the workspace is consistent`);
|
|
61
|
+
console.log("");
|
|
62
|
+
console.log("Each repository diagnoses itself:");
|
|
63
|
+
for (const p of snap.projects.filter((x) => x.state !== "missing")) console.log(" " + c.cyan(cdThen(p.project.path, "doctrina doctor")));
|
|
64
|
+
return errors.length > 0 ? EXIT.GATE : EXIT.OK;
|
|
65
|
+
}
|
|
46
66
|
if (!exists(path.join(projectRoot, ".doctrina"))) {
|
|
47
67
|
throw notADoctrinaProject();
|
|
48
68
|
}
|
package/src/commands/handoff.js
CHANGED
|
@@ -6,6 +6,8 @@ import { c } from "../lib/colors.js";
|
|
|
6
6
|
import { notADoctrinaProject } from "../lib/exit-codes.js";
|
|
7
7
|
import { collectSnapshot } from "../lib/snapshot.js";
|
|
8
8
|
import { renderView } from "../lib/views.js";
|
|
9
|
+
import { contextRouting, workspaceActions, workspaceFindings, workspaceHandoff, workspaceRootAt, workspaceSnapshot, workspaceStatus } from "../lib/workspace-view.js";
|
|
10
|
+
import { today } from "../lib/dates.js";
|
|
9
11
|
|
|
10
12
|
// Session handoff note: everything the NEXT session (a fresh agent, a
|
|
11
13
|
// teammate, tomorrow's you) needs to resume without re-reading the tree —
|
|
@@ -27,10 +29,14 @@ export const flags = { boolean: ["json"], string: [] };
|
|
|
27
29
|
|
|
28
30
|
export async function run(_positional, _flags) {
|
|
29
31
|
const projectRoot = process.cwd();
|
|
30
|
-
|
|
32
|
+
const wsRoot = workspaceRootAt(projectRoot);
|
|
33
|
+
if (!wsRoot && !exists(path.join(projectRoot, ".doctrina"))) {
|
|
31
34
|
throw notADoctrinaProject();
|
|
32
35
|
}
|
|
33
|
-
|
|
36
|
+
// At a workspace root: the planner's resume note — projects, open
|
|
37
|
+
// deliveries and their changes, the next actions (change 0230).
|
|
38
|
+
const lines = wsRoot ? workspaceHandoff(workspaceSnapshot(wsRoot), today()) : renderView("handoff", collectSnapshot(projectRoot));
|
|
39
|
+
for (const line of lines) console.log(line);
|
|
34
40
|
if (process.stdout.isTTY) {
|
|
35
41
|
console.log("");
|
|
36
42
|
console.log(c.gray("tip: pipe it — `doctrina handoff > note.md` — or paste it as the first message of the next session"));
|
|
@@ -46,6 +52,9 @@ with its task-by-task progress and the exact resume command, and the
|
|
|
46
52
|
prioritised next actions. Made to be pasted as the first message of the
|
|
47
53
|
next agent session (or piped to a file).
|
|
48
54
|
|
|
55
|
+
At a workspace root it is the planner's note instead: projects and
|
|
56
|
+
their state, open deliveries with each repository's changes, next actions.
|
|
57
|
+
|
|
49
58
|
The same view as \`doctrina status --view handoff\`; both render one
|
|
50
59
|
collection of the tree.
|
|
51
60
|
|
package/src/commands/init.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { attachCommand, shadowedCloneAt } from "../lib/workspace-view.js";
|
|
3
4
|
import process from "node:process";
|
|
4
5
|
import { exists, isDir, isFile, read, relPath, write } from "../lib/fs-ops.js";
|
|
5
6
|
import { locateTemplatesDir, loadTemplateTree, materialiseEntry, substitute } from "../lib/templates.js";
|
|
@@ -41,6 +42,15 @@ export async function run(_positional, flags) {
|
|
|
41
42
|
const force = flagBool(flags, "force", false);
|
|
42
43
|
const nonInteractive = flagBool(flags, "non-interactive", false);
|
|
43
44
|
const projectRoot = process.cwd();
|
|
45
|
+
// A clone of a shadowed repository (third audit, change 0234): its context
|
|
46
|
+
// lives in the workspace, and a .doctrina/ planted here is one the
|
|
47
|
+
// repository may not carry — and one the workspace would never see.
|
|
48
|
+
const clone = force ? null : shadowedCloneAt(projectRoot);
|
|
49
|
+
if (clone && !path.resolve(projectRoot).startsWith(path.resolve(clone.wsDir) + path.sep)) {
|
|
50
|
+
console.error(c.red("error:") + ` this repository is the workspace's shadowed "${clone.name}" (registered at ${clone.path}) — its context lives in the workspace, not here`);
|
|
51
|
+
console.error(c.gray("hint: ") + `link it here instead: \`${attachCommand(clone, projectRoot)}\` — or pass --force to init a separate project anyway`);
|
|
52
|
+
return EXIT.PRECONDITION;
|
|
53
|
+
}
|
|
44
54
|
const projectName = flagString(flags, "project-name") ?? path.basename(projectRoot);
|
|
45
55
|
const date = flagString(flags, "date") ?? today();
|
|
46
56
|
|
|
@@ -454,6 +464,8 @@ Options:
|
|
|
454
464
|
--from <path> Local conventions directory; if it contains AGENTS.md and/or
|
|
455
465
|
.doctrina/product.md, the content is folded into the new project
|
|
456
466
|
--date <YYYY-MM-DD> Override the system date
|
|
457
|
-
--force Overwrite existing files
|
|
467
|
+
--force Overwrite existing files that are still untouched scaffolds;
|
|
468
|
+
also init a second clone of a repository a workspace shadows
|
|
469
|
+
--overwrite-content With --force, also overwrite an authored AGENTS.md or product.md
|
|
458
470
|
--non-interactive Fail instead of prompting for missing values
|
|
459
471
|
`;
|
package/src/commands/intake.js
CHANGED
|
@@ -43,7 +43,7 @@ export async function run(positional, flags) {
|
|
|
43
43
|
// a bootstrap that was already done.
|
|
44
44
|
if (flagBool(flags, "converted", false)) {
|
|
45
45
|
ensureDoctrinaProject(projectRoot);
|
|
46
|
-
// Converting is the claim that the bootstrap is done — step
|
|
46
|
+
// Converting is the claim that the bootstrap is done — step 8 of the
|
|
47
47
|
// playbook, "fix everything the gates report", made checkable (change
|
|
48
48
|
// 0194). It used to convert over a tree `validate` failed, so the specs
|
|
49
49
|
// became "the source of truth" while an error still said they were not
|
|
@@ -95,7 +95,12 @@ export async function run(positional, flags) {
|
|
|
95
95
|
console.log(c.gray("hint: ") + "start implementation with `doctrina work \"<brief prompt>\"`");
|
|
96
96
|
return 0;
|
|
97
97
|
}
|
|
98
|
-
|
|
98
|
+
// An intake written by `--from-code` is a code inventory, and the
|
|
99
|
+
// playbook that reads it is the absorb (second audit, change 0233). The
|
|
100
|
+
// bootstrap one — "fill every product.md section from the intake" —
|
|
101
|
+
// handed a resuming agent the inventory as if it were the product's intent.
|
|
102
|
+
if (/code inventory/i.test(listHeader(read(intakePath), "Source") ?? "")) printAbsorbPlaybook(projectRoot);
|
|
103
|
+
else printBootstrapPlaybook(projectRoot);
|
|
99
104
|
return 0;
|
|
100
105
|
}
|
|
101
106
|
|
|
@@ -180,15 +185,21 @@ function intakeFromCode(projectRoot, force) {
|
|
|
180
185
|
source: "code inventory (`doctrina intake --from-code`)",
|
|
181
186
|
projectName: name, date: today(), force: true,
|
|
182
187
|
});
|
|
183
|
-
|
|
188
|
+
const plural = (/** @type {number} */ n, /** @type {string} */ word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
189
|
+
console.log(c.green("created") + ` ${relPath(projectRoot, intakePath)} — ${plural(inv.files, "file")} read: ${inv.stack.join(", ") || "no manifest"}; ${plural(inv.tests.files, "test file")}; ${plural(inv.areas.length, "candidate area")}`);
|
|
184
190
|
console.log("");
|
|
191
|
+
printAbsorbPlaybook(projectRoot);
|
|
192
|
+
return EXIT.OK;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** The absorb playbook — printed by `--from-code`, and again by a bare `intake` while it is pending. */
|
|
196
|
+
function printAbsorbPlaybook(projectRoot) {
|
|
185
197
|
const ws = findWorkspace(projectRoot);
|
|
186
198
|
printPlaybookTemplate(projectRoot, "absorb", {
|
|
187
199
|
WORKSPACE_STEP: ws
|
|
188
|
-
? `\
|
|
200
|
+
? `\n10. This project belongs to the workspace at ${ws.dir} — confirm it reads\n ready: ${c.cyan("doctrina workspace list")}, and commit the workspace.`
|
|
189
201
|
: "",
|
|
190
202
|
});
|
|
191
|
-
return EXIT.OK;
|
|
192
203
|
}
|
|
193
204
|
|
|
194
205
|
export const help = `
|
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
|
}
|
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,10 +1,11 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
|
-
import { exists, read } from "../lib/fs-ops.js";
|
|
4
|
+
import { exists, isFile, read } from "../lib/fs-ops.js";
|
|
5
5
|
import { findWorkspace, readWorkspace } from "../lib/workspace.js";
|
|
6
6
|
import { listDeliveries } from "../lib/delivery.js";
|
|
7
7
|
import { workspacePrimer } from "../lib/workspace-agents.js";
|
|
8
|
+
import { projectWorkspaceLine } from "../lib/workspace-view.js";
|
|
8
9
|
import { flagBool, flagString } from "../lib/args.js";
|
|
9
10
|
import { c } from "../lib/colors.js";
|
|
10
11
|
import { EXIT, notADoctrinaProject } from "../lib/exit-codes.js";
|
|
@@ -51,6 +52,14 @@ export async function run(_positional, flags) {
|
|
|
51
52
|
// --pulse: one line for a per-prompt hook — the open changes only, read
|
|
52
53
|
// without collecting the gates, because it runs before every prompt.
|
|
53
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
|
+
}
|
|
54
63
|
for (const line of pulse(openChanges(projectRoot), recentlyClosed(projectRoot, { limit: 1 }))) console.log(line);
|
|
55
64
|
return 0;
|
|
56
65
|
}
|
|
@@ -60,17 +69,55 @@ export async function run(_positional, flags) {
|
|
|
60
69
|
console.error(c.red("error:") + ` --hook needs one of ${HOOK_AGENTS.join(", ")} (got "${agent ?? ""}")`);
|
|
61
70
|
return EXIT.USAGE;
|
|
62
71
|
}
|
|
72
|
+
const as = hookFlavour(agent, projectRoot);
|
|
73
|
+
if (as === null) return 0;
|
|
63
74
|
const text = ["Doctrina session primer (`doctrina prime`) — the project's gates, rules and open work:", "",
|
|
64
|
-
...renderView("prime", collectSnapshot(projectRoot)), "", ...pulse(openChanges(projectRoot), recentlyClosed(projectRoot, { limit: 1 }))]
|
|
75
|
+
...renderView("prime", collectSnapshot(projectRoot)), ...workspaceLines(projectRoot), "", ...pulse(openChanges(projectRoot), recentlyClosed(projectRoot, { limit: 1 }))]
|
|
65
76
|
.join("\n").replace(/\x1b\[[0-9;]*m/g, "").slice(0, HOOK_MAX_CHARS);
|
|
66
|
-
console.log(
|
|
77
|
+
console.log(as === "cursor" ? JSON.stringify({ additional_context: text }) : text);
|
|
67
78
|
return 0;
|
|
68
79
|
}
|
|
69
80
|
const view = flagBool(flags, "rules", false) ? "rules" : "prime";
|
|
70
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);
|
|
71
83
|
return 0;
|
|
72
84
|
}
|
|
73
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
|
+
|
|
74
121
|
function primeWorkspace(wsDir, flags) {
|
|
75
122
|
const ws = readWorkspace(wsDir);
|
|
76
123
|
const open = listDeliveries(wsDir).filter((d) => d.status !== "closed");
|
|
@@ -87,8 +134,10 @@ function primeWorkspace(wsDir, flags) {
|
|
|
87
134
|
console.error(c.red("error:") + ` --hook needs one of ${HOOK_AGENTS.join(", ")} (got "${agent ?? ""}")`);
|
|
88
135
|
return EXIT.USAGE;
|
|
89
136
|
}
|
|
137
|
+
const as = hookFlavour(agent, wsDir);
|
|
138
|
+
if (as === null) return 0;
|
|
90
139
|
const text = [...lines, "", read(path.join(wsDir, "AGENTS.md"))].join("\n").replace(/\x1b\[[0-9;]*m/g, "").slice(0, HOOK_MAX_CHARS);
|
|
91
|
-
console.log(
|
|
140
|
+
console.log(as === "cursor" ? JSON.stringify({ additional_context: text }) : text);
|
|
92
141
|
return 0;
|
|
93
142
|
}
|
|
94
143
|
for (const line of lines) console.log(line);
|