@nodii/dev 0.6.29 → 0.6.30

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
@@ -87,7 +87,7 @@ nodii-dev login [--no-browser] [--scope KEYS] [--force]
87
87
  nodii-dev logout
88
88
  nodii-dev whoami # OAuth identity plus the registered agent
89
89
  nodii-dev config show
90
- nodii-dev doctor [--figma-file KEY] [--json] # preflight: everything a lane needs, read-only
90
+ nodii-dev doctor [--figma-file KEY] [--json] # preflight: Dev, hooks, local Ollama + Gather model, optional Figma
91
91
 
92
92
  nodii-dev api list [--tag TAG] [--json]
93
93
  nodii-dev api OPERATION_ID [--param name=value ...] [--body @file|-|JSON]
@@ -129,6 +129,15 @@ nodii-dev decisions get D123
129
129
  nodii-dev decisions search WORDS
130
130
  nodii-dev decisions create|lock|supersede|amend
131
131
 
132
+ nodii-dev lane gather [KEY] [--delta] [--objective TEXT] [--done-check TEXT]
133
+ [--paths A,B] [--components K,L]
134
+ nodii-dev lane freeze add KEY --section SECTION --file ITEM.json --reason TEXT
135
+ nodii-dev lane freeze drop KEY --section SECTION --item ID --reason TEXT
136
+ nodii-dev lane freeze swap KEY --section SECTION [--item ID] --file VALUE.json --reason TEXT
137
+ nodii-dev lane freeze show KEY
138
+ nodii-dev lane freeze history KEY
139
+ nodii-dev rules find "QUESTION" [--limit N]
140
+
132
141
  nodii-dev open-questions list|show|answer
133
142
  nodii-dev feature-docs list|show|claim|release|status
134
143
  nodii-dev contracts grpc|rest|outbox|saga|table publish|status
@@ -764,6 +773,13 @@ call until `lane gather --delta` serves the current rules and acknowledges the
764
773
  newest displayed version. `NODII_DEV_RUN_ID` makes this check per subagent run,
765
774
  so one child cannot acknowledge for its siblings.
766
775
 
776
+ Gather also persists the lane's bounded Freeze: the objective and done check,
777
+ strict repository/Figma boundaries, the current pinned clauses, and the lane's
778
+ lookups, decisions, and unconfirmed questions. Ordinary CLI calls print and
779
+ acknowledge a changed Freeze before continuing, and compact SessionStart uses
780
+ the same backstop. `lane freeze add|drop|swap` makes a reasoned, version-checked
781
+ change; `show|history` reads the current contract or its append-only history.
782
+
767
783
  `lane start` creates and starts a lane. It infers the repository (by short
768
784
  name, as Dev compares it), branch and worktree from git unless overridden, and resolves `--issue` keys to the UUIDs
769
785
  required by Dev. Lane keys are passed directly to every lane and Built
package/dist/cli.js CHANGED
@@ -26,6 +26,7 @@ import { harnessCommand } from "./harness.js";
26
26
  import { hookRuntime, recordHookFire, safetyPostHook, safetyPreHook } from "./safety.js";
27
27
  import { doctorCommand } from "./doctor.js";
28
28
  import { dispatchManagedHook } from "./managed-hooks.js";
29
+ import { freezeCommand } from "./freeze.js";
29
30
  import {
30
31
  figmaChangeSetShow,
31
32
  figmaChangeSetText,
@@ -95,8 +96,8 @@ Usage: nodii-dev <command> [options] [--json]
95
96
  login [--force] | logout | whoami | config show
96
97
  # login says so when the machine is already signed in; --force
97
98
  # replaces that one sign-in (never from a lane)
98
- doctor [--figma-file KEY] [--json] # preflight: sign-in, agent, permissions, machine, repo, lane slots
99
- # and, given a key (or NODII_DEV_FIGMA_FILE), that Figma file's registration
99
+ doctor [--figma-file KEY] [--json] # preflight: sign-in, agent, permissions, machine, repo, lane slots,
100
+ # local Ollama + pinned Gather model, and optional Figma registration
100
101
  agent register --name NAME | agent heartbeat
101
102
  issues list|show|create|update|claim|release
102
103
  issues comment ISSUE --body TEXT # or --body-file FILE; issues update --comment TEXT|--comment-file FILE adds one too
@@ -135,7 +136,7 @@ Usage: nodii-dev <command> [options] [--json]
135
136
  figma file add FILE_KEY --name TEXT --kind design-system|surfaces|prototype [--module M]
136
137
  # register a Figma file in Dev so it can be locked
137
138
  hook claude-figma-pre|claude-figma-post|safety-pre|safety-post|session-start|managed-dispatch|print-settings
138
- setup [--yes|--check] # dry-run by default
139
+ setup [--yes|--check] # dry-run by default; installs/checks Ollama and pulls the Gather model
139
140
  harness install|update [--include-drafts[=false]] [--adopt PATH,...] [--dry-run] [--system]
140
141
  # --system stages managed hooks and only prints operator-reviewed sudo commands
141
142
  harness check # 0 current, 1 differs from Dev, 2 not installed
@@ -150,6 +151,7 @@ Usage: nodii-dev <command> [options] [--json]
150
151
  rulebook list|show CODE [--version V]|diff CODE
151
152
  rules core # the generated core (M05-33); refused over the cap
152
153
  rules report [--section S] [--lane KEY] # delivery totals per document; rules served per lane
154
+ rules find "QUESTION" [--limit N] # answer an ad-hoc rules question from the indexed sections
153
155
  rules cap TOKENS # operator: set the core token cap (default 3000)
154
156
 
155
157
  coordinator start --machine NAME # register this machine's one watched coordinator; refuses a live second
@@ -162,7 +164,8 @@ Usage: nodii-dev <command> [options] [--json]
162
164
  # authoritative preflight, one worktree per repo, watched launch
163
165
  # uses the provisioned agents/lane-<key>.json Dev identity
164
166
  lane amend <key> --objective|--scope|--rules TEXT --reason TEXT
165
- lane gather [<key>] [--delta] # infers a launched worktree; Delta required after an amendment
167
+ lane gather [<key>] [--delta] [--objective TEXT] [--done-check TEXT]
168
+ # infers a launched worktree; explicit contract inputs support manual lane start
166
169
  run [--kind build|test_run|subagent|deploy] [--name TEXT] -- <command> [args...]
167
170
  # watched command: registers with NODII_DEV_RUN_ID,
168
171
  # heartbeats, and records exit code + redacted log tail
@@ -175,12 +178,18 @@ Usage: nodii-dev <command> [options] [--json]
175
178
  lane heartbeat|resume|land|verify|prioritize|deprioritize|stop <key>
176
179
  lane review <key> --file REVIEW.json # base/head, outcome and clause-cited findings
177
180
  lane reviews <key> # stored review history for the lane
181
+ lane freeze add <key> --section SECTION --file ITEM.json --reason TEXT
182
+ lane freeze drop <key> --section SECTION --item ID --reason TEXT
183
+ lane freeze swap <key> --section SECTION [--item ID] --file VALUE.json --reason TEXT
184
+ lane freeze show|history <key> # durable compact-safe contract and all versions
178
185
  lane non-issue <key> --reason TEXT --evidence TEXT --reroute KEY[,KEY]|none
179
186
  --branch carried:KEY|dropped[:REASON] [--commits SHA[,SHA]]
180
187
  # DEC-0011: terminal non_issue; issue -> wont_do; slot frees
181
188
  lane list [--kind KIND] [--status STATUS] [--mine] [--cursor CURSOR]
182
189
  lane show <key> | next <key> [--paths A,B] [--components K,L] [--role R]
183
190
  # next step, plus the rules Dev serves for it (DEC-0020)
191
+ lane gather [key] [--delta] [--paths A,B] [--components K,L]
192
+ # local document pick; prints Lane contract then Clause map
184
193
  built list | built ack <key...>
185
194
  machine list|show|set-coordinator|assign|report
186
195
  agent lane-limit|grant|revoke
@@ -574,6 +583,8 @@ export async function main(argv = process.argv.slice(2)) {
574
583
  if (command === "lane" && action === "launch") return emitCommand(await launchLaneCommand(rest[0], f));
575
584
  if (command === "lane" && action === "amend") return emitCommand(await amendLaneCommand(rest[0], f));
576
585
  if (command === "lane" && action === "gather") return emitCommand(await gatherLaneCommand(rest[0], f));
586
+ if (command === "lane" && action === "freeze")
587
+ return emitCommand(await freezeCommand(rest[0], rest.slice(1), f));
577
588
  if (command === "lane") return emitCommand(await laneCommand(action, rest, f));
578
589
  if (command === "next") return emitCommand(await nextCommand(action, f));
579
590
  if (command === "built") return emitCommand(await builtCommand(action, rest, f));
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.6.29";
7
+ export const VERSION = "0.6.30";
8
8
  export const configPath = () =>
9
9
  process.env.NODII_DEV_CONFIG_PATH ??
10
10
  join(homedir(), ".config/nodii-dev/config.json");
@@ -119,9 +119,30 @@ const staleCode = (raw) => {
119
119
  }
120
120
  };
121
121
 
122
- async function devFetch(method, path, bodyText, contentType, accept, extra = {}) {
123
- const config = await readConfig();
124
- const auth = await authProvider();
122
+ const withSignal = (promise, signal) => {
123
+ if (!signal) return promise;
124
+ if (signal.aborted) return Promise.reject(signal.reason ?? new Error("aborted"));
125
+ return new Promise((resolvePromise, rejectPromise) => {
126
+ const abort = () => rejectPromise(signal.reason ?? new Error("aborted"));
127
+ signal.addEventListener("abort", abort, { once: true });
128
+ Promise.resolve(promise).then(
129
+ (value) => { signal.removeEventListener("abort", abort); resolvePromise(value); },
130
+ (error) => { signal.removeEventListener("abort", abort); rejectPromise(error); },
131
+ );
132
+ });
133
+ };
134
+
135
+ async function devFetch(method, path, bodyText, contentType, accept, extra = {}, signal) {
136
+ const wait = async (promise) => {
137
+ try {
138
+ return await withSignal(promise, signal);
139
+ } catch (error) {
140
+ if (signal?.aborted) throw new CliError("DEV_TIMEOUT", `Dev request timed out: ${method} ${path}`, 1);
141
+ throw error;
142
+ }
143
+ };
144
+ const config = await wait(readConfig());
145
+ const auth = await wait(authProvider());
125
146
  const url = requestUrl(config, path);
126
147
  // The edge refuses a token minted before the principal's claims epoch with
127
148
  // 401 token_stale (refresh and retry once). The request never reached Dev,
@@ -132,26 +153,28 @@ async function devFetch(method, path, bodyText, contentType, accept, extra = {})
132
153
  for (;;) {
133
154
  const headers = { accept, ...extra };
134
155
  if (bodyText !== undefined) headers["content-type"] = contentType;
135
- Object.assign(headers, await auth.headers({ method, path, body: bodyText ?? "" }));
156
+ Object.assign(headers, await wait(auth.headers({ method, path, body: bodyText ?? "" })));
136
157
  let response;
137
158
  try {
138
- response = await fetch(url, { method, headers, body: bodyText });
159
+ response = await wait(fetch(url, { method, headers, body: bodyText, signal }));
139
160
  } catch (error) {
161
+ if (error?.code === "DEV_TIMEOUT") throw error;
162
+ if (signal?.aborted) throw new CliError("DEV_TIMEOUT", `Dev request timed out: ${method} ${path}`, 1);
140
163
  throw new CliError("DEV_UNREACHABLE", `Cannot reach Dev at ${new URL(url).origin}: ${error.message}`);
141
164
  }
142
- const raw = await response.text();
165
+ const raw = await wait(response.text());
143
166
  if (response.status !== 401 || typeof auth.refreshStale !== "function" || !staleCode(raw)) return { response, raw };
144
167
  if (refreshed) return { response, raw };
145
168
  // A cached token that is stale too is not adopted twice: the next one is refreshed.
146
169
  const used = String(headers.authorization ?? "").replace(/^Bearer /, "");
147
- if ((await auth.refreshStale(used, { force: adopted })) === "refreshed") refreshed = true;
170
+ if ((await wait(auth.refreshStale(used, { force: adopted }))) === "refreshed") refreshed = true;
148
171
  else adopted = true;
149
172
  }
150
173
  }
151
174
 
152
175
  /** A JSON call that keeps the status: `{ status, payload }`. Errors throw as devRawRequest's do. */
153
- export async function devExchange(method, path, bodyText, { contentType = "application/json", headers = {} } = {}) {
154
- const { response, raw } = await devFetch(method, path, bodyText, contentType, "application/json", headers);
176
+ export async function devExchange(method, path, bodyText, { contentType = "application/json", headers = {}, signal } = {}) {
177
+ const { response, raw } = await devFetch(method, path, bodyText, contentType, "application/json", headers, signal);
155
178
  let payload;
156
179
  try {
157
180
  payload = raw ? JSON.parse(raw) : {};
@@ -168,8 +191,8 @@ export async function devExchange(method, path, bodyText, { contentType = "appli
168
191
  return { status: response.status, payload };
169
192
  }
170
193
 
171
- export async function devRawRequest(method, path, bodyText, contentType = "application/json") {
172
- return (await devExchange(method, path, bodyText, { contentType })).payload;
194
+ export async function devRawRequest(method, path, bodyText, contentType = "application/json", options = {}) {
195
+ return (await devExchange(method, path, bodyText, { ...options, contentType })).payload;
173
196
  }
174
197
 
175
198
  /** A Dev route that answers with text (e.g. a document body as text/markdown). Errors are Dev's JSON. */
@@ -181,8 +204,8 @@ export async function devTextRequest(method, path) {
181
204
  throw new CliError(payload.code ?? `HTTP_${response.status}`, payload.message ?? `Dev returned HTTP ${response.status}`, 1, payload.details);
182
205
  }
183
206
 
184
- export const devRequest = (method, path, body) =>
185
- devRawRequest(method, path, body === undefined ? undefined : JSON.stringify(body));
207
+ export const devRequest = (method, path, body, options) =>
208
+ devRawRequest(method, path, body === undefined ? undefined : JSON.stringify(body), "application/json", options);
186
209
 
187
210
  /** A JSON call sent with `Prefer: respond-async` (RFC 7240): `{ status, payload }`, 202 when Dev took it as a job. */
188
211
  export const devAsyncRequest = async (method, path, body) =>