doctrina-cli 0.20.0 → 0.22.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 +7 -7
- package/package.json +1 -1
- package/src/commands/adapter.js +10 -0
- package/src/commands/change.js +202 -27
- package/src/commands/clarify.js +3 -206
- package/src/commands/close.js +1 -1
- package/src/commands/context.js +33 -2
- package/src/commands/contract.js +1 -0
- package/src/commands/coverage.js +69 -6
- package/src/commands/delivery.js +135 -17
- package/src/commands/doctor.js +83 -5
- package/src/commands/handoff.js +11 -2
- package/src/commands/hooks.js +56 -11
- package/src/commands/index-rebuild.js +47 -1
- package/src/commands/init.js +64 -8
- package/src/commands/intake.js +16 -5
- package/src/commands/metrics.js +16 -1
- package/src/commands/next.js +41 -6
- package/src/commands/onboard.js +113 -0
- package/src/commands/prime.js +53 -4
- package/src/commands/review.js +40 -4
- package/src/commands/search.js +2 -2
- package/src/commands/skill.js +2 -1
- package/src/commands/status.js +28 -0
- package/src/commands/templates.js +2 -1
- package/src/commands/triage.js +16 -1
- package/src/commands/ui.js +512 -0
- package/src/commands/upgrade.js +62 -4
- package/src/commands/validate.js +18 -0
- package/src/commands/verify.js +35 -7
- package/src/commands/why.js +1 -1
- package/src/commands/work.js +90 -42
- package/src/commands/workspace.js +89 -66
- package/src/index.js +107 -62
- package/src/lib/actions.js +62 -3
- package/src/lib/adapters.js +83 -1
- package/src/lib/agent-bridge.js +400 -0
- package/src/lib/applied-deltas.js +82 -0
- package/src/lib/args.js +34 -2
- package/src/lib/change-numbers.js +115 -0
- package/src/lib/change-ops.js +2 -2
- package/src/lib/clarify-scan.js +221 -0
- package/src/lib/code-inventory.js +594 -104
- package/src/lib/commands.js +58 -4
- package/src/lib/config.js +27 -8
- package/src/lib/conflict-markers.js +61 -0
- package/src/lib/console-model.js +668 -0
- package/src/lib/console-prefs.js +67 -0
- package/src/lib/console-room.js +201 -0
- package/src/lib/console-state.js +150 -0
- package/src/lib/console-view.js +557 -0
- package/src/lib/context-slice.js +34 -3
- package/src/lib/coverage-model.js +29 -6
- package/src/lib/delivery.js +90 -12
- package/src/lib/docs-impact.js +37 -2
- package/src/lib/fs-ops.js +103 -12
- package/src/lib/gates.js +7 -1
- package/src/lib/git.js +18 -0
- package/src/lib/index-json.js +10 -1
- package/src/lib/index-merge.js +90 -0
- package/src/lib/install.js +80 -0
- package/src/lib/intake-model.js +5 -2
- package/src/lib/json-out.js +9 -1
- package/src/lib/ledger.js +2 -3
- package/src/lib/lock.js +58 -0
- package/src/lib/merge-setup.js +55 -0
- package/src/lib/metrics-model.js +2 -2
- package/src/lib/onboard.js +197 -0
- package/src/lib/preset.js +177 -0
- package/src/lib/prompt.js +1 -1
- package/src/lib/properties.js +149 -0
- package/src/lib/runtime.js +19 -7
- package/src/lib/scan.js +13 -8
- package/src/lib/shell.js +45 -0
- package/src/lib/snapshot.js +5 -1
- package/src/lib/spec-ops.js +2 -2
- package/src/lib/task-card.js +101 -8
- package/src/lib/templates-model.js +3 -2
- package/src/lib/triage-model.js +75 -0
- package/src/lib/validation-model.js +23 -5
- package/src/lib/views.js +5 -4
- package/src/lib/work-model.js +8 -2
- package/src/lib/workspace-agents.js +67 -20
- package/src/lib/workspace-guard.js +65 -0
- package/src/lib/workspace-locate.js +141 -0
- package/src/lib/workspace-tools.js +104 -17
- package/src/lib/workspace-view.js +391 -0
- package/src/lib/workspace.js +83 -131
- package/templates/AGENTS.md.template +9 -5
- package/templates/README.md +4 -2
- package/templates/adapters/claude/.claude/commands/doctrina-onboard.md.template +15 -0
- package/templates/adapters/cursor/.cursor/rules/00-doctrina.mdc.template +2 -0
- 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/skills/doctrina-onboard/SKILL.md.template +18 -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/adapters/kiro/README.md +22 -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/init.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
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";
|
|
6
7
|
import {
|
|
7
|
-
surfaceBlock, findSurfaceBlock, agentChangelogBlock, findAgentChangelogBlock,
|
|
8
|
+
surfaceBlock, findSurfaceBlock, agentChangelogBlock, findAgentChangelogBlock, SURFACE_PROFILES,
|
|
8
9
|
} from "../lib/commands.js";
|
|
9
10
|
import * as idx from "../lib/index-json.js";
|
|
10
11
|
import { scaffoldConfig } from "../lib/config.js";
|
|
12
|
+
import { applyPreset, describePreset, planPreset, readPreset } from "../lib/preset.js";
|
|
13
|
+
import { deriveIndex } from "../lib/scan.js";
|
|
11
14
|
import { today } from "../lib/dates.js";
|
|
12
15
|
import { cliVersion } from "../lib/version.js";
|
|
13
16
|
import { flagBool, flagString, flagGivenWithoutValue } from "../lib/args.js";
|
|
@@ -30,17 +33,27 @@ const SUPPORTED_AGENTS = [
|
|
|
30
33
|
"devin",
|
|
31
34
|
"factory",
|
|
32
35
|
"jules",
|
|
36
|
+
"kiro",
|
|
33
37
|
];
|
|
34
38
|
|
|
35
39
|
export const flags = {
|
|
36
40
|
boolean: ["json", "force", "non-interactive", "overwrite-content"],
|
|
37
|
-
string: ["project-name", "project-description", "agent", "intake", "intake-text", "from", "date"],
|
|
41
|
+
string: ["project-name", "project-description", "agent", "intake", "intake-text", "from", "date", "surface", "preset"],
|
|
38
42
|
};
|
|
39
43
|
|
|
40
44
|
export async function run(_positional, flags) {
|
|
41
45
|
const force = flagBool(flags, "force", false);
|
|
42
46
|
const nonInteractive = flagBool(flags, "non-interactive", false);
|
|
43
47
|
const projectRoot = process.cwd();
|
|
48
|
+
// A clone of a shadowed repository (third audit, change 0234): its context
|
|
49
|
+
// lives in the workspace, and a .doctrina/ planted here is one the
|
|
50
|
+
// repository may not carry — and one the workspace would never see.
|
|
51
|
+
const clone = force ? null : shadowedCloneAt(projectRoot);
|
|
52
|
+
if (clone && !path.resolve(projectRoot).startsWith(path.resolve(clone.wsDir) + path.sep)) {
|
|
53
|
+
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`);
|
|
54
|
+
console.error(c.gray("hint: ") + `link it here instead: \`${attachCommand(clone, projectRoot)}\` — or pass --force to init a separate project anyway`);
|
|
55
|
+
return EXIT.PRECONDITION;
|
|
56
|
+
}
|
|
44
57
|
const projectName = flagString(flags, "project-name") ?? path.basename(projectRoot);
|
|
45
58
|
const date = flagString(flags, "date") ?? today();
|
|
46
59
|
|
|
@@ -132,6 +145,30 @@ export async function run(_positional, flags) {
|
|
|
132
145
|
}
|
|
133
146
|
}
|
|
134
147
|
|
|
148
|
+
// Which commands AGENTS.md lists (change 0251). A new project is born with
|
|
149
|
+
// the session loop only — `core` — and the rest one `--help` away; the
|
|
150
|
+
// choice is written into config.json, where `upgrade` reads it.
|
|
151
|
+
// The organisation's preset (change 0253), read BEFORE anything is
|
|
152
|
+
// written: a folder that is not one stops the init with nothing scaffolded.
|
|
153
|
+
let preset = null;
|
|
154
|
+
if (flags.has("preset")) {
|
|
155
|
+
try {
|
|
156
|
+
const dir = flagString(flags, "preset");
|
|
157
|
+
if (!dir) throw new Error("--preset needs the preset's folder");
|
|
158
|
+
preset = readPreset(dir);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.error(c.red("error:") + ` ${/** @type {Error} */ (err).message}`);
|
|
161
|
+
return 2;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// The preset's surface is the default when the flag names none: the block
|
|
165
|
+
// is generated once, from the profile the project will keep.
|
|
166
|
+
const surface = flagString(flags, "surface") ?? preset?.config.surface ?? "core";
|
|
167
|
+
if (!SURFACE_PROFILES.includes(surface) || flagGivenWithoutValue(flags, "surface")) {
|
|
168
|
+
console.error(c.red("error:") + ` --surface takes ${SURFACE_PROFILES.join(" or ")}${flagGivenWithoutValue(flags, "surface") ? "" : ` (got "${surface}")`}`);
|
|
169
|
+
return 2;
|
|
170
|
+
}
|
|
171
|
+
|
|
135
172
|
let agentSelector = flagString(flags, "agent");
|
|
136
173
|
// Interactive wizard step: when no --agent was given and we are on a real
|
|
137
174
|
// terminal, offer the adapter install instead of silently skipping it —
|
|
@@ -192,7 +229,7 @@ export async function run(_positional, flags) {
|
|
|
192
229
|
// if the static template lags a release (ADR 0015).
|
|
193
230
|
const agentsTemplatePath = path.join(templatesDir, "AGENTS.md.template");
|
|
194
231
|
const agentsTemplateBody = read(agentsTemplatePath);
|
|
195
|
-
const projectAgents = refreshSurfaceBlock(substitute(agentsTemplateBody, tokens));
|
|
232
|
+
const projectAgents = refreshSurfaceBlock(substitute(agentsTemplateBody, tokens), surface);
|
|
196
233
|
|
|
197
234
|
// --force used to mean "overwrite everything", which made it a content
|
|
198
235
|
// shredder: the only way to add an adapter to an existing project was
|
|
@@ -275,11 +312,21 @@ export async function run(_positional, flags) {
|
|
|
275
312
|
{
|
|
276
313
|
const dest = path.join(doctrinaDir, "config.json");
|
|
277
314
|
if (!isFile(dest) || force) {
|
|
278
|
-
write(dest, scaffoldConfig(), { force });
|
|
315
|
+
write(dest, scaffoldConfig({ surface }), { force });
|
|
279
316
|
console.log(c.green("created") + ` ${relPath(projectRoot, dest)}`);
|
|
280
317
|
}
|
|
281
318
|
}
|
|
282
319
|
|
|
320
|
+
// The preset, over the scaffold: what it adds is indexed with the rest.
|
|
321
|
+
if (preset) {
|
|
322
|
+
const plan = planPreset(projectRoot, preset);
|
|
323
|
+
applyPreset(projectRoot, preset, plan);
|
|
324
|
+
for (const line of describePreset(preset, plan, true)) console.log(line);
|
|
325
|
+
const derived = deriveIndex(projectRoot, idx.load(projectRoot));
|
|
326
|
+
derived.last_updated = date;
|
|
327
|
+
idx.save(projectRoot, derived);
|
|
328
|
+
}
|
|
329
|
+
|
|
283
330
|
// Adapters
|
|
284
331
|
if (agentSelector) {
|
|
285
332
|
const agents = agentSelector === "all" ? SUPPORTED_AGENTS : [agentSelector];
|
|
@@ -374,10 +421,10 @@ function normaliseScaffold(text) {
|
|
|
374
421
|
// Replace the template's doctrina:surface span with the one generated from
|
|
375
422
|
// the running CLI's catalog; pass the text through untouched when the
|
|
376
423
|
// template carries no markers (older custom templates).
|
|
377
|
-
function refreshSurfaceBlock(text) {
|
|
424
|
+
function refreshSurfaceBlock(text, profile = "full") {
|
|
378
425
|
const block = findSurfaceBlock(text);
|
|
379
426
|
if (!block) return text;
|
|
380
|
-
let out = text.slice(0, block.start) + surfaceBlock() + text.slice(block.end);
|
|
427
|
+
let out = text.slice(0, block.start) + surfaceBlock(profile) + text.slice(block.end);
|
|
381
428
|
|
|
382
429
|
// The agent-facing changelog is version-specific, so it is GENERATED at
|
|
383
430
|
// init like the surface block — not carried in the template. Writing it
|
|
@@ -443,8 +490,15 @@ Scaffold AGENTS.md and the .doctrina/ skeleton in the current directory.
|
|
|
443
490
|
Options:
|
|
444
491
|
--project-name <name> Override the project name (default: basename of cwd)
|
|
445
492
|
--project-description <text> One-sentence description
|
|
493
|
+
--surface core|full Which commands AGENTS.md lists: core (default) is the session
|
|
494
|
+
loop, with the rest deferred to \`doctrina --help\`; full lists
|
|
495
|
+
every command. Stored in .doctrina/config.json.
|
|
496
|
+
--preset <dir> Lay an organisation's preset over the scaffold: a folder with
|
|
497
|
+
preset.json (name, config) and optional templates/, decisions/,
|
|
498
|
+
skills/ and verify.json. Additive — see \`doctrina upgrade --preset\`
|
|
499
|
+
for a project that already exists.
|
|
446
500
|
--agent <name> Install adapter for one agent
|
|
447
|
-
(claude|codex|cursor|copilot|gemini|aider|windsurf|continue|amp|devin|factory|jules|all)
|
|
501
|
+
(claude|codex|cursor|copilot|gemini|aider|windsurf|continue|amp|devin|factory|jules|kiro|all)
|
|
448
502
|
--intake <file> Full project description; stored verbatim at .doctrina/intake.md,
|
|
449
503
|
used to derive the one-line description when absent, and the
|
|
450
504
|
bootstrap playbook is printed inline — no second command
|
|
@@ -454,6 +508,8 @@ Options:
|
|
|
454
508
|
--from <path> Local conventions directory; if it contains AGENTS.md and/or
|
|
455
509
|
.doctrina/product.md, the content is folded into the new project
|
|
456
510
|
--date <YYYY-MM-DD> Override the system date
|
|
457
|
-
--force Overwrite existing files
|
|
511
|
+
--force Overwrite existing files that are still untouched scaffolds;
|
|
512
|
+
also init a second clone of a repository a workspace shadows
|
|
513
|
+
--overwrite-content With --force, also overwrite an authored AGENTS.md or product.md
|
|
458
514
|
--non-interactive Fail instead of prompting for missing values
|
|
459
515
|
`;
|
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
|
}
|
|
@@ -39,7 +54,7 @@ export async function run(_positional, flags) {
|
|
|
39
54
|
if (flagBool(flags, "trend", false)) return printTrend(projectRoot);
|
|
40
55
|
// The window is the invocation's: refused the same with or without history.
|
|
41
56
|
const sinceRaw = flagString(flags, "since", "90");
|
|
42
|
-
const since = sinceWindow(sinceRaw);
|
|
57
|
+
const since = sinceWindow(sinceRaw ?? "90");
|
|
43
58
|
if (since === null) {
|
|
44
59
|
console.error(c.red("error:") + ` --since expects a day count, a date (YYYY-MM-DD) or "<n> <unit>s ago", got "${sinceRaw}"`);
|
|
45
60
|
console.error(c.gray("hint: ") + "examples: --since 30 · --since 2026-01-01 · --since \"3 months ago\"");
|
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
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { flagBool } from "../lib/args.js";
|
|
4
|
+
import { c } from "../lib/colors.js";
|
|
5
|
+
import { emitJson } from "../lib/json-out.js";
|
|
6
|
+
import { EXIT } from "../lib/exit-codes.js";
|
|
7
|
+
import { TRACKS } from "../lib/triage-model.js";
|
|
8
|
+
import { firstTaskCandidates, onboardState, onboardSteps, tourLines } from "../lib/onboard.js";
|
|
9
|
+
|
|
10
|
+
// `onboard` walks the whole cycle once, on this repository (change 0256).
|
|
11
|
+
// It reads where the project stands and prints the walk; the agent is the
|
|
12
|
+
// guide and the human the one being shown. It writes nothing and works
|
|
13
|
+
// before `init`, because the first step is the install.
|
|
14
|
+
|
|
15
|
+
export const jsonNative = true;
|
|
16
|
+
|
|
17
|
+
export const flags = { boolean: ["json"], string: [] };
|
|
18
|
+
|
|
19
|
+
export async function run(_positional, cmdFlags) {
|
|
20
|
+
const projectRoot = process.cwd();
|
|
21
|
+
const state = onboardState(projectRoot);
|
|
22
|
+
const { steps, current } = onboardSteps(state);
|
|
23
|
+
const wantsCandidates = current === "first-task" && state.installed;
|
|
24
|
+
const candidates = wantsCandidates ? firstTaskCandidates(projectRoot) : [];
|
|
25
|
+
|
|
26
|
+
if (flagBool(cmdFlags, "json", false)) {
|
|
27
|
+
emitJson("onboard", {
|
|
28
|
+
current,
|
|
29
|
+
steps: steps.map((s) => ({ id: s.id, title: s.title, done: s.done, run: s.run, ask: s.ask || null })),
|
|
30
|
+
candidates,
|
|
31
|
+
closed: state.closed,
|
|
32
|
+
});
|
|
33
|
+
return EXIT.OK;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(c.bold("Doctrina onboarding") + c.gray(" — the whole cycle once, on this repository"));
|
|
37
|
+
console.log("");
|
|
38
|
+
console.log("You are the guide. Do the step marked →, say in one sentence what it did,");
|
|
39
|
+
console.log("stop where it says ASK, then run `doctrina onboard` again: it reads the tree.");
|
|
40
|
+
console.log("");
|
|
41
|
+
steps.forEach((s, i) => {
|
|
42
|
+
const mark = s.done ? c.green("✓") : (s.id === current ? c.cyan("→") : " ");
|
|
43
|
+
console.log(` ${mark} ${i + 1}. ${s.done ? s.title.padEnd(31) + c.gray(s.evidence) : s.title}`);
|
|
44
|
+
});
|
|
45
|
+
console.log("");
|
|
46
|
+
|
|
47
|
+
if (current === null) {
|
|
48
|
+
console.log(c.bold("The cycle has run here") + c.gray(` — ${state.closed} change${state.closed === 1 ? "" : "s"} closed. For someone new to the project:`));
|
|
49
|
+
console.log("");
|
|
50
|
+
for (const line of tourLines(state)) console.log(` ${line}`);
|
|
51
|
+
console.log("");
|
|
52
|
+
printTracks();
|
|
53
|
+
return EXIT.OK;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const at = steps.findIndex((s) => s.id === current);
|
|
57
|
+
const step = steps[at];
|
|
58
|
+
console.log(c.bold(`Step ${at + 1} — ${step.title}`));
|
|
59
|
+
console.log(` ${c.gray("Tell the human:")} ${step.say}`);
|
|
60
|
+
if (wantsCandidates) {
|
|
61
|
+
console.log("");
|
|
62
|
+
if (candidates.length === 0) {
|
|
63
|
+
console.log(c.gray(" No candidate stands out in this repository (every criterion cites proof, no TODO in the code)."));
|
|
64
|
+
} else {
|
|
65
|
+
console.log(" Candidates read from this repository:");
|
|
66
|
+
for (const cand of candidates) console.log(` - ${cand.where.padEnd(24)} ${cand.text}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const after_ = "askAfter" in step && step.askAfter === true;
|
|
70
|
+
if (step.ask && !after_) console.log(` ${c.yellow("ASK")} the human ${step.ask}`);
|
|
71
|
+
console.log(step.ask && !after_ ? " Then run:" : " Run:");
|
|
72
|
+
for (const cmd of step.run) console.log(` ${c.cyan(cmd)}`);
|
|
73
|
+
if (step.ask && after_) console.log(` Then ${c.yellow("ASK")} the human ${step.ask}`);
|
|
74
|
+
if (current === "first-task") {
|
|
75
|
+
console.log("");
|
|
76
|
+
printTracks();
|
|
77
|
+
}
|
|
78
|
+
const after = steps[at + 1];
|
|
79
|
+
if (after) console.log(c.gray(`\nAfter it: step ${at + 2}, ${after.title.toLowerCase()} — \`doctrina onboard\` shows it.`));
|
|
80
|
+
return EXIT.OK;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function printTracks() {
|
|
84
|
+
console.log(c.gray("Not every edit is a change — the tracks, by what a request changes:"));
|
|
85
|
+
for (const t of TRACKS) console.log(c.gray(` ${t.id.padEnd(7)} ${t.changes}`));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const help = `
|
|
89
|
+
Usage: doctrina onboard [--json]
|
|
90
|
+
|
|
91
|
+
Walk the whole Doctrina cycle once, on this repository. Prints the six
|
|
92
|
+
steps — install, describe the project, pick a first task and open it,
|
|
93
|
+
write the delta and the tasks, implement and prove, close — each marked
|
|
94
|
+
done when the tree says so, and for the first one that is not: what to
|
|
95
|
+
tell the human, where to stop and ask, and the commands to run. Run it
|
|
96
|
+
again after each step; it stores nothing and reads the tree every time.
|
|
97
|
+
|
|
98
|
+
At "pick a first task" it lists candidates read from the repository: an
|
|
99
|
+
acceptance criterion that cites no proof (or a proof that is missing),
|
|
100
|
+
then a TODO or FIXME left in the code. They are suggestions; the human
|
|
101
|
+
chooses, or names a task of their own.
|
|
102
|
+
|
|
103
|
+
In a project that already closed a change, every step is done and it
|
|
104
|
+
prints the tour for someone new to the project instead: the commands to
|
|
105
|
+
read the project with, and the last closed change as a worked example.
|
|
106
|
+
|
|
107
|
+
It works before \`doctrina init\` (the first step is the install) and
|
|
108
|
+
writes nothing.
|
|
109
|
+
|
|
110
|
+
Options:
|
|
111
|
+
--json Machine-readable: the steps with their state, the current
|
|
112
|
+
step, and the first-task candidates
|
|
113
|
+
`;
|
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);
|
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);
|
|
@@ -100,13 +116,21 @@ export async function run(_positional, flags) {
|
|
|
100
116
|
// capability whose spec.md itself changed.
|
|
101
117
|
const specChanged = new Set(
|
|
102
118
|
all.filter((f) => /(^|\/)\.doctrina\/specs\/[^/]+\/spec\.md$/.test(f.replace(/\\/g, "/")))
|
|
103
|
-
.map((f) => f.replace(/\\/g, "/").match(/\.doctrina\/specs\/([^/]+)\/spec\.md$/)[1]),
|
|
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/search.js
CHANGED
|
@@ -59,8 +59,8 @@ export async function run(positional, flags) {
|
|
|
59
59
|
for (const cat of categories) {
|
|
60
60
|
const files = cat.file
|
|
61
61
|
? [path.join(projectRoot, cat.file)].filter(exists)
|
|
62
|
-
: isDir(path.join(projectRoot, cat.dir))
|
|
63
|
-
? walk(path.join(projectRoot, cat.dir)).filter((f) => f.endsWith(".md"))
|
|
62
|
+
: isDir(path.join(projectRoot, cat.dir ?? ""))
|
|
63
|
+
? walk(path.join(projectRoot, cat.dir ?? "")).filter((f) => f.endsWith(".md"))
|
|
64
64
|
: [];
|
|
65
65
|
|
|
66
66
|
const findings = [];
|
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
|