projectstore-codex 0.0.1 → 0.28.2
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/.codex-plugin/plugin.json +48 -0
- package/README.md +15 -7
- package/bin/projectstore-codex.mjs +88 -0
- package/hooks/hooks.json +59 -0
- package/node_modules/projectstore/.claude-plugin/marketplace.json +40 -0
- package/node_modules/projectstore/.claude-plugin/plugin.json +23 -0
- package/node_modules/projectstore/.mcp.json +14 -0
- package/node_modules/projectstore/AGENTS.md +26 -0
- package/node_modules/projectstore/LICENSE +21 -0
- package/node_modules/projectstore/README.md +284 -0
- package/node_modules/projectstore/agents/archaeologist.md +76 -0
- package/node_modules/projectstore/agents/clerk.md +93 -0
- package/node_modules/projectstore/agents/critic.md +94 -0
- package/node_modules/projectstore/agents/librarian.md +81 -0
- package/node_modules/projectstore/agents/planner.md +80 -0
- package/node_modules/projectstore/agents/reviewer.md +98 -0
- package/node_modules/projectstore/bin/projectstore.mjs +7 -0
- package/node_modules/projectstore/commands/adr.md +57 -0
- package/node_modules/projectstore/commands/agents.md +180 -0
- package/node_modules/projectstore/commands/bind.md +128 -0
- package/node_modules/projectstore/commands/codemap.md +50 -0
- package/node_modules/projectstore/commands/concept.md +17 -0
- package/node_modules/projectstore/commands/doctor.md +166 -0
- package/node_modules/projectstore/commands/epic.md +40 -0
- package/node_modules/projectstore/commands/graph.md +56 -0
- package/node_modules/projectstore/commands/kanban.md +40 -0
- package/node_modules/projectstore/commands/meeting.md +17 -0
- package/node_modules/projectstore/commands/reconcile.md +73 -0
- package/node_modules/projectstore/commands/research.md +17 -0
- package/node_modules/projectstore/commands/review.md +89 -0
- package/node_modules/projectstore/commands/runbook.md +17 -0
- package/node_modules/projectstore/commands/scaffold.md +23 -0
- package/node_modules/projectstore/commands/search.md +22 -0
- package/node_modules/projectstore/commands/spec.md +91 -0
- package/node_modules/projectstore/commands/status.md +27 -0
- package/node_modules/projectstore/commands/statusline.md +46 -0
- package/node_modules/projectstore/commands/story.md +113 -0
- package/node_modules/projectstore/docs/extending.md +172 -0
- package/node_modules/projectstore/docs/getting-started.md +133 -0
- package/node_modules/projectstore/docs/harnesses.md +176 -0
- package/node_modules/projectstore/docs/how-it-works.md +263 -0
- package/node_modules/projectstore/docs/images/loop-light.svg +94 -0
- package/node_modules/projectstore/docs/images/loop.svg +93 -0
- package/node_modules/projectstore/docs/images/statusline-hud.png +0 -0
- package/node_modules/projectstore/docs/images/team-light.svg +79 -0
- package/node_modules/projectstore/docs/images/team.svg +79 -0
- package/node_modules/projectstore/harnesses/claude-code.json +483 -0
- package/node_modules/projectstore/harnesses/codex.json +332 -0
- package/node_modules/projectstore/hooks/hooks.json +59 -0
- package/node_modules/projectstore/hooks/pre-compact.mjs +121 -0
- package/node_modules/projectstore/hooks/session-rules.mjs +63 -0
- package/node_modules/projectstore/hooks/session-start.mjs +301 -0
- package/node_modules/projectstore/hooks/session-stop.mjs +84 -0
- package/node_modules/projectstore/package.json +70 -0
- package/node_modules/projectstore/scaffold/checklists.json +88 -0
- package/node_modules/projectstore/scaffold/headings.json +171 -0
- package/node_modules/projectstore/scaffold/layouts/engineering.json +85 -0
- package/node_modules/projectstore/scripts/binding.mjs +165 -0
- package/node_modules/projectstore/scripts/build-adapters.mjs +264 -0
- package/node_modules/projectstore/scripts/cli.mjs +595 -0
- package/node_modules/projectstore/scripts/codemap.mjs +99 -0
- package/node_modules/projectstore/scripts/diff-refs.mjs +127 -0
- package/node_modules/projectstore/scripts/doctor.mjs +2127 -0
- package/node_modules/projectstore/scripts/draft.mjs +261 -0
- package/node_modules/projectstore/scripts/graph.mjs +219 -0
- package/node_modules/projectstore/scripts/harness.mjs +608 -0
- package/node_modules/projectstore/scripts/install-harness.mjs +1387 -0
- package/node_modules/projectstore/scripts/kanban.mjs +174 -0
- package/node_modules/projectstore/scripts/lib.mjs +3085 -0
- package/node_modules/projectstore/scripts/mcp.mjs +391 -0
- package/node_modules/projectstore/scripts/portable-registration.mjs +198 -0
- package/node_modules/projectstore/scripts/provenance.mjs +375 -0
- package/node_modules/projectstore/scripts/query.mjs +490 -0
- package/node_modules/projectstore/scripts/reconcile.mjs +422 -0
- package/node_modules/projectstore/scripts/statusline-launcher.mjs +141 -0
- package/node_modules/projectstore/scripts/statusline.mjs +253 -0
- package/node_modules/projectstore/scripts/story-section.mjs +209 -0
- package/node_modules/projectstore/scripts/surfaces.mjs +421 -0
- package/node_modules/projectstore/scripts/tokens.mjs +449 -0
- package/node_modules/projectstore/scripts/touch-session.mjs +336 -0
- package/node_modules/projectstore/scripts/version-guard.mjs +261 -0
- package/node_modules/projectstore/scripts/worktree.mjs +109 -0
- package/node_modules/projectstore/skills/projectstore-decision-detector/SKILL.md +40 -0
- package/node_modules/projectstore/skills/projectstore-peer-reviewer/SKILL.md +38 -0
- package/node_modules/projectstore/skills/projectstore-story-completion/SKILL.md +50 -0
- package/node_modules/projectstore/skills/projectstore-vault-communication/SKILL.md +96 -0
- package/node_modules/projectstore/templates/claude-md-block.md.tmpl +26 -0
- package/node_modules/projectstore/templates/de/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/de/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/de/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/de/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/de/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/de/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/de/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/de/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/de/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/de/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/de/strings.json +6 -0
- package/node_modules/projectstore/templates/en/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/en/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/en/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/en/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/en/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/en/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/en/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/en/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/en/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/en/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/en/strings.json +6 -0
- package/node_modules/projectstore/templates/es/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/es/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/es/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/es/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/es/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/es/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/es/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/es/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/es/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/es/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/es/strings.json +6 -0
- package/node_modules/projectstore/templates/fr/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/fr/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/fr/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/fr/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/fr/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/fr/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/fr/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/fr/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/fr/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/fr/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/fr/strings.json +6 -0
- package/node_modules/projectstore/templates/ru/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/ru/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/ru/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/ru/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/ru/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/ru/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/ru/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/ru/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/ru/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/ru/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/ru/strings.json +6 -0
- package/node_modules/projectstore/templates/zh/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/zh/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/zh/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/zh/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/zh/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/zh/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/zh/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/zh/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/zh/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/zh/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/zh/strings.json +6 -0
- package/package.json +35 -14
- package/skills/projectstore-adr/SKILL.md +76 -0
- package/skills/projectstore-agents/SKILL.md +50 -0
- package/skills/projectstore-archaeologist/SKILL.md +109 -0
- package/skills/projectstore-bind/SKILL.md +44 -0
- package/skills/projectstore-clerk/SKILL.md +126 -0
- package/skills/projectstore-codemap/SKILL.md +69 -0
- package/skills/projectstore-concept/SKILL.md +36 -0
- package/skills/projectstore-critic/SKILL.md +127 -0
- package/skills/projectstore-decision-detector/SKILL.md +59 -0
- package/skills/projectstore-doctor/SKILL.md +33 -0
- package/skills/projectstore-epic/SKILL.md +59 -0
- package/skills/projectstore-graph/SKILL.md +75 -0
- package/skills/projectstore-kanban/SKILL.md +60 -0
- package/skills/projectstore-librarian/SKILL.md +114 -0
- package/skills/projectstore-meeting/SKILL.md +36 -0
- package/skills/projectstore-peer-reviewer/SKILL.md +57 -0
- package/skills/projectstore-planner/SKILL.md +113 -0
- package/skills/projectstore-reconcile/SKILL.md +92 -0
- package/skills/projectstore-research/SKILL.md +36 -0
- package/skills/projectstore-review/SKILL.md +108 -0
- package/skills/projectstore-reviewer/SKILL.md +131 -0
- package/skills/projectstore-runbook/SKILL.md +36 -0
- package/skills/projectstore-scaffold/SKILL.md +42 -0
- package/skills/projectstore-search/SKILL.md +41 -0
- package/skills/projectstore-spec/SKILL.md +110 -0
- package/skills/projectstore-status/SKILL.md +47 -0
- package/skills/projectstore-statusline/SKILL.md +29 -0
- package/skills/projectstore-story/SKILL.md +132 -0
- package/skills/projectstore-story-completion/SKILL.md +69 -0
- package/skills/projectstore-vault-communication/SKILL.md +115 -0
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
// projectstore — cli.mjs
|
|
2
|
+
//
|
|
3
|
+
// The token-free front-end: `projectstore <verb>` as a thin argv/output
|
|
4
|
+
// shell over the same core operations the hooks and the command files call
|
|
5
|
+
// (distribution ADR decisions 3 and 5). No logic lives here — each verb row
|
|
6
|
+
// names the module it wraps and how (spawned, imported, or new), and the
|
|
7
|
+
// table is exported because it is a CONTRACT: the MCP read surface maps one
|
|
8
|
+
// tool per CLI verb (MCP ADR decision 2), and its drift test reads this file.
|
|
9
|
+
//
|
|
10
|
+
// Every --json result travels in one envelope, {schema_version, verb,
|
|
11
|
+
// project, ok, result}, built here and nowhere else — failures included, so
|
|
12
|
+
// a consumer always has something to parse. The bare scripts keep their own
|
|
13
|
+
// output (`node scripts/doctor.mjs --json` is still a bare array — the
|
|
14
|
+
// command files and the tests depend on it). Exit codes: 0 ok, 1 findings or
|
|
15
|
+
// a refusal, 2 usage or an internal failure, 3 not bound.
|
|
16
|
+
//
|
|
17
|
+
// The gate (distribution ADR decision 6) binds every write verb this bin
|
|
18
|
+
// exposes: the install family through install-harness.mjs's own preview and
|
|
19
|
+
// confirmation, and `reconcile --write` through the same rule — naming what
|
|
20
|
+
// is written (`--only <target>`) is the non-interactive confirmation, a bare
|
|
21
|
+
// `--write` asks on a terminal and refuses without one. There is no --yes.
|
|
22
|
+
//
|
|
23
|
+
// The project is resolved once, here — --project, then the neutral
|
|
24
|
+
// PROJECTSTORE_PROJECT_DIR, then a project-dir variable a harness declared,
|
|
25
|
+
// then cwd — and handed to every child through childEnv(), the one place a
|
|
26
|
+
// branded name is written. bin/ reads no branded variable; the portability
|
|
27
|
+
// suite greps it too.
|
|
28
|
+
//
|
|
29
|
+
// The bin runs ITS OWN copy of the core: sibling scripts resolve from this
|
|
30
|
+
// file's URL, and the plugin root handed to children and to the installer
|
|
31
|
+
// is this package's root — so `npx projectstore doctor` inside a Claude Code
|
|
32
|
+
// session runs the npm copy's doctor over the npm copy's templates, and
|
|
33
|
+
// reports the npm copy's version, rather than the marketplace copy's.
|
|
34
|
+
//
|
|
35
|
+
// The install verbs are imported lazily: hooks never import this module
|
|
36
|
+
// (the suite asserts it), but the MCP server will, and it needs no installer.
|
|
37
|
+
// Pure node, no external deps.
|
|
38
|
+
|
|
39
|
+
import { spawnSync } from "node:child_process";
|
|
40
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
41
|
+
import { resolve, dirname } from "node:path";
|
|
42
|
+
import { fileURLToPath } from "node:url";
|
|
43
|
+
import { parseArgs } from "node:util";
|
|
44
|
+
import { createInterface } from "node:readline/promises";
|
|
45
|
+
import { projectRootDeclared, childEnv, harnessIds, harnessForOverlay, pinPluginRoot } from "./harness.mjs";
|
|
46
|
+
import { readConfigAt, readOverlayAt, resolveAgentModel, writeOverlayAt, overlayId, layoutRoster } from "./lib.mjs";
|
|
47
|
+
import { READ_OPERATIONS, LINEAGE_KINDS, LINEAGE_DEFAULT_DEPTH, SEARCH_DEFAULT_LIMIT, GRAPH_EDGE_CAP, DIRECTIONS } from "./query.mjs";
|
|
48
|
+
// binding.mjs is a write module imported statically where the install family
|
|
49
|
+
// is lazy: it is a dependency-free leaf with no side effects, so the MCP
|
|
50
|
+
// server's module graph gains nothing it could trip on.
|
|
51
|
+
import { planBind, applyBind, renderBindPlan, bindResult, DEFAULT_LAYOUT, DEFAULT_LANGUAGE } from "./binding.mjs";
|
|
52
|
+
|
|
53
|
+
export const SCHEMA_VERSION = 1;
|
|
54
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
55
|
+
export const PACKAGE_ROOT = dirname(HERE);
|
|
56
|
+
const script = (name) => resolve(HERE, name);
|
|
57
|
+
|
|
58
|
+
export function packageVersion() {
|
|
59
|
+
try { return String(JSON.parse(readFileSync(resolve(PACKAGE_ROOT, "package.json"), "utf8")).version || ""); } catch { return ""; }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function envelope(verb, project, ok, result) {
|
|
63
|
+
return { schema_version: SCHEMA_VERSION, verb, project, ok, result };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// --project, then the neutral variable (MCP ADR decision 6), then a
|
|
67
|
+
// project-dir variable a harness declared (null when none is set — never
|
|
68
|
+
// cwd-by-inference from harness.mjs, which is what an in-process caller with
|
|
69
|
+
// its own cwd would otherwise inherit), then the caller's cwd.
|
|
70
|
+
export function resolveProject({ project = null, env = process.env, cwd = process.cwd() } = {}) {
|
|
71
|
+
if (project) return resolve(cwd, project);
|
|
72
|
+
if (env.PROJECTSTORE_PROJECT_DIR) return resolve(cwd, env.PROJECTSTORE_PROJECT_DIR);
|
|
73
|
+
const declared = projectRootDeclared(env);
|
|
74
|
+
return declared ? resolve(cwd, declared) : resolve(cwd);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── The verb table ────────────────────────────────────────────────────
|
|
78
|
+
//
|
|
79
|
+
// wraps: "script" (spawned), "module" (imported), "new" (code that exists
|
|
80
|
+
// only for the CLI). output: "envelope" (--json wraps), "text". mcp: the MCP
|
|
81
|
+
// tools that mirror the verb (MCP ADR decision 2), empty when none.
|
|
82
|
+
|
|
83
|
+
const opt = (name, arg, summary, multiple = false) => Object.freeze({ name, arg, summary, multiple });
|
|
84
|
+
const JSON_OPT = opt("json", false, "the envelope");
|
|
85
|
+
const READ_JSON = [JSON_OPT];
|
|
86
|
+
const HARNESS_OPT = opt("harness", "<id>", "the harness — and, non-interactively, the confirmation; there is no --yes", true);
|
|
87
|
+
const SURFACE_OPT = opt("surface", "<key>", "one surface and those beneath it", true);
|
|
88
|
+
// The layout move's remedy names this for any copy but the package's own
|
|
89
|
+
// registration (the layout spec, contract 12 as amended 2026-10-03): it makes
|
|
90
|
+
// "no host command" true by construction instead of by recognising the root.
|
|
91
|
+
const NO_REGISTER_OPT = opt("no-register", false, "leave the plugin registration alone: change only this project's files");
|
|
92
|
+
const INSTALL_OPTS = [HARNESS_OPT, SURFACE_OPT, NO_REGISTER_OPT, JSON_OPT];
|
|
93
|
+
const UNINSTALL_OPTS = [HARNESS_OPT, SURFACE_OPT, opt("global", false, "also remove the harness-global plugin registration"), JSON_OPT];
|
|
94
|
+
|
|
95
|
+
export const VERBS = Object.freeze([
|
|
96
|
+
Object.freeze({
|
|
97
|
+
verb: "doctor", summary: "Check the install wiring and the vault's consistency.",
|
|
98
|
+
module: "./doctor.mjs", wraps: "script", how: "spawn", output: "envelope", writes: false, requiresBinding: false, mcp: Object.freeze(["doctor"]),
|
|
99
|
+
options: [opt("install", false, "only the install section"), opt("vault", false, "only the vault section"), JSON_OPT],
|
|
100
|
+
run: runDoctor,
|
|
101
|
+
}),
|
|
102
|
+
Object.freeze({
|
|
103
|
+
verb: "reconcile", summary: "Regenerate the derived views (kanban, code map, graph, indexes).",
|
|
104
|
+
module: "./reconcile.mjs", wraps: "module", how: "import", output: "envelope", writes: true, requiresBinding: true, mcp: Object.freeze([]),
|
|
105
|
+
options: [opt("write", false, "apply the regeneration (asks on a terminal; --only names what is written and confirms headless)"), opt("only", "<target>", "one derived view"), JSON_OPT],
|
|
106
|
+
run: runReconcile,
|
|
107
|
+
}),
|
|
108
|
+
Object.freeze({
|
|
109
|
+
verb: "plan", summary: "Show what install would write for a harness, without writing.",
|
|
110
|
+
module: "./install-harness.mjs", wraps: "module", how: "import", output: "envelope", writes: false, requiresBinding: false, mcp: Object.freeze([]),
|
|
111
|
+
options: INSTALL_OPTS, run: runInstallVerb,
|
|
112
|
+
}),
|
|
113
|
+
Object.freeze({
|
|
114
|
+
verb: "install", summary: "Install projectstore's surfaces for a harness, behind a preview.",
|
|
115
|
+
module: "./install-harness.mjs", wraps: "module", how: "import", output: "envelope", writes: true, requiresBinding: false, mcp: Object.freeze([]),
|
|
116
|
+
options: INSTALL_OPTS, run: runInstallVerb,
|
|
117
|
+
}),
|
|
118
|
+
Object.freeze({
|
|
119
|
+
verb: "uninstall", summary: "Remove what install wrote, and only that.",
|
|
120
|
+
module: "./install-harness.mjs", wraps: "module", how: "import", output: "envelope", writes: true, requiresBinding: false, mcp: Object.freeze([]),
|
|
121
|
+
options: UNINSTALL_OPTS, run: runInstallVerb,
|
|
122
|
+
}),
|
|
123
|
+
Object.freeze({
|
|
124
|
+
verb: "upgrade", summary: "Re-run install after a plugin update; re-stamps what this installation wrote and leaves the rest.",
|
|
125
|
+
module: "./install-harness.mjs", wraps: "module", how: "import", output: "envelope", writes: true, requiresBinding: false, mcp: Object.freeze([]),
|
|
126
|
+
options: INSTALL_OPTS, run: runInstallVerb,
|
|
127
|
+
}),
|
|
128
|
+
Object.freeze({
|
|
129
|
+
verb: "status", summary: "The binding, what is in progress, and whether the derived views are fresh.",
|
|
130
|
+
module: "./query.mjs", wraps: "new", how: "import", output: "envelope", writes: false, requiresBinding: false, mcp: Object.freeze(["status"]),
|
|
131
|
+
options: READ_JSON, run: runRead("status"),
|
|
132
|
+
}),
|
|
133
|
+
Object.freeze({
|
|
134
|
+
verb: "orientation", summary: "The SessionStart skeleton and the facts behind it.",
|
|
135
|
+
module: "./query.mjs", wraps: "module", how: "import", output: "envelope", writes: false, requiresBinding: true, mcp: Object.freeze(["orientation"]),
|
|
136
|
+
options: READ_JSON, run: runRead("orientation"),
|
|
137
|
+
}),
|
|
138
|
+
Object.freeze({
|
|
139
|
+
verb: "search", summary: "Find a phrase in the vault's artifacts — deterministic, bounded, no shell.",
|
|
140
|
+
module: "./query.mjs", wraps: "new", how: "import", output: "envelope", writes: false, requiresBinding: true, mcp: Object.freeze(["search"]),
|
|
141
|
+
options: [opt("kind", "<type>", "only artifacts of this kind", true), opt("status", "<status>", "only artifacts in this status"), opt("limit", "<n>", `at most n matches (default ${SEARCH_DEFAULT_LIMIT}, hard cap 100)`), opt("include-derived", false, "search the derived views too"), opt("case-sensitive", false, "match case"), JSON_OPT],
|
|
142
|
+
run: runRead("search"),
|
|
143
|
+
}),
|
|
144
|
+
Object.freeze({
|
|
145
|
+
verb: "show", summary: "One artifact: its frontmatter, and its body or one section on request.",
|
|
146
|
+
module: "./query.mjs", wraps: "module", how: "import", output: "envelope", writes: false, requiresBinding: true, mcp: Object.freeze(["get_artifact"]),
|
|
147
|
+
options: [opt("body", false, "include the body"), opt("section", "<id>", "one section by its registry id (description, acceptance, …)"), JSON_OPT],
|
|
148
|
+
run: runRead("show"),
|
|
149
|
+
}),
|
|
150
|
+
Object.freeze({
|
|
151
|
+
verb: "graph", summary: "graph neighbors <path> | graph lineage <path> — the live link graph by vault path.",
|
|
152
|
+
module: "./query.mjs", wraps: "new", how: "import", output: "envelope", writes: false, requiresBinding: true, mcp: Object.freeze(["neighbors", "lineage"]),
|
|
153
|
+
options: [opt("kind", "<edge-kind>", "only edges of this kind (lineage: one of its four)", true), opt("direction", DIRECTIONS.join("|"), "neighbors: which edges"), opt("depth", "<n>", `lineage: how far (default ${LINEAGE_DEFAULT_DEPTH})`), opt("limit", "<n>", `neighbors: cap per direction (≤ ${GRAPH_EDGE_CAP})`), JSON_OPT],
|
|
154
|
+
run: runGraph,
|
|
155
|
+
}),
|
|
156
|
+
Object.freeze({
|
|
157
|
+
verb: "codemap", summary: "codemap --for <selector> — which code an epic or artifact maps to, or which artifacts map to a path.",
|
|
158
|
+
module: "./query.mjs", wraps: "new", how: "import", output: "envelope", writes: false, requiresBinding: true, mcp: Object.freeze(["code_refs"]),
|
|
159
|
+
options: [opt("for", "<selector>", "an epic id, an artifact, or a repo path"), opt("reverse", false, "read the selector as a path even if it names an artifact"), JSON_OPT],
|
|
160
|
+
run: runCodemap,
|
|
161
|
+
}),
|
|
162
|
+
Object.freeze({
|
|
163
|
+
verb: "agents", summary: "agents model <name> | agents show | agents configure — the harness overlay's agents block (ADR-008, read per invocation).",
|
|
164
|
+
module: "./lib.mjs", wraps: "new", how: "import", output: "envelope", writes: true, requiresBinding: false, mcp: Object.freeze([]),
|
|
165
|
+
options: [opt("harness", "<id>", "configure: the overlay to write — and, non-interactively, the confirmation; there is no --yes", true), opt("default", "<model>", "configure: agents.default.model (pins the clerk to sonnet unless --agent clerk=… says otherwise; an empty model clears it)"), opt("agent", "<name>=<model>", "configure: agents.per_agent.<name>.model (an empty model removes the key)", true), opt("reset", false, "configure: empty the agents block first; --default and --agent given with it apply on top"), JSON_OPT],
|
|
166
|
+
run: runAgents,
|
|
167
|
+
}),
|
|
168
|
+
Object.freeze({
|
|
169
|
+
verb: "bind", summary: "bind <vault> — bind this project to an existing vault (naming the vault is the confirmation).",
|
|
170
|
+
module: "./binding.mjs", wraps: "new", how: "import", output: "envelope", writes: true, requiresBinding: false, mcp: Object.freeze([]),
|
|
171
|
+
options: [opt("layout", "<name>", `the layout (default ${DEFAULT_LAYOUT})`), opt("language", "<code>", `the template language (default ${DEFAULT_LANGUAGE})`), opt("rebind", false, "point an already bound project at another vault; every other setting is kept"), JSON_OPT],
|
|
172
|
+
run: runBind(false),
|
|
173
|
+
}),
|
|
174
|
+
Object.freeze({
|
|
175
|
+
verb: "init", summary: "init <vault> — create the vault directory and bind to it; the layout's folders come from /projectstore:scaffold.",
|
|
176
|
+
module: "./binding.mjs", wraps: "new", how: "import", output: "envelope", writes: true, requiresBinding: false, mcp: Object.freeze([]),
|
|
177
|
+
options: [opt("layout", "<name>", `the layout (default ${DEFAULT_LAYOUT})`), opt("language", "<code>", `the template language (default ${DEFAULT_LANGUAGE})`), opt("rebind", false, "an already bound project: create the new vault and point the project at it; every other setting is kept"), JSON_OPT],
|
|
178
|
+
run: runBind(true),
|
|
179
|
+
}),
|
|
180
|
+
Object.freeze({
|
|
181
|
+
verb: "mcp", summary: "Serve the read tools over MCP (stdio) for the project named by --project or PROJECTSTORE_PROJECT_DIR; never the ambient cwd.",
|
|
182
|
+
module: "./mcp.mjs", wraps: "new", how: "import", output: "text", writes: false, requiresBinding: false, mcp: Object.freeze([]),
|
|
183
|
+
options: [], run: runMcp,
|
|
184
|
+
}),
|
|
185
|
+
Object.freeze({
|
|
186
|
+
verb: "version", summary: "Print the package version (also --version).",
|
|
187
|
+
module: null, wraps: "new", how: "import", output: "envelope", writes: false, requiresBinding: false, mcp: Object.freeze([]),
|
|
188
|
+
options: [JSON_OPT], run: runVersion,
|
|
189
|
+
}),
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
// Verbs the story names that land with a later slice — listed so
|
|
193
|
+
// `projectstore <verb>` says where instead of "unknown verb". Empty since
|
|
194
|
+
// A7: every verb the CLI story names has landed. The seam stays for the next
|
|
195
|
+
// story that adds a verb in slices.
|
|
196
|
+
export const PLANNED_VERBS = Object.freeze([]);
|
|
197
|
+
|
|
198
|
+
export function usage() {
|
|
199
|
+
const lines = ["usage: projectstore <verb> [options] [--project <dir>] [--json]", "", "verbs:"];
|
|
200
|
+
for (const v of VERBS) {
|
|
201
|
+
lines.push(` ${v.verb.padEnd(11)} ${v.summary}`);
|
|
202
|
+
for (const o of v.options) lines.push(` --${o.name}${o.arg ? " " + o.arg : ""}${o.multiple ? " (repeatable)" : ""} ${o.summary}`);
|
|
203
|
+
}
|
|
204
|
+
if (PLANNED_VERBS.length) lines.push("", ` planned: ${PLANNED_VERBS.map((v) => `${v.verb} (${v.lands})`).join(", ")}`);
|
|
205
|
+
lines.push("", ` harnesses: ${harnessIds().join(", ")}`, " --version print the package version", " exit codes: 0 ok, 1 findings or refusal, 2 usage, 3 not bound");
|
|
206
|
+
return lines.join("\n");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ─── run ───────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
export async function run(argv, { env = process.env, cwd = process.cwd(), stdin = process.stdin, stdout = process.stdout, stderr = process.stderr, ask = null } = {}) {
|
|
212
|
+
let parsed;
|
|
213
|
+
try {
|
|
214
|
+
parsed = parseArgs({
|
|
215
|
+
args: argv,
|
|
216
|
+
allowPositionals: true,
|
|
217
|
+
strict: true,
|
|
218
|
+
options: {
|
|
219
|
+
project: { type: "string" }, json: { type: "boolean" }, help: { type: "boolean", short: "h" }, version: { type: "boolean", short: "v" },
|
|
220
|
+
harness: { type: "string", multiple: true }, surface: { type: "string", multiple: true }, global: { type: "boolean" }, "no-register": { type: "boolean" },
|
|
221
|
+
write: { type: "boolean" }, only: { type: "string" }, install: { type: "boolean" }, vault: { type: "boolean" },
|
|
222
|
+
kind: { type: "string", multiple: true }, status: { type: "string" }, limit: { type: "string" }, "include-derived": { type: "boolean" }, "case-sensitive": { type: "boolean" },
|
|
223
|
+
body: { type: "boolean" }, section: { type: "string" }, direction: { type: "string" }, depth: { type: "string" }, for: { type: "string" }, reverse: { type: "boolean" },
|
|
224
|
+
layout: { type: "string" }, language: { type: "string" }, rebind: { type: "boolean" },
|
|
225
|
+
default: { type: "string" }, agent: { type: "string", multiple: true }, reset: { type: "boolean" },
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
} catch (e) {
|
|
229
|
+
// --json cannot be known before parsing; a raw scan is enough here.
|
|
230
|
+
if (argv.includes("--json")) stdout.write(JSON.stringify(envelope(argv.find((a) => !a.startsWith("-")) || null, null, false, { error: e.message }), null, 2) + "\n");
|
|
231
|
+
stderr.write(`${e.message}\n${usage()}\n`);
|
|
232
|
+
return 2;
|
|
233
|
+
}
|
|
234
|
+
const { values, positionals } = parsed;
|
|
235
|
+
// A failure before a verb runs still answers in the envelope under --json:
|
|
236
|
+
// a consumer (the MCP server, a script) always has something to parse.
|
|
237
|
+
const fail = (verb, project, message, code, { help = false } = {}) => {
|
|
238
|
+
if (values.json) stdout.write(JSON.stringify(envelope(verb, project, false, { error: message, exit: code }), null, 2) + "\n");
|
|
239
|
+
stderr.write(message + "\n" + (help ? usage() + "\n" : ""));
|
|
240
|
+
return code;
|
|
241
|
+
};
|
|
242
|
+
// In-process reads resolve layouts and registries from THIS package, as the
|
|
243
|
+
// children already do through ownEnv — not from whichever copy the host
|
|
244
|
+
// session's variable points at.
|
|
245
|
+
pinPluginRoot(PACKAGE_ROOT);
|
|
246
|
+
if (values.version) return runVersion({ values, stdout });
|
|
247
|
+
if (values.help || !positionals.length) { (values.help ? stdout : stderr).write(usage() + "\n"); return values.help ? 0 : 2; }
|
|
248
|
+
const verb = positionals[0];
|
|
249
|
+
const row = VERBS.find((v) => v.verb === verb);
|
|
250
|
+
if (!row) {
|
|
251
|
+
const planned = PLANNED_VERBS.find((v) => v.verb === verb);
|
|
252
|
+
return fail(verb, null, (planned ? `${verb} lands with roadmap ${planned.lands}; not in this release.` : `unknown verb: ${verb}`), 2, { help: true });
|
|
253
|
+
}
|
|
254
|
+
// The options map is global (parseArgs), the rows are not: an option a row
|
|
255
|
+
// does not declare is a usage error, so help cannot lie about what a verb
|
|
256
|
+
// takes.
|
|
257
|
+
const GLOBAL = new Set(["project", "json", "help", "version"]);
|
|
258
|
+
const declared = new Set(row.options.map((o) => o.name));
|
|
259
|
+
const stray = Object.keys(values).filter((k) => !GLOBAL.has(k) && !declared.has(k));
|
|
260
|
+
if (stray.length) return fail(verb, null, `${verb} does not take --${stray[0]}`, 2, { help: true });
|
|
261
|
+
const project = resolveProject({ project: values.project, env, cwd });
|
|
262
|
+
const cfg = readConfigAt(project);
|
|
263
|
+
if (row.requiresBinding && !cfg) return fail(verb, project, `${project} is not bound to a vault — run /projectstore:bind <vault> in a session, or \`projectstore bind <vault>\` (\`projectstore init <vault>\` also creates the vault).`, 3);
|
|
264
|
+
try {
|
|
265
|
+
return await row.run({ row, values, positionals: positionals.slice(1), cfg, project, env, cwd, stdin, stdout, stderr, ask });
|
|
266
|
+
} catch (e) {
|
|
267
|
+
// An internal failure is not "findings" (1) — it is 2, with an envelope
|
|
268
|
+
// when one was asked for.
|
|
269
|
+
const msg = e && e.message ? e.message : String(e);
|
|
270
|
+
if (values.json) stdout.write(JSON.stringify(envelope(verb, project, false, { error: msg }), null, 2) + "\n");
|
|
271
|
+
stderr.write(`${verb} failed: ${msg}\n`);
|
|
272
|
+
return 2;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// The environment a child or the installer runs in: the resolved project and
|
|
277
|
+
// THIS package as the plugin root, so the bin never answers for a sibling
|
|
278
|
+
// copy of the core.
|
|
279
|
+
function ownEnv(env, project) {
|
|
280
|
+
return childEnv(env, { projectRoot: project, pluginRoot: PACKAGE_ROOT });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// The one interactive question every write verb asks when nothing named the
|
|
284
|
+
// write. Streams and `ask` are parameters so it is testable without a tty.
|
|
285
|
+
async function confirmWrite(question, { stdin, stdout, ask }) {
|
|
286
|
+
if (ask) return /^y(es)?$/i.test(String(await ask(question)).trim());
|
|
287
|
+
if (!(stdin && stdin.isTTY && stdout && stdout.isTTY)) return null; // no terminal: refuse
|
|
288
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
289
|
+
try { return /^y(es)?$/i.test(String(await rl.question(question)).trim()); } finally { rl.close(); }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ─── verbs ─────────────────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
// The read verbs: one call into query.mjs, the result in the envelope or
|
|
295
|
+
// rendered. A usage error from the operation (a bad path, a missing query)
|
|
296
|
+
// is exit 2 with the message, never a stack trace.
|
|
297
|
+
function emitRead({ verb, values, project, stdout }, op, result, ok = true) {
|
|
298
|
+
if (values.json) stdout.write(JSON.stringify(envelope(verb, project, ok, result), null, 2) + "\n");
|
|
299
|
+
else stdout.write(op.render(result));
|
|
300
|
+
return ok ? 0 : 1;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function usageFail(e, { verb, values, project, stdout, stderr }) {
|
|
304
|
+
if (values.json) stdout.write(JSON.stringify(envelope(verb, project, false, { error: e.message }), null, 2) + "\n");
|
|
305
|
+
stderr.write(`${verb}: ${e.message}\n`);
|
|
306
|
+
return 2;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function runRead(name) {
|
|
310
|
+
return async (ctx) => {
|
|
311
|
+
const { row, values, positionals, cfg, project } = ctx;
|
|
312
|
+
const op = READ_OPERATIONS[name];
|
|
313
|
+
try {
|
|
314
|
+
let result;
|
|
315
|
+
if (name === "status") result = op.fn(cfg, { project });
|
|
316
|
+
else if (name === "orientation") result = await op.fn(cfg);
|
|
317
|
+
else if (name === "search") result = op.fn(cfg, positionals.join(" "), { kinds: values.kind || null, status: values.status ?? null, limit: values.limit, includeDerived: Boolean(values["include-derived"]), caseSensitive: Boolean(values["case-sensitive"]) });
|
|
318
|
+
else if (name === "show") result = op.fn(cfg, positionals[0], { body: Boolean(values.body), section: values.section ?? null });
|
|
319
|
+
return emitRead({ verb: row.verb, ...ctx }, op, result);
|
|
320
|
+
} catch (e) {
|
|
321
|
+
if (e && e.code === "USAGE") return usageFail(e, { verb: row.verb, ...ctx });
|
|
322
|
+
throw e;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// The MCP server, imported lazily like the install family: no other verb
|
|
328
|
+
// carries the protocol code. A project must have been SUPPLIED — the flag,
|
|
329
|
+
// the neutral variable or the harness's declared directory; resolveProject's
|
|
330
|
+
// cwd fallback is exactly what the MCP ADR's decision 6 forbids here.
|
|
331
|
+
async function runMcp({ values, project, env, stdin, stdout, stderr }) {
|
|
332
|
+
const supplied = Boolean(values.project || env.PROJECTSTORE_PROJECT_DIR || projectRootDeclared(env));
|
|
333
|
+
const { serve } = await import("./mcp.mjs");
|
|
334
|
+
return serve({ project: supplied ? project : null, env, stdin, stdout, stderr });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// git's user.name for a fresh config's default_author, from the project's
|
|
338
|
+
// own repository — never from the process cwd; the login name otherwise.
|
|
339
|
+
function gitAuthor(project, env) {
|
|
340
|
+
if (existsSync(project)) {
|
|
341
|
+
try {
|
|
342
|
+
const r = spawnSync("git", ["config", "--get", "user.name"], { cwd: project, encoding: "utf8", timeout: 5000 });
|
|
343
|
+
if (r.status === 0 && r.stdout.trim()) return r.stdout.trim();
|
|
344
|
+
} catch {}
|
|
345
|
+
}
|
|
346
|
+
return env.USER || env.USERNAME || "";
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// bind / init: the vault named on the command line is the confirmation (the
|
|
350
|
+
// distribution ADR's decision 6 read for a binding — there is no --yes and
|
|
351
|
+
// nothing to ask); a change of vault needs --rebind. Exit 1 on a refusal with
|
|
352
|
+
// the reason, 2 on usage, 0 when already bound to the same vault.
|
|
353
|
+
function runBind(init) {
|
|
354
|
+
return async (ctx) => {
|
|
355
|
+
const { row, values, positionals, project, env, stdout, stderr } = ctx;
|
|
356
|
+
const vault = positionals[0];
|
|
357
|
+
if (!vault) return usageFail(Object.assign(new Error(`${row.verb} takes the vault path`), { code: "USAGE" }), { verb: row.verb, ...ctx });
|
|
358
|
+
// The author is the caller's to find: the plan reads nothing ambient.
|
|
359
|
+
const plan = planBind(project, { vault, layout: values.layout ?? null, language: values.language ?? null, rebind: Boolean(values.rebind), init, author: gitAuthor(project, env), env });
|
|
360
|
+
const usage = plan.refusals.find((r) => r.code === "USAGE");
|
|
361
|
+
if (usage) return usageFail(Object.assign(new Error(usage.message), { code: "USAGE" }), { verb: row.verb, ...ctx });
|
|
362
|
+
let done = null;
|
|
363
|
+
if (plan.ok && plan.writes) done = applyBind(plan);
|
|
364
|
+
const result = bindResult(plan, done);
|
|
365
|
+
if (values.json) stdout.write(JSON.stringify(envelope(row.verb, project, plan.ok, result), null, 2) + "\n");
|
|
366
|
+
else (plan.ok ? stdout : stderr).write(renderBindPlan(plan, done));
|
|
367
|
+
return plan.ok ? 0 : 1;
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// agents model <name> — the model a plugin surface passes for that agent, from
|
|
372
|
+
// the active harness's overlay (ADR-008's two terms); agents show — the overlay
|
|
373
|
+
// as read, with the keys the allowlist rejected; agents configure — the one
|
|
374
|
+
// writer, behind the same gate as install: naming --harness is the
|
|
375
|
+
// confirmation, a bare non-TTY call refuses (layout spec, contracts 2–4).
|
|
376
|
+
async function runAgents(ctx) {
|
|
377
|
+
const { row, values, positionals, project, env, stdout, stderr, stdin, ask } = ctx;
|
|
378
|
+
const sub = positionals[0];
|
|
379
|
+
const usage = (m) => usageFail(Object.assign(new Error(m), { code: "USAGE" }), { verb: row.verb, ...ctx });
|
|
380
|
+
if (!["model", "show", "configure"].includes(sub)) return usage("agents takes model <name>, show, or configure");
|
|
381
|
+
// The flag IS the confirmation, so it names exactly one overlay: two are a
|
|
382
|
+
// question, not an answer.
|
|
383
|
+
if (values.harness && values.harness.length > 1) return usage("--harness names exactly one overlay (it is the confirmation); given twice");
|
|
384
|
+
const named = values.harness && values.harness[0];
|
|
385
|
+
if (named && !harnessIds().includes(named)) return usage(`unknown harness: ${named} — known: ${harnessIds().join(", ")}`);
|
|
386
|
+
const harness = named || overlayId(env);
|
|
387
|
+
const forConfigure = ["default", "agent", "reset"].filter((o) => values[o] !== undefined);
|
|
388
|
+
if (sub !== "configure" && forConfigure.length) return usage(`--${forConfigure[0]} is an option of agents configure`);
|
|
389
|
+
// A refusal goes to stderr, as bind's does; --json keeps its envelope on stdout.
|
|
390
|
+
const emit = (verb, ok, result, text) => { if (values.json) stdout.write(JSON.stringify(envelope(verb, project, ok, result), null, 2) + "\n"); else (ok ? stdout : stderr).write(text); return ok ? 0 : 1; };
|
|
391
|
+
const cfg = readConfigAt(project);
|
|
392
|
+
const roster = layoutRoster(cfg);
|
|
393
|
+
if (sub === "model") {
|
|
394
|
+
const name = positionals[1];
|
|
395
|
+
if (!name) return usage("agents model takes the agent's bare name (critic, planner, …)");
|
|
396
|
+
const r = resolveAgentModel(project, name, { harness });
|
|
397
|
+
return emit("agents model", true, r, `${name}: ${r.model ? `${r.model} (${r.source}, ${r.overlay})` : "no model configured — the agent's frontmatter decides"}\n`);
|
|
398
|
+
}
|
|
399
|
+
const before = readOverlayAt(project, harness);
|
|
400
|
+
const inBinding = Boolean(cfg && cfg.agents && typeof cfg.agents === "object");
|
|
401
|
+
if (sub === "show") {
|
|
402
|
+
// Resolved per roster agent (per configured name when no layout loads), so
|
|
403
|
+
// a reader reports what would run without restating the two-term rule.
|
|
404
|
+
const names = [...new Set([...(roster || []), ...Object.keys(before.agents.per_agent)])].sort();
|
|
405
|
+
const resolved = Object.fromEntries(names.map((n) => { const r = resolveAgentModel(project, n, { harness }); return [n, { model: r.model, source: r.source }]; }));
|
|
406
|
+
const unknown = roster ? Object.keys(before.agents.per_agent).filter((n) => !roster.includes(n)) : [];
|
|
407
|
+
const lines = [`overlay: ${before.path || "(none: no harness detected and none named)"}${before.path && !before.present ? " (absent)" : ""}`];
|
|
408
|
+
if (before.unparseable) lines.push(" not valid JSON");
|
|
409
|
+
if (before.agents.default) lines.push(` default: ${before.agents.default}`);
|
|
410
|
+
for (const n of names) lines.push(` ${n}: ${resolved[n].model ? `${resolved[n].model} (${resolved[n].source})` : "— (the agent's frontmatter)"}${unknown.includes(n) ? " — not in the roster: nothing runs under this name" : ""}`);
|
|
411
|
+
if (before.rejected.length) lines.push(` ignored (not an overlay key): ${before.rejected.join(", ")}`);
|
|
412
|
+
if (inBinding) lines.push(" the binding still carries an agents block — a pre-0.28 leftover; run upgrade");
|
|
413
|
+
return emit("agents show", true, { harness, path: before.path, present: before.present, unparseable: before.unparseable, agents: before.agents, resolved, roster, unknown, rejected: before.rejected, agents_in_binding: inBinding }, lines.join("\n") + "\n");
|
|
414
|
+
}
|
|
415
|
+
// configure
|
|
416
|
+
if (!harness) return usage("no harness detected and none named: agents configure names --harness <id>");
|
|
417
|
+
if (before.unparseable) return emit("agents configure", false, { error: `${before.path} is not valid JSON; fix or remove it first`, harness, path: before.path }, `${before.path} is not valid JSON; fix or remove it first\n`);
|
|
418
|
+
if (!values.reset && values.default === undefined && !(values.agent || []).length) return usage("agents configure takes --default <model>, --agent <name>=<model> (repeatable) or --reset");
|
|
419
|
+
// --reset empties the block first; --default and --agent then apply on top of
|
|
420
|
+
// the emptied block — "reset, then set" is one call, and nothing given is
|
|
421
|
+
// silently dropped.
|
|
422
|
+
const base = values.reset ? { default: null, per_agent: {} } : before.agents;
|
|
423
|
+
const next = { default: values.default !== undefined ? (values.default || null) : base.default, per_agent: { ...base.per_agent } };
|
|
424
|
+
for (const spec of values.agent || []) {
|
|
425
|
+
const m = /^([a-z][a-z0-9-]*)=(.*)$/.exec(spec);
|
|
426
|
+
if (!m) return usage(`--agent takes <name>=<model>, got ${spec}`);
|
|
427
|
+
// A name outside the roster would be written, listed, and never run.
|
|
428
|
+
if (roster && !roster.includes(m[1])) return usage(`no agent named ${m[1]} in the ${cfg.layout} roster — known: ${roster.join(", ")}`);
|
|
429
|
+
if (m[2]) next.per_agent[m[1]] = m[2]; else delete next.per_agent[m[1]];
|
|
430
|
+
}
|
|
431
|
+
// The clerk transcribes; a strong default must not lift it (commands/agents.md,
|
|
432
|
+
// ADR-008). WHICH model it is pinned to is the harness's vocabulary, so the
|
|
433
|
+
// manifest names it and this file does not: pinning every harness to
|
|
434
|
+
// "sonnet" wrote an Anthropic model name into a Codex overlay, which is a
|
|
435
|
+
// model that does not exist there. A manifest with no cheap model declared
|
|
436
|
+
// does not pin, and the preview says so rather than inventing one.
|
|
437
|
+
// No fallback model here, deliberately, and it is not the same call as
|
|
438
|
+
// SOURCE_WRITE_TOOLS_FALLBACK: a missing manifest cannot reach this line.
|
|
439
|
+
// harnessIds() derives from the manifests, so with none loadable `--harness`
|
|
440
|
+
// is refused as unknown and `overlayId(env)` is null, and configure has
|
|
441
|
+
// already returned "no harness detected and none named". A literal here would
|
|
442
|
+
// be the deleted `"sonnet"` restored for a case that cannot happen.
|
|
443
|
+
let pinnedClerk = false;
|
|
444
|
+
let clerkPinUnavailable = false;
|
|
445
|
+
const clerkAsked = (values.agent || []).some((s) => s.startsWith("clerk="));
|
|
446
|
+
if (next.default && !next.per_agent.clerk && !clerkAsked) {
|
|
447
|
+
const cheap = harnessForOverlay(harness)?.agent_translation?.cheap_model || null;
|
|
448
|
+
if (cheap) { next.per_agent.clerk = cheap; pinnedClerk = true; }
|
|
449
|
+
else clerkPinUnavailable = true;
|
|
450
|
+
}
|
|
451
|
+
// Keys inside the agents block that the allowlist rejected (an `effort`, a
|
|
452
|
+
// stray shape) are rewritten out by the write and announced here; keys
|
|
453
|
+
// outside the block are kept, and stay doctor's to name.
|
|
454
|
+
const dropped = before.rejected.filter((k) => k.startsWith("agents."));
|
|
455
|
+
const same = !dropped.length && JSON.stringify({ d: before.agents.default, p: before.agents.per_agent }) === JSON.stringify({ d: next.default, p: next.per_agent });
|
|
456
|
+
const preview = [`agents configure — ${harness} — ${before.path}`, ` default: ${before.agents.default || "—"} → ${next.default || "—"}`];
|
|
457
|
+
const names = new Set([...Object.keys(before.agents.per_agent), ...Object.keys(next.per_agent)]);
|
|
458
|
+
for (const n of [...names].sort()) preview.push(` ${n}: ${before.agents.per_agent[n] || "—"} → ${next.per_agent[n] || "—"}${pinnedClerk && n === "clerk" ? " (pinned: the clerk stays cheap under a strong default)" : ""}`);
|
|
459
|
+
// Silence here would read as "the clerk is fine", and it is not: it takes the
|
|
460
|
+
// strong default, which is the one thing ADR-008 says must not happen to it.
|
|
461
|
+
if (clerkPinUnavailable) preview.push(` clerk: takes the default — ${harness} declares no cheap model to pin it to, so name one with --agent clerk=<model>`);
|
|
462
|
+
if (dropped.length) preview.push(` drops from the agents block (an overlay carries only models): ${dropped.join(", ")}`);
|
|
463
|
+
if (same) return emit("agents configure", true, { harness, path: before.path, wrote: false, agents: next, pinnedClerk, clerkPinUnavailable, dropped }, preview.join("\n") + "\n Nothing to change.\n");
|
|
464
|
+
// The gate: a named harness confirms; otherwise ask, and refuse without a terminal.
|
|
465
|
+
let confirmed = Boolean(named);
|
|
466
|
+
if (!confirmed) { const a = await confirmWrite(preview.join("\n") + `\nWrite ${before.path}? [y/N] `, { stdin, stdout, ask }); if (a === null) return usage("a non-interactive agents configure names --harness <id> to confirm; there is no --yes"); confirmed = a; }
|
|
467
|
+
if (!confirmed) return emit("agents configure", false, { harness, path: before.path, wrote: false, declined: true, agents: next, dropped }, preview.join("\n") + "\n nothing written.\n");
|
|
468
|
+
const path = writeOverlayAt(project, harness, next);
|
|
469
|
+
const after = readOverlayAt(project, harness);
|
|
470
|
+
return emit("agents configure", true, { harness, path, wrote: true, agents: after.agents, pinnedClerk, clerkPinUnavailable, dropped, rejected: after.rejected, agents_in_binding: inBinding }, preview.join("\n") + `\n wrote ${path}.${inBinding ? " The binding still carries an agents block (pre-0.28); run upgrade to move it." : ""}\n`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function runGraph(ctx) {
|
|
474
|
+
const { row, values, positionals, cfg } = ctx;
|
|
475
|
+
const sub = positionals[0];
|
|
476
|
+
const path = positionals[1];
|
|
477
|
+
if (!["neighbors", "lineage"].includes(sub)) return usageFail(Object.assign(new Error("graph takes neighbors <path> or lineage <path>"), { code: "USAGE" }), { verb: row.verb, ...ctx });
|
|
478
|
+
const op = READ_OPERATIONS[sub];
|
|
479
|
+
try {
|
|
480
|
+
// The row declares the union; each mode takes its own — an option the
|
|
481
|
+
// mode would ignore is a usage error, not a silent no-op.
|
|
482
|
+
const notFor = sub === "neighbors" ? ["depth"] : ["direction", "limit"];
|
|
483
|
+
for (const o of notFor) if (values[o] !== undefined) throw Object.assign(new Error(`--${o} is not an option of graph ${sub}`), { code: "USAGE" });
|
|
484
|
+
const result = sub === "neighbors"
|
|
485
|
+
? op.fn(cfg, path, { kinds: values.kind || null, direction: values.direction, limit: values.limit })
|
|
486
|
+
: op.fn(cfg, path, { depth: values.depth, kinds: values.kind && values.kind.length ? values.kind : LINEAGE_KINDS });
|
|
487
|
+
return emitRead({ verb: `graph ${sub}`, ...ctx }, op, result);
|
|
488
|
+
} catch (e) {
|
|
489
|
+
if (e && e.code === "USAGE") return usageFail(e, { verb: row.verb, ...ctx });
|
|
490
|
+
throw e;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function runCodemap(ctx) {
|
|
495
|
+
const { row, values, cfg } = ctx;
|
|
496
|
+
const op = READ_OPERATIONS.codeRefs;
|
|
497
|
+
try {
|
|
498
|
+
if (!values.for) throw Object.assign(new Error("codemap takes --for <selector>; regeneration is reconcile --only codemap"), { code: "USAGE" });
|
|
499
|
+
return emitRead({ verb: row.verb, ...ctx }, op, op.fn(cfg, values.for, { reverse: Boolean(values.reverse) }));
|
|
500
|
+
} catch (e) {
|
|
501
|
+
if (e && e.code === "USAGE") return usageFail(e, { verb: row.verb, ...ctx });
|
|
502
|
+
throw e;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function runVersion({ values, stdout }) {
|
|
507
|
+
const version = packageVersion();
|
|
508
|
+
stdout.write(values.json ? JSON.stringify(envelope("version", null, true, { version }), null, 2) + "\n" : version + "\n");
|
|
509
|
+
return 0;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function spawnDoctor(project, env, flags) {
|
|
513
|
+
// A project that does not exist is still a project doctor can report on
|
|
514
|
+
// ("not bound") — so the cwd is only set when it can be.
|
|
515
|
+
return spawnSync(process.execPath, [script("doctor.mjs"), ...flags], { encoding: "utf8", cwd: existsSync(project) ? project : undefined, env: ownEnv(env, project), timeout: 60000, maxBuffer: 1 << 24 });
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Spawned, never imported (MCP ADR decision 2): doctor's report and main are
|
|
519
|
+
// not exported, and importing them would be a second doctor. doctor.mjs sets
|
|
520
|
+
// no exit code of its own and prints either JSON or text, so text mode is
|
|
521
|
+
// two spawns — one for the findings that decide the exit code, one for the
|
|
522
|
+
// report the user reads. Deliberate; do not "optimise" the second away
|
|
523
|
+
// without giving doctor an exit code first.
|
|
524
|
+
async function runDoctor({ values, project, env, stdout, stderr }) {
|
|
525
|
+
const sections = [values.install && "--install", values.vault && "--vault"].filter(Boolean);
|
|
526
|
+
const r = spawnDoctor(project, env, ["--json", ...sections]);
|
|
527
|
+
const fail = (why) => {
|
|
528
|
+
if (values.json) stdout.write(JSON.stringify(envelope("doctor", project, false, { error: why }), null, 2) + "\n");
|
|
529
|
+
stderr.write(`doctor failed: ${why}\n`);
|
|
530
|
+
return 2;
|
|
531
|
+
};
|
|
532
|
+
if (r.error) return fail(r.error.message);
|
|
533
|
+
if (r.status !== 0 && !r.stdout) return fail((r.stderr || "").trim() || `exit ${r.status}`);
|
|
534
|
+
let findings;
|
|
535
|
+
try { findings = JSON.parse(r.stdout); } catch { return fail("unparseable output"); }
|
|
536
|
+
const ok = !findings.some((f) => f.level === "issue");
|
|
537
|
+
if (values.json) stdout.write(JSON.stringify(envelope("doctor", project, ok, findings), null, 2) + "\n");
|
|
538
|
+
else {
|
|
539
|
+
const t = spawnDoctor(project, env, sections);
|
|
540
|
+
stdout.write(typeof t.stdout === "string" && t.stdout ? t.stdout : `${(t.stderr || "").trim() || "doctor printed nothing"}\n`);
|
|
541
|
+
}
|
|
542
|
+
return ok ? 0 : 1;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async function runReconcile({ values, project, env, stdin, stdout, stderr, ask }) {
|
|
546
|
+
const write = Boolean(values.write);
|
|
547
|
+
const only = values.only ?? null;
|
|
548
|
+
// The gate: --only names what is written and confirms headless; a bare
|
|
549
|
+
// --write asks on a terminal and refuses without one.
|
|
550
|
+
if (write && !only) {
|
|
551
|
+
const yes = await confirmWrite(`Regenerate every derived view of ${project}'s vault? [y/N] `, { stdin, stdout, ask });
|
|
552
|
+
if (yes === null) {
|
|
553
|
+
stderr.write("a bare reconcile --write in a non-TTY refuses; name what is written to confirm: --only <target>\n");
|
|
554
|
+
if (values.json) stdout.write(JSON.stringify(envelope("reconcile", project, false, { refused: "non-tty" }), null, 2) + "\n");
|
|
555
|
+
return 1;
|
|
556
|
+
}
|
|
557
|
+
if (!yes) { stdout.write("nothing written.\n"); return 0; }
|
|
558
|
+
}
|
|
559
|
+
const { runReconcile: core } = await import("./reconcile.mjs");
|
|
560
|
+
let out;
|
|
561
|
+
try {
|
|
562
|
+
out = core({ write, only, projectDir: project, env: ownEnv(env, project) });
|
|
563
|
+
} catch (e) {
|
|
564
|
+
const msg = e && e.message ? e.message : String(e);
|
|
565
|
+
stderr.write(msg + "\n");
|
|
566
|
+
if (values.json) stdout.write(JSON.stringify(envelope("reconcile", project, false, { error: msg }), null, 2) + "\n");
|
|
567
|
+
return e && e.code === "UNBOUND" ? 3 : 2;
|
|
568
|
+
}
|
|
569
|
+
const failed = write && out.summary && out.summary.failed > 0;
|
|
570
|
+
stdout.write(JSON.stringify(values.json ? envelope("reconcile", project, !failed, out) : out, null, 2) + "\n");
|
|
571
|
+
return failed ? 1 : 0;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async function runInstallVerb({ row, values, project, env, stdin, stdout, ask }) {
|
|
575
|
+
const ih = await import("./install-harness.mjs");
|
|
576
|
+
const opts = { harnesses: values.harness || [], surfaces: values.surface && values.surface.length ? values.surface : null, globalRemoval: Boolean(values.global), register: !values["no-register"], root: PACKAGE_ROOT, env: ownEnv(env, project), stdin, stdout, ask };
|
|
577
|
+
if (row.verb === "plan") {
|
|
578
|
+
const p = ih.plan(project, opts);
|
|
579
|
+
stdout.write(values.json ? JSON.stringify(envelope("plan", project, p.ok && !p.incomplete, { ...p, items: p.items.map(ih.publicItem) }), null, 2) + "\n" : ih.renderPreview(p));
|
|
580
|
+
return p.ok && !p.incomplete ? 0 : 1;
|
|
581
|
+
}
|
|
582
|
+
const r = await ih.runVerb(row.verb, project, opts);
|
|
583
|
+
// A registration the plan could not make (no host CLI) or a host command
|
|
584
|
+
// that failed is exit 1 with the rest applied (install spec contract 4′).
|
|
585
|
+
const ok = r.plan.ok && !r.plan.incomplete && !r.failed && (r.gate.confirmed || r.gate.why === "nothing-to-do");
|
|
586
|
+
if (values.json) {
|
|
587
|
+
stdout.write(JSON.stringify(envelope(row.verb, project, ok, { gate: r.gate, applied: r.applied, failed: r.failed, incomplete: r.plan.incomplete, plannedAgainst: r.plan.plannedAgainst, items: r.plan.items.map(ih.publicItem), refusals: r.plan.refusals, reports: r.plan.reports }), null, 2) + "\n");
|
|
588
|
+
} else {
|
|
589
|
+
stdout.write(r.preview);
|
|
590
|
+
if (r.gate.confirmed) stdout.write(ih.appliedLine(r));
|
|
591
|
+
else if (r.gate.why === "non-tty") stdout.write(`a bare ${row.verb} in a non-TTY refuses; name a harness to confirm: --harness ${r.plan.detected.map((d) => d.id).join(" | ") || harnessIds().join(" | ")}\n`);
|
|
592
|
+
else if (r.gate.why === "declined") stdout.write("nothing written.\n");
|
|
593
|
+
}
|
|
594
|
+
return ok ? 0 : 1;
|
|
595
|
+
}
|