@codeyam-editor/codeyam-editor 0.1.16-staging.ge120017 → 0.1.17-staging.gdcd54cf

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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -0
  3. package/npm/disk-pressure.js +48 -20
  4. package/npm/fleet-add-vm-steps.js +210 -0
  5. package/npm/fleet-build-data.js +11 -3
  6. package/npm/fleet-dashboard-data.js +284 -0
  7. package/npm/fleet-reset.js +6 -2
  8. package/npm/fleet-stuck-position.js +88 -0
  9. package/npm/fleet-stuck-status.js +16 -2
  10. package/npm/fleet-vm-card.js +320 -0
  11. package/npm/fleet-vm-poll.js +418 -0
  12. package/npm/ingress-rewire-retry.js +43 -0
  13. package/npm/launch-config-steps.js +513 -0
  14. package/npm/launch-config.js +21 -482
  15. package/npm/scenario-check-run.js +666 -0
  16. package/npm/scenario-check.js +352 -650
  17. package/npm/scenario-launch.js +100 -7
  18. package/npm/scenario-metrics.js +74 -9
  19. package/npm/scenario-playwright.js +89 -4
  20. package/npm/wasm-function-attribution.js +242 -0
  21. package/npm/wasm-name-section.js +121 -0
  22. package/npm/wasm-source-attribution.js +107 -0
  23. package/npm/wasm-test-module.js +31 -0
  24. package/package.json +5 -5
  25. package/ui/dist/assets/{ContainerInspector-Cig2PDjT.js → ContainerInspector-Dxc9Mueo.js} +1 -1
  26. package/ui/dist/assets/EditorShell-DB26RdeS.css +1 -0
  27. package/ui/dist/assets/EditorShell-icRqflQd.js +381 -0
  28. package/ui/dist/assets/ProjectLauncherScreen-iCf8wHB0.js +1 -0
  29. package/ui/dist/assets/{ScenarioDataPanel-BRUYhl1o.js → ScenarioDataPanel-C5kgmZMZ.js} +10 -10
  30. package/ui/dist/assets/{ScenarioDataPanelFullPage-DjwRM8HJ.js → ScenarioDataPanelFullPage-DoUNWztV.js} +1 -1
  31. package/ui/dist/assets/ScenarioRenderer-tS-1fxyi.js +26 -0
  32. package/ui/dist/assets/{index-Bkrhkuf5.js → index-B-G6cmd9.js} +8 -8
  33. package/ui/dist/assets/index-BdJK2jA3.css +1 -0
  34. package/ui/dist/assets/useEvents-CjsZBYAw.js +1 -0
  35. package/ui/dist/index.html +2 -2
  36. package/ui/dist/assets/EditorShell-DOuUJNSm.js +0 -368
  37. package/ui/dist/assets/EditorShell-DTyhLLc8.css +0 -1
  38. package/ui/dist/assets/ProjectLauncherScreen-DP25uSQm.js +0 -1
  39. package/ui/dist/assets/ScenarioRenderer-CcbccxCW.js +0 -26
  40. package/ui/dist/assets/index-Dw4EB88N.css +0 -1
  41. package/ui/dist/assets/useEvents-DNb1olaj.js +0 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CodeYam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # CodeYam Editor
2
+
3
+ A step-driven AI build editor with an isolated Live Preview, scenario capture, verification commands, and a Rust control API.
4
+
5
+ CodeYam Editor runs beside your project and drives an AI build agent through a planned, step-by-step workflow. Every feature is built together with the *scenarios* (the data states) that demonstrate it, shown in a live preview as you go and captured so they can be re-verified later. It works with any stack: web apps, backend services, CLIs, libraries.
6
+
7
+ ## Install
8
+
9
+ **Recommended: the one-line installer.** It installs through npm and puts `codeyam-editor` on your `PATH`:
10
+
11
+ ```bash
12
+ curl -fsSL https://codeyam.com/install.sh | sh
13
+ ```
14
+
15
+ It checks the prerequisites (Node 20 or newer, and on Linux a recent enough glibc), runs the global npm install, and adds npm's global `bin` directory to your shell profile, so a new shell finds `codeyam-editor`. It prints exactly what it changed.
16
+
17
+ **Plain npm** (you manage `PATH` yourself):
18
+
19
+ ```bash
20
+ npm install -g @codeyam-editor/codeyam-editor
21
+ ```
22
+
23
+ > If you use a custom npm prefix (for example `npm config set prefix ~/.npm-global` to avoid `sudo`), add its `bin` directory to `PATH` yourself. Otherwise the install succeeds but the shell reports `codeyam-editor: command not found`. The one-line installer does this for you.
24
+
25
+ **No install: a one-off run** through npx:
26
+
27
+ ```bash
28
+ npx @codeyam-editor/codeyam-editor
29
+ ```
30
+
31
+ ## Requirements
32
+
33
+ - Node.js 20 or newer.
34
+ - One of the supported platforms. npm installs the matching native binary automatically:
35
+ - macOS on Apple silicon (arm64) or Intel (x64)
36
+ - Linux x64 with glibc 2.28 or newer: Ubuntu 20.04+, Debian 10+, RHEL/Rocky/Alma 8+, Fedora 29+, Amazon Linux 2023. Older distros such as CentOS 7 and Amazon Linux 2 are not supported.
37
+ - Windows x64
38
+ - Playwright's Chromium, for scenario screenshots: `npx playwright install chromium`.
39
+
40
+ ## Quick start
41
+
42
+ In your project directory:
43
+
44
+ ```bash
45
+ codeyam-editor init # write .codeyam/editor.json
46
+ codeyam-editor start # start the editor and open it in your browser
47
+ ```
48
+
49
+ `start` prints the editor's local URL and opens it. Pass `--no-open` to skip the browser, or `--port <PORT>` to pick the port. `init --provider <claude|codex|gemini|opencode>` chooses the build agent (Claude by default).
50
+
51
+ ## Commands
52
+
53
+ | Command | What it does |
54
+ |---|---|
55
+ | `codeyam-editor start` | Start the editor (control API and your app's process). |
56
+ | `codeyam-editor init` | Initialize a new `.codeyam/editor.json` config. |
57
+ | `codeyam-editor mcp` | Run an MCP (Model Context Protocol) server over stdio, for Claude Desktop and other MCP clients. |
58
+ | `codeyam-editor editor` | The workflow subcommands the build agent uses: scenarios, glossary, audit, and more. |
59
+
60
+ Run `codeyam-editor <command> --help` for a command's options, and `codeyam-editor editor --help` for the full list of workflow subcommands.
61
+
62
+ ## Use with Claude
63
+
64
+ The editor works with Claude Code out of the box: `codeyam-editor init` sets up the project for it. For Claude Desktop or any other MCP client, register `codeyam-editor mcp` as a stdio MCP server. See [codeyam.com](https://codeyam.com/) for the other ways to install it in Claude.
65
+
66
+ ## Links
67
+
68
+ - Homepage: <https://codeyam.com/>
69
+
70
+ ## License
71
+
72
+ MIT. See `LICENSE`.
@@ -55,10 +55,11 @@ const PRUNABLE_CACHE_DIRS = [
55
55
  // keyed on this known mount path, NOT folded into the generic allowlist, so a
56
56
  // client project's own Rust target/ is never touched.
57
57
  //
58
- // Stack assumption: only self-host VMs that run cargo write to this volume;
59
- // guest/client VMs (Next.js, Vite, Tauri, Python, …) carry an empty or absent
60
- // volume, so cargoTargetReclaimPlan returns an empty plan there (a guaranteed
61
- // no-op — honoring the stack-agnosticism rule in CLAUDE.md).
58
+ // Stack assumption: every cargo build in the container writes to this volume —
59
+ // the editor's own on a self-host VM, and a Rust client project's on a client
60
+ // VM (60+ GB on the rapid-cad VMs, 2026-10-23). A VM whose project never runs
61
+ // cargo (Next.js, Vite, Python, …) carries an empty or absent volume, so
62
+ // cargoTargetReclaimPlan prunes nothing inside it there.
62
63
  const CARGO_TARGET_DIR = '/codeyam-build-target';
63
64
 
64
65
  // Sub-dirs of CARGO_TARGET_DIR that are ALWAYS safe to delete: pure coverage
@@ -73,33 +74,59 @@ const CARGO_TARGET_ALWAYS_PRUNE = ['llvm-cov-target'];
73
74
  // in-flight compile to free space — the failure mode this tiering avoids).
74
75
  const CARGO_TARGET_CRITICAL_CLEAN = ['debug'];
75
76
 
77
+ // Targeted tiers, in the order the guard runs them, at WARN and CRITICAL alike
78
+ // and regardless of buildInFlight: each removes only state no build will read
79
+ // again (scripts/cloud-disk-guard.sh::reclaim_targeted_tiers). Before them WARN
80
+ // freed nothing and CRITICAL could only wipe debug/ whole, so long-running Rust
81
+ // client VMs drifted to 100% and every reclaim cost a cold rebuild.
82
+ // - stale-incremental: superseded `incremental/<crate>-<hash>/s-*` state under
83
+ // every profile and target triple of CARGO_TARGET_DIR (needs the volume)
84
+ // - orphan-workspace-target: a project-local target/ while CARGO_TARGET_DIR
85
+ // points elsewhere
86
+ // - idle-base-run-targets: test-on-base scratch caches no live run holds
87
+ // - idle-scratch-targets: cargo targets left in agent scratch space
88
+ // The last three live outside the volume, so they apply on any VM.
89
+ const CARGO_TARGETED_TIERS = [
90
+ 'stale-incremental',
91
+ 'orphan-workspace-target',
92
+ 'idle-base-run-targets',
93
+ 'idle-scratch-targets',
94
+ ];
95
+ const TIERS_NEEDING_CARGO_TARGET = ['stale-incremental'];
96
+
76
97
  // Pure decision function for tiered cargo-target reclamation. Given the current
77
98
  // pressure `level`, whether a build is in flight, and whether this VM actually
78
- // has the cargo-target volume, return which sub-dirs to prune and whether the
79
- // heavy `debug` clean was deferred (so the caller can log it — no silent cap).
99
+ // has the cargo-target volume, return the targeted tiers to run, which sub-dirs
100
+ // to prune, and whether the heavy `debug` clean was deferred (so the caller can
101
+ // log it — no silent cap).
80
102
  //
81
103
  // Tiers (mirrored by the bash guard + watchdog):
82
- // - no cargo-target (guest/client VM) → prune nothing (total no-op)
104
+ // - warn / critical → targeted tiers first, always
105
+ // - no cargo-target (guest/client VM) → prune nothing in the volume
83
106
  // - warn → ALWAYS_PRUNE only (llvm-cov)
84
107
  // - critical & idle → ALWAYS_PRUNE + CRITICAL_CLEAN
85
108
  // - critical & build in flight → ALWAYS_PRUNE only, defer heavy
86
- // - ok / unknown level → fail-safe: prune nothing
109
+ // - ok / unknown level → fail-safe: run nothing
87
110
  function cargoTargetReclaimPlan({ level, buildInFlight, hasCargoTarget } = {}) {
88
- if (!hasCargoTarget) return { prune: [], deferredHeavyClean: false };
111
+ if (level !== 'warn' && level !== 'critical') {
112
+ // 'ok', undefined, or any unknown level → do nothing (fail safe).
113
+ return { targeted: [], prune: [], deferredHeavyClean: false };
114
+ }
115
+ const targeted = CARGO_TARGETED_TIERS.filter(
116
+ (tier) => hasCargoTarget || !TIERS_NEEDING_CARGO_TARGET.includes(tier)
117
+ );
118
+ if (!hasCargoTarget) return { targeted, prune: [], deferredHeavyClean: false };
89
119
  if (level === 'warn') {
90
- return { prune: [...CARGO_TARGET_ALWAYS_PRUNE], deferredHeavyClean: false };
120
+ return { targeted, prune: [...CARGO_TARGET_ALWAYS_PRUNE], deferredHeavyClean: false };
91
121
  }
92
- if (level === 'critical') {
93
- if (buildInFlight) {
94
- return { prune: [...CARGO_TARGET_ALWAYS_PRUNE], deferredHeavyClean: true };
95
- }
96
- return {
97
- prune: [...CARGO_TARGET_ALWAYS_PRUNE, ...CARGO_TARGET_CRITICAL_CLEAN],
98
- deferredHeavyClean: false,
99
- };
122
+ if (buildInFlight) {
123
+ return { targeted, prune: [...CARGO_TARGET_ALWAYS_PRUNE], deferredHeavyClean: true };
100
124
  }
101
- // 'ok', undefined, or any unknown level → do nothing (fail safe).
102
- return { prune: [], deferredHeavyClean: false };
125
+ return {
126
+ targeted,
127
+ prune: [...CARGO_TARGET_ALWAYS_PRUNE, ...CARGO_TARGET_CRITICAL_CLEAN],
128
+ deferredHeavyClean: false,
129
+ };
103
130
  }
104
131
 
105
132
  // Classify an integer disk-usage percent into a pressure level. A non-numeric,
@@ -159,6 +186,7 @@ module.exports = {
159
186
  CARGO_TARGET_DIR,
160
187
  CARGO_TARGET_ALWAYS_PRUNE,
161
188
  CARGO_TARGET_CRITICAL_CLEAN,
189
+ CARGO_TARGETED_TIERS,
162
190
  cargoTargetReclaimPlan,
163
191
  classifyDiskPressure,
164
192
  parseDfUsedPct,
@@ -0,0 +1,210 @@
1
+ 'use strict';
2
+
3
+ const lc = require('./launch-config');
4
+ const fa = require('./fleet-auth');
5
+ const fer = require('./fleet-early-roster');
6
+
7
+ // npm/fleet-add-vm-steps.js — what an Add (fleet-dashboard server.js `addVm`)
8
+ // runs, and in what order, decided before anything is spawned.
9
+ //
10
+ // addVm keeps every side effect: the pending-job sidecar, the exit-marker
11
+ // clear, startJob and the roster writes. These two steps only DECIDE:
12
+ //
13
+ // resolveAddCreds — which reachable VM donates claude/git credentials (or
14
+ // which donor-less fallback applies) and the seeding
15
+ // shell steps that follow from it;
16
+ // buildAddVmScript — the provision script itself: cloud:up, the container
17
+ // gate, the early-roster marker, creds, tunnels and the
18
+ // awaited ingress wiring, as one newline-joined string.
19
+ //
20
+ // The server's constants arrive under their server names, and `shq` is the
21
+ // server's single-quote shell escaper, so both read as they did inline.
22
+
23
+ // `vms` is the server's state.vms; `requestedAccount` is the account the add
24
+ // should seed (a per-add override, else the fleet's chosen one), already
25
+ // resolved by the server because choosing it reads the dashboard's settings.
26
+ function resolveAddCreds({
27
+ n, VMS, vms, requestedAccount, inst, INSTANCE_PREFIX, CLAUDE_CREDS_SRC, GITHUB_TOKEN,
28
+ CONTAINER, GCLOUD, ZONE, shq,
29
+ }) {
30
+ // Creds source: prefer a *reachable* donor (a VM that actually answered the
31
+ // last poll — VMS[0] may be a destroyed-but-still-rostered orphan, #24), else
32
+ // fall back to the operator-configured CLAUDE_CREDS_SRC / GITHUB_TOKEN. A full
33
+ // teardown empties VMS, so there's no donor at all — the fallback is what lets
34
+ // "destroy all → Add count=N" produce a working fleet numbered 1-N (improve34).
35
+ // A reachable donor whose credential failed the integrity probe is dropped
36
+ // HERE, before resolveCredsSource's first-donor fallback could pick it: the
37
+ // copy sink would refuse its credential only after the add had committed to
38
+ // it (VM-11 → VM-2, 2026-09-17). Each dropped donor is named in the add log.
39
+ const donorSplit = fa.partitionDonorsByIntegrity(
40
+ VMS.filter((x) => x !== n && vms[x] && vms[x].reachable),
41
+ vms,
42
+ );
43
+ const reachableDonors = donorSplit.usable;
44
+ // The donor is the reachable VM holding the requested account, so the account
45
+ // the Add dialog showed is the account actually seeded. When no reachable VM
46
+ // holds it, fall back to the usual donor and raise a CY_DEGRADED reason naming
47
+ // both (credsSteps below).
48
+ const preferredDonor = fa.selectDonorForAccount(reachableDonors, vms, requestedAccount);
49
+ const creds = lc.resolveCredsSource({
50
+ reachableDonors,
51
+ preferredDonor,
52
+ requestedAccount,
53
+ claudeCredsSrc: CLAUDE_CREDS_SRC,
54
+ githubToken: GITHUB_TOKEN,
55
+ });
56
+ const refInst = creds.donor != null ? INSTANCE_PREFIX + creds.donor : null; // claude/git source when present
57
+ const donorAuth = creds.donor != null && vms[creds.donor] && vms[creds.donor].providerAuth;
58
+ const seededAccount = (donorAuth && donorAuth.account && donorAuth.account.email) || '';
59
+ // Build the creds-seeding steps for the resolved mode. The "donor" path is the
60
+ // unchanged default (container→container claude + host-file git from a live
61
+ // VM); host-stash / token / none are the donor-less fallback. `add-degraded`
62
+ // (modes token/none — no claude auth source) is flagged after startJob below.
63
+ // The shell-string builder lives in npm/launch-config.js so it's unit-testable
64
+ // (see launch-config.test.js::buildCredsSteps).
65
+ const credsSteps = lc.buildCredsSteps({
66
+ mode: creds.mode,
67
+ refInst,
68
+ inst,
69
+ container: CONTAINER,
70
+ gcloud: GCLOUD,
71
+ zone: ZONE,
72
+ n,
73
+ claudeCredsSrc: CLAUDE_CREDS_SRC,
74
+ account: seededAccount,
75
+ });
76
+ // Fall back loudly, never silently: the operator asked for an account no
77
+ // reachable VM holds, so say which account was requested and what was seeded.
78
+ if (creds.requestedAccountUnavailable) {
79
+ const reason = lc.accountFallbackMessage({
80
+ requested: requestedAccount,
81
+ seeded: seededAccount,
82
+ mode: creds.mode,
83
+ donor: creds.donor,
84
+ inst,
85
+ });
86
+ credsSteps.unshift(`echo ${shq(reason)}`);
87
+ }
88
+ credsSteps.unshift(...donorSplit.skipped.map((d) => lc.donorSkippedLine(d)));
89
+ return { creds, credsSteps };
90
+ }
91
+
92
+ // The provision script startJob runs for an add. `exitMarker` / `exitMarkerTmp`
93
+ // are the durable add-<n>.exit paths, which the server clears before calling.
94
+ function buildAddVmScript(opts) {
95
+ const {
96
+ n, cfg, inst, port, previewPort, appPort, upArgs, proj, tunnelCmd, credsSteps,
97
+ exitMarker, exitMarkerTmp, resolveProjectName, REPO_ROOT, GCLOUD, ZONE, CONTAINER,
98
+ WIRING_AWAIT_SEC, GIT_RESEED_BODY, shq,
99
+ } = opts;
100
+ // Make git creds live in the running container. Both the donor host-file copy
101
+ // and cloud:up's token write land on the read-only STAGING mount but DON'T
102
+ // touch the in-memory `cache` daemon (seeded from the empty placeholder at
103
+ // container boot) — so git auth stays inert until re-seeded. Mirror the
104
+ // entrypoint seed (scripts/cloud-editor-entrypoint.sh) live; base64-framed so
105
+ // the multi-line snippet survives the gcloud-ssh + docker-exec quoting layers.
106
+ // `-s` self-skips when STAGING is still the empty placeholder (mode none).
107
+ const gitReseedB64 = Buffer.from(GIT_RESEED_BODY, 'utf8').toString('base64');
108
+ const gitReseedStep = `echo ${gitReseedB64} | ${GCLOUD} compute ssh ${inst} --tunnel-through-iap --zone=${ZONE} --command='docker exec -i ${CONTAINER} bash -c "base64 -d | bash"' 2>/dev/null || echo "WARN git cache re-seed exec failed"`;
109
+ const script = [
110
+ `CY_ADD_EXIT_MARKER=${shq(exitMarker)}`,
111
+ `CY_ADD_EXIT_MARKER_TMP=${shq(exitMarkerTmp)}`,
112
+ `trap 'cy_code=$?; printf "%s\\n%s\\n" "$cy_code" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$CY_ADD_EXIT_MARKER_TMP" 2>/dev/null && mv -f "$CY_ADD_EXIT_MARKER_TMP" "$CY_ADD_EXIT_MARKER" 2>/dev/null; exit $cy_code' EXIT`,
113
+ `cd ${shq(REPO_ROOT)}`,
114
+ `echo "provisioning ${inst} — GCP project ${resolveProjectName()} — project ${proj}, editor ${cfg.editorVariant}${cfg.editorBranch ? '@' + cfg.editorBranch : ''} (cloud:up — image build can take ~15-25 min under fleet load)..."`,
115
+ // Background cloud:up so we can poll the container on our own cadence while
116
+ // the image build runs (~15-25 min). Under --no-tunnel it exits cleanly the
117
+ // instant the container is up — the wait below treats that exit as normal.
118
+ `npm run cloud:up -- ${upArgs} > /tmp/dash-add-vm-${n}.log 2>&1 &`,
119
+ `UP_PID=$!`,
120
+ // Wait up to ~28 min of real time for the editor container — the cold image
121
+ // build is the long pole and gets slower as the fleet grows. The gate (pure,
122
+ // tested: lc.containerGateCmd) treats ANY single `Up` as terminal success
123
+ // (mirrors lc.classifyContainerWait); a clean cloud:up exit is the NORMAL
124
+ // --no-tunnel success path (fragility #33) while a NON-ZERO exit fails fast
125
+ // after one re-check; a transient gcloud-ssh blip is retried; and failure
126
+ // past the deadline tells a vanished instance from a stuck build, then reaps
127
+ // the `node cloud.js up` grandchild $UP_PID doesn't cover (anchored at
128
+ // `node`, so it can't match THIS script's own `sh -c` wrapper).
129
+ //
130
+ // It probes on a WIDENING cadence (10s → 30s → 60s), not every 10s: each
131
+ // probe is a `gcloud compute ssh`, i.e. an oslogin read against the
132
+ // fleet-wide 60/min per-user quota, and the flat 10s loop spent 6/min per
133
+ // in-flight add on that question alone — invisible to hostOpLimiter, which
134
+ // only sees the dashboard's own calls. A quota refusal on a probe prints a
135
+ // CY_API_QUOTA line startJob's output tap journals.
136
+ lc.containerGateCmd({ gcloud: GCLOUD, inst, zone: ZONE, probeCmd: tunnelCmd, logPath: `/tmp/dash-add-vm-${n}.log` }),
137
+ // Container gate passed: the instance exists and the editor container is Up.
138
+ // Announce it so the dashboard rosters this VM NOW rather than waiting for
139
+ // the whole script to exit 0. Rostering here is what breaks the deadlock
140
+ // that stranded VM-4..7 on 2026-07-20: everything below (creds, tunnels, the
141
+ // awaited LB wiring) can still fail, but a rostered VM stays VISIBLE to
142
+ // `reconcile-ingress`, which skips unrostered numbers — so the periodic
143
+ // sweep can finish the wiring instead of the VM becoming invisible and
144
+ // burning its bootstrap-recovery budget on a probe that can never pass
145
+ // without ingress. See npm/fleet-early-roster.js.
146
+ `echo ${shq(fer.rosterMarkerLine(n))}`,
147
+ // Container is up — but cloud:up is NOT done. wireVmIngressOnAdd runs as a
148
+ // TAIL step of cloud:up (the slow EXTERNAL_MANAGED backend-services create +
149
+ // NEG attach + url-map host-rule), AFTER the container gate. The old code
150
+ // reaped cloud:up here (`kill $UP_PID`) 15s after the container was Up, on the
151
+ // false premise that `--no-tunnel` had "already exited" — that SIGTERM'd
152
+ // `backend-services create` mid-flight and stranded the VM backendless at 503
153
+ // until the self-heal sweep recovered it. So DON'T kill cloud:up here: just
154
+ // settle for in-flight seeds + sweep any stray :14199 tunnel before we open
155
+ // ours. The wiring is AWAITED (lc.awaitWiringCmd) at the end of the script,
156
+ // after the creds/tunnel steps — which don't depend on it — have run.
157
+ // The sweep is anchored at the ssh executable (lc.editorForwardSweepCmd).
158
+ // Bare, `pkill -f "14199:127.0.0.1:14199"` matched this script's OWN
159
+ // `sh -c '<add script>'` wrapper — the script text contains this line — and
160
+ // every concurrent sibling add wrapper, SIGTERMing them 15s after the
161
+ // container gate passed. That killed the EXIT trap that writes
162
+ // add-<n>.exit, orphaned the detached cloud:up, and painted a red
163
+ // "Add failed" on healthy VMs (2026-07-31, VM-3..8).
164
+ `sleep 15; ${lc.editorForwardSweepCmd({ localPort: 14199 })}`,
165
+ // Seed claude + git auth: donor copy (default) or the donor-less fallback.
166
+ ...credsSteps,
167
+ // Activate git creds in the running container (host-file/STAGING write alone
168
+ // is inert against the booted cache daemon).
169
+ gitReseedStep,
170
+ // open the localhost tunnels (nohup; watchdog maintains them) as TWO
171
+ // INDEPENDENT ssh processes: the editor reachability forward (14299+N ->
172
+ // VM:14199) and a sibling Live Preview forward (15172+N -> VM:<appPort>)
173
+ // that the cloudflared-monitor fronts with a public "<N>-preview" tunnel.
174
+ // Splitting them is load-bearing — a preview-port re-wire then restarts ONLY
175
+ // the preview process, so the editor :14199 forward (the dashboard's
176
+ // reachability signal) stays up and the probe can't blip a false
177
+ // "unreachable". Separate log files keep each tunnel independently inspectable.
178
+ lc.editorTunnelCmd({ gcloud: GCLOUD, inst, zone: ZONE, editorPort: port, n }),
179
+ lc.previewTunnelCmd({ gcloud: GCLOUD, inst, zone: ZONE, previewPort, appPort, n }),
180
+ // Tell the editor's process-global provider-auth cache to forget the
181
+ // cold-boot probe's NeedsLogin verdict now that credsSteps has dropped a
182
+ // fresh .credentials.json onto the VM's /root/.claude/. Without this hop,
183
+ // /api/config keeps serving needs-login (UI shows ProviderAuthGate, Build
184
+ // tab locked) until the editor server is restarted — even though the
185
+ // creds are valid. Fail-soft: a few retries cover the nohup tunnel still
186
+ // settling; the manual restart-server workaround remains the fallback.
187
+ `echo "invalidating providerAuth cache on VM-${n}..."`,
188
+ `for i in 1 2 3 4 5; do if curl -sS -m 3 -X POST http://localhost:${port}/api/auth/refresh-cache >/dev/null 2>&1; then echo "providerAuth cache invalidated on VM-${n}"; break; fi; sleep 2; if [ "$i" = "5" ]; then echo "WARN providerAuth cache refresh failed on VM-${n} — restart editor if Build tab shows Connect"; fi; done`,
189
+ // Await cloud:up's slow wiring tail (started AFTER the container gate) rather
190
+ // than reaping it the instant the container was Up — the mid-`backend-services
191
+ // create` SIGTERM is the add-time 503 strand this closes. Waits for cloud:up to
192
+ // exit on its own (wiring done, success OR loud-fail) up to WIRING_AWAIT_SEC,
193
+ // force-reaping only past the deadline (the dashboard self-heal is the backstop).
194
+ // Placed here, after the creds/tunnel steps that don't depend on wiring, so the
195
+ // CY_DEGRADED grep below reads the FINAL wiring outcome — not a mid-flight log.
196
+ lc.awaitWiringCmd({ inst, deadlineSec: WIRING_AWAIT_SEC }),
197
+ // Republish any CY_DEGRADED marker cloud:up emitted (LB ingress wiring failed
198
+ // — the VM7-10 case) from its private log onto THIS job's stdout, where the
199
+ // completion scanner (finish()) reads markers from `buf`. Without this hop the
200
+ // marker stays buried in /tmp/dash-add-vm-N.log and the card never goes amber.
201
+ `grep -h '^CY_DEGRADED ' /tmp/dash-add-vm-${n}.log 2>/dev/null || true`,
202
+ // "+ wired" must be EARNED, not asserted: the unwired-but-green VM7-10 log
203
+ // came from claiming "+ wired" unconditionally here. State the provisioned
204
+ // fact only; the ingress-not-wired marker above carries the wiring state.
205
+ `echo "VM-${n} provisioned on localhost:${port} (preview localhost:${previewPort})"`,
206
+ ].join('\n');
207
+ return script;
208
+ }
209
+
210
+ module.exports = { resolveAddCreds, buildAddVmScript };
@@ -158,7 +158,15 @@ function buildVmEnvelope(v, ctx) {
158
158
  // decides manually whether to release it).
159
159
  headHolderUnreachable: !!ctx.headHolderUnreachable,
160
160
  queuePos: ctx.queuePos != null ? ctx.queuePos : null,
161
- queueLen: ctx.queueLen,
161
+ // The fleet queue's length when this VM is in it, else its own branch
162
+ // queue's length, else null (npm/fleet-stuck-position.js::resolveQueueLen).
163
+ queueLen: ctx.queueLen != null ? ctx.queueLen : null,
164
+ // This VM's OWN branch commit queue, read from its /api/branch-queue/status:
165
+ // { branch, position, length, inQueue, holdsHead, blocked, headMachine,
166
+ // headSuspect }, or null when the VM did not answer. Distinct from
167
+ // queuePos/isHead, which describe the fleet's pinned-branch queue only, so a
168
+ // VM on a client branch can say "position 4 of 5 on rapid-cad-7".
169
+ branchQueue: v.branchQueue || null,
162
170
  job: ctx.job || null,
163
171
  config: ctx.config || null,
164
172
  project: ctx.project != null ? ctx.project : null,
@@ -193,7 +201,7 @@ function buildVmEnvelope(v, ctx) {
193
201
  startedAt: v.startedAt || null,
194
202
  featureStartedAt: v.featureStartedAt || null,
195
203
  // The server-computed stuck / needs-you verdict (npm/fleet-stuck-status.js):
196
- // { stepElapsedMs, level: 'ok'|'slow'|'stuck', needsYou, needsYouElapsedMs }.
204
+ // { stepElapsedMs, level: 'ok'|'slow'|'stuck'|'queued', needsYou, needsYouElapsedMs }.
197
205
  // The authoritative level only changes on poll; the card colors the step
198
206
  // tenure badge by it. null when buildData couldn't compute one.
199
207
  attention: ctx.attention || null,
@@ -240,7 +248,7 @@ const REQUIRED_CARD_FIELDS = [
240
248
  'sessionIdleSeconds', 'sessionActivity', 'sessionWedged', 'brokerLive', 'brokerStatus',
241
249
  'awaitingInput', 'needsCleanup', 'feature', 'selectedPlan', 'step', 'mode', 'label', 'cycleClosed', 'status', 'orphan',
242
250
  'degradedReasons', 'setupSteps', 'attentionReasons',
243
- 'isHead', 'headHolderUnreachable', 'queuePos', 'queueLen',
251
+ 'isHead', 'headHolderUnreachable', 'queuePos', 'queueLen', 'branchQueue',
244
252
  'job', 'config', 'project', 'variant', 'cardIdentity', 'brokerHistory',
245
253
  'brokerDeath', 'providerAuth', 'previewMisconfigured', 'previewEndpoint', 'datastore', 'effects', 'currentProvider',
246
254
  'readiness', 'backendHealth', 'vmAccess',