Skip to content

1.2 — RunsManager: multi-run engine keyed on the append-only runs/index - #70

Merged
jack-champagne merged 4 commits into
mainfrom
rchari/57-runs-manager
Jul 4, 2026
Merged

1.2 — RunsManager: multi-run engine keyed on the append-only runs/index#70
jack-champagne merged 4 commits into
mainfrom
rchari/57-runs-manager

Conversation

@Rchari1

@Rchari1 Rchari1 commented Jul 3, 2026

Copy link
Copy Markdown
Member

Closes #57 (Phase 1.2, plan §3). β built the single-run slice; this is the multi-run evolution.

What changes

Discovery: runs/index replaces the latest symlink as the multi-run source of truth. The manager tails the append-only index (amico-run's appendIndex TSV) with the same LogTailer used for run.log; latest keeps being written (frozen contract) but is no longer followed. The β failure this fixes: a second concurrent solve swung latest and yanked tracking off the first run mid-flight — now every run without a FINISHED gets its own pipeline (replay → run-dir watch → log tail) and is tracked to completion.

Fan-out & selection

  • Per-run events land in the pure RunRegistry (state); the selected run drives the single-run Inspector + StatusBar. Selection auto-follows the newest started run (β latest-follow parity); selectRun(runId) is 1.3's seam for per-run views.
  • Completions and the promote-once prompt fire for every run, selected or not — a background solve converging still prompts.
  • Completion keys on FINISHED (never result.toml presence) — pinned by test.
  • Per-run PulseStream/SinkDedup (Render the live pulse client-side in the Run Inspector (pulseplot v1) #66 routing preserved: live tail is the meta carrier; replay-seeded meta arms the stream; selection replays are newest-record-only via ingestRunDir).
  • Poll backstop + idempotent consumers, as before (double-delivery between a selection replay and a live tail is tolerated by design).

Scheduler seam (1.1, #56/#68)

attachScheduler consumes the lifecycle stream — started registers + selects the run immediately (beats the index tail; also the only path for a non-default runsRoot). Typed structurally (SchedulerLike = #68's onEvent surface), so this PR is independent of #68's merge order and wires up unchanged once it lands.

Demo replay

A run that is FINISHED at discovery registers quietly (β's idle-at-launch parity) — so replayDemo now renders via explicit selection (pokeDiscovery() + selectRun()), with promote suppressed. Pinned by test.

Files

  • run_registry.ts (new, pure): index grammar + registry — vscode-free, unit-tested.
  • log_tailer.ts (new): LogTailer extracted verbatim from file_watcher.ts.
  • runs_manager.ts (new): the engine.
  • file_watcher.ts deleted (superseded); its state-machine tests ported to runs_manager.test.ts, all four preserved (idle-on-finished, warming→completion, Render the live pulse client-side in the Run Inspector (pulseplot v1) #66 tail routing, replay-seeded meta).
  • extension.ts: swap wiring; replayDemo explicit-selection.

Tests

15 new (6 registry + 9 manager: the 4 ported + concurrent-runs/background-completion+promote, re-select-no-promote-re-pop, missing-dir tolerance, Scheduler seam, demo path). Repo green: extension 103 · amico-run 47 · schema 34; typecheck + build clean.

Feeds

1.3 (#58) keys per-run inspector panes on selectRun/the registry; 1.4a (#61) exercises Scheduler → RunsManager → Inspector end-to-end.

🤖 Generated with Claude Code

Rchari1 added a commit that referenced this pull request Jul 3, 2026
The three test gaps the RunsManager review flagged:
- RunRegistry.backfill: fills ONLY missing metadata (first-registration wins),
  no-throw on unknown runId — mutation-verified (dropping the missing-only
  guard fails it).
- RunRegistry.all(): returns copies — a mutated snapshot can't corrupt registry
  state.
- cross-run PULSE routing: a background run's pulse RECORD is gated on selection
  (not just iter) — never reaches the inspector while another run is selected.
- stale-warming: selecting a run whose FINISHED landed inside the ≤700ms poll
  window shows completion, NOT warming (no terminal-badge inversion) —
  mutation-verified (reverting the disk re-check fails it).

extension 111 tests (+8), repo green (amico-run 47, schema 34); typecheck clean.
@Rchari1
Rchari1 marked this pull request as draft July 3, 2026 06:39
@Rchari1

Rchari1 commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

Status: drafted temporarily alongside #68/#72 while we close out a hands-on finding from live testing. The finding was environmental (stale lab Julia env — DirectTrajOpt 0.9.6 installed vs pinned 0.9.7 — crashed solves before pulse emission; full detail in the #72 comment). No defect found in the RunsManager during the live pass: index-driven discovery, per-run tracking, and completion registration all behaved. Env fix applied + verified; will mark ready after a fresh end-to-end pass.

@Rchari1

Rchari1 commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

✅ Ready again. End-to-end verification green after the env fix (detail on #72): live solve discovered via the index tail, tracked to completion, FINISHED-keyed, fidelity registered. No changes needed in this PR.

@jack-champagne

Copy link
Copy Markdown
Member

Context note (run-dir contract / #64) — folding into the full review.

Traced how this stack relates to the coming pre-solve formulation.toml + system÷formulation / hashing work (#64). Of the four spine PRs, #70 is the one contract-aware layer, and it's well-positioned:

Two things to confirm as part of the review (non-blocking):

  1. Validation delegates to @amicode/amico-run (validateManifest/Finished/Result) — so a future validateFormulation rides the SchemaPackage vNext bump. Expected Run-dir hashing / provenance — content-addressable identity (additive contract extension) #64 coupling; just flagging the dependency direction so it's designed in, not bolted on.
  2. RunCompletion is deliberately minimal (runId / runDir / status / fidelity). The rich problem-identity (system/gate) does not flow through the RunsManager — the catalog card hydrator (feat: catalog entry card — components, save-to-catalog flow, session catalog (UX2) #73) reads the run-dir files directly. Is minimal intended? If the runs list/tree ever wants to show gate/system per row, this payload grows — worth a one-line answer so we either design it in or consciously defer now.

Stack posture, for reference: #68 (Scheduler) and #72 (Inspector — protocol-only, zero file reads) are correctly contract-blind, so they need no changes when #64 lands. #78's corpus is the producer, so that's where we'll add "emit + assert formulation.toml" to keep the e2e guard honest — the fixtures already carry the formulation fields (delta, T, gate, system) in comments.

@Rchari1

Rchari1 commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

Thanks for tracing this against #64 — both confirmed:

  1. Yes, deliberate. All contract validation in this layer goes through @amicode/amico-run's validateFinished/validateResult (single site: readTerminal in runs_manager.ts; the pure reader ingestRunDir likewise never hand-rolls schema checks). A future validateFormulation lands in amico-run with the SchemaPackage bump and 1.2 — RunsManager: multi-run engine keyed on the append-only runs/index #70 consumes it as one more read + one more delegate call — dependency direction is schema → amico-run → extension by design.

  2. Minimal is intended — consciously deferred, and the seam is the registry, not the event. RunCompletion is the terminal signal (what changed), not the run record. If/when a runs tree wants gate/system per row, the right growth point is RunRecord (the registry already has the backfill pattern for exactly this: fill-missing-only metadata hydrated from run-dir reads), or render-time hydration the way the catalog card (feat: catalog entry card — components, save-to-catalog flow, session catalog (UX2) #73) does — not a fatter completion event fanning through every consumer. So: defer now, grow RunRecord when a consumer exists.

And agreed on the #78 corpus being the producer-side guard — the emitter's directive grammar makes "emit + assert formulation.toml" a small additive change when #64 lands (the fixtures already carry the fields in their parameter blocks for exactly that reason).

@Rchari1
Rchari1 marked this pull request as ready for review July 4, 2026 00:39

@jack-champagne jack-champagne left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static/design pass over the multi-run engine (read runs_manager.ts + run_registry.ts + the ingestRunDir reader end to end). Nothing here contests the live verification — index-driven discovery, per-run tracking, and FINISHED-keyed completion are sound and you've confirmed them end to end. Findings are edge-case robustness + one design seam:

  • #1 (auto-follow, :269) is the one worth settling before #72 — it's a design decision #72 inherits, not a 1.2 bug. Inline.
  • The rest are narrow-trigger or cleanup: torn-FINISHED retry asymmetry at discovery (:224), a latent stale-fidelity gap in markFinished, double-ingest on discovery, and the readTerminal/ingestRunDir duplication. All non-blocking.
  • Checked the "index line lands before the run dir exists" case (the :216 skip) — non-issue: amico-run writes the run dir + manifest before appending the index line (local_executor.ts:52-60, "manifest FIRST"), so that skip can't fire for the shipping executor. Worth preserving that ordering if another executor is ever added.

No merge-blocker — I'd just want #1 answered before this becomes #72's base.

Comment thread packages/extension/src/runs_manager.ts Outdated

// Auto-follow: a newly REGISTERED live run is by definition the newest
// start (index lines append in creation order) — β latest-follow parity.
this.selectRun(runId);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[direction] registerRun ends with an unconditional this.selectRun(runId), and selectRun only bails when the run is already selected (:171) — so any newly-registered live run auto-steals selection. Fine for 1.2 (no user selection yet; only visible if a real solve's index line lands in the same ~700ms tick as the demo's explicit selectRun). But this is the seam #72 builds on: once users can pick a run, selected is written by two intents and auto-follow always wins — a background solve starting yanks the view off the run the user deliberately opened, the exact bug class this PR fixes, reintroduced at the selection layer.

Ask: add a "pinned" notion now (auto-follow only when nothing is explicitly selected, or only the first live run) so #72 isn't retrofitting sticky-selection onto a latest-follow mechanism. Not a 1.2 blocker — cheaper to settle here than in #72.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Settled here (5353251), designed as you framed it: explicit selectRun PINS the selection (demo command today, 1.3's user clicks tomorrow); auto-follow — a newly-registered live run taking the view — only applies while nothing is pinned. selected is still written by two intents, but the explicit intent now always wins; a background solve registering can no longer steal the view. β latest-follow parity is preserved for the never-clicked case (pin starts false). Mutation-verified: forcing follow = true reds the pin test. #72 rebased on top — its activate() seam inherits sticky selection instead of retrofitting it.

if (finishedAtDiscovery) {
// Terminal at discovery: record it (status/fidelity for the registry) but
// render nothing and never re-pop the promote prompt (β launch parity).
const t = this.readTerminal(runDir);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] The finished-at-discovery branch calls readTerminal once and registers with no pipeline. If readTerminal returns undefined (FINISHED present but torn/invalid), the run sticks at status:undefined forever — nothing revisits it. The live path handles exactly this: checkFinished bails with if (!t) return; // torn/invalid FINISHED — next tick retries (:327).

Trigger is narrow (a torn FINISHED caught during launch replay of an already-finished run) and invisible today (nothing reads status until 1.3), so not a blocker — but mirror the live-path retry: don't finalize when readTerminal is incomplete, let a later pass re-read. (The promote suppression here is intended β launch-parity, not part of this.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed (5353251): the finished-at-discovery branch now finalizes only when readTerminal returns whole. A torn/invalid FINISHED falls through to the live path — pipeline attaches, and checkFinished's existing next-tick retry owns it (one retry mechanism, not two). Promote stays suppressed (terminal-at-discovery is a launch replay regardless of the torn write), and no "warming" either — the warming check is disk-keyed and FINISHED exists. Test stages a mid-write FINISHED (status = "comp), asserts phase stays live with no undefined-status record, then completes it and asserts status+fidelity land with promote still suppressed.

if (r.latestIter === undefined || iter > r.latestIter) r.latestIter = iter;
}

markFinished(runId: string, status: RunStatus, fidelity?: number): void {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] markFinished has no phase guard and overwrites fidelity only if defined (:92) — re-marking ("failed") after ("completed", 0.999) leaves status:"failed" with a stale fidelity:0.999. Safe today: the sole caller completeRun guards rec.phase === "finished" (runs_manager.ts:336). But this is a public registry method the 1.3 consumers will touch. Ask: guard phase inside markFinished too, or document that callers must.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guarded inside markFinished itself (5353251): if (!r || r.phase === "finished") return — first terminal wins, so a stray re-mark can't leave status:"failed" beside a stale fidelity. completeRun's own guard stays (it also gates teardown/logging), but the registry no longer relies on callers being polite — it's public surface for the 1.3 consumers, as you said. Unit test pins the re-mark no-op.

// to the inspector only if this run is (still) selected — at registration
// it never is; the display replay below covers it.
let logBytes = 0;
try { logBytes = ingestRunDir(runDir, this.pipelineSink(p), this.opts.promoteThreshold ?? 0.99); }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] Newly-discovered live runs get ingested twice: this pipeline replay (full run.log parse) then the fall-through selectRun → display replay (:178) re-parses the whole run.log again — two full reads back-to-back per run, scaling with log size × runs drained in one tick. The pipeline replay's only durable outputs are the log byte-offset and the armed pulse-meta. Worth collapsing to one pass, or having the display replay reuse the pipeline's offset.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Collapsed to one pass (5353251): the auto-follow decision moves before the registration replay, so the single pipelineSink ingest both seeds state (offset, armed meta, iter high-water) and feeds the display — displaySink now backs only explicit selection replays. Test asserts displaySink is never constructed during discovery while the replayed history still reaches the inspector. (On the rebased #72 the same single pass fans runId-tagged into the run's pane, and selectRun skips re-ingest entirely for runs with a live pipeline.)

/** FINISHED (+ result.toml fidelity) with the same validation + say-why
* logging as β (S4: a present-but-invalid result.toml is named, not
* silently dropped). */
private readTerminal(runDir: string): { status: RunStatus; fidelity?: number } | undefined {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] readTerminal re-implements the FINISHED-validate → status → result.toml-validate → fidelity → say-why sequence that ingestRunDir already runs for the live path. Schema delegation is clean on both sides (validateFinished/validateResult, as you noted on the #64 thread) — it's the orchestration that's duplicated, so a contract change (new FINISHED field, the coming formulation.toml) has to be edited in both or finished-at-discovery diverges from live-completed. Worth folding this onto the reader (e.g. a terminal-only mode of ingestRunDir).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Folded onto the reader (5353251): new readTerminalState(runDir, onInvalidResult?) in run_dir_reader.ts owns the FINISHED-validate → status → result.toml-validate → fidelity → say-why sequence ONCE; ingestRunDir and the manager's readTerminal both delegate (the manager passes its channel as the say-why callback, so S4 logging is unchanged on both paths). A FINISHED-field change or #64's formulation.toml is now a one-place edit — and this is exactly where validateFormulation will slot in.

@Rchari1
Rchari1 force-pushed the rchari/57-runs-manager branch from 6831d01 to f303949 Compare July 4, 2026 04:07
Rchari1 added a commit that referenced this pull request Jul 4, 2026
The three test gaps the RunsManager review flagged:
- RunRegistry.backfill: fills ONLY missing metadata (first-registration wins),
  no-throw on unknown runId — mutation-verified (dropping the missing-only
  guard fails it).
- RunRegistry.all(): returns copies — a mutated snapshot can't corrupt registry
  state.
- cross-run PULSE routing: a background run's pulse RECORD is gated on selection
  (not just iter) — never reaches the inspector while another run is selected.
- stale-warming: selecting a run whose FINISHED landed inside the ≤700ms poll
  window shows completion, NOT warming (no terminal-badge inversion) —
  mutation-verified (reverting the disk re-check fails it).

extension 111 tests (+8), repo green (amico-run 47, schema 34); typecheck clean.
Rchari1 added a commit that referenced this pull request Jul 4, 2026
…ished guard, single-pass discovery, one terminal orchestration

Addresses jack-champagne's static/design pass, one commit per nothing — all
five findings land together because #1/#4 reshape the same registerRun path:

#1 (design, the #72 seam): explicit selectRun PINS the selection; auto-follow
   (newest registered live run, β latest-follow parity) only applies while
   nothing is pinned. A background solve starting can no longer yank the view
   off a run the user deliberately opened. Mutation-verified.

#2: a FINISHED that is present but torn/invalid at discovery no longer
   finalizes as status:undefined-forever — the run falls through to the live
   path, whose checkFinished re-reads next tick (the retry the live lane
   already had). Promote stays suppressed (launch replay). No warming either
   (disk-checked: FINISHED exists).

#3: RunRegistry.markFinished guards phase itself (first terminal wins) — a
   stray re-mark can't leave status:"failed" beside a stale fidelity. The
   guard now lives on the public surface, not only in completeRun.

#4: discovery ingests the run dir ONCE. Auto-follow assigns selection BEFORE
   the registration replay, so the single pipelineSink pass both seeds state
   and feeds the display through routeIter/routePulse's selection gate —
   displaySink now backs only explicit selection replays.

#5: the FINISHED→status→result.toml→fidelity orchestration lives ONCE, in
   run_dir_reader.readTerminalState (say-why callback preserved via the
   manager's channel); ingestRunDir and RunsManager.readTerminal both
   delegate, so a contract change (e.g. #64's formulation.toml) is edited in
   one place.

Also rebased onto main (#68 Scheduler, #77 vsix-gate, #79 permission grant).
118 extension tests pass; typecheck + build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rchari1 added a commit that referenced this pull request Jul 4, 2026
…er → executor → run-dir → RunsManager → inspector (#61)

Two corpus fixtures (transmon X, cavity displacement — distinct telemetry
profiles: 2×8 vs 1×6, 4 vs 3 iters) + test/corpus/fake-julia, a node stand-in
the executor spawns exactly like julia (last-argv script, cwd=runDir). It reads
each fixture's AMICODE_SMOKE directive and emits the template's telemetry
grammar (PULSE_META / ITER / PULSE → run.log via the executor's tail) with a
small inter-iter delay so the LIVE tail path is exercised, then writes a
schema-conformant result.toml. Zero Julia/Piccolo cost: full chain in ~0.5s.

The end-to-end test pins what the unit suites can't — that the pieces AGREE:
- Scheduler (#56) lifecycle is strictly serial (B starts only after A's
  finished event) and satisfies RunsManager's structural seam;
- the executor's run-dir writes (run.toml/index/run.log/result.toml/FINISHED)
  are exactly what the manager's tailer/registry read back (fidelity + iter
  high-water land in the registry);
- telemetry reaches the inspector runId-keyed per run with no cross-tagging
  (asserted by per-run record dims, completion fidelity, iter records).

Runs in the regular vitest suite → already in CI's fast job; #62 promotes it to
a named required gate. Mutation-verified: a wrong result.toml fidelity reds the
registry + completion assertions.

Branch note: contains merge of rchari/56-scheduler (the corpus drives the real
Scheduler); diff collapses once #68/#70/#72 land.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rchari1 and others added 4 commits July 4, 2026 00:15
…ns/index (#57)

Replaces β's single-run RunsRootWatcher. Discovery now tails `runs/index`
(amico-run's appendIndex TSV) instead of following the `latest` symlink —
`latest` keeps being written (frozen contract) but a second concurrent solve no
longer yanks tracking off the first mid-flight: every run WITHOUT a FINISHED
gets its own pipeline (replay → run-dir watch → run.log tail) and is tracked to
completion.

- run_registry.ts (pure, vscode-free): parseIndexLine (tolerant of torn/blank
  lines — the tail heals) + RunRegistry (idempotent by runId; iter high-water;
  FINISHED-keyed terminal state).
- log_tailer.ts: LogTailer extracted verbatim from file_watcher.ts — reused for
  every run.log AND the index (both append-only).
- runs_manager.ts: per-run pipelines with per-run PulseStream/SinkDedup;
  runId-gated routing — the SELECTED run drives the single-run Inspector +
  StatusBar (selection auto-follows the newest started run, β latest-follow
  parity; `selectRun` is 1.3's seam), while completions + the promote-once
  prompt fire for EVERY run, selected or not. Completion keys on FINISHED
  (never result.toml presence). Poll backstop + idempotent consumers as before.
  attachScheduler consumes the #56 lifecycle (structural SchedulerLike so this
  is independent of #68's merge): `started` registers + selects immediately.
- extension.ts: RunsManager replaces the watcher; replayDemo now renders via
  EXPLICIT selection (pokeDiscovery + selectRun) since a finished-at-discovery
  run registers quietly (idle-at-launch parity) — promote stays suppressed.
- file_watcher.ts deleted (superseded); its statemachine tests ported to
  runs_manager.test.ts (idle-on-finished, warming→FINISHED-keyed completion,
  #66 pulse tail routing, replay-seeded meta) + new multi-run coverage:
  concurrent runs both tracked with background completion/promote, re-select
  replay without promote re-pop, missing-dir index lines tolerated, the
  Scheduler seam, and the demo-replay explicit-selection path.

15 new tests; repo green (extension 103, amico-run 47, schema 34); typecheck +
build clean. Closes #57.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ata backfill, watch guards

Adversarial review (0 must-fix, 2 should-fix) — applied:
- selectRun re-checks DISK for FINISHED before posting warming (β parity):
  registry phase can be ≤700ms stale, and warming-after-completion inverted the
  terminal badge — real once 1.3's user-driven selectRun lands.
- RunRegistry.backfill: a scheduler-registered run (runId+runDir only) gains
  createdAt/scriptPath when its index line lands — the 1.3 trees would
  otherwise see undefined metadata on every scheduler-launched run.
- fs.watch 'error' listeners on all three watcher sites (root, per-run dir,
  tailer) — an unhandled FSWatcher error is an uncaught host exception; the
  poll backstop keeps things live.
- RunRegistry.all() returns copies (1.3 callers can't mutate registry state);
  honest header comment on the transient replay/tail re-delivery window.
- test/log_tailer.test.ts (review gap — the tailer is now load-bearing for
  discovery): torn-line carry-over, truncation re-read, startOffset contract,
  poke self-attach. 19 tests green, typecheck clean.

Still owed next session (review nits, test-only): cross-run PULSE routing gate
test, backfill unit test, stale-warming pin test; inspector pendingPulse reset
on selection switch is deferred to 1.3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The three test gaps the RunsManager review flagged:
- RunRegistry.backfill: fills ONLY missing metadata (first-registration wins),
  no-throw on unknown runId — mutation-verified (dropping the missing-only
  guard fails it).
- RunRegistry.all(): returns copies — a mutated snapshot can't corrupt registry
  state.
- cross-run PULSE routing: a background run's pulse RECORD is gated on selection
  (not just iter) — never reaches the inspector while another run is selected.
- stale-warming: selecting a run whose FINISHED landed inside the ≤700ms poll
  window shows completion, NOT warming (no terminal-badge inversion) —
  mutation-verified (reverting the disk re-check fails it).

extension 111 tests (+8), repo green (amico-run 47, schema 34); typecheck clean.
…ished guard, single-pass discovery, one terminal orchestration

Addresses jack-champagne's static/design pass, one commit per nothing — all
five findings land together because #1/#4 reshape the same registerRun path:

#1 (design, the #72 seam): explicit selectRun PINS the selection; auto-follow
   (newest registered live run, β latest-follow parity) only applies while
   nothing is pinned. A background solve starting can no longer yank the view
   off a run the user deliberately opened. Mutation-verified.

#2: a FINISHED that is present but torn/invalid at discovery no longer
   finalizes as status:undefined-forever — the run falls through to the live
   path, whose checkFinished re-reads next tick (the retry the live lane
   already had). Promote stays suppressed (launch replay). No warming either
   (disk-checked: FINISHED exists).

#3: RunRegistry.markFinished guards phase itself (first terminal wins) — a
   stray re-mark can't leave status:"failed" beside a stale fidelity. The
   guard now lives on the public surface, not only in completeRun.

#4: discovery ingests the run dir ONCE. Auto-follow assigns selection BEFORE
   the registration replay, so the single pipelineSink pass both seeds state
   and feeds the display through routeIter/routePulse's selection gate —
   displaySink now backs only explicit selection replays.

#5: the FINISHED→status→result.toml→fidelity orchestration lives ONCE, in
   run_dir_reader.readTerminalState (say-why callback preserved via the
   manager's channel); ingestRunDir and RunsManager.readTerminal both
   delegate, so a contract change (e.g. #64's formulation.toml) is edited in
   one place.

Also rebased onto main (#68 Scheduler, #77 vsix-gate, #79 permission grant).
118 extension tests pass; typecheck + build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Rchari1
Rchari1 force-pushed the rchari/57-runs-manager branch from f303949 to 5353251 Compare July 4, 2026 04:16
Rchari1 added a commit that referenced this pull request Jul 4, 2026
…er → executor → run-dir → RunsManager → inspector (#61)

Two corpus fixtures (transmon X, cavity displacement — distinct telemetry
profiles: 2×8 vs 1×6, 4 vs 3 iters) + test/corpus/fake-julia, a node stand-in
the executor spawns exactly like julia (last-argv script, cwd=runDir). It reads
each fixture's AMICODE_SMOKE directive and emits the template's telemetry
grammar (PULSE_META / ITER / PULSE → run.log via the executor's tail) with a
small inter-iter delay so the LIVE tail path is exercised, then writes a
schema-conformant result.toml. Zero Julia/Piccolo cost: full chain in ~0.5s.

The end-to-end test pins what the unit suites can't — that the pieces AGREE:
- Scheduler (#56) lifecycle is strictly serial (B starts only after A's
  finished event) and satisfies RunsManager's structural seam;
- the executor's run-dir writes (run.toml/index/run.log/result.toml/FINISHED)
  are exactly what the manager's tailer/registry read back (fidelity + iter
  high-water land in the registry);
- telemetry reaches the inspector runId-keyed per run with no cross-tagging
  (asserted by per-run record dims, completion fidelity, iter records).

Runs in the regular vitest suite → already in CI's fast job; #62 promotes it to
a named required gate. Mutation-verified: a wrong result.toml fidelity reds the
registry + completion assertions.

Branch note: contains merge of rchari/56-scheduler (the corpus drives the real
Scheduler); diff collapses once #68/#70/#72 land.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Rchari1

Rchari1 commented Jul 4, 2026

Copy link
Copy Markdown
Member Author

All five findings addressed in 5353251 (inline replies on each thread), plus the stack is rebased onto current main (#68 Scheduler, #77 vsix-gate, #79 permission grant, and #73 — whose catalog.save promote wiring I ported from the deleted file_watcher.ts into promptPromote, so Kate's #47 flow survives the supersede).

Summary of the changes:

  1. Pinned selection (the 1.3: Run Inspector single→multi-run (runId-keyed protocol, per-run panes) #72 seam): explicit selectRun pins; auto-follow defers while pinned. Mutation-verified.
  2. Torn FINISHED at discovery → falls through to the live path; checkFinished's next-tick retry owns it (one retry mechanism). Promote stays suppressed.
  3. markFinished guards phase itself — first terminal wins on the public registry surface.
  4. Single-pass discovery — auto-follow decides before the replay; one ingestRunDir per registration seeds state and feeds the display; displaySink backs only explicit selections.
  5. One terminal orchestration — new readTerminalState in the reader; ingestRunDir + the manager both delegate (say-why via channel preserved). Run-dir hashing / provenance — content-addressable identity (additive contract extension) #64's formulation.toml will be a one-place edit.

#72 and #78 are rebased on top (the pin composes with 1.3's fan-out: registration fans runId-tagged into the run's pane; selectRun skips re-ingest for runs with a live pipeline and uses same-tick checkFinished for the FINISHED-in-poll-window case). Full suites green up the stack: 125 (#70) → 128 (#72) → 136 (#78).

@jack-champagne

Copy link
Copy Markdown
Member

Re-verified against 5353251 (+ fde5e64, 0c98acd) — thanks, this covers most of it:

One still open — #3: a run whose dir isn't on disk yet at its index-line tick is still — skipped (runs_manager.ts:229) with no retry, unlike the finished case that just got one. Is that intentional (amico-run always creates the run dir before appending the index line, so it can't happen), or should it get the same next-tick retry? A one-line confirmation and I'll close it out.

@jack-champagne
jack-champagne merged commit 84cbc73 into main Jul 4, 2026
5 checks passed
@jack-champagne
jack-champagne deleted the rchari/57-runs-manager branch July 4, 2026 04:53
@jack-champagne
jack-champagne restored the rchari/57-runs-manager branch July 4, 2026 05:29
@jack-champagne
jack-champagne deleted the rchari/57-runs-manager branch July 4, 2026 05:29
Rchari1 added a commit that referenced this pull request Jul 4, 2026
…er → executor → run-dir → RunsManager → inspector (#61)

Two corpus fixtures (transmon X, cavity displacement — distinct telemetry
profiles: 2×8 vs 1×6, 4 vs 3 iters) + test/corpus/fake-julia, a node stand-in
the executor spawns exactly like julia (last-argv script, cwd=runDir). It reads
each fixture's AMICODE_SMOKE directive and emits the template's telemetry
grammar (PULSE_META / ITER / PULSE → run.log via the executor's tail) with a
small inter-iter delay so the LIVE tail path is exercised, then writes a
schema-conformant result.toml. Zero Julia/Piccolo cost: full chain in ~0.5s.

The end-to-end test pins what the unit suites can't — that the pieces AGREE:
- Scheduler (#56) lifecycle is strictly serial (B starts only after A's
  finished event) and satisfies RunsManager's structural seam;
- the executor's run-dir writes (run.toml/index/run.log/result.toml/FINISHED)
  are exactly what the manager's tailer/registry read back (fidelity + iter
  high-water land in the registry);
- telemetry reaches the inspector runId-keyed per run with no cross-tagging
  (asserted by per-run record dims, completion fidelity, iter records).

Runs in the regular vitest suite → already in CI's fast job; #62 promotes it to
a named required gate. Mutation-verified: a wrong result.toml fidelity reds the
registry + completion assertions.

Branch note: contains merge of rchari/56-scheduler (the corpus drives the real
Scheduler); diff collapses once #68/#70/#72 land.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jack-champagne pushed a commit that referenced this pull request Jul 6, 2026
…2e (#78)

* feat(1.3): Run Inspector single→multi-run (runId-keyed protocol, per-run panes)

Freeze-2 reshape (#58): the host↔webview message protocol is now runId-keyed
and both the host and the webview fan into per-run panes. Single→multi only —
the pane markup stays the current pulseplot (design lane, UX4 #49).

Host (run_inspector.ts): a PaneBuffer per runId + activeRunId; runId-keyed
surface postPulse/postIterationRecord/postCompletion/setWarmingUp/setRunLabel +
new activate(runId). Per-run 5 Hz pulse throttle. resolveWebviewView replays
EVERY pane from its buffer (S36) with positional ordering, then posts activate
last. setWarmingUp guarded from clobbering a pane that already has data/terminal
state. pulse stays plot-only (deliberately does not clear warming).

Webview (media/ui/views/inspector.ts): createPanel() instances the former
single-run view per runId (no shared globals); a router keys panels by runId,
activate toggles the one visible pane, background/late messages only touch their
own pane. Pane-hiding uses two-class selectors so it wins over layout.css
`.stack` on specificity, not stylesheet order.

RunsManager (runs_manager.ts): fans every run's live events into the inspector
runId-tagged (routeIter/routePulse ungated); registration replay is state-only
so the selected run never double-posts; selectRun adds activate; the single
status bar stays selection-gated; completion + promote still fire per-run.

Tests: runs_manager + inspector_view_contract updated to the runId-keyed API and
fan-out semantics; added per-run isolation, per-run-throttle independence, S36
reopen, activate-last, warming-guard. New happy-dom webview test covers the
router itself (per-run isolation, activate toggle, empty-state, plot-only pulse)
— closes the coverage gap flagged in adversarial review. All invariants
mutation-verified. 121 tests pass; typecheck + build clean. S6 (formulation
preview) deferred — no formulation-emit in the frozen contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(1.3): flow RunCompletion whole through completeRun — the #84/#81 seam

Jack's #72 merge-seam heads-up: #81 adds `formulation?` to RunCompletion, and
completeRun was the third completion path cherry-picking fields positionally
(runId/status/fidelity) — once #81 landed, live-completed runs would carry
formulation: undefined while replayed runs got it (the exact bug Kate caught
on onFinished, reintroduced here).

completeRun now takes the WHOLE RunCompletion; both feeders (ingestRunDir's
sink verbatim, checkFinished via {runId, runDir, ...readTerminalState()})
funnel the object from the one shared read. An additive field is now a
one-place edit (RunCompletion + readTerminalState) and reaches every consumer
by construction — consumers cherry-pick at the leaf. Documented as the #84
funnel on both the type and completeRun; the full N-reader consolidation
(catalog hydrator etc.) stays #84.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(1.4a): smoke corpus — seconds-scale end-to-end fixtures, Scheduler → executor → run-dir → RunsManager → inspector (#61)

Two corpus fixtures (transmon X, cavity displacement — distinct telemetry
profiles: 2×8 vs 1×6, 4 vs 3 iters) + test/corpus/fake-julia, a node stand-in
the executor spawns exactly like julia (last-argv script, cwd=runDir). It reads
each fixture's AMICODE_SMOKE directive and emits the template's telemetry
grammar (PULSE_META / ITER / PULSE → run.log via the executor's tail) with a
small inter-iter delay so the LIVE tail path is exercised, then writes a
schema-conformant result.toml. Zero Julia/Piccolo cost: full chain in ~0.5s.

The end-to-end test pins what the unit suites can't — that the pieces AGREE:
- Scheduler (#56) lifecycle is strictly serial (B starts only after A's
  finished event) and satisfies RunsManager's structural seam;
- the executor's run-dir writes (run.toml/index/run.log/result.toml/FINISHED)
  are exactly what the manager's tailer/registry read back (fidelity + iter
  high-water land in the registry);
- telemetry reaches the inspector runId-keyed per run with no cross-tagging
  (asserted by per-run record dims, completion fidelity, iter records).

Runs in the regular vitest suite → already in CI's fast job; #62 promotes it to
a named required gate. Mutation-verified: a wrong result.toml fidelity reds the
registry + completion assertions.

Branch note: contains merge of rchari/56-scheduler (the corpus drives the real
Scheduler); diff collapses once #68/#70/#72 land.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(1.4a): review #78 — failure-lane fixture, throwing pumpUntil, wiring-vs-format scope note

1. failing_solve.jl (exit=1): the previously-dead `exit=` directive support now
   has a fixture — a solve that emits two iterations then dies. Asserts the
   full failure path end-to-end: executor writes FINISHED{failed}, no
   result.toml, registry terminal with fidelity undefined but latestIter=2
   (pre-crash telemetry tracked), completion fans runId-keyed, promote never
   fires.
2. pumpUntil now THROWS on timeout with a named condition — a wiring
   regression fails fast at the offending await instead of an opaque hang.
3. Scope note in the suite header: this guards WIRING (fake ↔ parser), not
   FORMAT (template ↔ parser) — that boundary is #83's.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
jack-champagne added a commit that referenced this pull request Jul 7, 2026
…/stop truth, model pin, audit sweep, formatter (#89)

* feat(1.1): Scheduler — serial run queue built to the ratified Executor contract (#56)

`enqueue(spec, {concurrent?}) → ScheduledRun{queueId, handle: Promise<RunHandle>, cancel}`
plus a multi-consumer lifecycle stream (queued/started/finished/cancelled/error)
for RunsManager/StatusBar (1.2). Lives in @amicode/amico-run (node-only, no
vscode) beside the LocalExecutor it drives.

Built TO the Track C contract (ratified 2026-07-02) so Δ8's RemoteExecutor
drops in with zero reshape:
- S12: enqueue resolves to the executor's RunHandle UNTOUCHED (identity
  passthrough, pinned by test) — downstream never sees an executor type.
- (b) abort() is a request, not a kill: the pump advances ONLY when `finished`
  resolves; a post-abort() run still holds the queue (pinned by test).
- (c) per-executor warming budget: the Scheduler owns NO timers — structurally
  pinned (test greps the source for setTimeout/setInterval).
- (d) `finished` never rejects per contract; a rogue rejection is survived
  (error event) rather than wedging every queued run.

Semantics: strictly serial; cancel() dequeues only pre-start (a live run is
stopped via RunHandle.abort(), never the queue); a submit() ConfigError rejects
that entry's handle, emits `error`, and the queue advances; `concurrent: true`
is the NAMED Phase-4 seam — rejected loudly (ConfigError) instead of silently
serializing. Listener errors are isolated from the pump.

TDD: 12 tests (RED first) — serial ordering, S12 identity, opts passthrough,
abort≠terminated, lifecycle sequence with positions, cancel pre/post start,
ConfigError advance, the concurrent seam, multi-listener + dispose, throwing
listener, and the no-timers structural pin. Repo suite green (schema 34,
amico-run 59, extension 80); build + typecheck clean.

Closes #56.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(1.1): scheduler review — microtask-deferred re-pump + enforce the defensive claims

Adversarial review (mutation-tested; 0 must-fix, 2 should-fix) — all folded in:

- finally's re-pump is now queueMicrotask-deferred: a contract-violating
  executor whose submit() throws SYNCHRONOUSLY previously made the finally a
  direct recursion — a backlog of such failures accumulated behind a pending
  run blew the stack on drain (RangeError) and STRANDED the rest of the queue.
  Mutation-verified: reverting to the direct call fails the new test
  (RangeError + timeout); the deferral drains 8000 sync-throwers flat.
- The rogue-`finished`-rejection branch is now enforced, not just advertised:
  new test pins error-event + queue-advance (mutation-verified: deleting the
  branch fails it). Rejection reason normalized (instanceof Error) to match
  the submit path.
- Explicit unhandledRejection pin for the internal handle.catch suppression
  (an untouched ScheduledRun.handle never trips the process on cancel).
- cancel() docstring now names all three false cases (started / already
  cancelled / mid-submit, where handle may still reject); no-timers grep
  widened to setImmediate|Date.now.

15 tests green; repo suite green; typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat: catalog entry card — components, save-to-catalog flow, session catalog (UX2) (#73)

* feat(47): catalog entry card — components, save-to-catalog flow, session catalog (UX2)

The catalog card (Krishna p5, UX2) shipped end to end as a seam
prototype. Identity is Hamiltonian-anchored and user-named; the card is
the feedback artifact for the open field-selection questions.

Components (media/ui):
- chip: the entry's handle — gate · system · tags · #index. The system
  slot carries the USER-ASSIGNED name (researchers think in named
  devices, "Emerald-Q3"); derived family is the fallback. Tags render
  as dashed "proposed" segments — none of tags/index/name are in
  catalog-entry.schema.json, and the marking is deliberate.
- catalogcard: header (chip + run-id + Tune/Warm-Start/Promote actions,
  VS Code secondary-button styling, right-justified) → pulseplot
  (reused, hydrated) → metadata / pulse-data / high-level-metrics
  panels → sibling-chips row. Metrics are uniform hero cards in a
  wrapping flex row: fidelity · gate time (params.T) · spectral
  bandwidth (95%-power, computed client-side from the knots, DC
  removed; definitional choices marked proposed) · robustness (empty
  proposed slot — needs perturbed rollouts recorded at solve time).
  Solver telemetry (iterations/wall) lives in metadata: provenance,
  not pulse quality. Drive labels are u_i, matching the trajectory
  component and Piccolo's plot defaults.

Flow + shell (src):
- Save to catalog: a converged run's promote prompt (live) or the demo
  replay prompt → optional system-name and tags input → a pointer
  entry in the session catalog (workspaceState — NOT the Phase-3
  CatalogStore; Q91/Q92 open) → the card opens, hydrated from the real
  run artifacts (run.toml identity; result.toml fidelity/params with
  gate/system lifted; run.log pulse lines → the plot).
- Catalog tree view: chip-shaped rows (gate · system · fidelity, tags
  in description/tooltip), newest-first, deduped by run_id; click
  reopens the card. amicode.catalog.refresh actually registered.
- The solve-template change that records params.gate/params.system on
  NEW runs ships separately; the bundled demo fixture already carries
  them (schema-validated).

Tests: pulse-line grammar fixtures, hydration (field mapping, gate
lift, newest-record pulse, degradation), session tree (dedup, order,
open-card command, empty state).

Closes #47's Kate-lane scope as amended (actions row instead of the
"what do next" section; resume hand-off design returns to UX1 — see
the deferred-items comment on #46).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(47): remove-from-catalog — non-destructive unsave via the tree context menu

Right-click a Catalog row → "Remove from Catalog": deletes the POINTER
record only (workspaceState) — the run dir and pulse.jld2 stay on disk,
per the raw-data-trust principle. Archive/supersede lifecycle belongs to
the Phase-3 CatalogStore (Q94/Q95), deliberately not faked here.

Kept off the command palette (when: false) — the action only means
something on a row. Tests: removal preserves remaining order, is
idempotent, and rows carry the context value gating the menu.

Part of #47.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(47): reveal-or-create dedupe for catalog-card panels (review)

Clicking the same catalog row repeatedly spawned duplicate panels.
Per Jack's review: run_id → live-panel map, second open re-focuses via
reveal, disposal cleans the map so a closed tab re-creates fresh.
Shell test covers all three; vscode stub grows createWebviewPanel +
registerCommand to support it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* 1.2 — RunsManager: multi-run engine keyed on the append-only runs/index (#70)

* feat(1.2): RunsManager — multi-run engine keyed on the append-only runs/index (#57)

Replaces β's single-run RunsRootWatcher. Discovery now tails `runs/index`
(amico-run's appendIndex TSV) instead of following the `latest` symlink —
`latest` keeps being written (frozen contract) but a second concurrent solve no
longer yanks tracking off the first mid-flight: every run WITHOUT a FINISHED
gets its own pipeline (replay → run-dir watch → run.log tail) and is tracked to
completion.

- run_registry.ts (pure, vscode-free): parseIndexLine (tolerant of torn/blank
  lines — the tail heals) + RunRegistry (idempotent by runId; iter high-water;
  FINISHED-keyed terminal state).
- log_tailer.ts: LogTailer extracted verbatim from file_watcher.ts — reused for
  every run.log AND the index (both append-only).
- runs_manager.ts: per-run pipelines with per-run PulseStream/SinkDedup;
  runId-gated routing — the SELECTED run drives the single-run Inspector +
  StatusBar (selection auto-follows the newest started run, β latest-follow
  parity; `selectRun` is 1.3's seam), while completions + the promote-once
  prompt fire for EVERY run, selected or not. Completion keys on FINISHED
  (never result.toml presence). Poll backstop + idempotent consumers as before.
  attachScheduler consumes the #56 lifecycle (structural SchedulerLike so this
  is independent of #68's merge): `started` registers + selects immediately.
- extension.ts: RunsManager replaces the watcher; replayDemo now renders via
  EXPLICIT selection (pokeDiscovery + selectRun) since a finished-at-discovery
  run registers quietly (idle-at-launch parity) — promote stays suppressed.
- file_watcher.ts deleted (superseded); its statemachine tests ported to
  runs_manager.test.ts (idle-on-finished, warming→FINISHED-keyed completion,
  #66 pulse tail routing, replay-seeded meta) + new multi-run coverage:
  concurrent runs both tracked with background completion/promote, re-select
  replay without promote re-pop, missing-dir index lines tolerated, the
  Scheduler seam, and the demo-replay explicit-selection path.

15 new tests; repo green (extension 103, amico-run 47, schema 34); typecheck +
build clean. Closes #57.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(1.2): runs-manager review — disk-checked warming, scheduler metadata backfill, watch guards

Adversarial review (0 must-fix, 2 should-fix) — applied:
- selectRun re-checks DISK for FINISHED before posting warming (β parity):
  registry phase can be ≤700ms stale, and warming-after-completion inverted the
  terminal badge — real once 1.3's user-driven selectRun lands.
- RunRegistry.backfill: a scheduler-registered run (runId+runDir only) gains
  createdAt/scriptPath when its index line lands — the 1.3 trees would
  otherwise see undefined metadata on every scheduler-launched run.
- fs.watch 'error' listeners on all three watcher sites (root, per-run dir,
  tailer) — an unhandled FSWatcher error is an uncaught host exception; the
  poll backstop keeps things live.
- RunRegistry.all() returns copies (1.3 callers can't mutate registry state);
  honest header comment on the transient replay/tail re-delivery window.
- test/log_tailer.test.ts (review gap — the tailer is now load-bearing for
  discovery): torn-line carry-over, truncation re-read, startOffset contract,
  poke self-attach. 19 tests green, typecheck clean.

Still owed next session (review nits, test-only): cross-run PULSE routing gate
test, backfill unit test, stale-warming pin test; inspector pendingPulse reset
on selection switch is deferred to 1.3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(1.2): close the owed #70 review-nit gaps (all mutation-verified)

The three test gaps the RunsManager review flagged:
- RunRegistry.backfill: fills ONLY missing metadata (first-registration wins),
  no-throw on unknown runId — mutation-verified (dropping the missing-only
  guard fails it).
- RunRegistry.all(): returns copies — a mutated snapshot can't corrupt registry
  state.
- cross-run PULSE routing: a background run's pulse RECORD is gated on selection
  (not just iter) — never reaches the inspector while another run is selected.
- stale-warming: selecting a run whose FINISHED landed inside the ≤700ms poll
  window shows completion, NOT warming (no terminal-badge inversion) —
  mutation-verified (reverting the disk re-check fails it).

extension 111 tests (+8), repo green (amico-run 47, schema 34); typecheck clean.

* fix(1.2): review #70 — pinned selection, torn-FINISHED retry, markFinished guard, single-pass discovery, one terminal orchestration

Addresses jack-champagne's static/design pass, one commit per nothing — all
five findings land together because #1/#4 reshape the same registerRun path:

#1 (design, the #72 seam): explicit selectRun PINS the selection; auto-follow
   (newest registered live run, β latest-follow parity) only applies while
   nothing is pinned. A background solve starting can no longer yank the view
   off a run the user deliberately opened. Mutation-verified.

#2: a FINISHED that is present but torn/invalid at discovery no longer
   finalizes as status:undefined-forever — the run falls through to the live
   path, whose checkFinished re-reads next tick (the retry the live lane
   already had). Promote stays suppressed (launch replay). No warming either
   (disk-checked: FINISHED exists).

#3: RunRegistry.markFinished guards phase itself (first terminal wins) — a
   stray re-mark can't leave status:"failed" beside a stale fidelity. The
   guard now lives on the public surface, not only in completeRun.

#4: discovery ingests the run dir ONCE. Auto-follow assigns selection BEFORE
   the registration replay, so the single pipelineSink pass both seeds state
   and feeds the display through routeIter/routePulse's selection gate —
   displaySink now backs only explicit selection replays.

#5: the FINISHED→status→result.toml→fidelity orchestration lives ONCE, in
   run_dir_reader.readTerminalState (say-why callback preserved via the
   manager's channel); ingestRunDir and RunsManager.readTerminal both
   delegate, so a contract change (e.g. #64's formulation.toml) is edited in
   one place.

Also rebased onto main (#68 Scheduler, #77 vsix-gate, #79 permission grant).
118 extension tests pass; typecheck + build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* 1.3: Run Inspector single→multi-run (runId-keyed protocol, per-run panes) (#72)

* feat(1.3): Run Inspector single→multi-run (runId-keyed protocol, per-run panes)

Freeze-2 reshape (#58): the host↔webview message protocol is now runId-keyed
and both the host and the webview fan into per-run panes. Single→multi only —
the pane markup stays the current pulseplot (design lane, UX4 #49).

Host (run_inspector.ts): a PaneBuffer per runId + activeRunId; runId-keyed
surface postPulse/postIterationRecord/postCompletion/setWarmingUp/setRunLabel +
new activate(runId). Per-run 5 Hz pulse throttle. resolveWebviewView replays
EVERY pane from its buffer (S36) with positional ordering, then posts activate
last. setWarmingUp guarded from clobbering a pane that already has data/terminal
state. pulse stays plot-only (deliberately does not clear warming).

Webview (media/ui/views/inspector.ts): createPanel() instances the former
single-run view per runId (no shared globals); a router keys panels by runId,
activate toggles the one visible pane, background/late messages only touch their
own pane. Pane-hiding uses two-class selectors so it wins over layout.css
`.stack` on specificity, not stylesheet order.

RunsManager (runs_manager.ts): fans every run's live events into the inspector
runId-tagged (routeIter/routePulse ungated); registration replay is state-only
so the selected run never double-posts; selectRun adds activate; the single
status bar stays selection-gated; completion + promote still fire per-run.

Tests: runs_manager + inspector_view_contract updated to the runId-keyed API and
fan-out semantics; added per-run isolation, per-run-throttle independence, S36
reopen, activate-last, warming-guard. New happy-dom webview test covers the
router itself (per-run isolation, activate toggle, empty-state, plot-only pulse)
— closes the coverage gap flagged in adversarial review. All invariants
mutation-verified. 121 tests pass; typecheck + build clean. S6 (formulation
preview) deferred — no formulation-emit in the frozen contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(1.3): flow RunCompletion whole through completeRun — the #84/#81 seam

Jack's #72 merge-seam heads-up: #81 adds `formulation?` to RunCompletion, and
completeRun was the third completion path cherry-picking fields positionally
(runId/status/fidelity) — once #81 landed, live-completed runs would carry
formulation: undefined while replayed runs got it (the exact bug Kate caught
on onFinished, reintroduced here).

completeRun now takes the WHOLE RunCompletion; both feeders (ingestRunDir's
sink verbatim, checkFinished via {runId, runDir, ...readTerminalState()})
funnel the object from the one shared read. An additive field is now a
one-place edit (RunCompletion + readTerminalState) and reaches every consumer
by construction — consumers cherry-pick at the leaf. Documented as the #84
funnel on both the type and completeRun; the full N-reader consolidation
(catalog hydrator etc.) stays #84.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* 1.4a: smoke corpus — Scheduler → executor → RunsManager → Inspector e2e (#78)

* feat(1.3): Run Inspector single→multi-run (runId-keyed protocol, per-run panes)

Freeze-2 reshape (#58): the host↔webview message protocol is now runId-keyed
and both the host and the webview fan into per-run panes. Single→multi only —
the pane markup stays the current pulseplot (design lane, UX4 #49).

Host (run_inspector.ts): a PaneBuffer per runId + activeRunId; runId-keyed
surface postPulse/postIterationRecord/postCompletion/setWarmingUp/setRunLabel +
new activate(runId). Per-run 5 Hz pulse throttle. resolveWebviewView replays
EVERY pane from its buffer (S36) with positional ordering, then posts activate
last. setWarmingUp guarded from clobbering a pane that already has data/terminal
state. pulse stays plot-only (deliberately does not clear warming).

Webview (media/ui/views/inspector.ts): createPanel() instances the former
single-run view per runId (no shared globals); a router keys panels by runId,
activate toggles the one visible pane, background/late messages only touch their
own pane. Pane-hiding uses two-class selectors so it wins over layout.css
`.stack` on specificity, not stylesheet order.

RunsManager (runs_manager.ts): fans every run's live events into the inspector
runId-tagged (routeIter/routePulse ungated); registration replay is state-only
so the selected run never double-posts; selectRun adds activate; the single
status bar stays selection-gated; completion + promote still fire per-run.

Tests: runs_manager + inspector_view_contract updated to the runId-keyed API and
fan-out semantics; added per-run isolation, per-run-throttle independence, S36
reopen, activate-last, warming-guard. New happy-dom webview test covers the
router itself (per-run isolation, activate toggle, empty-state, plot-only pulse)
— closes the coverage gap flagged in adversarial review. All invariants
mutation-verified. 121 tests pass; typecheck + build clean. S6 (formulation
preview) deferred — no formulation-emit in the frozen contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(1.3): flow RunCompletion whole through completeRun — the #84/#81 seam

Jack's #72 merge-seam heads-up: #81 adds `formulation?` to RunCompletion, and
completeRun was the third completion path cherry-picking fields positionally
(runId/status/fidelity) — once #81 landed, live-completed runs would carry
formulation: undefined while replayed runs got it (the exact bug Kate caught
on onFinished, reintroduced here).

completeRun now takes the WHOLE RunCompletion; both feeders (ingestRunDir's
sink verbatim, checkFinished via {runId, runDir, ...readTerminalState()})
funnel the object from the one shared read. An additive field is now a
one-place edit (RunCompletion + readTerminalState) and reaches every consumer
by construction — consumers cherry-pick at the leaf. Documented as the #84
funnel on both the type and completeRun; the full N-reader consolidation
(catalog hydrator etc.) stays #84.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(1.4a): smoke corpus — seconds-scale end-to-end fixtures, Scheduler → executor → run-dir → RunsManager → inspector (#61)

Two corpus fixtures (transmon X, cavity displacement — distinct telemetry
profiles: 2×8 vs 1×6, 4 vs 3 iters) + test/corpus/fake-julia, a node stand-in
the executor spawns exactly like julia (last-argv script, cwd=runDir). It reads
each fixture's AMICODE_SMOKE directive and emits the template's telemetry
grammar (PULSE_META / ITER / PULSE → run.log via the executor's tail) with a
small inter-iter delay so the LIVE tail path is exercised, then writes a
schema-conformant result.toml. Zero Julia/Piccolo cost: full chain in ~0.5s.

The end-to-end test pins what the unit suites can't — that the pieces AGREE:
- Scheduler (#56) lifecycle is strictly serial (B starts only after A's
  finished event) and satisfies RunsManager's structural seam;
- the executor's run-dir writes (run.toml/index/run.log/result.toml/FINISHED)
  are exactly what the manager's tailer/registry read back (fidelity + iter
  high-water land in the registry);
- telemetry reaches the inspector runId-keyed per run with no cross-tagging
  (asserted by per-run record dims, completion fidelity, iter records).

Runs in the regular vitest suite → already in CI's fast job; #62 promotes it to
a named required gate. Mutation-verified: a wrong result.toml fidelity reds the
registry + completion assertions.

Branch note: contains merge of rchari/56-scheduler (the corpus drives the real
Scheduler); diff collapses once #68/#70/#72 land.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(1.4a): review #78 — failure-lane fixture, throwing pumpUntil, wiring-vs-format scope note

1. failing_solve.jl (exit=1): the previously-dead `exit=` directive support now
   has a fixture — a solve that emits two iterations then dies. Asserts the
   full failure path end-to-end: executor writes FINISHED{failed}, no
   result.toml, registry terminal with fidelity undefined but latestIter=2
   (pre-crash telemetry tracked), completion fans runId-keyed, promote never
   fires.
2. pumpUntil now THROWS on timeout with a named condition — a wiring
   regression fails fast at the offending await instead of an opaque hang.
3. Scope note in the suite header: this guards WIRING (fake ↔ parser), not
   FORMAT (template ↔ parser) — that boundary is #83's.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* feat: theme-calculated Harmoniqs yellow — OKLCH-solved brand accent (brand-wide)

brand_accent.ts computes the deployed accent from the active theme at
webview boot: the canonical #FFF676 ships EXACTLY wherever contrast vs
the theme's editor background clears 3:1 (all dark themes); light themes
get the closest-to-brand gold by binary-searching lightness with hue +
chroma held (gamut-clamped). Two tokens with different jobs: lines
(--color-accent, contrast-solved: borders/rings/marks) and fills
(--color-accent-fill, always the brand lemon — black text on it ≈ 19:1;
a 3:1-darkened gold passes WCAG math but reads muddy under text).
--color-on-accent is contrast-picked; yellow is never text. Recomputed
live on theme switch. Inspector + catalog-card webviews apply at boot;
brand.css statics remain the no-JS fallback. Pill atom gains a dot-less
badge variant (dot = process state; badges describe things).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* workbench: run picker + pane-ticker pause + runId-tagged controls (+ Kate's theme accent picked)

- amicode.selectRun: QuickPick over the registry (newest first; live/completed/
  stopped/failed icons, iter + fidelity + script). Picking pins; "Follow
  latest" releases the pin via RunsManager.resumeAutoFollow() (jumps to the
  newest live run). Pre-UX4 utility — unblocks real multi-run testing.
- Hidden panes pause their 1 Hz elapsed-strip ticker (Panel.setActive from the
  router's activate); resumes with a fresh render on re-activation.
- Control-row messages carry their pane's runId (post-UX4 correctness;
  commands still resolve the selected run today).
- Cherry-picked Kate's 154650c: OKLCH theme-calculated brand accent — fixes
  the hardcoded #FFF676 that dies on light themes (audit P0-1, extension side).

408 tests pass; typecheck + build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* workbench: wire catalog what-next — tune/warm-start stage a concrete chat prompt (clipboard + open chat); promote says Phase-3 honestly

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* workbench: chat theme bridge — iframe boots with ?colorScheme= from the editor theme; live re-theme via onDidChangeActiveColorTheme → two-lane relay (origin-pinned) → app's setColorScheme

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: one-spine mirror breadcrumb — readTerminalState semantics are mirrored in the fork's run-terminal.ts; change both in one change-set

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* workbench: animated H-robot mark in the Run Inspector (replaces the <0||0> text ket; breathe + eye-blink, reduced-motion aware); openRunDir reveals run.toml (bare-dir reveal errors on macOS) with openExternal fallback; inspector auto-open defaults ON

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* workbench: inspector mark = the house silhouette glyph (AmicoSpinner geometry + pulse-opacity language, reduced-motion aware); allow workbench.action.showCommands over the chat bridge (⌘⇧P → VS Code palette)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* workbench: chat auto-opens when the server is ready (amicode.chat.autoOpen, default on)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* workbench: inspector never reveals during the boot index replay — only a run that STARTS while the user works auto-opens it (chat remains the only boot-time surface)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* workbench: boot-quiet test coverage — fresh-run warming asserts the post-boot path; new pin that boot replay never warms/reveals

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* workbench: clipboard bridge — the framed app requests paste over the message bridge; extension answers with vscode.env.clipboard (webviews can't delegate clipboard-read into iframes)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* workbench: open-external bridge — framed app opens https links via vscode.env.openExternal (target=_blank is dead in the webview iframe)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* status bar: stalled-run gate — FINISHED-less run with run.log silent >10min shows 'stalled' (warning), never a perpetual 'running · iter N' from boot replay; mirrors fork isStalled

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* score: never leak the 'issimo' entitlement codename into chat (read as truncated Piccolissimo); dedupe doubled routing paragraph in stage-1 notes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* stop always terminates: escalation ladder (cooperative STOP → stalled runs get kill+finalize immediately → healthy runs get a 120s grace then an explicit Force-stop offer)

- stopPlan: FINISHED → no-op; fresh run.log → cooperative; log cold past the
  stall threshold (or logless zombie dir) → force
- findRunPids: two-key match — cmdline references the run's solve script AND
  process cwd IS the run dir (sibling runs share the script, never the cwd;
  no pattern-kills, ever)
- forceStop: TERM → 1.5s → KILL survivors → forceFinalize writes the terminal
  FINISHED (status aborted, atomic rename; run.log breadcrumb) so every
  contract reader on both spines converges and the UI clears
- never a silent kill on a live solver: one long Ipopt iteration can look
  wedged — the grace path asks before forcing

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(stop): probe lsof at /usr/sbin (macOS) and /usr/bin (Linux) — hardcoded macOS path silently disabled the kill path on Linux (findRunPids proved nothing → force-finalized runs with the solver still alive)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(stop): realpath both sides of the cwd ownership proof — lsof reports physical paths, so a symlinked runs root (/tmp → /private/tmp on macOS) made every pid unprovable and the kill path a no-op

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(stop): forceStop yields to a FINISHED that appeared during the TERM→KILL window — the orchestrator's truthful verdict (failed/143) must not be overwritten with aborted (disk vs registry vs fork endpoints would disagree forever)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(stop): re-prove pid ownership before the SIGKILL sweep — a pid freed by TERM can be reused by an unrelated process inside the 1.5s window

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(stop): JSON-decode script_path from run.toml — values are written JSON-escaped, so escaped chars never matched ps argv and dropped the script-path key from the ownership match

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(stop): pin JSON-decoding of escaped script_path values

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(stop): every stop toast and the force-stop dialog name the run — a nameless dialog 120s later reads as the wrong run being wedged

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(stop): double-stop guard + escalation-timer disposal — a second Stop no longer stacks a second 120s dialog, and the timer dies with the extension instead of firing after deactivate

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(stop): tolerate a deleted run dir — STOP write and finalize are best-effort so a removed dir can't crash the command before the UI entry is cleared

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf(runs): 2s TTL cache on liveStatus — boot replay of a long run.log paid one statSync per iter line

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(runs): the poll backstop downgrades the selected run to 'stalled' — routeIter only fires when a line ARRIVES (i.e. not stalled), so a run that wedged mid-watch kept 'running · iter N' forever; downgrade-only so warming/iter flow stays untouched + pin test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(runs): selectRun consults liveStatus instead of hardcoding 'running' (picking a stalled run stamped a lie nothing would correct); finished runs' display replay no longer flickers stalled/running per line — completion sets the bar once

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: remove empty runs_manager_boot.test.ts accidentally created by a shell append probe two commits ago (vitest fails on suite-less files)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(picker): stalled runs show '$(warning) stalled · iter N' — the picker advertised a wedge as '$(pulse) live', contradicting the status bar

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bridge): https scheme check is case-insensitive (RFC 3986); clipboard replies gated on panel visibility — a hidden panel rendering LLM-driven content must not sample the OS clipboard in the background

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(scores-e2e): version-agnostic score-marker assertion — hardcoded 'v1' while SCORE.md is v3, red on every creds-bearing machine

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(runs): one STALL_AFTER_MS (runs_manager imports run_controls's) and the one-spine mirror comment now names the stall threshold + display vocabulary as mirrored semantics

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(runs): one STALL_AFTER_MS (runs_manager imports run_controls's export); one-spine mirror comment names the stall threshold + display vocabulary as mirrored semantics

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* style: prettier 3.6.2 over the branch's touched files + a minimal .prettierrc (printWidth 120, semi) — the repo had no formatter configured; config formalizes the existing conventions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* style: prettier over the branch's touched amico-run files (missed in the previous style commit)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* style: prettier repo-wide (new .prettierrc/.prettierignore) — one-time full-repo normalization so the formatter is enforceable from here

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(config): fallback-only model pin — without one, opencode's default resolution gambles on provider ordering and (with Google creds) picked a hanging preview model for every headless/agent turn; anthropic > GA gemini flash, and a user's global model always wins (1.17.3 preserve contract) + tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(vsix-gate): fix by splitting package step — explicit build + fetch:opencode + vsce

* test(slow): live-turn extractor is model-agnostic — Gemini opens with the amicode_ask TOOL CALL and no prose, so the ask input (question+options) counts as the turn text; production model pin wired into the e2e servers

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(bridge): save-file lane — the run-card gallery's PNG export routes through a save dialog (downloads are dead in the framed app); PNG-only, basename-sanitized, size-bounded, relay-allowlisted

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(config): google pin moves to gemini-2.5-flash — the newest flash is capacity-throttled at peak ('model overloaded' → failed turns render as 'model undefined' stubs in chat); the GA flash answered in 1.4s during the same window

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(config): creds-free default model — opencode/deepseek-v4-flash-free (zen free tier, no user quota; answered a tool-bearing turn in ~3s while Gemini kept capacity-throttling); anthropic still wins when creds exist, user global model always wins

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci: authenticate the private-fork opencode fetch (GH_TOKEN)

fast/vsix-gate/boot-smoke fetch the vendored opencode binary from the
PRIVATE harmoniqs/opencode release via `gh release download`. The default
Actions GITHUB_TOKEN is scoped to this repo only, so gh is unauthenticated
for harmoniqs/opencode and the fetch exits 1 — reding every job that vendors
opencode (schema-roundtrip is untouched; it needs no binary).

Pass a cross-repo token as GH_TOKEN to the three fetch:opencode step defs
(boot-smoke's single step covers both matrix legs). Mirrors #80, adapted to
this branch's split vsix-gate step.

Requires repo secret OPENCODE_FETCH_TOKEN: a fine-grained PAT scoped to
harmoniqs/opencode with Contents:Read (validated end-to-end — downloads both
assets and the bytes match opencode.lock.json's SHA256 gate). Stays red until
the secret exists; green once it's set, no further push needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(extension): bump opencode.lock to v1.17.3-amicode.2

Pin the vendored opencode binary to the new fork release cut from
rchari/amicode-fixes (opencode PR #1): Gemini tool-schema fix,
/amicode run-cards + profile endpoints, one-spine run truth,
multi-drive pulse fix, save-file bridge.

darwin-arm64 + linux-x64 sha256 updated. vsix build verified locally
(linux-x64 fetch + sha match + vsce package -> amicode.vsix).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Kate <katebonner277@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Jack Champagne <jackchampagne.r@gmail.com>
Co-authored-by: Jack Champagne <jack@harmoniqs.co>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants