@nodii/dev 0.5.0 → 0.6.1

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 CHANGED
@@ -36,6 +36,11 @@ refuses those flags. The first command you run after upgrading deletes the old
36
36
  config and its stored secret (Keychain item or file) without printing it, then
37
37
  asks you to run `nodii-dev login`.
38
38
 
39
+ `nodii-dev` is only the Dev control CLI. The local-stack CLI is `./nodii-stack`,
40
+ run from the `nodii-local-stack` checkout (renamed per operator ruling G3-6,
41
+ with no alias under the old name). Typing one of its verbs here, such as
42
+ `nodii-dev doctor`, fails and names the `./nodii-stack` command instead.
43
+
39
44
  Every command accepts `--json`; without it, output is formatted for humans.
40
45
  Dev failures print Dev's error code to stderr and exit non-zero.
41
46
 
@@ -137,8 +142,83 @@ nodii-dev people refresh-names
137
142
  nodii-dev setup # preview files that would change
138
143
  nodii-dev setup --yes # merge and write the harness
139
144
  nodii-dev setup --check # exit non-zero when it has drifted
145
+
146
+ nodii-dev rulebook list [--section rulebook|services] [--kind KIND] [--q TEXT]
147
+ nodii-dev rulebook show CODE [--version V] # the current text, from Dev
148
+ nodii-dev rulebook diff CODE [--from V] [--to V]
149
+
150
+ nodii-dev docs check PATH... # local M-05 checks only
151
+ nodii-dev docs validate PATH... --class CLASS # Dev's dry run of the upload
152
+ nodii-dev docs upload PATH... --class ruling|conformance|editorial
153
+ [--base CODE=VERSION ...] [--decision DECISION.json]
154
+ [--conformance-ref PR|REPO@SHA] [--meaning-unchanged REASON]
155
+ [--wait [--wait-minutes N]]
156
+ nodii-dev docs ratify --domain KEY [--domain KEY ...] --decision DECISION.json [--wait]
157
+ nodii-dev docs parked [CODE] [--status pending|acknowledged]
158
+
159
+ nodii-dev harness install [--include-drafts] [--adopt PATH,...] [--dry-run]
160
+ nodii-dev harness update [--include-drafts] [--adopt PATH,...] [--dry-run]
161
+ nodii-dev harness update --check # exit 1 when Dev would write anything different
140
162
  ```
141
163
 
164
+ ## AD documents
165
+
166
+ AD documents (the rulebook and per-service feature docs) live in Dev, one
167
+ current text per document with its version history (M-05). `docs upload`
168
+ takes files or folders (folders are searched for `.md` files, skipping `_`
169
+ and `.` entries) and checks every document locally before sending anything:
170
+ front matter, the section order for its kind, unique clause ids that use the
171
+ document's code, no bare decision ids (`D123`; cite `pre:D123`), and the
172
+ parked (`Undecided`) items, which it lists. All problems are printed with
173
+ Dev's error codes, and nothing is sent until there are none. Dev then runs the
174
+ same checks, plus the ones that need its data, in one transaction.
175
+
176
+ - `--class` is required on every write (M05-8). A `ruling` carries its
177
+ decision (`--decision`, with `amends`), and the decision locks with the new
178
+ versions or nothing is written. An `editorial` write that changes a Rules
179
+ clause is refused unless `--meaning-unchanged REASON` is given (M05-10).
180
+ - A new document is based on nothing. For a document Dev already has, pass
181
+ the version you started from with `--base CODE=VERSION` (M05-11). The CLI
182
+ never fills it in from Dev, so a stale edit is refused instead of
183
+ overwriting newer text.
184
+ - **Parked items:** when a document has parked items the operator has not
185
+ acknowledged, Dev refuses the upload and records the list. The CLI prints
186
+ it and exits `4`. The operator acknowledges it in Dev (this CLI is an OAuth
187
+ app and cannot). Re-run the upload then, or pass `--wait` to poll and retry
188
+ once it is acknowledged.
189
+ - `docs ratify` is the bulk ratification mode: one decision locks v1.0 of
190
+ every draft document in the named domains, which then become AD (M05-2).
191
+ Upload the drafts first (`--class conformance`), then ratify domain by
192
+ domain. The decision file holds `title`, `context`, `options`, `ruling`,
193
+ `consequences` and `provenance`; Dev mints the `DEC-nnnn` id and generates
194
+ the amends. A domain that is already AD can be named again to ratify drafts
195
+ added to it since (a new feature doc in a ratified service).
196
+ - `docs validate` also prints Dev's warnings for each document (they never
197
+ block) and any problems with the decision's amends.
198
+
199
+ Like every other command, these reach Dev only through the platform edge
200
+ (`/devops/api/v1/documents`, `/domains`, `/harness`, …) with the OAuth login.
201
+
202
+ ## Agent harness
203
+
204
+ `harness install` writes the repository's agent harness as Dev generates it
205
+ from the ratified methodology: the `CLAUDE.md` and `AGENTS.md` pointer
206
+ blocks, one pointer skill per methodology document, the Claude Code hooks
207
+ merged into `.claude/settings.json`, and Dev's `.nodii/harness.json`. Every
208
+ generated file carries the methodology version it came from. Nothing about
209
+ the rules is authored locally. Run it in the service's own repository: it
210
+ installs one service at a time, and there is no fleet mode. `--include-drafts`
211
+ uses draft methodology while none is ratified.
212
+
213
+ The CLI records what it wrote in `.nodii/harness-state.json` and never touches
214
+ anything else. An existing file it did not write is refused, unless the
215
+ operator names it with `--adopt PATH` (a `CLAUDE.md` then keeps its text and
216
+ gains the managed block; `settings.json` keeps its other keys and hooks).
217
+ `harness update` re-syncs: it prints the methodology version change, which
218
+ source documents moved, and each file created, updated or removed. It
219
+ removes only files, blocks and hook entries it wrote, and it refuses (writing
220
+ nothing) when one of its files was edited by hand.
221
+
142
222
  ## Build lanes and repository setup
143
223
 
144
224
  `lane start` creates and starts a lane. It infers the repository, branch and
@@ -190,7 +270,9 @@ runtime, people, agents, inbox/gates, design, runbooks,
190
270
  doctrine and exceptions, changelog, health verdicts, approval kinds,
191
271
  notification registry, skills, questions, decisions, drift, requests,
192
272
  pipelines, runners, routing, usage, audit, reconciliation, cron, composite
193
- surfaces, service edges, lanes, swaps, ideas, and terminology. `api list` is the authoritative discovery view;
273
+ surfaces, service edges, lanes, swaps, ideas, terminology, and the AD era: `rulebook`
274
+ (documents, plus `rulebook versions CODE`), `ad-decisions`, `domains`, `standardizations`,
275
+ `archive` (`archive resolve --ref pre:D1366`), and `harness methodology`. `api list` is the authoritative discovery view;
194
276
  `api OPERATION_ID` makes every operation callable even without an alias.
195
277
 
196
278
  `figma check` exits `0` when allowed and `3` when denied. The pre-hook permits
package/dist/api.js CHANGED
@@ -95,12 +95,14 @@ export const GROUPS = {
95
95
  sagas: "Sagas", "outbox-topics": ["Outbox", "OutboxTopics"], tables: "Tables", libraries: "Libraries", workers: "Workers",
96
96
  projects: "Projects", issues: "Issues", backlog: "Backlogs", backlogs: "Backlogs", builds: "Delivery", changes: "Delivery",
97
97
  runtime: "Runtime", people: "People", agents: "Agents", inbox: "Inbox", gate: "Inbox", design: "Design", runbooks: "Runbooks",
98
- doctrine: ["Doctrine", "DoctrineExceptions"], rulebook: "Doctrine", changelog: "Changelog", "health-verdicts": "HealthVerdicts",
98
+ doctrine: ["Doctrine", "DoctrineExceptions"], changelog: "Changelog", "health-verdicts": "HealthVerdicts",
99
99
  "approval-kinds": "ApprovalKinds", "notification-registry": "Notification registry", skills: "Skills", "open-questions": "OpenQuestions",
100
100
  decisions: "Decisions", drift: "Drift", requests: "Requests", pipelines: "Pipelines", slices: "Slices", "sql-lookups": "SqlLookups",
101
101
  runners: "Runners", routing: "Routing", usage: "Usage", audit: "AuditLog", reconciliation: "Reconciliation", cron: "Cron", health: "Health",
102
102
  composite: "Composite", "service-edges": "ServiceEdges", lanes: "Lanes",
103
103
  swaps: "Swaps", ideas: "Ideas", terminology: "Terminology",
104
+ rulebook: "ad-documents", "ad-decisions": "ad-decisions", domains: "ad-domains", standardizations: "standardizations",
105
+ archive: "archive", harness: "harness",
104
106
  };
105
107
 
106
108
  export const ALIASES = {
@@ -129,7 +131,7 @@ export const ALIASES = {
129
131
  "gate:fleet": "getFleetGate", "gate:service": "getServiceGate", "gate:dry-run": "getGateDryRun",
130
132
  "runbooks:list": "listRunbooks", "runbooks:show": "getRunbook", "runbooks:create": "registerRunbook", "runbooks:update": "updateRunbook",
131
133
  "doctrine:list": "listDoctrine", "doctrine:show": "getDoctrine", "doctrine:create": "createDoctrine", "doctrine:revise": "reviseDoctrine",
132
- "rulebook:list": "listDoctrine", "rulebook:show": "getDoctrine", "changelog:list": "listChangelog", "changelog:append": "appendChangelog",
134
+ "changelog:list": "listChangelog", "changelog:append": "appendChangelog",
133
135
  "health-verdicts:list": "listHealthVerdicts", "health-verdicts:post": "postHealthVerdict",
134
136
  "approval-kinds:list": "listApprovalKinds", "approval-kinds:show": "getApprovalKind", "approval-kinds:create": "createApprovalKind",
135
137
  "notification-registry:list": "listNotificationEvents", "notification-registry:show": "getNotificationEvent", "notification-registry:propose": "proposeNotificationEvent",
@@ -143,6 +145,12 @@ export const ALIASES = {
143
145
  "service-edges:list": "listServiceEdges", "service-edges:show": "getServiceEdgesForService",
144
146
  "swaps:list": "listSwaps", "swaps:show": "getSwap", "swaps:create": "createSwap", "swaps:close": "closeSwap",
145
147
  "ideas:list": "listIdeas", "ideas:show": "getIdea", "ideas:create": "createIdea", "ideas:status": "updateIdeaStatus",
148
+ "rulebook:versions": "listAdDocumentVersions", "rulebook:parked": "getAdDocumentParked",
149
+ "ad-decisions:list": "listAdDecisions", "ad-decisions:show": "getAdDecision", "ad-decisions:lock": "lockAndAmendAdDecision",
150
+ "domains:list": "listAdDomains", "domains:show": "getAdDomain",
151
+ "standardizations:list": "listStandardizations", "standardizations:show": "getStandardization", "standardizations:update": "updateStandardization",
152
+ "archive:search": "searchPreDevArchive", "archive:resolve": "resolvePreCitation",
153
+ "harness:show": "generateHarness", "harness:methodology": "getHarnessMethodologyVersion",
146
154
  "terminology:list": "listTerminology", "terminology:show": "getTerminologyTerm", "terminology:create": "createTerminologyTerm", "terminology:update": "updateTerminologyTerm",
147
155
  };
148
156
 
package/dist/cli.js CHANGED
@@ -24,6 +24,8 @@ import {
24
24
  } from "./oauth.js";
25
25
  import { heartbeatCurrentLane, postHook, preHook, settingsSnippet, workflowSuggestion } from "./hooks.js";
26
26
  import { setupCommand } from "./setup.js";
27
+ import { harnessCommand } from "./harness.js";
28
+ import { docsCommand, rulebookCommand } from "./upload.js";
27
29
  import {
28
30
  ALIASES,
29
31
  GROUPS,
@@ -59,6 +61,14 @@ Usage: nodii-dev <command> [options] [--json]
59
61
  figma lock|check|log-write|submit|apply|published|abandon|release
60
62
  hook claude-figma-pre|claude-figma-post|print-settings
61
63
  setup [--yes|--check] # dry-run by default
64
+ harness install|update [--include-drafts[=false]] [--adopt PATH,...] [--dry-run]
65
+ harness update --check # exit 1 when Dev would write anything different
66
+ docs check|validate|upload <path...> --class ruling|conformance|editorial
67
+ [--base CODE=VERSION] [--decision FILE] [--conformance-ref REF]
68
+ [--meaning-unchanged REASON] [--wait]
69
+ docs ratify --domain KEY [--domain KEY ...] --decision FILE [--wait]
70
+ docs parked [CODE] [--status pending|acknowledged]
71
+ rulebook list|show CODE [--version V]|diff CODE
62
72
 
63
73
  lane start <key> --title TEXT --kind KIND --side SIDE [--repo REPO] [--issue ISSUE]
64
74
  lane step <key> --side SIDE --start|--done|--block <step>
@@ -79,10 +89,15 @@ Resource commands cover every OpenAPI tag, including services, feature-docs,
79
89
  contracts, sagas, outbox-topics, tables, libraries, workers, projects, issues,
80
90
  backlogs, builds, changes, deploy-reports, runtime, people, agents, inbox,
81
91
  gate, design, runbooks, doctrine, changelog, health-verdicts, approval-kinds,
82
- notification-registry, skills, open-questions, decisions, drift, and requests.
92
+ notification-registry, skills, open-questions, decisions, drift, requests,
93
+ rulebook, ad-decisions, domains, standardizations, archive, and harness.
83
94
 
84
95
  Run a command with --help or see the package README for all flags.`;
85
96
 
97
+ // Verbs of the local-stack CLI, which shared this name until it became
98
+ // ./nodii-stack (operator G3-6, no alias). Named here only to point there.
99
+ const STACK_VERBS = new Set(["up", "down", "doctor", "sync", "seed", "wipe", "reset", "rebuild", "restart", "smoke", "urls", "peek", "trace", "stats", "bootstrap", "provenance", "stale", "status", "logs", "world", "totp", "token", "bench"]);
100
+
86
101
  async function stdinText() {
87
102
  let value = "";
88
103
  for await (const chunk of stdin) value += chunk;
@@ -248,6 +263,13 @@ export async function main(argv = process.argv.slice(2)) {
248
263
  }
249
264
  const emitCommand = async (result) => emit(result.payload, json, result.human);
250
265
  if (command === "setup") return emitCommand(await setupCommand(f));
266
+ if (command === "harness" && ["install", "update"].includes(action)) {
267
+ // `--include-drafts` is a switch: `--include-drafts false` would leave `false` here and turn drafts on.
268
+ if (rest.length) throw new CliError("USAGE", `harness ${action} takes no arguments, got: ${rest.join(" ")}. Write a flag value as --include-drafts=false.`, 2);
269
+ return emitCommand(await harnessCommand(action, f));
270
+ }
271
+ if (command === "docs") return emitCommand(await docsCommand(action, rest, f));
272
+ if (command === "rulebook" && ["list", "show", "diff"].includes(action)) return emitCommand(await rulebookCommand(action, rest, f));
251
273
  if (command === "lane") return emitCommand(await laneCommand(action, rest, f));
252
274
  if (command === "next") return emitCommand(await nextCommand(action, f));
253
275
  if (command === "built") return emitCommand(await builtCommand(action, rest, f));
@@ -568,7 +590,7 @@ export async function main(argv = process.argv.slice(2)) {
568
590
  }
569
591
  throw new CliError(
570
592
  "USAGE",
571
- `Unknown command: ${[command, action].filter(Boolean).join(" ")}`,
593
+ `Unknown command: ${[command, action].filter(Boolean).join(" ")}${STACK_VERBS.has(command) ? `. The local-stack CLI is \`./nodii-stack ${command}\`, run from the nodii-local-stack checkout.` : ""}`,
572
594
  2,
573
595
  );
574
596
  }
package/dist/core.js CHANGED
@@ -4,7 +4,7 @@ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
4
  import { homedir, platform } from "node:os";
5
5
  import { dirname, join } from "node:path";
6
6
 
7
- export const VERSION = "0.5.0";
7
+ export const VERSION = "0.6.1";
8
8
  export const configPath = () =>
9
9
  process.env.NODII_DEV_CONFIG_PATH ??
10
10
  join(homedir(), ".config/nodii-dev/config.json");
@@ -105,10 +105,10 @@ export function requestUrl(config, path) {
105
105
  return `${config.edgeUrl.replace(/\/$/, "")}/devops${path}`;
106
106
  }
107
107
 
108
- export async function devRawRequest(method, path, bodyText, contentType = "application/json") {
108
+ async function devFetch(method, path, bodyText, contentType, accept) {
109
109
  const config = await readConfig();
110
110
  const auth = await authProvider();
111
- const headers = { accept: "application/json" };
111
+ const headers = { accept };
112
112
  if (bodyText !== undefined) headers["content-type"] = contentType;
113
113
  Object.assign(headers, await auth.headers({ method, path, body: bodyText ?? "" }));
114
114
  const url = requestUrl(config, path);
@@ -118,7 +118,11 @@ export async function devRawRequest(method, path, bodyText, contentType = "appli
118
118
  } catch (error) {
119
119
  throw new CliError("DEV_UNREACHABLE", `Cannot reach Dev at ${new URL(url).origin}: ${error.message}`);
120
120
  }
121
- const raw = await response.text();
121
+ return { response, raw: await response.text() };
122
+ }
123
+
124
+ export async function devRawRequest(method, path, bodyText, contentType = "application/json") {
125
+ const { response, raw } = await devFetch(method, path, bodyText, contentType, "application/json");
122
126
  let payload;
123
127
  try {
124
128
  payload = raw ? JSON.parse(raw) : {};
@@ -135,6 +139,15 @@ export async function devRawRequest(method, path, bodyText, contentType = "appli
135
139
  return payload;
136
140
  }
137
141
 
142
+ /** A Dev route that answers with text (e.g. a document body as text/markdown). Errors are Dev's JSON. */
143
+ export async function devTextRequest(method, path) {
144
+ const { response, raw } = await devFetch(method, path, undefined, undefined, "text/markdown, text/plain, application/json");
145
+ if (response.ok) return raw;
146
+ let payload = {};
147
+ try { payload = JSON.parse(raw); } catch { payload = { message: raw }; }
148
+ throw new CliError(payload.code ?? `HTTP_${response.status}`, payload.message ?? `Dev returned HTTP ${response.status}`, 1, payload.details);
149
+ }
150
+
138
151
  export const devRequest = (method, path, body) =>
139
152
  devRawRequest(method, path, body === undefined ? undefined : JSON.stringify(body));
140
153
 
@@ -147,6 +160,9 @@ export async function checkLogin() {
147
160
  }
148
161
 
149
162
  export const encode = (value) => encodeURIComponent(value);
163
+ // Switches never take the next token as their value, so
164
+ // `docs upload --wait rulebook/` keeps rulebook/ as a path.
165
+ const SWITCHES = new Set(["json", "wait", "dry-run", "check", "all", "fleet", "help", "yes", "mine", "no-browser", "include-drafts"]);
150
166
  export function parseArgs(argv) {
151
167
  const flags = {};
152
168
  const args = [];
@@ -156,11 +172,13 @@ export function parseArgs(argv) {
156
172
  args.push(token);
157
173
  continue;
158
174
  }
159
- const [key, inline] = token.slice(2).split("=", 2);
175
+ const at = token.indexOf("=");
176
+ const key = at === -1 ? token.slice(2) : token.slice(2, at);
177
+ const inline = at === -1 ? undefined : token.slice(at + 1);
160
178
  const value =
161
179
  inline !== undefined
162
180
  ? inline
163
- : argv[i + 1] && !argv[i + 1].startsWith("--")
181
+ : !SWITCHES.has(key) && argv[i + 1] && !argv[i + 1].startsWith("--")
164
182
  ? argv[++i]
165
183
  : true;
166
184
  flags[key] =