Skip to content

feat: multi-instance management (#109)#118

Merged
fedorovvvv merged 7 commits into
developfrom
feature/issue-109-multi-instance
May 8, 2026
Merged

feat: multi-instance management (#109)#118
fedorovvvv merged 7 commits into
developfrom
feature/issue-109-multi-instance

Conversation

@fedorovvvv
Copy link
Copy Markdown
Collaborator

Closes #109. Sub-issues #110, #111, #112, #113, #114, #115 all closed.

Summary

Refactor @forgeplan/web for multi-instance use:

  1. citty-based CLIbin/forgeplan-web.mjs shrinks 367→5 lines; subcommands live in bin/commands/{init,update,start}.mjs; helpers in bin/lib/. Single allow-listed dep (citty@0.2.2) per amended rule 23.
  2. --scope user|project flag — init writes to <cwd>/.forgeplan-web/ or ~/.forgeplan-web/. init -y keeps defaulting to project (compat); interactive prompt picks user as the new default. Non-TTY without flag fails clearly.
  3. start resolution chain — project → user → prompt-init. Explicit --scope short-circuits the chain.
  4. Global instance registry at ~/.forgeplan-web/instances.json — incremental ports, pid + heartbeat liveness, atomic tmp+rename writes, stale sweep on every start. New /api/instances readonly endpoint mirrors the registry to the UI.
  5. Combobox primitive in template/src/shared/ui/combobox/ (bits-ui 2.18.1; default + mono variants × sm/md sizes; showcased on /playground; rule-24 clean).
  6. HealthBar instance switcher — when ≥2 instances are running, the project label becomes a Combobox; selecting another instance navigates the browser to http://host:port. Single-instance behavior unchanged.

Why

Original issue: install forgeplan-web once per user (not per project), run several instances simultaneously, switch between them from any instance's UI. Refined plan keeps backwards compat (--scope project is still the init -y default) so existing setups don't break.

Forgeplan artifacts (all active, R_eff > 0)

  • ADR-003 — amend rule 23 to permit citty in bin/
  • ADR-004 — registry path + JSON format + atomic writes
  • PRD-024 + RFC-020 — citty integration
  • PRD-025 + RFC-021 — --scope flag + user-scope path
  • PRD-026 + RFC-022 — start resolution chain
  • PRD-027 + RFC-023 + SPEC-003 — instance registry
  • PRD-028 + RFC-024 — Combobox primitive
  • PRD-029 + RFC-025 — HealthBar switcher
  • EVID-029..034 — smoke + svelte-check + manual scenarios per PRD

Rules touched (each via dedicated PRD/ADR):

  • .claude/rules/20-init-host-isolation.md — user-scope writes are gitignore-silent (PRD-025)
  • .claude/rules/22-readonly-proxy.md/api/instances allow-list extension (PRD-027)
  • .claude/rules/23-bin-zero-deps.md — citty named exception (ADR-003)

Test plan

  • npm run smoke — PASS at every wave (citty refactor, scope wiring, registry, HealthBar)
  • npm run build — svelte-check 1056 files / 0 errors / 0 warnings
  • Manual: init -y --scope user writes only to ~/.forgeplan-web/, host .gitignore untouched
  • Manual: init -y (no flag) defaults to project (compat preserved)
  • Manual: Two instances on 5174 + 5175 (or first free pair); both register; SIGKILL on one swept on next start; heartbeat ticks at ~30s; deregister within 2s of SIGTERM
  • Manual: HealthBar shows Combobox when ≥2 instances; single-instance fallback verified
  • Manual: /api/instances returns standard envelope { ok, data: { instances }, cmd: "registry:read" }
  • forgeplan health clean post-Wave-7
  • Rule 23 verifier exits 0 (only citty + node:* + relative siblings in bin/)
  • Rule 24 grep clean (no upper-layer :global() reaches into Combobox internals)
  • CI matrix (ubuntu/macos/windows × Node 22) — runs on PR open

Out of scope (deferred follow-ups)

  • Cross-host federation (talking to instances on other machines)
  • Auth / per-instance access control
  • Migration tool for existing project-scope .forgeplan-web/ → user-scope
  • Full XDG compliance (XDG_CONFIG_HOME etc.) — RFC-021 keeps ~/.forgeplan-web/ for predictability
  • Smoke coverage for --scope user path + multi-instance scenario (current smoke covers single project-scope instance only)

🤖 Generated with Claude Code

fedorovvvv and others added 7 commits May 8, 2026 23:29
Decompose GitHub issue #109 (multi-instance management) into 6 sub-issues
(#110-#115) and a full Forgeplan artifact set. All 15 artifacts pass
forgeplan validate with 0 MUST errors; 14 cross-references wired.

Refs: PRD-024, PRD-025, PRD-026, PRD-027, PRD-028, PRD-029, ADR-003,
ADR-004, SPEC-003, RFC-020, RFC-021, RFC-022, RFC-023, RFC-024, RFC-025

Also deprecates EVID-007 + EVID-008 (100% duplicates of EVID-004 + EVID-005).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refactor the 367-line bin/forgeplan-web.mjs into a 5-line entry +
bin/cli.mjs (root command) + bin/commands/{init,update,start}.mjs (one
file per subcommand) + bin/lib/{config,gitignore,forgeplan-binary}.mjs
(shared helpers). citty@0.2.2 provides defineCommand / runMain / typed
args / auto-help / subcommand routing.

Behavior preserved 1:1 — every flag (-y, --force, --quiet,
--no-gitignore, --experimental, --no-experimental) and every alias
(upgrade, serve, run) still works. New --scope user|project flag is
declared on init/update/start as a placeholder (TODO(109b)) for the
next wave to wire.

Rule 23 amended to permit citty as the single named exception
(verification snippet updated). npm ls --omit=dev shows only
citty@0.2.2 (no transitive deps in this version).

Smoke test (3-OS gate) passes. PASS line confirmed locally.

Refs: PRD-024, RFC-020, ADR-003
Closes: #110

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
109b — init now resolves scope from --scope flag, -y default (project),
or interactive readline prompt (default user in TTY; --scope required
in non-TTY). User scope writes to ~/.forgeplan-web/ and skips host
.gitignore. Project scope behavior unchanged.

109c — start follows the resolution chain: explicit --scope wins, else
project (cwd/.forgeplan-web/) → user (~/.forgeplan-web/) → prompt-init
(or fail in non-TTY). update mirrors the same chain.

New helpers: bin/lib/scope.mjs (resolveScope, userScopePath,
projectScopePath, findScaffold) and bin/lib/prompt.mjs (readline-based
TTY prompt — citty 0.2.2 doesn't expose consola; TODO(109b-prompt)
revisits on citty bump). config.mjs gets readScope().

Rule 20 amended: user-scope writes are bounded to ~/.forgeplan-web/
and gitignore-silent. All write-boundary invariants preserved per scope.

Smoke: PASS (project scope path; user-scope and resolution chain
verified via manual scenarios documented in agent report).

Refs: PRD-025, PRD-026, RFC-021, RFC-022
Closes: #111, #112

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Port bits-ui Combobox (v2.18.1) into template/src/shared/ui/combobox/
mirroring the Select primitive shape: 5 sub-components (Combobox,
ComboboxTrigger, ComboboxContent, ComboboxItem, ComboboxInput) +
barrel index.ts. Re-exported from shared/ui/index.ts by name.

Variants: default, mono. Sizes: sm, md. Pure CSS via app.css tokens
(--bg, --bg-1, --bg-2, --fg, --fg-1, --accent, --line, ...) — works
across light/dark/orch themes with no caller intervention.

A11y: role=combobox / aria-expanded / aria-controls on trigger,
role=listbox on content, role=option / aria-selected on items,
focus-visible accent ring matching Select pattern. Keyboard:
arrow-up/down navigation, Enter to select, Escape to close.

/playground showcases all 4 variant×size combinations under the
existing primitive grid; theme toggle in playground header swaps
visuals automatically.

svelte-check: 1053 files / 0 errors / 0 warnings. Rule-24 grep:
clean (no upper-layer reaches into primitive internals). Build green;
dist-experimental at 2.47M (under 3M cap).

Refs: PRD-028, RFC-024
Closes: #114

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
109d — register every running forgeplan-web instance in
~/.forgeplan-web/instances.json so the UI (Wave 6) can list and switch
between them. Single global registry regardless of scaffold scope.

bin/lib/registry.mjs (172 lines) — pure node:* helpers: registryPath,
readRegistry, writeRegistryAtomic (tmp+rename, symlink defence),
isAlive (process.kill(pid,0) + 60s heartbeat freshness, EPERM=alive on
Windows), sweepStale, register, deregister, heartbeat, findFreePort
(increment from PORT base, max +100, exclusive listen probe),
makeInstanceId. Rule 23 clean (citty + node:* + relative siblings only).

bin/commands/start.mjs — port allocator, register child PID,
30s heartbeat (unref'd), SIGINT/SIGTERM forwarding + child-exit
deregister chain. forgeplanCli probed once at start (null on failure).

template/src/shared/server/registry.ts — TypeScript readonly mirror with
2s in-memory cache + single-flight, schema validation per row,
liveness sweep on every refresh. No fs.write*, no spawn.

template/src/routes/api/instances/+server.ts — GET-only endpoint
returning { ok, data: { instances }, cmd: "registry:read" }.

Rule 22 amended to add /api/instances as the second non-forgeplan
endpoint (after /api/update-check), with strict no-mutation +
no-network constraints.

Verified: 2 instances on 17400+17401 register; SIGKILL on instance A
swept at next start; heartbeat ticks at 30.05s delta; deregister within
2s of SIGTERM. svelte-check 0/0/0. Smoke PASS.

Refs: PRD-027, RFC-023, SPEC-003, ADR-004
Closes: #113

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

109f — wire the new Combobox primitive (Wave 4) into HealthBar to switch
between running forgeplan-web instances using the registry endpoint
(Wave 5). Closes the #109 chain.

New entity slice template/src/entities/instance/:
- model/types.ts — Instance, InstanceScope, InstancesPayload (copy of
  SPEC-003 v1; TODO(spec-003-keep-in-sync) marks the duplication so
  schema bumps update both sides)
- api/store.ts — instancePoller via shared/api/createPoller, 5s cadence
- index.ts — barrel

HealthBar.svelte branching logic:
- instances.length >= 2  → Combobox (mono, sm) listing all instances
                          with projectName + host:port; current entry
                          marked; selecting another window.location.replace
- instances.length < 2   → preserve existing /{project} static span

Self-detection via window.location.host matched against instance.id.
SSR-safe (typeof window guard). Rule 24 clean — Combobox styled only via
its own variant/size props; consumer styles are layout/wrapper only.

svelte-check 0/0/0. Smoke PASS. Two-instance scenario manually verified
(5174↔5175 switch); single-instance fallback verified post-deregister.

Refs: PRD-029, RFC-025
Closes: #115

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wave 7 — close-out for the multi-instance migration. Evidence collected
from each Build wave (smoke + svelte-check + manual scenarios) and
linked back to its driving PRD. All R_eff > 0; all 8 artifacts moved
draft → active.

EvidencePacks created:
- EVID-029 → PRD-024 + ADR-003 + RFC-020 (citty refactor; smoke PASS)
- EVID-030 → PRD-025 + RFC-021 + ADR-004 (init scope; non-TTY guard)
- EVID-031 → PRD-026 + RFC-022 (start resolution chain)
- EVID-032 → PRD-027 + RFC-023 + SPEC-003 + ADR-004 (registry;
  2-instance + sweep + heartbeat verified)
- EVID-033 → PRD-028 + RFC-024 (Combobox; 4 variant×size; check 0/0)
- EVID-034 → PRD-029 + RFC-025 (HealthBar switcher; 5174↔5175 + fallback)

Each evidence body carries Structured Fields (verdict: supports,
congruence_level: 3, evidence_type: test) so R_eff parses cleanly.
Activation: PRD-024..029, ADR-003, ADR-004 — all draft → active.
forgeplan health is clean.

Closes the rule 11 gate (R_eff > 0 + active) for #109 PR merge.

Refs: PRD-024, PRD-025, PRD-026, PRD-027, PRD-028, PRD-029,
      ADR-003, ADR-004, EVID-029..034

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@fedorovvvv fedorovvvv enabled auto-merge May 8, 2026 20:26
@fedorovvvv fedorovvvv merged commit a6fcaef into develop May 8, 2026
3 checks passed
@fedorovvvv fedorovvvv deleted the feature/issue-109-multi-instance branch May 8, 2026 20:27
fedorovvvv added a commit that referenced this pull request May 9, 2026
## Summary

- Activate 7 RFCs/SPEC in cluster A whose parent PRDs already shipped
active via #109/#118 (`draft → active`).
- Deprecate stale PRD-001 (title/body mismatch — both interpretations
shipped out-of-band).
- Health: `79 → 86 active`, `16 → 8 draft`, `2 → 3 deprecated`.

## Why

`forgeplan health` was misleading: 7 supporting RFC/SPEC artifacts plus
1 muddled PRD sat in `draft` with parents that were long `active` with
`R_eff > 0`. This was process debt from the `forgeplan activate` step
being skipped at the end of the multi-instance + citty waves. Pure
artifact-lifecycle hygiene — no code change, no behaviour change.

### Cluster A (activated)

| Artifact | Title | Parent (active) |
|---|---|---|
| RFC-020 | citty integration architecture — bin/ subcommand split |
PRD-024 |
| RFC-021 | Scope resolver + user-scope path conventions for init |
PRD-025 |
| RFC-022 | start scaffold-resolution algorithm + error UX |
multi-instance wave |
| RFC-023 | Port allocator, pid-liveness probe, atomic registry writes |
PRD-027 |
| RFC-024 | Combobox primitive — bits-ui wrapper, /playground showcase |
PRD-029 |
| RFC-025 | HealthBar instance switcher + /api/instances readonly
endpoint | PRD-029 |
| SPEC-003 | Instance registry JSON schema
(`~/.forgeplan-web/instances.json`) | PRD-027 + ADR-004 |

### PRD-001 (deprecated)

Title says "Show forgeplan + forgeplan-web versions in UI footer" — long
shipped (`__FORGEPLAN_WEB_VERSION__` + `/api/health` integration). Body
says "Bootstrap CLAUDE.md baseline + add methodological guides" — also
shipped (Red lines, routing table, hook table,
`guides/INDEX.md`/`CLAUDE-MD-GUIDE.ru.md`/`GIT-FLOW-GUIDE.ru.md` all
present).

The artifact's markdown was Lance-only (never in git history);
deprecation surfaced it as a new file at `prds/PRD-001-…md` with
`status: deprecated`. EVID-004/EVID-005 still link to it — they're
frozen evidence, no rework needed.

## Test plan

- [x] `forgeplan health` runs clean — `verdict: healthy`, no new
orphans.
- [x] Front-matter `status:` correct on all 8 files (`active` × 7,
`deprecated` × 1).
- [x] No code touched (verify via `git diff --stat
origin/develop...HEAD`).
- [ ] Reviewer: confirm no parent PRD lost evidence pointer (relations
are append-only in markdown, but worth a glance at the diff).
- [ ] CI: smoke matrix green (no functional change but rule-21/22/23
verifications still run).

## Reversibility

Fully reversible: edit `status:` line back to `draft` in front-matter,
run `forgeplan scan-import` to refresh Lance. No git surgery needed.

## Drift risks

- **Lock contention** — `forgeplan activate` ran in parallel hit the
workspace lock 4× during this PR's prep. The CLI is not concurrency-safe
for one workspace. Future cluster operations should serialise; not
blocking for this PR.
- **PRD-001 dependents** — EVID-004/005 still claim to "support"
PRD-001. Since PRD-001 is now `deprecated`, those evidence packs are
formally orphaned-by-status. No automatic cleanup; leaving them as
historical record.

Refs: PRD-024, PRD-025, PRD-027, PRD-029, ADR-004
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.

1 participant