@nodii/dev 0.6.35 → 0.6.36

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
@@ -145,6 +145,7 @@ nodii-dev gather tune start --cases all|ID,ID --corpus-ref REF [--trigger T] [--
145
145
  nodii-dev gather tune exec RUN_ID [--pickers shortlist-only,gemma-e4b,gemma-12b,gemma-26b]
146
146
  nodii-dev gather tune trial RUN_ID --case ID --picker P --iteration N [--documents A,B]
147
147
  nodii-dev gather tune finalize RUN_ID
148
+ nodii-dev gather timings [--model M] [--last N]
148
149
 
149
150
  nodii-dev open-questions list|show|answer
150
151
  nodii-dev feature-docs list|show|claim|release|status
@@ -154,7 +155,8 @@ nodii-dev libraries register-version LIBRARY_ID --version VERSION
154
155
  nodii-dev state diff
155
156
  nodii-dev heartbeat audit [SERVICE]
156
157
 
157
- nodii-dev run [--kind build|test_run|subagent|deploy] [--name TEXT] -- COMMAND [ARGS...]
158
+ nodii-dev run [--kind build|test_run|subagent|deploy] [--name TEXT] [--artifact PATH] -- COMMAND [ARGS...]
159
+ nodii-dev run --remote-ssm INSTANCE SCRIPT [--artifact PATH] [--quiet-minutes N] [--poll-seconds N] [--region R]
158
160
 
159
161
  nodii-dev design entries [--kind KIND] [--group GROUP] [--q TEXT]
160
162
  nodii-dev design entry KEY
@@ -371,7 +373,7 @@ nodii-dev rulebook diff CODE [--from V] [--to V]
371
373
  nodii-dev docs check PATH... # local M-05 checks only
372
374
  nodii-dev docs validate PATH... --class CLASS # Dev's dry run of the upload
373
375
  nodii-dev docs upload PATH... --class ruling|conformance|editorial
374
- [--base CODE=VERSION ...] [--decision DECISION.json] [--pending]
376
+ [--base CODE=VERSION ...] [--decision DECISION.json]
375
377
  [--conformance-ref PR|REPO@SHA] [--meaning-unchanged REASON]
376
378
  [--wait [--wait-minutes N]]
377
379
  nodii-dev docs ratify --domain KEY [--domain KEY ...] --decision DECISION.json [--wait]
@@ -620,10 +622,9 @@ Dev's error codes, and nothing is sent until there are none. Dev then runs the
620
622
  same checks, plus the ones that need its data, in one transaction.
621
623
 
622
624
  - `--class` is required on every write (M05-8). A `ruling` carries its
623
- decision (`--decision`, with `amends`). Agents must add `--pending`: Dev
624
- stores the ruling versions under a pending decision without activating
625
- them, and a signed-in staff person locks it later. Omitting `--pending`
626
- requests the staff-only immediate lock-and-amend action. An `editorial`
625
+ decision (`--decision`, with `amends`), and the decision locks as the change
626
+ lands (DEV-DEC-AS-COMMIT-1). `--pending` is still accepted but does nothing:
627
+ the CLI prints "decisions lock on upload" and doesn't send it. An `editorial`
627
628
  write that changes a Rules clause is refused unless
628
629
  `--meaning-unchanged REASON` is given (M05-10).
629
630
  - A new document is based on nothing. For a document Dev already has, pass
@@ -791,6 +792,41 @@ their output may be swallowed; the next visible call prints and acknowledges
791
792
  them. Unacknowledged amendments will join this preflight when the amendment API
792
793
  from AGENT-HARNESS-14 is available.
793
794
 
795
+ ### Result artifacts
796
+
797
+ A run that succeeds can attach one text artifact, such as a benchmark table as
798
+ JSON: `--artifact PATH`. It reaches the waiter whole, as the Result's
799
+ `artifact` evidence item. The note is the content, redacted like the log
800
+ tail. The ref is `NAME · BYTES bytes · sha256:HEX`. Dev keeps at most 10,000
801
+ characters in a note, so an artifact that is missing, empty or larger fails the
802
+ run (`Artifact not attached: …`). It never arrives cut.
803
+
804
+ ### Remote runs over SSM
805
+
806
+ `nodii-dev run --remote-ssm INSTANCE SCRIPT` runs a local shell script on an EC2
807
+ instance through SSM (`AWS-RunShellScript`), using your own `aws` CLI
808
+ credentials (`--region` passes through). It is a watched run like a local one.
809
+ It registers with `NODII_DEV_RUN_ID` as its waiter, heartbeats on every poll,
810
+ and ends with the exit code, the remote log tail and, on success, the artifact
811
+ (`--artifact` is a path on the instance, relative to the run directory).
812
+
813
+ The script runs detached on the instance in its own session, from
814
+ `/var/tmp/nodii-dev-runs/RUN_ID`, writing a log and then an exit-code file
815
+ there. Every `--poll-seconds` (default 30), a short SSM command reads the log's
816
+ size and tail and the exit code, and checks whether the process is alive. SSM
817
+ keeps only the first 24,000 characters of a command's output, so the script's
818
+ own invocation can't show progress. The run fails on:
819
+
820
+ - a non-zero exit;
821
+ - a process that is gone with no exit code (killed, or the instance restarted);
822
+ - a missing run directory;
823
+ - no new output, or no answer from the instance, for `--quiet-minutes`
824
+ (default 10).
825
+
826
+ A quiet script is cancelled with its whole process group. A long job should
827
+ print progress. Scripts are limited to 32 KB; copy larger code to the instance
828
+ first.
829
+
794
830
  ## Build lanes and repository setup
795
831
 
796
832
  `coordinator start --machine NAME` registers the machine's one watched
@@ -808,7 +844,10 @@ target agent and machine, fresh account-capacity telemetry, repository grants,
808
844
  lane limit, open issues and (when `--figma` is present) the authenticated Figma
809
845
  account. Only after that passes does the CLI create one worktree per repository,
810
846
  atomically register the lane and watched run, and launch the runtime with the
811
- single-line brief `You are lane KEY. Run nodii-dev lane gather.`. Ordinary
847
+ single-line brief `You are lane KEY. Run nodii-dev lane gather.`. It
848
+ dispatches an agent only onto a started lane: if Dev registers the lane as
849
+ anything but `building` (its start recorded), the CLI writes no marker, starts
850
+ no runtime, records the launch failure, and exits `LANE_NOT_STARTED`. Ordinary
812
851
  notes use `--notes`; a scope-narrowing note uses `--narrowing-note` and must
813
852
  also name the remainder's destination with `--narrowing-remainder`.
814
853
  The launched process uses the already-provisioned lowercase identity (for
@@ -846,6 +885,19 @@ acknowledge a changed Freeze before continuing, and compact SessionStart uses
846
885
  the same backstop. `lane freeze add|drop|swap` makes a reasoned, version-checked
847
886
  change; `show|history` reads the current contract or its append-only history.
848
887
 
888
+ A lane Gather warms the local picker (Gemma) as it starts, alongside Dev's
889
+ requests. Dev's requests share a 60 s deadline. The deadline stops while the
890
+ pick runs: the pick waits for the warm-up (up to 180 s), then the first pick
891
+ has 120 s once the model is warm (`PICKER_TIMEOUT` past it, and the model is
892
+ unloaded). Each pick is logged on this Mac (`~/.cache/nodii-dev/gather-timings.jsonl`,
893
+ or `NODII_DEV_GATHER_TIMINGS`). Gather prints the pick's warm-up and pick time
894
+ with this Mac's p50/p95, and `gather timings` prints the spread.
895
+ If this Mac has no picker (the model isn't installed, or no Ollama answers),
896
+ Gather falls back to the agent picker on the same shortlist. It prints
897
+ `picker: agent (gemma4:12b not installed; run nodii-dev setup)` and the
898
+ shortlist, then the agent records its pick with `--picker agent --shortlist ID
899
+ --documents …`. A wrong model digest or a timeout still fails.
900
+
849
901
  `lane start` creates and starts a lane. It infers the repository (by short
850
902
  name, as Dev compares it), branch and worktree from git unless overridden, and resolves `--issue` keys to the UUIDs
851
903
  required by Dev. Lane keys are passed directly to every lane and Built
package/dist/api.js CHANGED
@@ -162,6 +162,12 @@ export const ALIASES = {
162
162
 
163
163
  // Decisions a person makes in Dev (operator authority): nodii-dev never sends them.
164
164
  export const PERSON_ONLY = new Set(["approveDesignGuidelineChangeSet", "rejectDesignGuidelineChangeSet", "mergeDesignRelease"]);
165
+ // Signed-in staff only: Dev answers an agent with OPERATOR_REQUIRED by design.
166
+ // nodii-dev still sends them, for a person signed in to the CLI.
167
+ export const STAFF_ONLY = new Map([
168
+ ["ratifyAdDocument", "staff only: ratify on the document's Rules record page; an agent gets OPERATOR_REQUIRED"],
169
+ ]);
170
+
165
171
  export function refusePersonOnly(operationId) {
166
172
  if (PERSON_ONLY.has(operationId))
167
173
  throw new CliError("PERSON_ONLY", `${operationId} is a person's decision in Dev; nodii-dev does not send it.`, 2);
package/dist/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
2
3
  import { stdin, stdout } from "node:process";
3
4
  import {
4
5
  CliError,
@@ -53,7 +54,7 @@ import {
53
54
  } from "./figma.js";
54
55
  export { figmaLock, submitMode } from "./figma.js";
55
56
  import { tuneCommand } from "./tune.js";
56
- import { TUNING_MODELS, measurePickerLatency } from "./gather.js";
57
+ import { PICKER_MODEL, TUNING_MODELS, measurePickerLatency, pickTimingStats, pickTimingText, pickTimingsPath, readPickTimings } from "./gather.js";
57
58
  import { docsCommand, rulebookCommand, rulesCommand } from "./upload.js";
58
59
  import { changeSetCommand, guidelineCommand } from "./guidelines.js";
59
60
  import { designCheck, designCheckText, designRoles, designRolesText } from "./design.js";
@@ -70,6 +71,7 @@ import {
70
71
  operationsFrom,
71
72
  parseParams,
72
73
  readBody,
74
+ STAFF_ONLY,
73
75
  } from "./api.js";
74
76
  import {
75
77
  agentWorkflowCommand,
@@ -91,6 +93,7 @@ import {
91
93
  showWaitingResults,
92
94
  superviseRun,
93
95
  } from "./runs.js";
96
+ import { awsCli, superviseRemoteRun } from "./remote.js";
94
97
 
95
98
  const HELP = `nodii-dev ${VERSION}
96
99
 
@@ -148,7 +151,7 @@ Usage: nodii-dev <command> [options] [--json]
148
151
  watchdog install [--interval SECONDS] # install/update the per-Mac launchd liveness sweep (default 60s)
149
152
  watchdog sweep # launchd entrypoint: report dead runs, stale sockets, unpushed worktrees
150
153
  docs check|validate|upload <path...> --class ruling|conformance|editorial
151
- [--base CODE=VERSION] [--decision FILE] [--pending] [--conformance-ref REF]
154
+ [--base CODE=VERSION] [--decision FILE] [--conformance-ref REF]
152
155
  [--meaning-unchanged REASON] [--wait]
153
156
  docs ratify --domain KEY [--domain KEY ...] --decision FILE [--wait]
154
157
  docs parked [CODE] [--status pending|acknowledged]
@@ -169,6 +172,8 @@ Usage: nodii-dev <command> [options] [--json]
169
172
  gather tune finalize <run-id> # Dev scores the full 5-picker x 5-repeat matrix
170
173
  gather picker-latency [--picker gemma-12b|gemma-e4b|gemma-26b]
171
174
  # cold (model unloaded first) and warm pick latency, for the tuning record
175
+ gather timings [--model M] [--last N] # this Mac's lane Gather picks: warm-up and first-pick p50/p95
176
+ # (each lane gather logs one; the first pick has 120 s once warm)
172
177
 
173
178
  coordinator start --machine NAME # register this machine's one watched coordinator; refuses a live second
174
179
  lane launch <key> --done-check TEXT --kind KIND --side SIDE
@@ -189,9 +194,14 @@ Usage: nodii-dev <command> [options] [--json]
189
194
  # recorded trigger shortlist; with --shortlist/--documents it records
190
195
  # the pick (picker: agent) and the Clause map
191
196
  lane gather show [<key>] [--gather ID] # the lane's Clause map (the Freeze references it, not its clauses)
192
- run [--kind build|test_run|subagent|deploy] [--name TEXT] -- <command> [args...]
197
+ run [--kind build|test_run|subagent|deploy] [--name TEXT] [--artifact PATH] -- <command> [args...]
193
198
  # watched command: registers with NODII_DEV_RUN_ID,
194
- # heartbeats, and records exit code + redacted log tail
199
+ # heartbeats, and records exit code + redacted log tail;
200
+ # --artifact attaches that file (text, <= 10,000 chars) to the Result
201
+ run --remote-ssm INSTANCE SCRIPT [--artifact PATH] [--quiet-minutes N] [--poll-seconds N] [--region R]
202
+ # SCRIPT runs detached on the EC2 instance through SSM (your aws CLI);
203
+ # fails on a non-zero exit, a vanished process, or no new output for
204
+ # N min (default 10, then cancelled); --artifact is a path there
195
205
 
196
206
  lane start <key> --title TEXT --kind KIND --side SIDE [--repo REPO] [--issue ISSUE]
197
207
  lane step <key> --side SIDE --start|--done|--block <step> [--reason TEXT]
@@ -220,7 +230,8 @@ Usage: nodii-dev <command> [options] [--json]
220
230
  agent lane-limit|grant|revoke
221
231
  people reveal <id> --reason TEXT | people refresh-names
222
232
 
223
- api list [--tag TAG]
233
+ api list [--tag TAG] # staff-only operations are marked (ratifyAdDocument:
234
+ # agents get OPERATOR_REQUIRED by design)
224
235
  api <operationId> [--param k=v] [--body @file|-|JSON]
225
236
  <resource> <action> [ID ...] [--field VALUE]
226
237
 
@@ -479,6 +490,29 @@ export async function main(argv = process.argv.slice(2)) {
479
490
  return console.log(VERSION);
480
491
  if (argv.length === 1 && ["--help", "-h"].includes(argv[0]))
481
492
  return console.log(HELP);
493
+ if (argv[0] === "run" && argv.some((x) => x === "--remote-ssm" || x.startsWith("--remote-ssm="))) {
494
+ const { args: scriptArgs, flags: remoteFlags } = parseArgs(argv.slice(1));
495
+ const single = (flag) => {
496
+ const value = remoteFlags[flag];
497
+ if (Array.isArray(value) || value === true) throw new CliError("USAGE", `run --${flag} needs one value.`, 2);
498
+ return value;
499
+ };
500
+ if (scriptArgs.length !== 1)
501
+ throw new CliError("USAGE", "run --remote-ssm INSTANCE SCRIPT [--artifact PATH] [--quiet-minutes N] [--poll-seconds N] [--region R] [--kind K] [--name TEXT]", 2);
502
+ const number = (flag) => (single(flag) === undefined ? undefined : Number(single(flag)));
503
+ const result = await superviseRemoteRun({
504
+ instance: single("remote-ssm"),
505
+ script: scriptArgs[0],
506
+ artifact: single("artifact"),
507
+ quietMinutes: number("quiet-minutes"),
508
+ pollSeconds: number("poll-seconds"),
509
+ kind: single("kind"),
510
+ name: single("name"),
511
+ aws: awsCli({ region: single("region") }),
512
+ });
513
+ process.exitCode = result.exitCode;
514
+ return;
515
+ }
482
516
  if (["run", "__run-supervisor"].includes(argv[0])) {
483
517
  const boundary = argv.indexOf("--");
484
518
  if (boundary === -1 || boundary === argv.length - 1)
@@ -494,6 +528,9 @@ export async function main(argv = process.argv.slice(2)) {
494
528
  );
495
529
  const kind = runFlags.kind;
496
530
  const name = runFlags.name;
531
+ const artifact = runFlags.artifact;
532
+ if (Array.isArray(artifact) || artifact === true)
533
+ throw new CliError("USAGE", "run --artifact needs one path.", 2);
497
534
  if (Array.isArray(kind) || kind === true)
498
535
  throw new CliError("USAGE", "run --kind needs one value.", 2);
499
536
  if (Array.isArray(name) || name === true)
@@ -506,7 +543,7 @@ export async function main(argv = process.argv.slice(2)) {
506
543
  stdout.on("error", ignoreBrokenPipe);
507
544
  process.stderr.on("error", ignoreBrokenPipe);
508
545
  try {
509
- const result = await superviseRun({ command, kind, name });
546
+ const result = await superviseRun({ command, kind, name, artifact });
510
547
  process.exitCode = result.exitCode;
511
548
  return;
512
549
  } finally {
@@ -514,7 +551,7 @@ export async function main(argv = process.argv.slice(2)) {
514
551
  process.stderr.off("error", ignoreBrokenPipe);
515
552
  }
516
553
  }
517
- const exitCode = await launchRunSupervisor(command, { kind, name });
554
+ const exitCode = await launchRunSupervisor(command, { kind, name, artifact: artifact && resolve(artifact) });
518
555
  process.exitCode = exitCode;
519
556
  return;
520
557
  }
@@ -528,8 +565,8 @@ export async function main(argv = process.argv.slice(2)) {
528
565
  const selected = f.tag === undefined || f.tag === true
529
566
  ? operations
530
567
  : operations.filter((x) => x.tags.some((t) => t.toLowerCase() === String(f.tag).toLowerCase()));
531
- return emit(selected.map(({ operationId, method, path, tags }) => ({ operationId, method, path, tags })), json,
532
- (rows) => rows.map((x) => `${x.operationId}\t${x.method}\t${x.path}\t${x.tags.join(",")}`).join("\n"));
568
+ return emit(selected.map(({ operationId, method, path, tags }) => ({ operationId, method, path, tags, ...(STAFF_ONLY.has(operationId) ? { staffOnly: true } : {}) })), json,
569
+ (rows) => rows.map((x) => `${x.operationId}\t${x.method}\t${x.path}\t${x.tags.join(",")}${x.staffOnly ? `\t# ${STAFF_ONLY.get(x.operationId)}` : ""}`).join("\n"));
533
570
  }
534
571
  if (!action) throw new CliError("USAGE", "api needs an operationId or list", 2);
535
572
  const entry = operations.find((x) => x.operationId === action);
@@ -599,6 +636,15 @@ export async function main(argv = process.argv.slice(2)) {
599
636
  const line = (label, x) => `${label} ${x.wallMs} ms (Ollama total ${x.totalMs ?? "?"} ms, model load ${x.loadMs ?? "?"} ms)`;
600
637
  return emit(result, json, (r) => `${r.model}\n${line("cold", r.cold)}\n${line("warm", r.warm)}`);
601
638
  }
639
+ if (command === "gather" && action === "timings") {
640
+ const last = f.last === undefined ? 50 : Number(f.last);
641
+ if (!Number.isInteger(last) || last < 1) throw new CliError("USAGE", "gather timings [--model M] [--last N]", 2);
642
+ const model = typeof f.model === "string" ? f.model : PICKER_MODEL;
643
+ const path = pickTimingsPath();
644
+ const entries = await readPickTimings({ path, model, last });
645
+ const stats = pickTimingStats(entries);
646
+ return emit({ model, path, ...stats, entries }, json, () => pickTimingText(stats, { model, path }));
647
+ }
602
648
  if (command === "gather" && action === "tune") {
603
649
  const result = await tuneCommand(rest[0], rest.slice(1), f);
604
650
  emit(result.payload, json, result.human);
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.35";
7
+ export const VERSION = "0.6.36";
8
8
  export const configPath = () =>
9
9
  process.env.NODII_DEV_CONFIG_PATH ??
10
10
  join(homedir(), ".config/nodii-dev/config.json");