From 149c38336e480307b02df886b8dbbaa9b600014b Mon Sep 17 00:00:00 2001 From: Alfa Team Date: Sun, 28 Jun 2026 07:50:01 -0400 Subject: [PATCH 1/5] ALFA 009 add team-aware bootstrap --- ...-team-aware-bootstrap_branch-validation.md | 10 + ...aware-bootstrap_manual-validation-notes.md | 63 ++ ...71_ALFA_009-team-aware-bootstrap_report.md | 99 ++ ...m-aware-bootstrap_requirement-checklist.md | 25 + ...-team-aware-bootstrap_validation-report.md | 22 + dev/reports/codex_changed_files.txt | 25 +- dev/reports/codex_review.diff | 936 +++++++++++++++++- dev/scripts/start-dev.mjs | 346 +++++++ dev/scripts/team-port-config.mjs | 89 ++ .../dev-runtime/TeamAwareBootstrap.test.mjs | 197 ++++ package.json | 3 + 11 files changed, 1779 insertions(+), 36 deletions(-) create mode 100644 dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_branch-validation.md create mode 100644 dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md create mode 100644 dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md create mode 100644 dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md create mode 100644 dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md create mode 100644 dev/scripts/start-dev.mjs create mode 100644 dev/scripts/team-port-config.mjs create mode 100644 dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_branch-validation.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_branch-validation.md new file mode 100644 index 000000000..aa96a1077 --- /dev/null +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_branch-validation.md @@ -0,0 +1,10 @@ +# PR_26171_ALFA_009 Branch Validation + +| Check | Status | Evidence | +| --- | --- | --- | +| Failed dirty attempt recorded | PASS | Dirty files listed in PR report before reset. | +| Failed dirty attempt discarded | PASS | Ran `git reset --hard` and `git clean -fd`. | +| Clean `main` confirmed before implementation | PASS | `main...origin/main` was `0 0`; worktree was clean. | +| Branch recreated from `main` | PASS | Current branch is `PR_26171_ALFA_009-team-aware-bootstrap` at source commit `a4e671ec8`. | +| Scope check | PASS | Changes limited to approved bootstrap scripts, package scripts, tests, and reports. | +| Validation | PASS | Targeted validation commands passed. | diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md new file mode 100644 index 000000000..729a5f789 --- /dev/null +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md @@ -0,0 +1,63 @@ +# PR_26171_ALFA_009 Manual Validation Notes + +## Primary Alfa Owner Command + +```powershell +npm run dev:bootstrap -- --team alfa +``` + +Expected diagnostics: + +- `Team: alfa` +- `Role: owner` +- `Web URL: http://127.0.0.1:5510` +- `API URL: http://127.0.0.1:5511/api` + +## Primary Alfa Codex Command + +```powershell +npm run dev:bootstrap -- --team alfa --role codex +``` + +Expected diagnostics: + +- `Team: alfa` +- `Role: codex` +- `Web URL: http://127.0.0.1:5512` +- `API URL: http://127.0.0.1:5513/api` + +## Legacy Command + +```powershell +npm run dev:local-api +``` + +Expected result: + +- Legacy startup remains available. +- The legacy command still runs `node --use-system-ca ./dev/scripts/start-local-api-server.mjs`. + +## Other Startup Commands + +```powershell +npm run dev:bootstrap -- --team owner +npm run dev:bootstrap -- --team bravo +npm run dev:bootstrap -- --team charlie +npm run dev:bootstrap -- --team delta +npm run dev:bootstrap -- --team echo +npm run dev:bootstrap -- --team foxtrot +npm run dev:bootstrap -- --team golf +npm run dev:bootstrap -- --team hotel +``` + +## Failure Checks + +```powershell +npm run dev:bootstrap -- --team omega +npm run dev:bootstrap -- --team alfa --role reviewer +``` + +Expected result: + +- Unknown team fails with the supported team list. +- Unknown role fails with the supported role list. diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md new file mode 100644 index 000000000..3aab1164d --- /dev/null +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md @@ -0,0 +1,99 @@ +# PR_26171_ALFA_009 Team-Aware Bootstrap Report + +## Purpose + +Implement team-aware local dev bootstrap commands with role-aware port assignment while preserving the legacy local API command. + +## Reset Note + +Before rebuilding from clean `main`, the failed dirty attempt included: + +- `dev/reports/codex_changed_files.txt` +- `dev/reports/codex_review.diff` +- `dev/scripts/start-local-api-server.mjs` +- `package.json` +- untracked bootstrap reports +- untracked `dev/scripts/start-dev.mjs` +- untracked `dev/scripts/team-port-config.mjs` +- untracked `dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs` + +Per OWNER instruction, those uncommitted changes were discarded with `git reset --hard` and `git clean -fd`, then this branch was recreated from clean `main`. + +## Implementation Summary + +- Added `npm run dev:bootstrap`. +- Added `npm run dev:api`. +- Added `npm run dev:web`. +- Preserved `npm run dev:local-api` unchanged as the legacy alias. +- Added `dev/scripts/team-port-config.mjs` for full team name and role-aware port resolution. +- Added `dev/scripts/start-dev.mjs` as the bootstrap orchestrator. +- Added focused node tests for team resolution, role offsets, invalid team/role validation, script wiring, `.env` loading behavior, and bootstrap diagnostics. + +## Supported Teams + +`owner`, `alfa`, `bravo`, `charlie`, `delta`, `echo`, `foxtrot`, `golf`, `hotel` + +## Supported Roles + +`owner`, `codex` + +## Port Mapping + +`owner` role uses base ports. `codex` role uses base ports plus 2. + +| Team | Owner Web | Owner API | Codex Web | Codex API | +| --- | ---: | ---: | ---: | ---: | +| owner | 5500 | 5501 | 5502 | 5503 | +| alfa | 5510 | 5511 | 5512 | 5513 | +| bravo | 5520 | 5521 | 5522 | 5523 | +| charlie | 5530 | 5531 | 5532 | 5533 | +| delta | 5540 | 5541 | 5542 | 5543 | +| echo | 5550 | 5551 | 5552 | 5553 | +| foxtrot | 5560 | 5561 | 5562 | 5563 | +| golf | 5570 | 5571 | 5572 | 5573 | +| hotel | 5580 | 5581 | 5582 | 5583 | + +## Commands Added + +```powershell +npm run dev:bootstrap +npm run dev:api +npm run dev:web +``` + +## Legacy Command Preserved + +```powershell +npm run dev:local-api +``` + +## Exact Startup Commands + +```powershell +npm run dev:bootstrap -- --team owner +npm run dev:bootstrap -- --team alfa +npm run dev:bootstrap -- --team bravo +npm run dev:bootstrap -- --team charlie +npm run dev:bootstrap -- --team delta +npm run dev:bootstrap -- --team echo +npm run dev:bootstrap -- --team foxtrot +npm run dev:bootstrap -- --team golf +npm run dev:bootstrap -- --team hotel +``` + +Codex role example: + +```powershell +npm run dev:bootstrap -- --team alfa --role codex +``` + +## Files Changed + +- `package.json` - updated +- `dev/scripts/start-dev.mjs` - added +- `dev/scripts/team-port-config.mjs` - added +- `dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs` - added + +## Validation Summary + +All requested targeted validation passed. diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md new file mode 100644 index 000000000..4d9238295 --- /dev/null +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md @@ -0,0 +1,25 @@ +# PR_26171_ALFA_009 Requirement Checklist + +| Requirement | Status | Evidence | +| --- | --- | --- | +| Start from clean `main` | PASS | Reset failed attempt, switched to `main`, pulled fast-forward, confirmed `main...origin/main` was `0 0`, then recreated branch. | +| Add `npm run dev:bootstrap` | PASS | Added to `package.json`. | +| Keep `npm run dev:local-api` as legacy alias | PASS | Existing command remains unchanged. | +| Add `npm run dev:api` | PASS | Added to `package.json`. | +| Add `npm run dev:web` | PASS | Added to `package.json`. | +| Create `dev/scripts/start-dev.mjs` | PASS | Added orchestrator. | +| Create `dev/scripts/team-port-config.mjs` | PASS | Added team/role port resolver. | +| Support full team names only | PASS | Supported teams are owner, alfa, bravo, charlie, delta, echo, foxtrot, golf, hotel. | +| Support roles owner/codex | PASS | Role resolver supports owner and codex only. | +| Codex role uses base ports plus 2 | PASS | Covered by targeted unit test. | +| Do not use unsupported old team names | PASS | Scan returned no matches in new bootstrap files. | +| Do not use single-letter public team commands | PASS | Scan returned no matches in new bootstrap files. | +| Validate unknown team names | PASS | `omega` fails clearly with supported team list. | +| Validate unknown role names | PASS | `reviewer` fails clearly with supported role list. | +| Load environment | PASS | Orchestrator loads `.env` values without overriding existing environment values. | +| Resolve team ports | PASS | Unit tests cover all teams and both roles. | +| Start Local API | PASS | `dev:bootstrap` starts the existing Local API server on resolved API port. | +| Start web server when requested | PASS | `dev:bootstrap` starts web plus API; `dev:web` mode is wired. | +| Print startup diagnostics | PASS | Diagnostics include mode, team, role, web URL, API URL, environment source, supported teams, and supported roles. | +| Preserve legacy behavior | PASS | `dev:local-api` command unchanged and startup validation passed. | +| No unrelated cleanup | PASS | Scope limited to package scripts, new dev scripts, targeted tests, and reports. | diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md new file mode 100644 index 000000000..1ca55c459 --- /dev/null +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md @@ -0,0 +1,22 @@ +# PR_26171_ALFA_009 Validation Report + +## Validation Results + +| Command | Result | Notes | +| --- | --- | --- | +| `node --check dev/scripts/team-port-config.mjs` | PASS | Syntax valid. | +| `node --check dev/scripts/start-dev.mjs` | PASS | Syntax valid. | +| `node --check dev/scripts/start-local-api-server.mjs` | PASS | Legacy startup entrypoint remains valid. | +| `node --check dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs` | PASS | Test syntax valid. | +| `node ./dev/scripts/run-node-test-files.mjs dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs dev/tests/dev-runtime/LocalApiStartupLogging.test.mjs` | PASS | 2 targeted files passed; 10 subtests passed. | +| Unsupported team token scan in new bootstrap files | PASS | No unsupported old team names or single-letter public team commands found. | +| `npm run validate:canonical-structure` | PASS | Canonical repository structure guardrail passed. | +| `npm run dev:bootstrap -- --team alfa` | PASS | Started with team `alfa`, role `owner`, web `5510`, API `5511`, then process tree was stopped. | +| `npm run dev:bootstrap -- --team alfa --role codex` | PASS | Started with team `alfa`, role `codex`, web `5512`, API `5513`, then process tree was stopped. | +| `npm run dev:bootstrap -- --team omega` | PASS | Failed clearly with supported team list. | +| `npm run dev:bootstrap -- --team alfa --role reviewer` | PASS | Failed clearly with supported role list. | +| `npm run dev:local-api` | PASS | Legacy command started and printed legacy startup diagnostics, then process tree was stopped. | + +## Notes + +The startup-command validations used short-lived process launches and `taskkill /T /F` cleanup after expected diagnostics appeared, so no dev servers were left running. diff --git a/dev/reports/codex_changed_files.txt b/dev/reports/codex_changed_files.txt index afe6337e7..8fde59a92 100644 --- a/dev/reports/codex_changed_files.txt +++ b/dev/reports/codex_changed_files.txt @@ -1 +1,24 @@ -dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS.md +# git status --short +M package.json +?? dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_branch-validation.md +?? dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md +?? dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md +?? dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md +?? dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md +?? dev/scripts/start-dev.mjs +?? dev/scripts/team-port-config.mjs +?? dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs + +# git ls-files --others --exclude-standard +dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_branch-validation.md +dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md +dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md +dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md +dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md +dev/scripts/start-dev.mjs +dev/scripts/team-port-config.mjs +dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs + +# git diff --stat +package.json | 3 +++ + 1 file changed, 3 insertions(+) \ No newline at end of file diff --git a/dev/reports/codex_review.diff b/dev/reports/codex_review.diff index b517e1966..66bab2215 100644 --- a/dev/reports/codex_review.diff +++ b/dev/reports/codex_review.diff @@ -1,47 +1,913 @@ -diff --git a/dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS.md b/dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS.md -index 5b1b8f9a6..c6f5c5744 100644 ---- a/dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS.md -+++ b/dev/build/ProjectInstructions/PROJECT_INSTRUCTIONS.md -@@ -6,9 +6,42 @@ - - Codex and future wrappers must request this file directly. All other Project Instructions documents are loaded indirectly through the references below. - -+## Canonical Source Rule +diff --git a/package.json b/package.json +index 2a6d66c76..65e67bf62 100644 +--- a/package.json ++++ b/package.json +@@ -25,6 +25,9 @@ + "test:lane:integration": "node ./dev/scripts/run-targeted-test-lanes.mjs --lane integration", + "test:lane:engine-src": "node ./dev/scripts/run-targeted-test-lanes.mjs --lane engine-src", + "test:lane:samples": "node ./dev/scripts/run-targeted-test-lanes.mjs --lane samples --include-samples", ++ "dev:bootstrap": "node --use-system-ca ./dev/scripts/start-dev.mjs --mode bootstrap", ++ "dev:api": "node --use-system-ca ./dev/scripts/start-dev.mjs --mode api", ++ "dev:web": "node ./dev/scripts/start-dev.mjs --mode web", + "dev:local-api": "node --use-system-ca ./dev/scripts/start-local-api-server.mjs", + "validate:local-postgres-runtime": "node --use-system-ca ./dev/scripts/validate-local-postgres-runtime.mjs", + "validate:database-drift": "node ./dev/scripts/validate-database-drift.mjs", +diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_branch-validation.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_branch-validation.md +new file mode 100644 +index 000000000..aa96a1077 +--- /dev/null ++++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_branch-validation.md +@@ -0,0 +1,10 @@ ++# PR_26171_ALFA_009 Branch Validation + -+The repository is the canonical source of truth for project rules. ++| Check | Status | Evidence | ++| --- | --- | --- | ++| Failed dirty attempt recorded | PASS | Dirty files listed in PR report before reset. | ++| Failed dirty attempt discarded | PASS | Ran `git reset --hard` and `git clean -fd`. | ++| Clean `main` confirmed before implementation | PASS | `main...origin/main` was `0 0`; worktree was clean. | ++| Branch recreated from `main` | PASS | Current branch is `PR_26171_ALFA_009-team-aware-bootstrap` at source commit `a4e671ec8`. | ++| Scope check | PASS | Changes limited to approved bootstrap scripts, package scripts, tests, and reports. | ++| Validation | PASS | Targeted validation commands passed. | +diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md +new file mode 100644 +index 000000000..729a5f789 +--- /dev/null ++++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md +@@ -0,0 +1,63 @@ ++# PR_26171_ALFA_009 Manual Validation Notes + -+Codex, ChatGPT, wrappers, and team operators must use the current repository documentation under `dev/build/ProjectInstructions/` as authority for active rules. Prior chat history, remembered conversation context, generated reports, archived documents, local notes, or stale external instructions must not override repository documentation. ++## Primary Alfa Owner Command + -+If current chat instructions conflict with repository documentation, Codex must follow the active repository documentation or HARD STOP and request OWNER direction. ++```powershell ++npm run dev:bootstrap -- --team alfa ++``` ++ ++Expected diagnostics: ++ ++- `Team: alfa` ++- `Role: owner` ++- `Web URL: http://127.0.0.1:5510` ++- `API URL: http://127.0.0.1:5511/api` ++ ++## Primary Alfa Codex Command ++ ++```powershell ++npm run dev:bootstrap -- --team alfa --role codex ++``` ++ ++Expected diagnostics: ++ ++- `Team: alfa` ++- `Role: codex` ++- `Web URL: http://127.0.0.1:5512` ++- `API URL: http://127.0.0.1:5513/api` ++ ++## Legacy Command ++ ++```powershell ++npm run dev:local-api ++``` ++ ++Expected result: + -+## Canonical Team Naming ++- Legacy startup remains available. ++- The legacy command still runs `node --use-system-ca ./dev/scripts/start-local-api-server.mjs`. + -+Canonical active teams: ++## Other Startup Commands + -+- Owner -+- Alfa -+- Bravo -+- Charlie -+- Golf ++```powershell ++npm run dev:bootstrap -- --team owner ++npm run dev:bootstrap -- --team bravo ++npm run dev:bootstrap -- --team charlie ++npm run dev:bootstrap -- --team delta ++npm run dev:bootstrap -- --team echo ++npm run dev:bootstrap -- --team foxtrot ++npm run dev:bootstrap -- --team golf ++npm run dev:bootstrap -- --team hotel ++``` + -+Official NATO spellings: ++## Failure Checks + -+```text -+Alfa, Bravo, Charlie, Delta, Echo, Foxtrot, Golf, Hotel, India, Juliett, Kilo, Lima, Mike, November, Oscar, Papa, Quebec, Romeo, Sierra, Tango, Uniform, Victor, Whiskey, Xray, Yankee, Zulu. ++```powershell ++npm run dev:bootstrap -- --team omega ++npm run dev:bootstrap -- --team alfa --role reviewer +``` + -+Rules: ++Expected result: ++ ++- Unknown team fails with the supported team list. ++- Unknown role fails with the supported role list. +diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md +new file mode 100644 +index 000000000..3aab1164d +--- /dev/null ++++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md +@@ -0,0 +1,99 @@ ++# PR_26171_ALFA_009 Team-Aware Bootstrap Report ++ ++## Purpose ++ ++Implement team-aware local dev bootstrap commands with role-aware port assignment while preserving the legacy local API command. ++ ++## Reset Note ++ ++Before rebuilding from clean `main`, the failed dirty attempt included: ++ ++- `dev/reports/codex_changed_files.txt` ++- `dev/reports/codex_review.diff` ++- `dev/scripts/start-local-api-server.mjs` ++- `package.json` ++- untracked bootstrap reports ++- untracked `dev/scripts/start-dev.mjs` ++- untracked `dev/scripts/team-port-config.mjs` ++- untracked `dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs` ++ ++Per OWNER instruction, those uncommitted changes were discarded with `git reset --hard` and `git clean -fd`, then this branch was recreated from clean `main`. ++ ++## Implementation Summary ++ ++- Added `npm run dev:bootstrap`. ++- Added `npm run dev:api`. ++- Added `npm run dev:web`. ++- Preserved `npm run dev:local-api` unchanged as the legacy alias. ++- Added `dev/scripts/team-port-config.mjs` for full team name and role-aware port resolution. ++- Added `dev/scripts/start-dev.mjs` as the bootstrap orchestrator. ++- Added focused node tests for team resolution, role offsets, invalid team/role validation, script wiring, `.env` loading behavior, and bootstrap diagnostics. ++ ++## Supported Teams ++ ++`owner`, `alfa`, `bravo`, `charlie`, `delta`, `echo`, `foxtrot`, `golf`, `hotel` ++ ++## Supported Roles ++ ++`owner`, `codex` ++ ++## Port Mapping ++ ++`owner` role uses base ports. `codex` role uses base ports plus 2. ++ ++| Team | Owner Web | Owner API | Codex Web | Codex API | ++| --- | ---: | ---: | ---: | ---: | ++| owner | 5500 | 5501 | 5502 | 5503 | ++| alfa | 5510 | 5511 | 5512 | 5513 | ++| bravo | 5520 | 5521 | 5522 | 5523 | ++| charlie | 5530 | 5531 | 5532 | 5533 | ++| delta | 5540 | 5541 | 5542 | 5543 | ++| echo | 5550 | 5551 | 5552 | 5553 | ++| foxtrot | 5560 | 5561 | 5562 | 5563 | ++| golf | 5570 | 5571 | 5572 | 5573 | ++| hotel | 5580 | 5581 | 5582 | 5583 | ++ ++## Commands Added ++ ++```powershell ++npm run dev:bootstrap ++npm run dev:api ++npm run dev:web ++``` ++ ++## Legacy Command Preserved ++ ++```powershell ++npm run dev:local-api ++``` ++ ++## Exact Startup Commands ++ ++```powershell ++npm run dev:bootstrap -- --team owner ++npm run dev:bootstrap -- --team alfa ++npm run dev:bootstrap -- --team bravo ++npm run dev:bootstrap -- --team charlie ++npm run dev:bootstrap -- --team delta ++npm run dev:bootstrap -- --team echo ++npm run dev:bootstrap -- --team foxtrot ++npm run dev:bootstrap -- --team golf ++npm run dev:bootstrap -- --team hotel ++``` ++ ++Codex role example: ++ ++```powershell ++npm run dev:bootstrap -- --team alfa --role codex ++``` ++ ++## Files Changed ++ ++- `package.json` - updated ++- `dev/scripts/start-dev.mjs` - added ++- `dev/scripts/team-port-config.mjs` - added ++- `dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs` - added ++ ++## Validation Summary ++ ++All requested targeted validation passed. +diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md +new file mode 100644 +index 000000000..4d9238295 +--- /dev/null ++++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md +@@ -0,0 +1,25 @@ ++# PR_26171_ALFA_009 Requirement Checklist ++ ++| Requirement | Status | Evidence | ++| --- | --- | --- | ++| Start from clean `main` | PASS | Reset failed attempt, switched to `main`, pulled fast-forward, confirmed `main...origin/main` was `0 0`, then recreated branch. | ++| Add `npm run dev:bootstrap` | PASS | Added to `package.json`. | ++| Keep `npm run dev:local-api` as legacy alias | PASS | Existing command remains unchanged. | ++| Add `npm run dev:api` | PASS | Added to `package.json`. | ++| Add `npm run dev:web` | PASS | Added to `package.json`. | ++| Create `dev/scripts/start-dev.mjs` | PASS | Added orchestrator. | ++| Create `dev/scripts/team-port-config.mjs` | PASS | Added team/role port resolver. | ++| Support full team names only | PASS | Supported teams are owner, alfa, bravo, charlie, delta, echo, foxtrot, golf, hotel. | ++| Support roles owner/codex | PASS | Role resolver supports owner and codex only. | ++| Codex role uses base ports plus 2 | PASS | Covered by targeted unit test. | ++| Do not use unsupported old team names | PASS | Scan returned no matches in new bootstrap files. | ++| Do not use single-letter public team commands | PASS | Scan returned no matches in new bootstrap files. | ++| Validate unknown team names | PASS | `omega` fails clearly with supported team list. | ++| Validate unknown role names | PASS | `reviewer` fails clearly with supported role list. | ++| Load environment | PASS | Orchestrator loads `.env` values without overriding existing environment values. | ++| Resolve team ports | PASS | Unit tests cover all teams and both roles. | ++| Start Local API | PASS | `dev:bootstrap` starts the existing Local API server on resolved API port. | ++| Start web server when requested | PASS | `dev:bootstrap` starts web plus API; `dev:web` mode is wired. | ++| Print startup diagnostics | PASS | Diagnostics include mode, team, role, web URL, API URL, environment source, supported teams, and supported roles. | ++| Preserve legacy behavior | PASS | `dev:local-api` command unchanged and startup validation passed. | ++| No unrelated cleanup | PASS | Scope limited to package scripts, new dev scripts, targeted tests, and reports. | +diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md +new file mode 100644 +index 000000000..1ca55c459 +--- /dev/null ++++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md +@@ -0,0 +1,22 @@ ++# PR_26171_ALFA_009 Validation Report ++ ++## Validation Results ++ ++| Command | Result | Notes | ++| --- | --- | --- | ++| `node --check dev/scripts/team-port-config.mjs` | PASS | Syntax valid. | ++| `node --check dev/scripts/start-dev.mjs` | PASS | Syntax valid. | ++| `node --check dev/scripts/start-local-api-server.mjs` | PASS | Legacy startup entrypoint remains valid. | ++| `node --check dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs` | PASS | Test syntax valid. | ++| `node ./dev/scripts/run-node-test-files.mjs dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs dev/tests/dev-runtime/LocalApiStartupLogging.test.mjs` | PASS | 2 targeted files passed; 10 subtests passed. | ++| Unsupported team token scan in new bootstrap files | PASS | No unsupported old team names or single-letter public team commands found. | ++| `npm run validate:canonical-structure` | PASS | Canonical repository structure guardrail passed. | ++| `npm run dev:bootstrap -- --team alfa` | PASS | Started with team `alfa`, role `owner`, web `5510`, API `5511`, then process tree was stopped. | ++| `npm run dev:bootstrap -- --team alfa --role codex` | PASS | Started with team `alfa`, role `codex`, web `5512`, API `5513`, then process tree was stopped. | ++| `npm run dev:bootstrap -- --team omega` | PASS | Failed clearly with supported team list. | ++| `npm run dev:bootstrap -- --team alfa --role reviewer` | PASS | Failed clearly with supported role list. | ++| `npm run dev:local-api` | PASS | Legacy command started and printed legacy startup diagnostics, then process tree was stopped. | ++ ++## Notes ++ ++The startup-command validations used short-lived process launches and `taskkill /T /F` cleanup after expected diagnostics appeared, so no dev servers were left running. +diff --git a/dev/scripts/start-dev.mjs b/dev/scripts/start-dev.mjs +new file mode 100644 +index 000000000..625360989 +--- /dev/null ++++ b/dev/scripts/start-dev.mjs +@@ -0,0 +1,346 @@ ++import fs from "node:fs/promises"; ++import { existsSync, readFileSync } from "node:fs"; ++import http from "node:http"; ++import path from "node:path"; ++import process from "node:process"; ++import { fileURLToPath, pathToFileURL } from "node:url"; ++import { localAdminNotesHeaderPartialPath } from "../../src/dev-runtime/admin/admin-notes-menu.mjs"; ++import { startLocalApiServer } from "../../src/dev-runtime/server/local-api-server.mjs"; ++import { ++ parseRoleArgument, ++ parseTeamArgument, ++ resolveTeamPortConfig, ++ supportedBootstrapRolesLabel, ++ supportedBootstrapTeamsLabel, ++} from "./team-port-config.mjs"; ++ ++const repoRoot = fileURLToPath(new URL("../..", import.meta.url)); ++const DEFAULT_HOST = "127.0.0.1"; ++const RUNTIME_ENV_FILE = ".env"; ++const VALID_MODES = Object.freeze(["bootstrap", "api", "web"]); ++const HOP_BY_HOP_HEADERS = new Set([ ++ "connection", ++ "content-length", ++ "keep-alive", ++ "proxy-authenticate", ++ "proxy-authorization", ++ "te", ++ "trailer", ++ "transfer-encoding", ++ "upgrade", ++]); ++ ++function parseRuntimeEnvLine(line) { ++ const trimmed = line.trim(); ++ if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) { ++ return null; ++ } ++ const index = trimmed.indexOf("="); ++ const key = trimmed.slice(0, index).trim(); ++ let value = trimmed.slice(index + 1).trim(); ++ if (!key) { ++ return null; ++ } ++ if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) { ++ value = value.slice(1, -1); ++ } ++ return { key, value }; ++} ++ ++export function loadRuntimeEnv({ ++ cwd = process.cwd(), ++ env = process.env, ++} = {}) { ++ const envPath = path.resolve(cwd, RUNTIME_ENV_FILE); ++ if (!existsSync(envPath)) { ++ return { ++ loaded: false, ++ loadedKeys: 0, ++ }; ++ } ++ let loadedKeys = 0; ++ readFileSync(envPath, "utf8").split(/\r?\n/).forEach((line) => { ++ const parsed = parseRuntimeEnvLine(line); ++ if (!parsed || env[parsed.key] !== undefined) { ++ return; ++ } ++ env[parsed.key] = parsed.value; ++ loadedKeys += 1; ++ }); ++ return { ++ loaded: true, ++ loadedKeys, ++ }; ++} ++ ++function contentTypeForPath(filePath) { ++ const extension = path.extname(filePath).toLowerCase(); ++ if (extension === ".html") return "text/html; charset=utf-8"; ++ if (extension === ".js" || extension === ".mjs") return "text/javascript; charset=utf-8"; ++ if (extension === ".json") return "application/json"; ++ if (extension === ".css") return "text/css; charset=utf-8"; ++ if (extension === ".svg") return "image/svg+xml"; ++ if (extension === ".png") return "image/png"; ++ if (extension === ".jpg" || extension === ".jpeg") return "image/jpeg"; ++ if (extension === ".webp") return "image/webp"; ++ if (extension === ".gif") return "image/gif"; ++ if (extension === ".wav") return "audio/wav"; ++ if (extension === ".mp3") return "audio/mpeg"; ++ if (extension === ".ogg") return "audio/ogg"; ++ if (extension === ".m4a") return "audio/mp4"; ++ if (extension === ".woff2") return "font/woff2"; ++ if (extension === ".woff") return "font/woff"; ++ if (extension === ".ttf") return "font/ttf"; ++ if (extension === ".otf") return "font/otf"; ++ if (extension === ".csv") return "text/csv; charset=utf-8"; ++ if (extension === ".txt") return "text/plain; charset=utf-8"; ++ return "application/octet-stream"; ++} ++ ++function isInsideRepoRoot(absolutePath) { ++ const relativePath = path.relative(repoRoot, absolutePath); ++ return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)); ++} ++ ++function resolveBrowserRoutePath(decodedPath) { ++ const normalizedPath = path.normalize(decodedPath).replace(/^(\.\.[/\\])+/, ""); ++ const webPath = normalizedPath.replace(/\\/g, "/"); ++ if (webPath === "/") { ++ return "/index.html"; ++ } ++ if (webPath === "/tools" || webPath.startsWith("/tools/")) { ++ return `/toolbox${webPath.slice("/tools".length)}`; ++ } ++ if (webPath === "/admin/admin-notes.html") { ++ return "/src/dev-runtime/admin/notes.html"; ++ } ++ return normalizedPath; ++} ++ ++function parseModeArgument(args = []) { ++ const values = Array.from(args); ++ for (let index = 0; index < values.length; index += 1) { ++ const argument = values[index]; ++ if (argument === "--mode") { ++ const value = values[index + 1]; ++ if (!value || value.startsWith("--")) { ++ throw new Error(`Missing bootstrap mode after --mode. Use one of: ${VALID_MODES.join(", ")}.`); ++ } ++ return value.trim().toLowerCase(); ++ } ++ if (argument.startsWith("--mode=")) { ++ const value = argument.slice("--mode=".length).trim().toLowerCase(); ++ if (!value) { ++ throw new Error(`Missing bootstrap mode after --mode=. Use one of: ${VALID_MODES.join(", ")}.`); ++ } ++ return value; ++ } ++ } ++ return "bootstrap"; ++} ++ ++export function parseBootstrapOptions(args = []) { ++ const mode = parseModeArgument(args); ++ if (!VALID_MODES.includes(mode)) { ++ throw new Error(`Unknown bootstrap mode "${mode}". Use one of: ${VALID_MODES.join(", ")}.`); ++ } ++ const role = parseRoleArgument(args); ++ const team = parseTeamArgument(args); ++ const ports = resolveTeamPortConfig({ role, team }); ++ return Object.freeze({ ++ api: mode === "api" || mode === "bootstrap", ++ mode, ++ ports, ++ role: ports.role, ++ team: ports.team, ++ web: mode === "web" || mode === "bootstrap", ++ }); ++} ++ ++function localUrl(host, port) { ++ return `http://${host}:${port}`; ++} ++ ++function apiUrl(host, port) { ++ return `${localUrl(host, port)}/api`; ++} ++ ++export function applyBootstrapEnvironment({ ++ env = process.env, ++ host = DEFAULT_HOST, ++ ports, ++}) { ++ env.GAMEFOUNDRY_LOCAL_API_HOST = env.GAMEFOUNDRY_LOCAL_API_HOST || host; ++ env.GAMEFOUNDRY_LOCAL_API_PORT = String(ports.apiPort); ++ env.GAMEFOUNDRY_API_URL = apiUrl(host, ports.apiPort); ++ env.GAMEFOUNDRY_SITE_URL = localUrl(host, ports.webPort); ++ return env; ++} ++ ++export function formatBootstrapDiagnostics({ ++ apiBaseUrl, ++ mode, ++ role, ++ runtimeEnv, ++ team, ++ webBaseUrl, ++}) { ++ return [ ++ "GameFoundry team-aware dev bootstrap", ++ `Mode: ${mode}`, ++ `Team: ${team}`, ++ `Role: ${role}`, ++ `Web URL: ${webBaseUrl}`, ++ `API URL: ${apiBaseUrl}/api`, ++ "Environment source: .env + process environment", ++ `.env loaded: ${runtimeEnv.loaded ? `yes (${runtimeEnv.loadedKeys} new key(s))` : "no"}`, ++ `Supported teams: ${supportedBootstrapTeamsLabel()}`, ++ `Supported roles: ${supportedBootstrapRolesLabel()}`, ++ "Legacy API alias remains: npm run dev:local-api", ++ "Press Ctrl+C to stop.", ++ ]; ++} ++ ++async function readRequestBody(request) { ++ const chunks = []; ++ for await (const chunk of request) { ++ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); ++ } ++ return chunks.length ? Buffer.concat(chunks) : undefined; ++} ++ ++async function proxyApiRequest(request, response, requestUrl, apiBaseUrl) { ++ const targetUrl = new URL(`${requestUrl.pathname}${requestUrl.search}`, apiBaseUrl); ++ const headers = {}; ++ Object.entries(request.headers).forEach(([key, value]) => { ++ if (!HOP_BY_HOP_HEADERS.has(key.toLowerCase()) && value !== undefined) { ++ headers[key] = value; ++ } ++ }); ++ const method = request.method || "GET"; ++ const upstreamResponse = await fetch(targetUrl, { ++ body: ["GET", "HEAD"].includes(method.toUpperCase()) ? undefined : await readRequestBody(request), ++ headers, ++ method, ++ }); ++ response.statusCode = upstreamResponse.status; ++ upstreamResponse.headers.forEach((value, key) => { ++ if (!HOP_BY_HOP_HEADERS.has(key.toLowerCase())) { ++ response.setHeader(key, value); ++ } ++ }); ++ const body = Buffer.from(await upstreamResponse.arrayBuffer()); ++ response.end(body); ++} ++ ++export async function startStaticWebServer({ ++ apiBaseUrl, ++ host = DEFAULT_HOST, ++ port, ++} = {}) { ++ if (!apiBaseUrl) { ++ throw new Error("startStaticWebServer requires apiBaseUrl."); ++ } ++ const server = http.createServer(async (request, response) => { ++ try { ++ const requestUrl = new URL(request.url || "/", `http://${host}:${port}`); ++ if (requestUrl.pathname === "/api" || requestUrl.pathname.startsWith("/api/")) { ++ await proxyApiRequest(request, response, requestUrl, apiBaseUrl); ++ return; ++ } ++ const decodedPath = decodeURIComponent(requestUrl.pathname); ++ const normalizedPath = resolveBrowserRoutePath(decodedPath); ++ const absolutePath = path.resolve(repoRoot, `.${normalizedPath}`); ++ if (!isInsideRepoRoot(absolutePath)) { ++ response.statusCode = 403; ++ response.end("Forbidden"); ++ return; ++ } ++ let targetPath = absolutePath; ++ const stat = await fs.stat(targetPath).catch(() => null); ++ if (stat && stat.isDirectory()) { ++ targetPath = path.join(targetPath, "index.html"); ++ } ++ targetPath = localAdminNotesHeaderPartialPath(repoRoot, targetPath); ++ const responseContents = await fs.readFile(targetPath); ++ response.statusCode = 200; ++ response.setHeader("Content-Type", contentTypeForPath(targetPath)); ++ response.end(responseContents); ++ } catch { ++ response.statusCode = 404; ++ response.end("Not Found"); ++ } ++ }); ++ ++ await new Promise((resolve, reject) => { ++ server.once("error", reject); ++ server.listen(port, host, resolve); ++ }); ++ ++ return { ++ baseUrl: localUrl(host, port), ++ close: async () => { ++ await new Promise((resolve, reject) => { ++ server.close((error) => { ++ if (error) reject(error); ++ else resolve(); ++ }); ++ }); ++ }, ++ server, ++ }; ++} ++ ++export async function startBootstrapRuntime(options = parseBootstrapOptions(process.argv.slice(2))) { ++ const runtimeEnv = loadRuntimeEnv(); ++ const host = process.env.GAMEFOUNDRY_LOCAL_API_HOST || DEFAULT_HOST; ++ applyBootstrapEnvironment({ host, ports: options.ports }); ++ const apiBaseUrl = localUrl(host, options.ports.apiPort); ++ const webBaseUrl = localUrl(host, options.ports.webPort); ++ const servers = []; ++ ++ if (options.api) { ++ servers.push(await startLocalApiServer({ host, port: options.ports.apiPort })); ++ } ++ if (options.web) { ++ servers.push(await startStaticWebServer({ apiBaseUrl, host, port: options.ports.webPort })); ++ } ++ ++ return { ++ close: async () => { ++ await Promise.all(servers.map((server) => server.close())); ++ }, ++ diagnostics: formatBootstrapDiagnostics({ ++ apiBaseUrl, ++ mode: options.mode, ++ role: options.role, ++ runtimeEnv, ++ team: options.team, ++ webBaseUrl, ++ }), ++ servers, ++ }; ++} ++ ++async function main() { ++ let runtime; ++ try { ++ runtime = await startBootstrapRuntime(); ++ runtime.diagnostics.forEach((line) => console.log(line)); ++ } catch (error) { ++ console.error(error instanceof Error ? error.message : String(error)); ++ process.exitCode = 1; ++ return; ++ } ++ ++ for (const signal of ["SIGINT", "SIGTERM"]) { ++ process.once(signal, async () => { ++ await runtime.close(); ++ process.exit(0); ++ }); ++ } ++} ++ ++if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { ++ await main(); ++} +diff --git a/dev/scripts/team-port-config.mjs b/dev/scripts/team-port-config.mjs +new file mode 100644 +index 000000000..2dfa98fc3 +--- /dev/null ++++ b/dev/scripts/team-port-config.mjs +@@ -0,0 +1,89 @@ ++const TEAM_BASE_PORTS = Object.freeze({ ++ owner: Object.freeze({ apiPort: 5501, webPort: 5500 }), ++ alfa: Object.freeze({ apiPort: 5511, webPort: 5510 }), ++ bravo: Object.freeze({ apiPort: 5521, webPort: 5520 }), ++ charlie: Object.freeze({ apiPort: 5531, webPort: 5530 }), ++ delta: Object.freeze({ apiPort: 5541, webPort: 5540 }), ++ echo: Object.freeze({ apiPort: 5551, webPort: 5550 }), ++ foxtrot: Object.freeze({ apiPort: 5561, webPort: 5560 }), ++ golf: Object.freeze({ apiPort: 5571, webPort: 5570 }), ++ hotel: Object.freeze({ apiPort: 5581, webPort: 5580 }), ++}); ++ ++const ROLE_PORT_OFFSETS = Object.freeze({ ++ owner: 0, ++ codex: 2, ++}); ++ ++export const DEFAULT_BOOTSTRAP_ROLE = "owner"; ++export const DEFAULT_BOOTSTRAP_TEAM = "owner"; ++export const SUPPORTED_BOOTSTRAP_ROLES = Object.freeze(Object.keys(ROLE_PORT_OFFSETS)); ++export const SUPPORTED_BOOTSTRAP_TEAMS = Object.freeze(Object.keys(TEAM_BASE_PORTS)); ++ ++function normalizeToken(value, fallback) { ++ return String(value || fallback).trim().toLowerCase(); ++} ++ ++export function supportedBootstrapRolesLabel() { ++ return SUPPORTED_BOOTSTRAP_ROLES.join(", "); ++} ++ ++export function supportedBootstrapTeamsLabel() { ++ return SUPPORTED_BOOTSTRAP_TEAMS.join(", "); ++} ++ ++export function resolveTeamPortConfig({ ++ role = DEFAULT_BOOTSTRAP_ROLE, ++ team = DEFAULT_BOOTSTRAP_TEAM, ++} = {}) { ++ const normalizedTeam = normalizeToken(team, DEFAULT_BOOTSTRAP_TEAM); ++ const normalizedRole = normalizeToken(role, DEFAULT_BOOTSTRAP_ROLE); ++ const basePorts = TEAM_BASE_PORTS[normalizedTeam]; ++ if (!basePorts) { ++ throw new Error( ++ `Unknown bootstrap team "${String(team || "").trim() || "(empty)"}". Use one of: ${supportedBootstrapTeamsLabel()}.` ++ ); ++ } ++ const offset = ROLE_PORT_OFFSETS[normalizedRole]; ++ if (offset === undefined) { ++ throw new Error( ++ `Unknown bootstrap role "${String(role || "").trim() || "(empty)"}". Use one of: ${supportedBootstrapRolesLabel()}.` ++ ); ++ } ++ return Object.freeze({ ++ apiPort: basePorts.apiPort + offset, ++ role: normalizedRole, ++ team: normalizedTeam, ++ webPort: basePorts.webPort + offset, ++ }); ++} ++ ++function parseNamedArgument(args, name, fallback, supportedLabel) { ++ const values = Array.from(args); ++ for (let index = 0; index < values.length; index += 1) { ++ const argument = values[index]; ++ if (argument === `--${name}`) { ++ const value = values[index + 1]; ++ if (!value || value.startsWith("--")) { ++ throw new Error(`Missing bootstrap ${name} after --${name}. Use one of: ${supportedLabel()}.`); ++ } ++ return normalizeToken(value, fallback); ++ } ++ if (argument.startsWith(`--${name}=`)) { ++ const value = argument.slice(`--${name}=`.length); ++ if (!value) { ++ throw new Error(`Missing bootstrap ${name} after --${name}=. Use one of: ${supportedLabel()}.`); ++ } ++ return normalizeToken(value, fallback); ++ } ++ } ++ return fallback; ++} ++ ++export function parseRoleArgument(args = []) { ++ return parseNamedArgument(args, "role", DEFAULT_BOOTSTRAP_ROLE, supportedBootstrapRolesLabel); ++} ++ ++export function parseTeamArgument(args = []) { ++ return parseNamedArgument(args, "team", DEFAULT_BOOTSTRAP_TEAM, supportedBootstrapTeamsLabel); ++} +diff --git a/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs b/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs +new file mode 100644 +index 000000000..0576d4db2 +--- /dev/null ++++ b/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs +@@ -0,0 +1,197 @@ ++import assert from "node:assert/strict"; ++import { readFileSync } from "node:fs"; ++import test from "node:test"; ++import { fileURLToPath } from "node:url"; ++import { ++ applyBootstrapEnvironment, ++ formatBootstrapDiagnostics, ++ loadRuntimeEnv, ++ parseBootstrapOptions, ++} from "../../scripts/start-dev.mjs"; ++import { ++ parseRoleArgument, ++ parseTeamArgument, ++ resolveTeamPortConfig, ++ supportedBootstrapRolesLabel, ++ supportedBootstrapTeamsLabel, ++} from "../../scripts/team-port-config.mjs"; ++ ++const repoRoot = fileURLToPath(new URL("../../..", import.meta.url)); ++ ++const BASE_PORTS = Object.freeze({ ++ owner: Object.freeze({ apiPort: 5501, webPort: 5500 }), ++ alfa: Object.freeze({ apiPort: 5511, webPort: 5510 }), ++ bravo: Object.freeze({ apiPort: 5521, webPort: 5520 }), ++ charlie: Object.freeze({ apiPort: 5531, webPort: 5530 }), ++ delta: Object.freeze({ apiPort: 5541, webPort: 5540 }), ++ echo: Object.freeze({ apiPort: 5551, webPort: 5550 }), ++ foxtrot: Object.freeze({ apiPort: 5561, webPort: 5560 }), ++ golf: Object.freeze({ apiPort: 5571, webPort: 5570 }), ++ hotel: Object.freeze({ apiPort: 5581, webPort: 5580 }), ++}); ++ ++test("team port config resolves owner role base ports", () => { ++ Object.entries(BASE_PORTS).forEach(([team, ports]) => { ++ assert.deepEqual(resolveTeamPortConfig({ team }), { ++ apiPort: ports.apiPort, ++ role: "owner", ++ team, ++ webPort: ports.webPort, ++ }); ++ }); ++}); ++ ++test("team port config resolves codex role ports as base ports plus two", () => { ++ Object.entries(BASE_PORTS).forEach(([team, ports]) => { ++ assert.deepEqual(resolveTeamPortConfig({ role: "codex", team }), { ++ apiPort: ports.apiPort + 2, ++ role: "codex", ++ team, ++ webPort: ports.webPort + 2, ++ }); ++ }); ++}); ++ ++test("team and role parsers accept canonical argument forms", () => { ++ assert.equal(parseTeamArgument([]), "owner"); ++ assert.equal(parseTeamArgument(["--team", "alfa"]), "alfa"); ++ assert.equal(parseTeamArgument(["--team=bravo"]), "bravo"); ++ assert.equal(parseRoleArgument([]), "owner"); ++ assert.equal(parseRoleArgument(["--role", "codex"]), "codex"); ++ assert.equal(parseRoleArgument(["--role=owner"]), "owner"); ++}); ++ ++test("invalid teams and roles fail with supported values", () => { ++ assert.throws( ++ () => resolveTeamPortConfig({ team: "omega" }), ++ /Unknown bootstrap team "omega".*owner, alfa, bravo, charlie, delta, echo, foxtrot, golf, hotel/ ++ ); ++ assert.throws( ++ () => resolveTeamPortConfig({ role: "reviewer", team: "alfa" }), ++ /Unknown bootstrap role "reviewer".*owner, codex/ ++ ); ++ assert.throws( ++ () => parseTeamArgument(["--team"]), ++ /Missing bootstrap team after --team/ ++ ); ++ assert.throws( ++ () => parseRoleArgument(["--role"]), ++ /Missing bootstrap role after --role/ ++ ); ++}); ++ ++test("bootstrap option parser maps npm script modes to server startup plan", () => { ++ assert.deepEqual(parseBootstrapOptions(["--mode", "bootstrap", "--team", "alfa"]), { ++ api: true, ++ mode: "bootstrap", ++ ports: { ++ apiPort: 5511, ++ role: "owner", ++ team: "alfa", ++ webPort: 5510, ++ }, ++ role: "owner", ++ team: "alfa", ++ web: true, ++ }); ++ assert.deepEqual(parseBootstrapOptions(["--mode=bootstrap", "--team=alfa", "--role=codex"]), { ++ api: true, ++ mode: "bootstrap", ++ ports: { ++ apiPort: 5513, ++ role: "codex", ++ team: "alfa", ++ webPort: 5512, ++ }, ++ role: "codex", ++ team: "alfa", ++ web: true, ++ }); ++ assert.deepEqual(parseBootstrapOptions(["--mode=api", "--team=charlie", "--role=codex"]), { ++ api: true, ++ mode: "api", ++ ports: { ++ apiPort: 5533, ++ role: "codex", ++ team: "charlie", ++ webPort: 5532, ++ }, ++ role: "codex", ++ team: "charlie", ++ web: false, ++ }); ++ assert.deepEqual(parseBootstrapOptions(["--mode=web", "--team=owner"]), { ++ api: false, ++ mode: "web", ++ ports: { ++ apiPort: 5501, ++ role: "owner", ++ team: "owner", ++ webPort: 5500, ++ }, ++ role: "owner", ++ team: "owner", ++ web: true, ++ }); ++ assert.throws( ++ () => parseBootstrapOptions(["--mode=deploy", "--team=alfa"]), ++ /Unknown bootstrap mode "deploy"/ ++ ); ++}); ++ ++test("bootstrap environment applies resolved team and role ports for browser API routing", () => { ++ const env = { ++ GAMEFOUNDRY_API_URL: "http://127.0.0.1:5501/api", ++ GAMEFOUNDRY_SITE_URL: "http://127.0.0.1:5500", ++ }; ++ applyBootstrapEnvironment({ ++ env, ++ host: "127.0.0.1", ++ ports: resolveTeamPortConfig({ role: "codex", team: "alfa" }), ++ }); ++ ++ assert.equal(env.GAMEFOUNDRY_LOCAL_API_HOST, "127.0.0.1"); ++ assert.equal(env.GAMEFOUNDRY_LOCAL_API_PORT, "5513"); ++ assert.equal(env.GAMEFOUNDRY_API_URL, "http://127.0.0.1:5513/api"); ++ assert.equal(env.GAMEFOUNDRY_SITE_URL, "http://127.0.0.1:5512"); ++}); ++ ++test("startup diagnostics and package scripts expose team-aware bootstrap commands", () => { ++ const lines = formatBootstrapDiagnostics({ ++ apiBaseUrl: "http://127.0.0.1:5513", ++ mode: "bootstrap", ++ role: "codex", ++ runtimeEnv: { ++ loaded: true, ++ loadedKeys: 3, ++ }, ++ team: "alfa", ++ webBaseUrl: "http://127.0.0.1:5512", ++ }); ++ ++ assert.equal(lines.includes("Team: alfa"), true); ++ assert.equal(lines.includes("Role: codex"), true); ++ assert.equal(lines.includes("Web URL: http://127.0.0.1:5512"), true); ++ assert.equal(lines.includes("API URL: http://127.0.0.1:5513/api"), true); ++ assert.equal(lines.includes(`Supported teams: ${supportedBootstrapTeamsLabel()}`), true); ++ assert.equal(lines.includes(`Supported roles: ${supportedBootstrapRolesLabel()}`), true); ++ ++ const packageJson = JSON.parse(readFileSync(`${repoRoot}/package.json`, "utf8")); ++ assert.equal(packageJson.scripts["dev:bootstrap"], "node --use-system-ca ./dev/scripts/start-dev.mjs --mode bootstrap"); ++ assert.equal(packageJson.scripts["dev:api"], "node --use-system-ca ./dev/scripts/start-dev.mjs --mode api"); ++ assert.equal(packageJson.scripts["dev:web"], "node ./dev/scripts/start-dev.mjs --mode web"); ++ assert.equal(packageJson.scripts["dev:local-api"], "node --use-system-ca ./dev/scripts/start-local-api-server.mjs"); ++}); + -+- Use official NATO spelling for all team names. -+- Do not use `Alpha`, `Beta`, or `Gamma` for active teams. -+- Deprecated or historical names must not be used for active team routing, new work ownership, or current governance. -+- Historical PR names, branch names, and references may remain unchanged when they are already merged or clearly referenced as history. -+- New branches, PRs, reports, and documentation must use canonical team names. -+- Repository documentation overrides prior conversations. ++test("runtime env loader applies .env values without overriding existing process values", () => { ++ const env = { ++ GAMEFOUNDRY_API_URL: "http://127.0.0.1:5999/api", ++ }; ++ const result = loadRuntimeEnv({ ++ cwd: repoRoot, ++ env, ++ }); + - ## Current Version/Date - --- Project Instructions Version: 2026-06-28.PR_26179_OWNER_010 -+- Project Instructions Version: 2026-06-28.PR_26172_OWNER_034 - - Date: 2026-06-28 - - Owner: OWNER ++ assert.equal(typeof result.loaded, "boolean"); ++ assert.equal(env.GAMEFOUNDRY_API_URL, "http://127.0.0.1:5999/api"); ++}); diff --git a/dev/scripts/start-dev.mjs b/dev/scripts/start-dev.mjs new file mode 100644 index 000000000..625360989 --- /dev/null +++ b/dev/scripts/start-dev.mjs @@ -0,0 +1,346 @@ +import fs from "node:fs/promises"; +import { existsSync, readFileSync } from "node:fs"; +import http from "node:http"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { localAdminNotesHeaderPartialPath } from "../../src/dev-runtime/admin/admin-notes-menu.mjs"; +import { startLocalApiServer } from "../../src/dev-runtime/server/local-api-server.mjs"; +import { + parseRoleArgument, + parseTeamArgument, + resolveTeamPortConfig, + supportedBootstrapRolesLabel, + supportedBootstrapTeamsLabel, +} from "./team-port-config.mjs"; + +const repoRoot = fileURLToPath(new URL("../..", import.meta.url)); +const DEFAULT_HOST = "127.0.0.1"; +const RUNTIME_ENV_FILE = ".env"; +const VALID_MODES = Object.freeze(["bootstrap", "api", "web"]); +const HOP_BY_HOP_HEADERS = new Set([ + "connection", + "content-length", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailer", + "transfer-encoding", + "upgrade", +]); + +function parseRuntimeEnvLine(line) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) { + return null; + } + const index = trimmed.indexOf("="); + const key = trimmed.slice(0, index).trim(); + let value = trimmed.slice(index + 1).trim(); + if (!key) { + return null; + } + if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) { + value = value.slice(1, -1); + } + return { key, value }; +} + +export function loadRuntimeEnv({ + cwd = process.cwd(), + env = process.env, +} = {}) { + const envPath = path.resolve(cwd, RUNTIME_ENV_FILE); + if (!existsSync(envPath)) { + return { + loaded: false, + loadedKeys: 0, + }; + } + let loadedKeys = 0; + readFileSync(envPath, "utf8").split(/\r?\n/).forEach((line) => { + const parsed = parseRuntimeEnvLine(line); + if (!parsed || env[parsed.key] !== undefined) { + return; + } + env[parsed.key] = parsed.value; + loadedKeys += 1; + }); + return { + loaded: true, + loadedKeys, + }; +} + +function contentTypeForPath(filePath) { + const extension = path.extname(filePath).toLowerCase(); + if (extension === ".html") return "text/html; charset=utf-8"; + if (extension === ".js" || extension === ".mjs") return "text/javascript; charset=utf-8"; + if (extension === ".json") return "application/json"; + if (extension === ".css") return "text/css; charset=utf-8"; + if (extension === ".svg") return "image/svg+xml"; + if (extension === ".png") return "image/png"; + if (extension === ".jpg" || extension === ".jpeg") return "image/jpeg"; + if (extension === ".webp") return "image/webp"; + if (extension === ".gif") return "image/gif"; + if (extension === ".wav") return "audio/wav"; + if (extension === ".mp3") return "audio/mpeg"; + if (extension === ".ogg") return "audio/ogg"; + if (extension === ".m4a") return "audio/mp4"; + if (extension === ".woff2") return "font/woff2"; + if (extension === ".woff") return "font/woff"; + if (extension === ".ttf") return "font/ttf"; + if (extension === ".otf") return "font/otf"; + if (extension === ".csv") return "text/csv; charset=utf-8"; + if (extension === ".txt") return "text/plain; charset=utf-8"; + return "application/octet-stream"; +} + +function isInsideRepoRoot(absolutePath) { + const relativePath = path.relative(repoRoot, absolutePath); + return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)); +} + +function resolveBrowserRoutePath(decodedPath) { + const normalizedPath = path.normalize(decodedPath).replace(/^(\.\.[/\\])+/, ""); + const webPath = normalizedPath.replace(/\\/g, "/"); + if (webPath === "/") { + return "/index.html"; + } + if (webPath === "/tools" || webPath.startsWith("/tools/")) { + return `/toolbox${webPath.slice("/tools".length)}`; + } + if (webPath === "/admin/admin-notes.html") { + return "/src/dev-runtime/admin/notes.html"; + } + return normalizedPath; +} + +function parseModeArgument(args = []) { + const values = Array.from(args); + for (let index = 0; index < values.length; index += 1) { + const argument = values[index]; + if (argument === "--mode") { + const value = values[index + 1]; + if (!value || value.startsWith("--")) { + throw new Error(`Missing bootstrap mode after --mode. Use one of: ${VALID_MODES.join(", ")}.`); + } + return value.trim().toLowerCase(); + } + if (argument.startsWith("--mode=")) { + const value = argument.slice("--mode=".length).trim().toLowerCase(); + if (!value) { + throw new Error(`Missing bootstrap mode after --mode=. Use one of: ${VALID_MODES.join(", ")}.`); + } + return value; + } + } + return "bootstrap"; +} + +export function parseBootstrapOptions(args = []) { + const mode = parseModeArgument(args); + if (!VALID_MODES.includes(mode)) { + throw new Error(`Unknown bootstrap mode "${mode}". Use one of: ${VALID_MODES.join(", ")}.`); + } + const role = parseRoleArgument(args); + const team = parseTeamArgument(args); + const ports = resolveTeamPortConfig({ role, team }); + return Object.freeze({ + api: mode === "api" || mode === "bootstrap", + mode, + ports, + role: ports.role, + team: ports.team, + web: mode === "web" || mode === "bootstrap", + }); +} + +function localUrl(host, port) { + return `http://${host}:${port}`; +} + +function apiUrl(host, port) { + return `${localUrl(host, port)}/api`; +} + +export function applyBootstrapEnvironment({ + env = process.env, + host = DEFAULT_HOST, + ports, +}) { + env.GAMEFOUNDRY_LOCAL_API_HOST = env.GAMEFOUNDRY_LOCAL_API_HOST || host; + env.GAMEFOUNDRY_LOCAL_API_PORT = String(ports.apiPort); + env.GAMEFOUNDRY_API_URL = apiUrl(host, ports.apiPort); + env.GAMEFOUNDRY_SITE_URL = localUrl(host, ports.webPort); + return env; +} + +export function formatBootstrapDiagnostics({ + apiBaseUrl, + mode, + role, + runtimeEnv, + team, + webBaseUrl, +}) { + return [ + "GameFoundry team-aware dev bootstrap", + `Mode: ${mode}`, + `Team: ${team}`, + `Role: ${role}`, + `Web URL: ${webBaseUrl}`, + `API URL: ${apiBaseUrl}/api`, + "Environment source: .env + process environment", + `.env loaded: ${runtimeEnv.loaded ? `yes (${runtimeEnv.loadedKeys} new key(s))` : "no"}`, + `Supported teams: ${supportedBootstrapTeamsLabel()}`, + `Supported roles: ${supportedBootstrapRolesLabel()}`, + "Legacy API alias remains: npm run dev:local-api", + "Press Ctrl+C to stop.", + ]; +} + +async function readRequestBody(request) { + const chunks = []; + for await (const chunk of request) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + return chunks.length ? Buffer.concat(chunks) : undefined; +} + +async function proxyApiRequest(request, response, requestUrl, apiBaseUrl) { + const targetUrl = new URL(`${requestUrl.pathname}${requestUrl.search}`, apiBaseUrl); + const headers = {}; + Object.entries(request.headers).forEach(([key, value]) => { + if (!HOP_BY_HOP_HEADERS.has(key.toLowerCase()) && value !== undefined) { + headers[key] = value; + } + }); + const method = request.method || "GET"; + const upstreamResponse = await fetch(targetUrl, { + body: ["GET", "HEAD"].includes(method.toUpperCase()) ? undefined : await readRequestBody(request), + headers, + method, + }); + response.statusCode = upstreamResponse.status; + upstreamResponse.headers.forEach((value, key) => { + if (!HOP_BY_HOP_HEADERS.has(key.toLowerCase())) { + response.setHeader(key, value); + } + }); + const body = Buffer.from(await upstreamResponse.arrayBuffer()); + response.end(body); +} + +export async function startStaticWebServer({ + apiBaseUrl, + host = DEFAULT_HOST, + port, +} = {}) { + if (!apiBaseUrl) { + throw new Error("startStaticWebServer requires apiBaseUrl."); + } + const server = http.createServer(async (request, response) => { + try { + const requestUrl = new URL(request.url || "/", `http://${host}:${port}`); + if (requestUrl.pathname === "/api" || requestUrl.pathname.startsWith("/api/")) { + await proxyApiRequest(request, response, requestUrl, apiBaseUrl); + return; + } + const decodedPath = decodeURIComponent(requestUrl.pathname); + const normalizedPath = resolveBrowserRoutePath(decodedPath); + const absolutePath = path.resolve(repoRoot, `.${normalizedPath}`); + if (!isInsideRepoRoot(absolutePath)) { + response.statusCode = 403; + response.end("Forbidden"); + return; + } + let targetPath = absolutePath; + const stat = await fs.stat(targetPath).catch(() => null); + if (stat && stat.isDirectory()) { + targetPath = path.join(targetPath, "index.html"); + } + targetPath = localAdminNotesHeaderPartialPath(repoRoot, targetPath); + const responseContents = await fs.readFile(targetPath); + response.statusCode = 200; + response.setHeader("Content-Type", contentTypeForPath(targetPath)); + response.end(responseContents); + } catch { + response.statusCode = 404; + response.end("Not Found"); + } + }); + + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(port, host, resolve); + }); + + return { + baseUrl: localUrl(host, port), + close: async () => { + await new Promise((resolve, reject) => { + server.close((error) => { + if (error) reject(error); + else resolve(); + }); + }); + }, + server, + }; +} + +export async function startBootstrapRuntime(options = parseBootstrapOptions(process.argv.slice(2))) { + const runtimeEnv = loadRuntimeEnv(); + const host = process.env.GAMEFOUNDRY_LOCAL_API_HOST || DEFAULT_HOST; + applyBootstrapEnvironment({ host, ports: options.ports }); + const apiBaseUrl = localUrl(host, options.ports.apiPort); + const webBaseUrl = localUrl(host, options.ports.webPort); + const servers = []; + + if (options.api) { + servers.push(await startLocalApiServer({ host, port: options.ports.apiPort })); + } + if (options.web) { + servers.push(await startStaticWebServer({ apiBaseUrl, host, port: options.ports.webPort })); + } + + return { + close: async () => { + await Promise.all(servers.map((server) => server.close())); + }, + diagnostics: formatBootstrapDiagnostics({ + apiBaseUrl, + mode: options.mode, + role: options.role, + runtimeEnv, + team: options.team, + webBaseUrl, + }), + servers, + }; +} + +async function main() { + let runtime; + try { + runtime = await startBootstrapRuntime(); + runtime.diagnostics.forEach((line) => console.log(line)); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; + return; + } + + for (const signal of ["SIGINT", "SIGTERM"]) { + process.once(signal, async () => { + await runtime.close(); + process.exit(0); + }); + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + await main(); +} diff --git a/dev/scripts/team-port-config.mjs b/dev/scripts/team-port-config.mjs new file mode 100644 index 000000000..2dfa98fc3 --- /dev/null +++ b/dev/scripts/team-port-config.mjs @@ -0,0 +1,89 @@ +const TEAM_BASE_PORTS = Object.freeze({ + owner: Object.freeze({ apiPort: 5501, webPort: 5500 }), + alfa: Object.freeze({ apiPort: 5511, webPort: 5510 }), + bravo: Object.freeze({ apiPort: 5521, webPort: 5520 }), + charlie: Object.freeze({ apiPort: 5531, webPort: 5530 }), + delta: Object.freeze({ apiPort: 5541, webPort: 5540 }), + echo: Object.freeze({ apiPort: 5551, webPort: 5550 }), + foxtrot: Object.freeze({ apiPort: 5561, webPort: 5560 }), + golf: Object.freeze({ apiPort: 5571, webPort: 5570 }), + hotel: Object.freeze({ apiPort: 5581, webPort: 5580 }), +}); + +const ROLE_PORT_OFFSETS = Object.freeze({ + owner: 0, + codex: 2, +}); + +export const DEFAULT_BOOTSTRAP_ROLE = "owner"; +export const DEFAULT_BOOTSTRAP_TEAM = "owner"; +export const SUPPORTED_BOOTSTRAP_ROLES = Object.freeze(Object.keys(ROLE_PORT_OFFSETS)); +export const SUPPORTED_BOOTSTRAP_TEAMS = Object.freeze(Object.keys(TEAM_BASE_PORTS)); + +function normalizeToken(value, fallback) { + return String(value || fallback).trim().toLowerCase(); +} + +export function supportedBootstrapRolesLabel() { + return SUPPORTED_BOOTSTRAP_ROLES.join(", "); +} + +export function supportedBootstrapTeamsLabel() { + return SUPPORTED_BOOTSTRAP_TEAMS.join(", "); +} + +export function resolveTeamPortConfig({ + role = DEFAULT_BOOTSTRAP_ROLE, + team = DEFAULT_BOOTSTRAP_TEAM, +} = {}) { + const normalizedTeam = normalizeToken(team, DEFAULT_BOOTSTRAP_TEAM); + const normalizedRole = normalizeToken(role, DEFAULT_BOOTSTRAP_ROLE); + const basePorts = TEAM_BASE_PORTS[normalizedTeam]; + if (!basePorts) { + throw new Error( + `Unknown bootstrap team "${String(team || "").trim() || "(empty)"}". Use one of: ${supportedBootstrapTeamsLabel()}.` + ); + } + const offset = ROLE_PORT_OFFSETS[normalizedRole]; + if (offset === undefined) { + throw new Error( + `Unknown bootstrap role "${String(role || "").trim() || "(empty)"}". Use one of: ${supportedBootstrapRolesLabel()}.` + ); + } + return Object.freeze({ + apiPort: basePorts.apiPort + offset, + role: normalizedRole, + team: normalizedTeam, + webPort: basePorts.webPort + offset, + }); +} + +function parseNamedArgument(args, name, fallback, supportedLabel) { + const values = Array.from(args); + for (let index = 0; index < values.length; index += 1) { + const argument = values[index]; + if (argument === `--${name}`) { + const value = values[index + 1]; + if (!value || value.startsWith("--")) { + throw new Error(`Missing bootstrap ${name} after --${name}. Use one of: ${supportedLabel()}.`); + } + return normalizeToken(value, fallback); + } + if (argument.startsWith(`--${name}=`)) { + const value = argument.slice(`--${name}=`.length); + if (!value) { + throw new Error(`Missing bootstrap ${name} after --${name}=. Use one of: ${supportedLabel()}.`); + } + return normalizeToken(value, fallback); + } + } + return fallback; +} + +export function parseRoleArgument(args = []) { + return parseNamedArgument(args, "role", DEFAULT_BOOTSTRAP_ROLE, supportedBootstrapRolesLabel); +} + +export function parseTeamArgument(args = []) { + return parseNamedArgument(args, "team", DEFAULT_BOOTSTRAP_TEAM, supportedBootstrapTeamsLabel); +} diff --git a/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs b/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs new file mode 100644 index 000000000..0576d4db2 --- /dev/null +++ b/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs @@ -0,0 +1,197 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { + applyBootstrapEnvironment, + formatBootstrapDiagnostics, + loadRuntimeEnv, + parseBootstrapOptions, +} from "../../scripts/start-dev.mjs"; +import { + parseRoleArgument, + parseTeamArgument, + resolveTeamPortConfig, + supportedBootstrapRolesLabel, + supportedBootstrapTeamsLabel, +} from "../../scripts/team-port-config.mjs"; + +const repoRoot = fileURLToPath(new URL("../../..", import.meta.url)); + +const BASE_PORTS = Object.freeze({ + owner: Object.freeze({ apiPort: 5501, webPort: 5500 }), + alfa: Object.freeze({ apiPort: 5511, webPort: 5510 }), + bravo: Object.freeze({ apiPort: 5521, webPort: 5520 }), + charlie: Object.freeze({ apiPort: 5531, webPort: 5530 }), + delta: Object.freeze({ apiPort: 5541, webPort: 5540 }), + echo: Object.freeze({ apiPort: 5551, webPort: 5550 }), + foxtrot: Object.freeze({ apiPort: 5561, webPort: 5560 }), + golf: Object.freeze({ apiPort: 5571, webPort: 5570 }), + hotel: Object.freeze({ apiPort: 5581, webPort: 5580 }), +}); + +test("team port config resolves owner role base ports", () => { + Object.entries(BASE_PORTS).forEach(([team, ports]) => { + assert.deepEqual(resolveTeamPortConfig({ team }), { + apiPort: ports.apiPort, + role: "owner", + team, + webPort: ports.webPort, + }); + }); +}); + +test("team port config resolves codex role ports as base ports plus two", () => { + Object.entries(BASE_PORTS).forEach(([team, ports]) => { + assert.deepEqual(resolveTeamPortConfig({ role: "codex", team }), { + apiPort: ports.apiPort + 2, + role: "codex", + team, + webPort: ports.webPort + 2, + }); + }); +}); + +test("team and role parsers accept canonical argument forms", () => { + assert.equal(parseTeamArgument([]), "owner"); + assert.equal(parseTeamArgument(["--team", "alfa"]), "alfa"); + assert.equal(parseTeamArgument(["--team=bravo"]), "bravo"); + assert.equal(parseRoleArgument([]), "owner"); + assert.equal(parseRoleArgument(["--role", "codex"]), "codex"); + assert.equal(parseRoleArgument(["--role=owner"]), "owner"); +}); + +test("invalid teams and roles fail with supported values", () => { + assert.throws( + () => resolveTeamPortConfig({ team: "omega" }), + /Unknown bootstrap team "omega".*owner, alfa, bravo, charlie, delta, echo, foxtrot, golf, hotel/ + ); + assert.throws( + () => resolveTeamPortConfig({ role: "reviewer", team: "alfa" }), + /Unknown bootstrap role "reviewer".*owner, codex/ + ); + assert.throws( + () => parseTeamArgument(["--team"]), + /Missing bootstrap team after --team/ + ); + assert.throws( + () => parseRoleArgument(["--role"]), + /Missing bootstrap role after --role/ + ); +}); + +test("bootstrap option parser maps npm script modes to server startup plan", () => { + assert.deepEqual(parseBootstrapOptions(["--mode", "bootstrap", "--team", "alfa"]), { + api: true, + mode: "bootstrap", + ports: { + apiPort: 5511, + role: "owner", + team: "alfa", + webPort: 5510, + }, + role: "owner", + team: "alfa", + web: true, + }); + assert.deepEqual(parseBootstrapOptions(["--mode=bootstrap", "--team=alfa", "--role=codex"]), { + api: true, + mode: "bootstrap", + ports: { + apiPort: 5513, + role: "codex", + team: "alfa", + webPort: 5512, + }, + role: "codex", + team: "alfa", + web: true, + }); + assert.deepEqual(parseBootstrapOptions(["--mode=api", "--team=charlie", "--role=codex"]), { + api: true, + mode: "api", + ports: { + apiPort: 5533, + role: "codex", + team: "charlie", + webPort: 5532, + }, + role: "codex", + team: "charlie", + web: false, + }); + assert.deepEqual(parseBootstrapOptions(["--mode=web", "--team=owner"]), { + api: false, + mode: "web", + ports: { + apiPort: 5501, + role: "owner", + team: "owner", + webPort: 5500, + }, + role: "owner", + team: "owner", + web: true, + }); + assert.throws( + () => parseBootstrapOptions(["--mode=deploy", "--team=alfa"]), + /Unknown bootstrap mode "deploy"/ + ); +}); + +test("bootstrap environment applies resolved team and role ports for browser API routing", () => { + const env = { + GAMEFOUNDRY_API_URL: "http://127.0.0.1:5501/api", + GAMEFOUNDRY_SITE_URL: "http://127.0.0.1:5500", + }; + applyBootstrapEnvironment({ + env, + host: "127.0.0.1", + ports: resolveTeamPortConfig({ role: "codex", team: "alfa" }), + }); + + assert.equal(env.GAMEFOUNDRY_LOCAL_API_HOST, "127.0.0.1"); + assert.equal(env.GAMEFOUNDRY_LOCAL_API_PORT, "5513"); + assert.equal(env.GAMEFOUNDRY_API_URL, "http://127.0.0.1:5513/api"); + assert.equal(env.GAMEFOUNDRY_SITE_URL, "http://127.0.0.1:5512"); +}); + +test("startup diagnostics and package scripts expose team-aware bootstrap commands", () => { + const lines = formatBootstrapDiagnostics({ + apiBaseUrl: "http://127.0.0.1:5513", + mode: "bootstrap", + role: "codex", + runtimeEnv: { + loaded: true, + loadedKeys: 3, + }, + team: "alfa", + webBaseUrl: "http://127.0.0.1:5512", + }); + + assert.equal(lines.includes("Team: alfa"), true); + assert.equal(lines.includes("Role: codex"), true); + assert.equal(lines.includes("Web URL: http://127.0.0.1:5512"), true); + assert.equal(lines.includes("API URL: http://127.0.0.1:5513/api"), true); + assert.equal(lines.includes(`Supported teams: ${supportedBootstrapTeamsLabel()}`), true); + assert.equal(lines.includes(`Supported roles: ${supportedBootstrapRolesLabel()}`), true); + + const packageJson = JSON.parse(readFileSync(`${repoRoot}/package.json`, "utf8")); + assert.equal(packageJson.scripts["dev:bootstrap"], "node --use-system-ca ./dev/scripts/start-dev.mjs --mode bootstrap"); + assert.equal(packageJson.scripts["dev:api"], "node --use-system-ca ./dev/scripts/start-dev.mjs --mode api"); + assert.equal(packageJson.scripts["dev:web"], "node ./dev/scripts/start-dev.mjs --mode web"); + assert.equal(packageJson.scripts["dev:local-api"], "node --use-system-ca ./dev/scripts/start-local-api-server.mjs"); +}); + +test("runtime env loader applies .env values without overriding existing process values", () => { + const env = { + GAMEFOUNDRY_API_URL: "http://127.0.0.1:5999/api", + }; + const result = loadRuntimeEnv({ + cwd: repoRoot, + env, + }); + + assert.equal(typeof result.loaded, "boolean"); + assert.equal(env.GAMEFOUNDRY_API_URL, "http://127.0.0.1:5999/api"); +}); diff --git a/package.json b/package.json index 2a6d66c76..65e67bf62 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,9 @@ "test:lane:integration": "node ./dev/scripts/run-targeted-test-lanes.mjs --lane integration", "test:lane:engine-src": "node ./dev/scripts/run-targeted-test-lanes.mjs --lane engine-src", "test:lane:samples": "node ./dev/scripts/run-targeted-test-lanes.mjs --lane samples --include-samples", + "dev:bootstrap": "node --use-system-ca ./dev/scripts/start-dev.mjs --mode bootstrap", + "dev:api": "node --use-system-ca ./dev/scripts/start-dev.mjs --mode api", + "dev:web": "node ./dev/scripts/start-dev.mjs --mode web", "dev:local-api": "node --use-system-ca ./dev/scripts/start-local-api-server.mjs", "validate:local-postgres-runtime": "node --use-system-ca ./dev/scripts/validate-local-postgres-runtime.mjs", "validate:database-drift": "node ./dev/scripts/validate-database-drift.mjs", From 3594ef8694507a66878f5ea46598e762dcac64a3 Mon Sep 17 00:00:00 2001 From: Alfa Team Date: Sun, 28 Jun 2026 08:02:01 -0400 Subject: [PATCH 2/5] ALFA 009 add owner browser launch --- ...aware-bootstrap_manual-validation-notes.md | 2 + ...71_ALFA_009-team-aware-bootstrap_report.md | 10 + ...m-aware-bootstrap_requirement-checklist.md | 3 + ...-team-aware-bootstrap_validation-report.md | 8 +- dev/reports/codex_changed_files.txt | 33 +- dev/reports/codex_review.diff | 1208 +++++------------ dev/scripts/start-dev.mjs | 76 +- .../dev-runtime/TeamAwareBootstrap.test.mjs | 119 ++ 8 files changed, 555 insertions(+), 904 deletions(-) diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md index 729a5f789..c40377fb2 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md @@ -12,6 +12,7 @@ Expected diagnostics: - `Role: owner` - `Web URL: http://127.0.0.1:5510` - `API URL: http://127.0.0.1:5511/api` +- `Browser launch: opened http://127.0.0.1:5510/index.html` ## Primary Alfa Codex Command @@ -25,6 +26,7 @@ Expected diagnostics: - `Role: codex` - `Web URL: http://127.0.0.1:5512` - `API URL: http://127.0.0.1:5513/api` +- `Browser launch: suppressed for codex role` ## Legacy Command diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md index 3aab1164d..1690a93ec 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md @@ -28,6 +28,8 @@ Per OWNER instruction, those uncommitted changes were discarded with `git reset - Added `dev/scripts/team-port-config.mjs` for full team name and role-aware port resolution. - Added `dev/scripts/start-dev.mjs` as the bootstrap orchestrator. - Added focused node tests for team resolution, role offsets, invalid team/role validation, script wiring, `.env` loading behavior, and bootstrap diagnostics. +- Added automatic browser launch for owner-role `dev:bootstrap` after the API and web servers are both ready. +- Suppressed automatic browser launch for the `codex` role. ## Supported Teams @@ -87,6 +89,14 @@ Codex role example: npm run dev:bootstrap -- --team alfa --role codex ``` +## Browser Launch Behavior + +- `owner` role launches the browser automatically to the selected team's `index.html`. +- `codex` role suppresses browser launch. +- Browser launch happens only after both API and web servers are ready. +- The Alfa owner launch target is `http://127.0.0.1:5510/index.html`. +- The Alfa codex launch target is suppressed while using web `5512` and API `5513`. + ## Files Changed - `package.json` - updated diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md index 4d9238295..4600ad790 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md @@ -21,5 +21,8 @@ | Start Local API | PASS | `dev:bootstrap` starts the existing Local API server on resolved API port. | | Start web server when requested | PASS | `dev:bootstrap` starts web plus API; `dev:web` mode is wired. | | Print startup diagnostics | PASS | Diagnostics include mode, team, role, web URL, API URL, environment source, supported teams, and supported roles. | +| Owner role launches browser automatically | PASS | Live startup validation opened `http://127.0.0.1:5510/index.html` after API and web were ready. | +| Codex role suppresses browser launch | PASS | Live startup validation reported `Browser launch: suppressed for codex role`. | +| Wait for both servers before browser launch | PASS | Targeted unit test asserts API ready, web ready, then browser launch order. | | Preserve legacy behavior | PASS | `dev:local-api` command unchanged and startup validation passed. | | No unrelated cleanup | PASS | Scope limited to package scripts, new dev scripts, targeted tests, and reports. | diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md index 1ca55c459..ea82082a8 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md @@ -8,15 +8,15 @@ | `node --check dev/scripts/start-dev.mjs` | PASS | Syntax valid. | | `node --check dev/scripts/start-local-api-server.mjs` | PASS | Legacy startup entrypoint remains valid. | | `node --check dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs` | PASS | Test syntax valid. | -| `node ./dev/scripts/run-node-test-files.mjs dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs dev/tests/dev-runtime/LocalApiStartupLogging.test.mjs` | PASS | 2 targeted files passed; 10 subtests passed. | +| `node ./dev/scripts/run-node-test-files.mjs dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs dev/tests/dev-runtime/LocalApiStartupLogging.test.mjs` | PASS | 2 targeted files passed; 14 subtests passed. | | Unsupported team token scan in new bootstrap files | PASS | No unsupported old team names or single-letter public team commands found. | | `npm run validate:canonical-structure` | PASS | Canonical repository structure guardrail passed. | -| `npm run dev:bootstrap -- --team alfa` | PASS | Started with team `alfa`, role `owner`, web `5510`, API `5511`, then process tree was stopped. | -| `npm run dev:bootstrap -- --team alfa --role codex` | PASS | Started with team `alfa`, role `codex`, web `5512`, API `5513`, then process tree was stopped. | +| `npm run dev:bootstrap -- --team alfa` | PASS | Started with team `alfa`, role `owner`, web `5510`, API `5511`, launched browser to `http://127.0.0.1:5510/index.html`, then process tree was stopped. | +| `npm run dev:bootstrap -- --team alfa --role codex` | PASS | Started with team `alfa`, role `codex`, web `5512`, API `5513`, suppressed browser launch, then process tree was stopped. | | `npm run dev:bootstrap -- --team omega` | PASS | Failed clearly with supported team list. | | `npm run dev:bootstrap -- --team alfa --role reviewer` | PASS | Failed clearly with supported role list. | | `npm run dev:local-api` | PASS | Legacy command started and printed legacy startup diagnostics, then process tree was stopped. | ## Notes -The startup-command validations used short-lived process launches and `taskkill /T /F` cleanup after expected diagnostics appeared, so no dev servers were left running. +The startup-command validations used short-lived process launches and `taskkill /T /F` cleanup after expected diagnostics appeared, so no dev servers were left running. The owner-role validation opened the configured browser target after both server diagnostics were available. diff --git a/dev/reports/codex_changed_files.txt b/dev/reports/codex_changed_files.txt index 8fde59a92..0143fa0cf 100644 --- a/dev/reports/codex_changed_files.txt +++ b/dev/reports/codex_changed_files.txt @@ -1,24 +1,19 @@ # git status --short -M package.json -?? dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_branch-validation.md -?? dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md -?? dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md -?? dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md -?? dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md -?? dev/scripts/start-dev.mjs -?? dev/scripts/team-port-config.mjs -?? dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs +M dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md + M dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md + M dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md + M dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md + M dev/scripts/start-dev.mjs + M dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs # git ls-files --others --exclude-standard -dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_branch-validation.md -dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md -dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md -dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md -dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md -dev/scripts/start-dev.mjs -dev/scripts/team-port-config.mjs -dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs +(no output) # git diff --stat -package.json | 3 +++ - 1 file changed, 3 insertions(+) \ No newline at end of file +...team-aware-bootstrap_manual-validation-notes.md | 2 + + ...R_26171_ALFA_009-team-aware-bootstrap_report.md | 10 ++ + ...9-team-aware-bootstrap_requirement-checklist.md | 3 + + ...A_009-team-aware-bootstrap_validation-report.md | 8 +- + dev/scripts/start-dev.mjs | 76 +++++++++++-- + dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs | 119 +++++++++++++++++++++ + 6 files changed, 208 insertions(+), 10 deletions(-) \ No newline at end of file diff --git a/dev/reports/codex_review.diff b/dev/reports/codex_review.diff index 66bab2215..d6060d413 100644 --- a/dev/reports/codex_review.diff +++ b/dev/reports/codex_review.diff @@ -1,913 +1,371 @@ -diff --git a/package.json b/package.json -index 2a6d66c76..65e67bf62 100644 ---- a/package.json -+++ b/package.json -@@ -25,6 +25,9 @@ - "test:lane:integration": "node ./dev/scripts/run-targeted-test-lanes.mjs --lane integration", - "test:lane:engine-src": "node ./dev/scripts/run-targeted-test-lanes.mjs --lane engine-src", - "test:lane:samples": "node ./dev/scripts/run-targeted-test-lanes.mjs --lane samples --include-samples", -+ "dev:bootstrap": "node --use-system-ca ./dev/scripts/start-dev.mjs --mode bootstrap", -+ "dev:api": "node --use-system-ca ./dev/scripts/start-dev.mjs --mode api", -+ "dev:web": "node ./dev/scripts/start-dev.mjs --mode web", - "dev:local-api": "node --use-system-ca ./dev/scripts/start-local-api-server.mjs", - "validate:local-postgres-runtime": "node --use-system-ca ./dev/scripts/validate-local-postgres-runtime.mjs", - "validate:database-drift": "node ./dev/scripts/validate-database-drift.mjs", -diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_branch-validation.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_branch-validation.md -new file mode 100644 -index 000000000..aa96a1077 ---- /dev/null -+++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_branch-validation.md -@@ -0,0 +1,10 @@ -+# PR_26171_ALFA_009 Branch Validation -+ -+| Check | Status | Evidence | -+| --- | --- | --- | -+| Failed dirty attempt recorded | PASS | Dirty files listed in PR report before reset. | -+| Failed dirty attempt discarded | PASS | Ran `git reset --hard` and `git clean -fd`. | -+| Clean `main` confirmed before implementation | PASS | `main...origin/main` was `0 0`; worktree was clean. | -+| Branch recreated from `main` | PASS | Current branch is `PR_26171_ALFA_009-team-aware-bootstrap` at source commit `a4e671ec8`. | -+| Scope check | PASS | Changes limited to approved bootstrap scripts, package scripts, tests, and reports. | -+| Validation | PASS | Targeted validation commands passed. | diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md -new file mode 100644 -index 000000000..729a5f789 ---- /dev/null +index 729a5f789..c40377fb2 100644 +--- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md -@@ -0,0 +1,63 @@ -+# PR_26171_ALFA_009 Manual Validation Notes -+ -+## Primary Alfa Owner Command -+ -+```powershell -+npm run dev:bootstrap -- --team alfa -+``` -+ -+Expected diagnostics: -+ -+- `Team: alfa` -+- `Role: owner` -+- `Web URL: http://127.0.0.1:5510` -+- `API URL: http://127.0.0.1:5511/api` -+ -+## Primary Alfa Codex Command -+ -+```powershell -+npm run dev:bootstrap -- --team alfa --role codex -+``` -+ -+Expected diagnostics: -+ -+- `Team: alfa` -+- `Role: codex` -+- `Web URL: http://127.0.0.1:5512` -+- `API URL: http://127.0.0.1:5513/api` -+ -+## Legacy Command -+ -+```powershell -+npm run dev:local-api -+``` -+ -+Expected result: -+ -+- Legacy startup remains available. -+- The legacy command still runs `node --use-system-ca ./dev/scripts/start-local-api-server.mjs`. -+ -+## Other Startup Commands -+ -+```powershell -+npm run dev:bootstrap -- --team owner -+npm run dev:bootstrap -- --team bravo -+npm run dev:bootstrap -- --team charlie -+npm run dev:bootstrap -- --team delta -+npm run dev:bootstrap -- --team echo -+npm run dev:bootstrap -- --team foxtrot -+npm run dev:bootstrap -- --team golf -+npm run dev:bootstrap -- --team hotel -+``` -+ -+## Failure Checks -+ -+```powershell -+npm run dev:bootstrap -- --team omega -+npm run dev:bootstrap -- --team alfa --role reviewer -+``` -+ -+Expected result: -+ -+- Unknown team fails with the supported team list. -+- Unknown role fails with the supported role list. +@@ -12,6 +12,7 @@ Expected diagnostics: + - `Role: owner` + - `Web URL: http://127.0.0.1:5510` + - `API URL: http://127.0.0.1:5511/api` ++- `Browser launch: opened http://127.0.0.1:5510/index.html` + + ## Primary Alfa Codex Command + +@@ -25,6 +26,7 @@ Expected diagnostics: + - `Role: codex` + - `Web URL: http://127.0.0.1:5512` + - `API URL: http://127.0.0.1:5513/api` ++- `Browser launch: suppressed for codex role` + + ## Legacy Command + diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md -new file mode 100644 -index 000000000..3aab1164d ---- /dev/null +index 3aab1164d..1690a93ec 100644 +--- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md -@@ -0,0 +1,99 @@ -+# PR_26171_ALFA_009 Team-Aware Bootstrap Report -+ -+## Purpose -+ -+Implement team-aware local dev bootstrap commands with role-aware port assignment while preserving the legacy local API command. -+ -+## Reset Note -+ -+Before rebuilding from clean `main`, the failed dirty attempt included: -+ -+- `dev/reports/codex_changed_files.txt` -+- `dev/reports/codex_review.diff` -+- `dev/scripts/start-local-api-server.mjs` -+- `package.json` -+- untracked bootstrap reports -+- untracked `dev/scripts/start-dev.mjs` -+- untracked `dev/scripts/team-port-config.mjs` -+- untracked `dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs` -+ -+Per OWNER instruction, those uncommitted changes were discarded with `git reset --hard` and `git clean -fd`, then this branch was recreated from clean `main`. -+ -+## Implementation Summary -+ -+- Added `npm run dev:bootstrap`. -+- Added `npm run dev:api`. -+- Added `npm run dev:web`. -+- Preserved `npm run dev:local-api` unchanged as the legacy alias. -+- Added `dev/scripts/team-port-config.mjs` for full team name and role-aware port resolution. -+- Added `dev/scripts/start-dev.mjs` as the bootstrap orchestrator. -+- Added focused node tests for team resolution, role offsets, invalid team/role validation, script wiring, `.env` loading behavior, and bootstrap diagnostics. -+ -+## Supported Teams -+ -+`owner`, `alfa`, `bravo`, `charlie`, `delta`, `echo`, `foxtrot`, `golf`, `hotel` -+ -+## Supported Roles -+ -+`owner`, `codex` -+ -+## Port Mapping -+ -+`owner` role uses base ports. `codex` role uses base ports plus 2. -+ -+| Team | Owner Web | Owner API | Codex Web | Codex API | -+| --- | ---: | ---: | ---: | ---: | -+| owner | 5500 | 5501 | 5502 | 5503 | -+| alfa | 5510 | 5511 | 5512 | 5513 | -+| bravo | 5520 | 5521 | 5522 | 5523 | -+| charlie | 5530 | 5531 | 5532 | 5533 | -+| delta | 5540 | 5541 | 5542 | 5543 | -+| echo | 5550 | 5551 | 5552 | 5553 | -+| foxtrot | 5560 | 5561 | 5562 | 5563 | -+| golf | 5570 | 5571 | 5572 | 5573 | -+| hotel | 5580 | 5581 | 5582 | 5583 | -+ -+## Commands Added -+ -+```powershell -+npm run dev:bootstrap -+npm run dev:api -+npm run dev:web -+``` -+ -+## Legacy Command Preserved -+ -+```powershell -+npm run dev:local-api -+``` -+ -+## Exact Startup Commands -+ -+```powershell -+npm run dev:bootstrap -- --team owner -+npm run dev:bootstrap -- --team alfa -+npm run dev:bootstrap -- --team bravo -+npm run dev:bootstrap -- --team charlie -+npm run dev:bootstrap -- --team delta -+npm run dev:bootstrap -- --team echo -+npm run dev:bootstrap -- --team foxtrot -+npm run dev:bootstrap -- --team golf -+npm run dev:bootstrap -- --team hotel -+``` -+ -+Codex role example: -+ -+```powershell -+npm run dev:bootstrap -- --team alfa --role codex -+``` -+ -+## Files Changed -+ -+- `package.json` - updated -+- `dev/scripts/start-dev.mjs` - added -+- `dev/scripts/team-port-config.mjs` - added -+- `dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs` - added -+ -+## Validation Summary -+ -+All requested targeted validation passed. +@@ -28,6 +28,8 @@ Per OWNER instruction, those uncommitted changes were discarded with `git reset + - Added `dev/scripts/team-port-config.mjs` for full team name and role-aware port resolution. + - Added `dev/scripts/start-dev.mjs` as the bootstrap orchestrator. + - Added focused node tests for team resolution, role offsets, invalid team/role validation, script wiring, `.env` loading behavior, and bootstrap diagnostics. ++- Added automatic browser launch for owner-role `dev:bootstrap` after the API and web servers are both ready. ++- Suppressed automatic browser launch for the `codex` role. + + ## Supported Teams + +@@ -87,6 +89,14 @@ Codex role example: + npm run dev:bootstrap -- --team alfa --role codex + ``` + ++## Browser Launch Behavior ++ ++- `owner` role launches the browser automatically to the selected team's `index.html`. ++- `codex` role suppresses browser launch. ++- Browser launch happens only after both API and web servers are ready. ++- The Alfa owner launch target is `http://127.0.0.1:5510/index.html`. ++- The Alfa codex launch target is suppressed while using web `5512` and API `5513`. ++ + ## Files Changed + + - `package.json` - updated diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md -new file mode 100644 -index 000000000..4d9238295 ---- /dev/null +index 4d9238295..4600ad790 100644 +--- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md -@@ -0,0 +1,25 @@ -+# PR_26171_ALFA_009 Requirement Checklist -+ -+| Requirement | Status | Evidence | -+| --- | --- | --- | -+| Start from clean `main` | PASS | Reset failed attempt, switched to `main`, pulled fast-forward, confirmed `main...origin/main` was `0 0`, then recreated branch. | -+| Add `npm run dev:bootstrap` | PASS | Added to `package.json`. | -+| Keep `npm run dev:local-api` as legacy alias | PASS | Existing command remains unchanged. | -+| Add `npm run dev:api` | PASS | Added to `package.json`. | -+| Add `npm run dev:web` | PASS | Added to `package.json`. | -+| Create `dev/scripts/start-dev.mjs` | PASS | Added orchestrator. | -+| Create `dev/scripts/team-port-config.mjs` | PASS | Added team/role port resolver. | -+| Support full team names only | PASS | Supported teams are owner, alfa, bravo, charlie, delta, echo, foxtrot, golf, hotel. | -+| Support roles owner/codex | PASS | Role resolver supports owner and codex only. | -+| Codex role uses base ports plus 2 | PASS | Covered by targeted unit test. | -+| Do not use unsupported old team names | PASS | Scan returned no matches in new bootstrap files. | -+| Do not use single-letter public team commands | PASS | Scan returned no matches in new bootstrap files. | -+| Validate unknown team names | PASS | `omega` fails clearly with supported team list. | -+| Validate unknown role names | PASS | `reviewer` fails clearly with supported role list. | -+| Load environment | PASS | Orchestrator loads `.env` values without overriding existing environment values. | -+| Resolve team ports | PASS | Unit tests cover all teams and both roles. | -+| Start Local API | PASS | `dev:bootstrap` starts the existing Local API server on resolved API port. | -+| Start web server when requested | PASS | `dev:bootstrap` starts web plus API; `dev:web` mode is wired. | -+| Print startup diagnostics | PASS | Diagnostics include mode, team, role, web URL, API URL, environment source, supported teams, and supported roles. | -+| Preserve legacy behavior | PASS | `dev:local-api` command unchanged and startup validation passed. | -+| No unrelated cleanup | PASS | Scope limited to package scripts, new dev scripts, targeted tests, and reports. | +@@ -21,5 +21,8 @@ + | Start Local API | PASS | `dev:bootstrap` starts the existing Local API server on resolved API port. | + | Start web server when requested | PASS | `dev:bootstrap` starts web plus API; `dev:web` mode is wired. | + | Print startup diagnostics | PASS | Diagnostics include mode, team, role, web URL, API URL, environment source, supported teams, and supported roles. | ++| Owner role launches browser automatically | PASS | Live startup validation opened `http://127.0.0.1:5510/index.html` after API and web were ready. | ++| Codex role suppresses browser launch | PASS | Live startup validation reported `Browser launch: suppressed for codex role`. | ++| Wait for both servers before browser launch | PASS | Targeted unit test asserts API ready, web ready, then browser launch order. | + | Preserve legacy behavior | PASS | `dev:local-api` command unchanged and startup validation passed. | + | No unrelated cleanup | PASS | Scope limited to package scripts, new dev scripts, targeted tests, and reports. | diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md -new file mode 100644 -index 000000000..1ca55c459 ---- /dev/null +index 1ca55c459..ea82082a8 100644 +--- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md -@@ -0,0 +1,22 @@ -+# PR_26171_ALFA_009 Validation Report -+ -+## Validation Results -+ -+| Command | Result | Notes | -+| --- | --- | --- | -+| `node --check dev/scripts/team-port-config.mjs` | PASS | Syntax valid. | -+| `node --check dev/scripts/start-dev.mjs` | PASS | Syntax valid. | -+| `node --check dev/scripts/start-local-api-server.mjs` | PASS | Legacy startup entrypoint remains valid. | -+| `node --check dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs` | PASS | Test syntax valid. | -+| `node ./dev/scripts/run-node-test-files.mjs dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs dev/tests/dev-runtime/LocalApiStartupLogging.test.mjs` | PASS | 2 targeted files passed; 10 subtests passed. | -+| Unsupported team token scan in new bootstrap files | PASS | No unsupported old team names or single-letter public team commands found. | -+| `npm run validate:canonical-structure` | PASS | Canonical repository structure guardrail passed. | -+| `npm run dev:bootstrap -- --team alfa` | PASS | Started with team `alfa`, role `owner`, web `5510`, API `5511`, then process tree was stopped. | -+| `npm run dev:bootstrap -- --team alfa --role codex` | PASS | Started with team `alfa`, role `codex`, web `5512`, API `5513`, then process tree was stopped. | -+| `npm run dev:bootstrap -- --team omega` | PASS | Failed clearly with supported team list. | -+| `npm run dev:bootstrap -- --team alfa --role reviewer` | PASS | Failed clearly with supported role list. | -+| `npm run dev:local-api` | PASS | Legacy command started and printed legacy startup diagnostics, then process tree was stopped. | -+ -+## Notes -+ -+The startup-command validations used short-lived process launches and `taskkill /T /F` cleanup after expected diagnostics appeared, so no dev servers were left running. +@@ -8,15 +8,15 @@ + | `node --check dev/scripts/start-dev.mjs` | PASS | Syntax valid. | + | `node --check dev/scripts/start-local-api-server.mjs` | PASS | Legacy startup entrypoint remains valid. | + | `node --check dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs` | PASS | Test syntax valid. | +-| `node ./dev/scripts/run-node-test-files.mjs dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs dev/tests/dev-runtime/LocalApiStartupLogging.test.mjs` | PASS | 2 targeted files passed; 10 subtests passed. | ++| `node ./dev/scripts/run-node-test-files.mjs dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs dev/tests/dev-runtime/LocalApiStartupLogging.test.mjs` | PASS | 2 targeted files passed; 14 subtests passed. | + | Unsupported team token scan in new bootstrap files | PASS | No unsupported old team names or single-letter public team commands found. | + | `npm run validate:canonical-structure` | PASS | Canonical repository structure guardrail passed. | +-| `npm run dev:bootstrap -- --team alfa` | PASS | Started with team `alfa`, role `owner`, web `5510`, API `5511`, then process tree was stopped. | +-| `npm run dev:bootstrap -- --team alfa --role codex` | PASS | Started with team `alfa`, role `codex`, web `5512`, API `5513`, then process tree was stopped. | ++| `npm run dev:bootstrap -- --team alfa` | PASS | Started with team `alfa`, role `owner`, web `5510`, API `5511`, launched browser to `http://127.0.0.1:5510/index.html`, then process tree was stopped. | ++| `npm run dev:bootstrap -- --team alfa --role codex` | PASS | Started with team `alfa`, role `codex`, web `5512`, API `5513`, suppressed browser launch, then process tree was stopped. | + | `npm run dev:bootstrap -- --team omega` | PASS | Failed clearly with supported team list. | + | `npm run dev:bootstrap -- --team alfa --role reviewer` | PASS | Failed clearly with supported role list. | + | `npm run dev:local-api` | PASS | Legacy command started and printed legacy startup diagnostics, then process tree was stopped. | + + ## Notes + +-The startup-command validations used short-lived process launches and `taskkill /T /F` cleanup after expected diagnostics appeared, so no dev servers were left running. ++The startup-command validations used short-lived process launches and `taskkill /T /F` cleanup after expected diagnostics appeared, so no dev servers were left running. The owner-role validation opened the configured browser target after both server diagnostics were available. diff --git a/dev/scripts/start-dev.mjs b/dev/scripts/start-dev.mjs -new file mode 100644 -index 000000000..625360989 ---- /dev/null +index 625360989..15d3ac1fa 100644 +--- a/dev/scripts/start-dev.mjs +++ b/dev/scripts/start-dev.mjs -@@ -0,0 +1,346 @@ -+import fs from "node:fs/promises"; -+import { existsSync, readFileSync } from "node:fs"; -+import http from "node:http"; -+import path from "node:path"; -+import process from "node:process"; -+import { fileURLToPath, pathToFileURL } from "node:url"; -+import { localAdminNotesHeaderPartialPath } from "../../src/dev-runtime/admin/admin-notes-menu.mjs"; -+import { startLocalApiServer } from "../../src/dev-runtime/server/local-api-server.mjs"; -+import { -+ parseRoleArgument, -+ parseTeamArgument, -+ resolveTeamPortConfig, -+ supportedBootstrapRolesLabel, -+ supportedBootstrapTeamsLabel, -+} from "./team-port-config.mjs"; -+ -+const repoRoot = fileURLToPath(new URL("../..", import.meta.url)); -+const DEFAULT_HOST = "127.0.0.1"; -+const RUNTIME_ENV_FILE = ".env"; -+const VALID_MODES = Object.freeze(["bootstrap", "api", "web"]); -+const HOP_BY_HOP_HEADERS = new Set([ -+ "connection", -+ "content-length", -+ "keep-alive", -+ "proxy-authenticate", -+ "proxy-authorization", -+ "te", -+ "trailer", -+ "transfer-encoding", -+ "upgrade", -+]); -+ -+function parseRuntimeEnvLine(line) { -+ const trimmed = line.trim(); -+ if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("=")) { -+ return null; -+ } -+ const index = trimmed.indexOf("="); -+ const key = trimmed.slice(0, index).trim(); -+ let value = trimmed.slice(index + 1).trim(); -+ if (!key) { -+ return null; -+ } -+ if ((value.startsWith("\"") && value.endsWith("\"")) || (value.startsWith("'") && value.endsWith("'"))) { -+ value = value.slice(1, -1); -+ } -+ return { key, value }; -+} -+ -+export function loadRuntimeEnv({ -+ cwd = process.cwd(), -+ env = process.env, -+} = {}) { -+ const envPath = path.resolve(cwd, RUNTIME_ENV_FILE); -+ if (!existsSync(envPath)) { -+ return { -+ loaded: false, -+ loadedKeys: 0, -+ }; -+ } -+ let loadedKeys = 0; -+ readFileSync(envPath, "utf8").split(/\r?\n/).forEach((line) => { -+ const parsed = parseRuntimeEnvLine(line); -+ if (!parsed || env[parsed.key] !== undefined) { -+ return; -+ } -+ env[parsed.key] = parsed.value; -+ loadedKeys += 1; -+ }); -+ return { -+ loaded: true, -+ loadedKeys, -+ }; -+} -+ -+function contentTypeForPath(filePath) { -+ const extension = path.extname(filePath).toLowerCase(); -+ if (extension === ".html") return "text/html; charset=utf-8"; -+ if (extension === ".js" || extension === ".mjs") return "text/javascript; charset=utf-8"; -+ if (extension === ".json") return "application/json"; -+ if (extension === ".css") return "text/css; charset=utf-8"; -+ if (extension === ".svg") return "image/svg+xml"; -+ if (extension === ".png") return "image/png"; -+ if (extension === ".jpg" || extension === ".jpeg") return "image/jpeg"; -+ if (extension === ".webp") return "image/webp"; -+ if (extension === ".gif") return "image/gif"; -+ if (extension === ".wav") return "audio/wav"; -+ if (extension === ".mp3") return "audio/mpeg"; -+ if (extension === ".ogg") return "audio/ogg"; -+ if (extension === ".m4a") return "audio/mp4"; -+ if (extension === ".woff2") return "font/woff2"; -+ if (extension === ".woff") return "font/woff"; -+ if (extension === ".ttf") return "font/ttf"; -+ if (extension === ".otf") return "font/otf"; -+ if (extension === ".csv") return "text/csv; charset=utf-8"; -+ if (extension === ".txt") return "text/plain; charset=utf-8"; -+ return "application/octet-stream"; -+} -+ -+function isInsideRepoRoot(absolutePath) { -+ const relativePath = path.relative(repoRoot, absolutePath); -+ return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)); -+} -+ -+function resolveBrowserRoutePath(decodedPath) { -+ const normalizedPath = path.normalize(decodedPath).replace(/^(\.\.[/\\])+/, ""); -+ const webPath = normalizedPath.replace(/\\/g, "/"); -+ if (webPath === "/") { -+ return "/index.html"; -+ } -+ if (webPath === "/tools" || webPath.startsWith("/tools/")) { -+ return `/toolbox${webPath.slice("/tools".length)}`; -+ } -+ if (webPath === "/admin/admin-notes.html") { -+ return "/src/dev-runtime/admin/notes.html"; -+ } -+ return normalizedPath; -+} -+ -+function parseModeArgument(args = []) { -+ const values = Array.from(args); -+ for (let index = 0; index < values.length; index += 1) { -+ const argument = values[index]; -+ if (argument === "--mode") { -+ const value = values[index + 1]; -+ if (!value || value.startsWith("--")) { -+ throw new Error(`Missing bootstrap mode after --mode. Use one of: ${VALID_MODES.join(", ")}.`); -+ } -+ return value.trim().toLowerCase(); -+ } -+ if (argument.startsWith("--mode=")) { -+ const value = argument.slice("--mode=".length).trim().toLowerCase(); -+ if (!value) { -+ throw new Error(`Missing bootstrap mode after --mode=. Use one of: ${VALID_MODES.join(", ")}.`); -+ } -+ return value; -+ } -+ } -+ return "bootstrap"; -+} -+ -+export function parseBootstrapOptions(args = []) { -+ const mode = parseModeArgument(args); -+ if (!VALID_MODES.includes(mode)) { -+ throw new Error(`Unknown bootstrap mode "${mode}". Use one of: ${VALID_MODES.join(", ")}.`); -+ } -+ const role = parseRoleArgument(args); -+ const team = parseTeamArgument(args); -+ const ports = resolveTeamPortConfig({ role, team }); -+ return Object.freeze({ -+ api: mode === "api" || mode === "bootstrap", -+ mode, -+ ports, -+ role: ports.role, -+ team: ports.team, -+ web: mode === "web" || mode === "bootstrap", -+ }); -+} -+ -+function localUrl(host, port) { -+ return `http://${host}:${port}`; -+} -+ -+function apiUrl(host, port) { -+ return `${localUrl(host, port)}/api`; -+} -+ -+export function applyBootstrapEnvironment({ -+ env = process.env, -+ host = DEFAULT_HOST, -+ ports, -+}) { -+ env.GAMEFOUNDRY_LOCAL_API_HOST = env.GAMEFOUNDRY_LOCAL_API_HOST || host; -+ env.GAMEFOUNDRY_LOCAL_API_PORT = String(ports.apiPort); -+ env.GAMEFOUNDRY_API_URL = apiUrl(host, ports.apiPort); -+ env.GAMEFOUNDRY_SITE_URL = localUrl(host, ports.webPort); -+ return env; -+} -+ -+export function formatBootstrapDiagnostics({ -+ apiBaseUrl, -+ mode, -+ role, -+ runtimeEnv, -+ team, -+ webBaseUrl, -+}) { -+ return [ -+ "GameFoundry team-aware dev bootstrap", -+ `Mode: ${mode}`, -+ `Team: ${team}`, -+ `Role: ${role}`, -+ `Web URL: ${webBaseUrl}`, -+ `API URL: ${apiBaseUrl}/api`, -+ "Environment source: .env + process environment", -+ `.env loaded: ${runtimeEnv.loaded ? `yes (${runtimeEnv.loadedKeys} new key(s))` : "no"}`, -+ `Supported teams: ${supportedBootstrapTeamsLabel()}`, -+ `Supported roles: ${supportedBootstrapRolesLabel()}`, -+ "Legacy API alias remains: npm run dev:local-api", -+ "Press Ctrl+C to stop.", -+ ]; +@@ -1,5 +1,6 @@ + import fs from "node:fs/promises"; + import { existsSync, readFileSync } from "node:fs"; ++import { spawn } from "node:child_process"; + import http from "node:http"; + import path from "node:path"; + import process from "node:process"; +@@ -165,6 +166,14 @@ function apiUrl(host, port) { + return `${localUrl(host, port)}/api`; + } + ++export function teamIndexUrl(webBaseUrl) { ++ return `${String(webBaseUrl || "").replace(/\/+$/, "")}/index.html`; +} + -+async function readRequestBody(request) { -+ const chunks = []; -+ for await (const chunk of request) { -+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); -+ } -+ return chunks.length ? Buffer.concat(chunks) : undefined; ++export function shouldLaunchBrowser(options) { ++ return Boolean(options?.api && options?.web && options?.role === "owner"); +} + -+async function proxyApiRequest(request, response, requestUrl, apiBaseUrl) { -+ const targetUrl = new URL(`${requestUrl.pathname}${requestUrl.search}`, apiBaseUrl); -+ const headers = {}; -+ Object.entries(request.headers).forEach(([key, value]) => { -+ if (!HOP_BY_HOP_HEADERS.has(key.toLowerCase()) && value !== undefined) { -+ headers[key] = value; -+ } -+ }); -+ const method = request.method || "GET"; -+ const upstreamResponse = await fetch(targetUrl, { -+ body: ["GET", "HEAD"].includes(method.toUpperCase()) ? undefined : await readRequestBody(request), -+ headers, -+ method, -+ }); -+ response.statusCode = upstreamResponse.status; -+ upstreamResponse.headers.forEach((value, key) => { -+ if (!HOP_BY_HOP_HEADERS.has(key.toLowerCase())) { -+ response.setHeader(key, value); -+ } -+ }); -+ const body = Buffer.from(await upstreamResponse.arrayBuffer()); -+ response.end(body); -+} -+ -+export async function startStaticWebServer({ -+ apiBaseUrl, -+ host = DEFAULT_HOST, -+ port, + export function applyBootstrapEnvironment({ + env = process.env, + host = DEFAULT_HOST, +@@ -179,6 +188,7 @@ export function applyBootstrapEnvironment({ + + export function formatBootstrapDiagnostics({ + apiBaseUrl, ++ browserLaunch, + mode, + role, + runtimeEnv, +@@ -192,6 +202,9 @@ export function formatBootstrapDiagnostics({ + `Role: ${role}`, + `Web URL: ${webBaseUrl}`, + `API URL: ${apiBaseUrl}/api`, ++ browserLaunch?.enabled ++ ? `Browser launch: opened ${browserLaunch.url}` ++ : `Browser launch: ${browserLaunch?.reason || "not requested"}`, + "Environment source: .env + process environment", + `.env loaded: ${runtimeEnv.loaded ? `yes (${runtimeEnv.loadedKeys} new key(s))` : "no"}`, + `Supported teams: ${supportedBootstrapTeamsLabel()}`, +@@ -201,6 +214,36 @@ export function formatBootstrapDiagnostics({ + ]; + } + ++export function launchBrowser(url, { ++ platform = process.platform, ++ spawnFn = spawn, +} = {}) { -+ if (!apiBaseUrl) { -+ throw new Error("startStaticWebServer requires apiBaseUrl."); ++ let command; ++ let args; ++ if (platform === "win32") { ++ command = "cmd"; ++ args = ["/c", "start", "", url]; ++ } else if (platform === "darwin") { ++ command = "open"; ++ args = [url]; ++ } else { ++ command = "xdg-open"; ++ args = [url]; + } -+ const server = http.createServer(async (request, response) => { -+ try { -+ const requestUrl = new URL(request.url || "/", `http://${host}:${port}`); -+ if (requestUrl.pathname === "/api" || requestUrl.pathname.startsWith("/api/")) { -+ await proxyApiRequest(request, response, requestUrl, apiBaseUrl); -+ return; -+ } -+ const decodedPath = decodeURIComponent(requestUrl.pathname); -+ const normalizedPath = resolveBrowserRoutePath(decodedPath); -+ const absolutePath = path.resolve(repoRoot, `.${normalizedPath}`); -+ if (!isInsideRepoRoot(absolutePath)) { -+ response.statusCode = 403; -+ response.end("Forbidden"); -+ return; -+ } -+ let targetPath = absolutePath; -+ const stat = await fs.stat(targetPath).catch(() => null); -+ if (stat && stat.isDirectory()) { -+ targetPath = path.join(targetPath, "index.html"); -+ } -+ targetPath = localAdminNotesHeaderPartialPath(repoRoot, targetPath); -+ const responseContents = await fs.readFile(targetPath); -+ response.statusCode = 200; -+ response.setHeader("Content-Type", contentTypeForPath(targetPath)); -+ response.end(responseContents); -+ } catch { -+ response.statusCode = 404; -+ response.end("Not Found"); -+ } -+ }); -+ -+ await new Promise((resolve, reject) => { -+ server.once("error", reject); -+ server.listen(port, host, resolve); ++ const child = spawnFn(command, args, { ++ detached: true, ++ stdio: "ignore", ++ windowsHide: true, + }); -+ ++ child?.unref?.(); + return { -+ baseUrl: localUrl(host, port), -+ close: async () => { -+ await new Promise((resolve, reject) => { -+ server.close((error) => { -+ if (error) reject(error); -+ else resolve(); -+ }); -+ }); -+ }, -+ server, -+ }; -+} -+ -+export async function startBootstrapRuntime(options = parseBootstrapOptions(process.argv.slice(2))) { -+ const runtimeEnv = loadRuntimeEnv(); -+ const host = process.env.GAMEFOUNDRY_LOCAL_API_HOST || DEFAULT_HOST; -+ applyBootstrapEnvironment({ host, ports: options.ports }); -+ const apiBaseUrl = localUrl(host, options.ports.apiPort); -+ const webBaseUrl = localUrl(host, options.ports.webPort); -+ const servers = []; -+ -+ if (options.api) { -+ servers.push(await startLocalApiServer({ host, port: options.ports.apiPort })); -+ } -+ if (options.web) { -+ servers.push(await startStaticWebServer({ apiBaseUrl, host, port: options.ports.webPort })); -+ } -+ -+ return { -+ close: async () => { -+ await Promise.all(servers.map((server) => server.close())); -+ }, -+ diagnostics: formatBootstrapDiagnostics({ -+ apiBaseUrl, -+ mode: options.mode, -+ role: options.role, -+ runtimeEnv, -+ team: options.team, -+ webBaseUrl, -+ }), -+ servers, ++ args, ++ command, ++ enabled: true, ++ url, + }; +} + -+async function main() { -+ let runtime; -+ try { -+ runtime = await startBootstrapRuntime(); -+ runtime.diagnostics.forEach((line) => console.log(line)); -+ } catch (error) { -+ console.error(error instanceof Error ? error.message : String(error)); -+ process.exitCode = 1; -+ return; -+ } -+ -+ for (const signal of ["SIGINT", "SIGTERM"]) { -+ process.once(signal, async () => { -+ await runtime.close(); -+ process.exit(0); -+ }); -+ } -+} -+ -+if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { -+ await main(); -+} -diff --git a/dev/scripts/team-port-config.mjs b/dev/scripts/team-port-config.mjs -new file mode 100644 -index 000000000..2dfa98fc3 ---- /dev/null -+++ b/dev/scripts/team-port-config.mjs -@@ -0,0 +1,89 @@ -+const TEAM_BASE_PORTS = Object.freeze({ -+ owner: Object.freeze({ apiPort: 5501, webPort: 5500 }), -+ alfa: Object.freeze({ apiPort: 5511, webPort: 5510 }), -+ bravo: Object.freeze({ apiPort: 5521, webPort: 5520 }), -+ charlie: Object.freeze({ apiPort: 5531, webPort: 5530 }), -+ delta: Object.freeze({ apiPort: 5541, webPort: 5540 }), -+ echo: Object.freeze({ apiPort: 5551, webPort: 5550 }), -+ foxtrot: Object.freeze({ apiPort: 5561, webPort: 5560 }), -+ golf: Object.freeze({ apiPort: 5571, webPort: 5570 }), -+ hotel: Object.freeze({ apiPort: 5581, webPort: 5580 }), -+}); -+ -+const ROLE_PORT_OFFSETS = Object.freeze({ -+ owner: 0, -+ codex: 2, -+}); -+ -+export const DEFAULT_BOOTSTRAP_ROLE = "owner"; -+export const DEFAULT_BOOTSTRAP_TEAM = "owner"; -+export const SUPPORTED_BOOTSTRAP_ROLES = Object.freeze(Object.keys(ROLE_PORT_OFFSETS)); -+export const SUPPORTED_BOOTSTRAP_TEAMS = Object.freeze(Object.keys(TEAM_BASE_PORTS)); -+ -+function normalizeToken(value, fallback) { -+ return String(value || fallback).trim().toLowerCase(); -+} -+ -+export function supportedBootstrapRolesLabel() { -+ return SUPPORTED_BOOTSTRAP_ROLES.join(", "); -+} -+ -+export function supportedBootstrapTeamsLabel() { -+ return SUPPORTED_BOOTSTRAP_TEAMS.join(", "); -+} -+ -+export function resolveTeamPortConfig({ -+ role = DEFAULT_BOOTSTRAP_ROLE, -+ team = DEFAULT_BOOTSTRAP_TEAM, -+} = {}) { -+ const normalizedTeam = normalizeToken(team, DEFAULT_BOOTSTRAP_TEAM); -+ const normalizedRole = normalizeToken(role, DEFAULT_BOOTSTRAP_ROLE); -+ const basePorts = TEAM_BASE_PORTS[normalizedTeam]; -+ if (!basePorts) { -+ throw new Error( -+ `Unknown bootstrap team "${String(team || "").trim() || "(empty)"}". Use one of: ${supportedBootstrapTeamsLabel()}.` -+ ); -+ } -+ const offset = ROLE_PORT_OFFSETS[normalizedRole]; -+ if (offset === undefined) { -+ throw new Error( -+ `Unknown bootstrap role "${String(role || "").trim() || "(empty)"}". Use one of: ${supportedBootstrapRolesLabel()}.` -+ ); -+ } -+ return Object.freeze({ -+ apiPort: basePorts.apiPort + offset, -+ role: normalizedRole, -+ team: normalizedTeam, -+ webPort: basePorts.webPort + offset, -+ }); -+} -+ -+function parseNamedArgument(args, name, fallback, supportedLabel) { -+ const values = Array.from(args); -+ for (let index = 0; index < values.length; index += 1) { -+ const argument = values[index]; -+ if (argument === `--${name}`) { -+ const value = values[index + 1]; -+ if (!value || value.startsWith("--")) { -+ throw new Error(`Missing bootstrap ${name} after --${name}. Use one of: ${supportedLabel()}.`); -+ } -+ return normalizeToken(value, fallback); -+ } -+ if (argument.startsWith(`--${name}=`)) { -+ const value = argument.slice(`--${name}=`.length); -+ if (!value) { -+ throw new Error(`Missing bootstrap ${name} after --${name}=. Use one of: ${supportedLabel()}.`); -+ } -+ return normalizeToken(value, fallback); -+ } -+ } -+ return fallback; -+} -+ -+export function parseRoleArgument(args = []) { -+ return parseNamedArgument(args, "role", DEFAULT_BOOTSTRAP_ROLE, supportedBootstrapRolesLabel); -+} + async function readRequestBody(request) { + const chunks = []; + for await (const chunk of request) { +@@ -291,27 +334,48 @@ export async function startStaticWebServer({ + }; + } + +-export async function startBootstrapRuntime(options = parseBootstrapOptions(process.argv.slice(2))) { +- const runtimeEnv = loadRuntimeEnv(); +- const host = process.env.GAMEFOUNDRY_LOCAL_API_HOST || DEFAULT_HOST; +- applyBootstrapEnvironment({ host, ports: options.ports }); ++export async function startBootstrapRuntime( ++ options = parseBootstrapOptions(process.argv.slice(2)), ++ { ++ apiServerStarter = startLocalApiServer, ++ browserLauncher = launchBrowser, ++ env = process.env, ++ loadEnv = () => loadRuntimeEnv({ env }), ++ webServerStarter = startStaticWebServer, ++ } = {}, ++) { ++ const runtimeEnv = loadEnv(); ++ const host = env.GAMEFOUNDRY_LOCAL_API_HOST || DEFAULT_HOST; ++ applyBootstrapEnvironment({ env, host, ports: options.ports }); + const apiBaseUrl = localUrl(host, options.ports.apiPort); + const webBaseUrl = localUrl(host, options.ports.webPort); + const servers = []; + + if (options.api) { +- servers.push(await startLocalApiServer({ host, port: options.ports.apiPort })); ++ servers.push(await apiServerStarter({ host, port: options.ports.apiPort })); + } + if (options.web) { +- servers.push(await startStaticWebServer({ apiBaseUrl, host, port: options.ports.webPort })); ++ servers.push(await webServerStarter({ apiBaseUrl, host, port: options.ports.webPort })); + } + ++ const browserUrl = teamIndexUrl(webBaseUrl); ++ const browserLaunch = shouldLaunchBrowser(options) ++ ? browserLauncher(browserUrl) ++ : { ++ enabled: false, ++ reason: options.role === "codex" ++ ? "suppressed for codex role" ++ : "requires bootstrap mode with API and web servers", ++ url: browserUrl, ++ }; + -+export function parseTeamArgument(args = []) { -+ return parseNamedArgument(args, "team", DEFAULT_BOOTSTRAP_TEAM, supportedBootstrapTeamsLabel); -+} + return { + close: async () => { + await Promise.all(servers.map((server) => server.close())); + }, + diagnostics: formatBootstrapDiagnostics({ + apiBaseUrl, ++ browserLaunch, + mode: options.mode, + role: options.role, + runtimeEnv, diff --git a/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs b/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs -new file mode 100644 -index 000000000..0576d4db2 ---- /dev/null +index 0576d4db2..2a4e16fe6 100644 +--- a/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs +++ b/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs -@@ -0,0 +1,197 @@ -+import assert from "node:assert/strict"; -+import { readFileSync } from "node:fs"; -+import test from "node:test"; -+import { fileURLToPath } from "node:url"; -+import { -+ applyBootstrapEnvironment, -+ formatBootstrapDiagnostics, -+ loadRuntimeEnv, -+ parseBootstrapOptions, -+} from "../../scripts/start-dev.mjs"; -+import { -+ parseRoleArgument, -+ parseTeamArgument, -+ resolveTeamPortConfig, -+ supportedBootstrapRolesLabel, -+ supportedBootstrapTeamsLabel, -+} from "../../scripts/team-port-config.mjs"; -+ -+const repoRoot = fileURLToPath(new URL("../../..", import.meta.url)); -+ -+const BASE_PORTS = Object.freeze({ -+ owner: Object.freeze({ apiPort: 5501, webPort: 5500 }), -+ alfa: Object.freeze({ apiPort: 5511, webPort: 5510 }), -+ bravo: Object.freeze({ apiPort: 5521, webPort: 5520 }), -+ charlie: Object.freeze({ apiPort: 5531, webPort: 5530 }), -+ delta: Object.freeze({ apiPort: 5541, webPort: 5540 }), -+ echo: Object.freeze({ apiPort: 5551, webPort: 5550 }), -+ foxtrot: Object.freeze({ apiPort: 5561, webPort: 5560 }), -+ golf: Object.freeze({ apiPort: 5571, webPort: 5570 }), -+ hotel: Object.freeze({ apiPort: 5581, webPort: 5580 }), -+}); -+ -+test("team port config resolves owner role base ports", () => { -+ Object.entries(BASE_PORTS).forEach(([team, ports]) => { -+ assert.deepEqual(resolveTeamPortConfig({ team }), { -+ apiPort: ports.apiPort, -+ role: "owner", -+ team, -+ webPort: ports.webPort, -+ }); -+ }); -+}); -+ -+test("team port config resolves codex role ports as base ports plus two", () => { -+ Object.entries(BASE_PORTS).forEach(([team, ports]) => { -+ assert.deepEqual(resolveTeamPortConfig({ role: "codex", team }), { -+ apiPort: ports.apiPort + 2, -+ role: "codex", -+ team, -+ webPort: ports.webPort + 2, -+ }); -+ }); -+}); -+ -+test("team and role parsers accept canonical argument forms", () => { -+ assert.equal(parseTeamArgument([]), "owner"); -+ assert.equal(parseTeamArgument(["--team", "alfa"]), "alfa"); -+ assert.equal(parseTeamArgument(["--team=bravo"]), "bravo"); -+ assert.equal(parseRoleArgument([]), "owner"); -+ assert.equal(parseRoleArgument(["--role", "codex"]), "codex"); -+ assert.equal(parseRoleArgument(["--role=owner"]), "owner"); -+}); -+ -+test("invalid teams and roles fail with supported values", () => { -+ assert.throws( -+ () => resolveTeamPortConfig({ team: "omega" }), -+ /Unknown bootstrap team "omega".*owner, alfa, bravo, charlie, delta, echo, foxtrot, golf, hotel/ -+ ); -+ assert.throws( -+ () => resolveTeamPortConfig({ role: "reviewer", team: "alfa" }), -+ /Unknown bootstrap role "reviewer".*owner, codex/ -+ ); -+ assert.throws( -+ () => parseTeamArgument(["--team"]), -+ /Missing bootstrap team after --team/ -+ ); -+ assert.throws( -+ () => parseRoleArgument(["--role"]), -+ /Missing bootstrap role after --role/ -+ ); -+}); -+ -+test("bootstrap option parser maps npm script modes to server startup plan", () => { -+ assert.deepEqual(parseBootstrapOptions(["--mode", "bootstrap", "--team", "alfa"]), { -+ api: true, -+ mode: "bootstrap", -+ ports: { -+ apiPort: 5511, -+ role: "owner", -+ team: "alfa", -+ webPort: 5510, +@@ -5,8 +5,12 @@ import { fileURLToPath } from "node:url"; + import { + applyBootstrapEnvironment, + formatBootstrapDiagnostics, ++ launchBrowser, + loadRuntimeEnv, + parseBootstrapOptions, ++ shouldLaunchBrowser, ++ startBootstrapRuntime, ++ teamIndexUrl, + } from "../../scripts/start-dev.mjs"; + import { + parseRoleArgument, +@@ -159,6 +163,11 @@ test("bootstrap environment applies resolved team and role ports for browser API + test("startup diagnostics and package scripts expose team-aware bootstrap commands", () => { + const lines = formatBootstrapDiagnostics({ + apiBaseUrl: "http://127.0.0.1:5513", ++ browserLaunch: { ++ enabled: false, ++ reason: "suppressed for codex role", ++ url: "http://127.0.0.1:5512/index.html", + }, -+ role: "owner", -+ team: "alfa", -+ web: true, -+ }); -+ assert.deepEqual(parseBootstrapOptions(["--mode=bootstrap", "--team=alfa", "--role=codex"]), { -+ api: true, -+ mode: "bootstrap", -+ ports: { -+ apiPort: 5513, -+ role: "codex", -+ team: "alfa", -+ webPort: 5512, + mode: "bootstrap", + role: "codex", + runtimeEnv: { +@@ -173,6 +182,7 @@ test("startup diagnostics and package scripts expose team-aware bootstrap comman + assert.equal(lines.includes("Role: codex"), true); + assert.equal(lines.includes("Web URL: http://127.0.0.1:5512"), true); + assert.equal(lines.includes("API URL: http://127.0.0.1:5513/api"), true); ++ assert.equal(lines.includes("Browser launch: suppressed for codex role"), true); + assert.equal(lines.includes(`Supported teams: ${supportedBootstrapTeamsLabel()}`), true); + assert.equal(lines.includes(`Supported roles: ${supportedBootstrapRolesLabel()}`), true); + +@@ -195,3 +205,112 @@ test("runtime env loader applies .env values without overriding existing process + assert.equal(typeof result.loaded, "boolean"); + assert.equal(env.GAMEFOUNDRY_API_URL, "http://127.0.0.1:5999/api"); + }); ++ ++test("browser launch targets team index after API and web servers are ready for owner role", async () => { ++ const events = []; ++ const runtime = await startBootstrapRuntime( ++ parseBootstrapOptions(["--mode=bootstrap", "--team=alfa"]), ++ { ++ apiServerStarter: async ({ port }) => { ++ events.push(`api:${port}`); ++ return { ++ close: async () => events.push("api:closed"), ++ }; ++ }, ++ browserLauncher: (url) => { ++ events.push(`browser:${url}`); ++ return { ++ enabled: true, ++ url, ++ }; ++ }, ++ env: {}, ++ loadEnv: () => ({ ++ loaded: false, ++ loadedKeys: 0, ++ }), ++ webServerStarter: async ({ port }) => { ++ events.push(`web:${port}`); ++ return { ++ close: async () => events.push("web:closed"), ++ }; ++ }, + }, -+ role: "codex", -+ team: "alfa", -+ web: true, -+ }); -+ assert.deepEqual(parseBootstrapOptions(["--mode=api", "--team=charlie", "--role=codex"]), { -+ api: true, -+ mode: "api", -+ ports: { -+ apiPort: 5533, -+ role: "codex", -+ team: "charlie", -+ webPort: 5532, -+ }, -+ role: "codex", -+ team: "charlie", -+ web: false, -+ }); -+ assert.deepEqual(parseBootstrapOptions(["--mode=web", "--team=owner"]), { -+ api: false, -+ mode: "web", -+ ports: { -+ apiPort: 5501, -+ role: "owner", -+ team: "owner", -+ webPort: 5500, -+ }, -+ role: "owner", -+ team: "owner", -+ web: true, -+ }); -+ assert.throws( -+ () => parseBootstrapOptions(["--mode=deploy", "--team=alfa"]), -+ /Unknown bootstrap mode "deploy"/ + ); ++ ++ assert.deepEqual(events, [ ++ "api:5511", ++ "web:5510", ++ "browser:http://127.0.0.1:5510/index.html", ++ ]); ++ assert.equal(runtime.diagnostics.includes("Browser launch: opened http://127.0.0.1:5510/index.html"), true); ++ await runtime.close(); ++ assert.deepEqual(events.slice(-2), ["api:closed", "web:closed"]); +}); + -+test("bootstrap environment applies resolved team and role ports for browser API routing", () => { -+ const env = { -+ GAMEFOUNDRY_API_URL: "http://127.0.0.1:5501/api", -+ GAMEFOUNDRY_SITE_URL: "http://127.0.0.1:5500", -+ }; -+ applyBootstrapEnvironment({ -+ env, -+ host: "127.0.0.1", -+ ports: resolveTeamPortConfig({ role: "codex", team: "alfa" }), -+ }); ++test("codex role suppresses browser launch even after API and web servers are ready", async () => { ++ const events = []; ++ const runtime = await startBootstrapRuntime( ++ parseBootstrapOptions(["--mode=bootstrap", "--team=alfa", "--role=codex"]), ++ { ++ apiServerStarter: async () => { ++ events.push("api"); ++ return { ++ close: async () => {}, ++ }; ++ }, ++ browserLauncher: (url) => { ++ events.push(`browser:${url}`); ++ return { ++ enabled: true, ++ url, ++ }; ++ }, ++ env: {}, ++ loadEnv: () => ({ ++ loaded: false, ++ loadedKeys: 0, ++ }), ++ webServerStarter: async () => { ++ events.push("web"); ++ return { ++ close: async () => {}, ++ }; ++ }, ++ }, ++ ); + -+ assert.equal(env.GAMEFOUNDRY_LOCAL_API_HOST, "127.0.0.1"); -+ assert.equal(env.GAMEFOUNDRY_LOCAL_API_PORT, "5513"); -+ assert.equal(env.GAMEFOUNDRY_API_URL, "http://127.0.0.1:5513/api"); -+ assert.equal(env.GAMEFOUNDRY_SITE_URL, "http://127.0.0.1:5512"); ++ assert.deepEqual(events, ["api", "web"]); ++ assert.equal(runtime.diagnostics.includes("Browser launch: suppressed for codex role"), true); ++ await runtime.close(); +}); + -+test("startup diagnostics and package scripts expose team-aware bootstrap commands", () => { -+ const lines = formatBootstrapDiagnostics({ -+ apiBaseUrl: "http://127.0.0.1:5513", -+ mode: "bootstrap", -+ role: "codex", -+ runtimeEnv: { -+ loaded: true, -+ loadedKeys: 3, ++test("browser launch helper uses platform conventions and team index URLs", () => { ++ const launches = []; ++ const result = launchBrowser(teamIndexUrl("http://127.0.0.1:5510/"), { ++ platform: "win32", ++ spawnFn: (command, args, options) => { ++ launches.push({ args, command, options }); ++ return { ++ unref: () => launches.push({ unref: true }), ++ }; + }, -+ team: "alfa", -+ webBaseUrl: "http://127.0.0.1:5512", + }); + -+ assert.equal(lines.includes("Team: alfa"), true); -+ assert.equal(lines.includes("Role: codex"), true); -+ assert.equal(lines.includes("Web URL: http://127.0.0.1:5512"), true); -+ assert.equal(lines.includes("API URL: http://127.0.0.1:5513/api"), true); -+ assert.equal(lines.includes(`Supported teams: ${supportedBootstrapTeamsLabel()}`), true); -+ assert.equal(lines.includes(`Supported roles: ${supportedBootstrapRolesLabel()}`), true); -+ -+ const packageJson = JSON.parse(readFileSync(`${repoRoot}/package.json`, "utf8")); -+ assert.equal(packageJson.scripts["dev:bootstrap"], "node --use-system-ca ./dev/scripts/start-dev.mjs --mode bootstrap"); -+ assert.equal(packageJson.scripts["dev:api"], "node --use-system-ca ./dev/scripts/start-dev.mjs --mode api"); -+ assert.equal(packageJson.scripts["dev:web"], "node ./dev/scripts/start-dev.mjs --mode web"); -+ assert.equal(packageJson.scripts["dev:local-api"], "node --use-system-ca ./dev/scripts/start-local-api-server.mjs"); -+}); -+ -+test("runtime env loader applies .env values without overriding existing process values", () => { -+ const env = { -+ GAMEFOUNDRY_API_URL: "http://127.0.0.1:5999/api", -+ }; -+ const result = loadRuntimeEnv({ -+ cwd: repoRoot, -+ env, ++ assert.deepEqual(result, { ++ args: ["/c", "start", "", "http://127.0.0.1:5510/index.html"], ++ command: "cmd", ++ enabled: true, ++ url: "http://127.0.0.1:5510/index.html", + }); ++ assert.equal(launches[0].options.detached, true); ++ assert.equal(launches[0].options.stdio, "ignore"); ++ assert.deepEqual(launches[1], { unref: true }); ++}); + -+ assert.equal(typeof result.loaded, "boolean"); -+ assert.equal(env.GAMEFOUNDRY_API_URL, "http://127.0.0.1:5999/api"); ++test("browser launch policy only opens for owner role when API and web are both active", () => { ++ assert.equal(shouldLaunchBrowser(parseBootstrapOptions(["--mode=bootstrap", "--team=alfa"])), true); ++ assert.equal(shouldLaunchBrowser(parseBootstrapOptions(["--mode=bootstrap", "--team=alfa", "--role=codex"])), false); ++ assert.equal(shouldLaunchBrowser(parseBootstrapOptions(["--mode=api", "--team=alfa"])), false); ++ assert.equal(shouldLaunchBrowser(parseBootstrapOptions(["--mode=web", "--team=alfa"])), false); +}); diff --git a/dev/scripts/start-dev.mjs b/dev/scripts/start-dev.mjs index 625360989..15d3ac1fa 100644 --- a/dev/scripts/start-dev.mjs +++ b/dev/scripts/start-dev.mjs @@ -1,5 +1,6 @@ import fs from "node:fs/promises"; import { existsSync, readFileSync } from "node:fs"; +import { spawn } from "node:child_process"; import http from "node:http"; import path from "node:path"; import process from "node:process"; @@ -165,6 +166,14 @@ function apiUrl(host, port) { return `${localUrl(host, port)}/api`; } +export function teamIndexUrl(webBaseUrl) { + return `${String(webBaseUrl || "").replace(/\/+$/, "")}/index.html`; +} + +export function shouldLaunchBrowser(options) { + return Boolean(options?.api && options?.web && options?.role === "owner"); +} + export function applyBootstrapEnvironment({ env = process.env, host = DEFAULT_HOST, @@ -179,6 +188,7 @@ export function applyBootstrapEnvironment({ export function formatBootstrapDiagnostics({ apiBaseUrl, + browserLaunch, mode, role, runtimeEnv, @@ -192,6 +202,9 @@ export function formatBootstrapDiagnostics({ `Role: ${role}`, `Web URL: ${webBaseUrl}`, `API URL: ${apiBaseUrl}/api`, + browserLaunch?.enabled + ? `Browser launch: opened ${browserLaunch.url}` + : `Browser launch: ${browserLaunch?.reason || "not requested"}`, "Environment source: .env + process environment", `.env loaded: ${runtimeEnv.loaded ? `yes (${runtimeEnv.loadedKeys} new key(s))` : "no"}`, `Supported teams: ${supportedBootstrapTeamsLabel()}`, @@ -201,6 +214,36 @@ export function formatBootstrapDiagnostics({ ]; } +export function launchBrowser(url, { + platform = process.platform, + spawnFn = spawn, +} = {}) { + let command; + let args; + if (platform === "win32") { + command = "cmd"; + args = ["/c", "start", "", url]; + } else if (platform === "darwin") { + command = "open"; + args = [url]; + } else { + command = "xdg-open"; + args = [url]; + } + const child = spawnFn(command, args, { + detached: true, + stdio: "ignore", + windowsHide: true, + }); + child?.unref?.(); + return { + args, + command, + enabled: true, + url, + }; +} + async function readRequestBody(request) { const chunks = []; for await (const chunk of request) { @@ -291,27 +334,48 @@ export async function startStaticWebServer({ }; } -export async function startBootstrapRuntime(options = parseBootstrapOptions(process.argv.slice(2))) { - const runtimeEnv = loadRuntimeEnv(); - const host = process.env.GAMEFOUNDRY_LOCAL_API_HOST || DEFAULT_HOST; - applyBootstrapEnvironment({ host, ports: options.ports }); +export async function startBootstrapRuntime( + options = parseBootstrapOptions(process.argv.slice(2)), + { + apiServerStarter = startLocalApiServer, + browserLauncher = launchBrowser, + env = process.env, + loadEnv = () => loadRuntimeEnv({ env }), + webServerStarter = startStaticWebServer, + } = {}, +) { + const runtimeEnv = loadEnv(); + const host = env.GAMEFOUNDRY_LOCAL_API_HOST || DEFAULT_HOST; + applyBootstrapEnvironment({ env, host, ports: options.ports }); const apiBaseUrl = localUrl(host, options.ports.apiPort); const webBaseUrl = localUrl(host, options.ports.webPort); const servers = []; if (options.api) { - servers.push(await startLocalApiServer({ host, port: options.ports.apiPort })); + servers.push(await apiServerStarter({ host, port: options.ports.apiPort })); } if (options.web) { - servers.push(await startStaticWebServer({ apiBaseUrl, host, port: options.ports.webPort })); + servers.push(await webServerStarter({ apiBaseUrl, host, port: options.ports.webPort })); } + const browserUrl = teamIndexUrl(webBaseUrl); + const browserLaunch = shouldLaunchBrowser(options) + ? browserLauncher(browserUrl) + : { + enabled: false, + reason: options.role === "codex" + ? "suppressed for codex role" + : "requires bootstrap mode with API and web servers", + url: browserUrl, + }; + return { close: async () => { await Promise.all(servers.map((server) => server.close())); }, diagnostics: formatBootstrapDiagnostics({ apiBaseUrl, + browserLaunch, mode: options.mode, role: options.role, runtimeEnv, diff --git a/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs b/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs index 0576d4db2..2a4e16fe6 100644 --- a/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs +++ b/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs @@ -5,8 +5,12 @@ import { fileURLToPath } from "node:url"; import { applyBootstrapEnvironment, formatBootstrapDiagnostics, + launchBrowser, loadRuntimeEnv, parseBootstrapOptions, + shouldLaunchBrowser, + startBootstrapRuntime, + teamIndexUrl, } from "../../scripts/start-dev.mjs"; import { parseRoleArgument, @@ -159,6 +163,11 @@ test("bootstrap environment applies resolved team and role ports for browser API test("startup diagnostics and package scripts expose team-aware bootstrap commands", () => { const lines = formatBootstrapDiagnostics({ apiBaseUrl: "http://127.0.0.1:5513", + browserLaunch: { + enabled: false, + reason: "suppressed for codex role", + url: "http://127.0.0.1:5512/index.html", + }, mode: "bootstrap", role: "codex", runtimeEnv: { @@ -173,6 +182,7 @@ test("startup diagnostics and package scripts expose team-aware bootstrap comman assert.equal(lines.includes("Role: codex"), true); assert.equal(lines.includes("Web URL: http://127.0.0.1:5512"), true); assert.equal(lines.includes("API URL: http://127.0.0.1:5513/api"), true); + assert.equal(lines.includes("Browser launch: suppressed for codex role"), true); assert.equal(lines.includes(`Supported teams: ${supportedBootstrapTeamsLabel()}`), true); assert.equal(lines.includes(`Supported roles: ${supportedBootstrapRolesLabel()}`), true); @@ -195,3 +205,112 @@ test("runtime env loader applies .env values without overriding existing process assert.equal(typeof result.loaded, "boolean"); assert.equal(env.GAMEFOUNDRY_API_URL, "http://127.0.0.1:5999/api"); }); + +test("browser launch targets team index after API and web servers are ready for owner role", async () => { + const events = []; + const runtime = await startBootstrapRuntime( + parseBootstrapOptions(["--mode=bootstrap", "--team=alfa"]), + { + apiServerStarter: async ({ port }) => { + events.push(`api:${port}`); + return { + close: async () => events.push("api:closed"), + }; + }, + browserLauncher: (url) => { + events.push(`browser:${url}`); + return { + enabled: true, + url, + }; + }, + env: {}, + loadEnv: () => ({ + loaded: false, + loadedKeys: 0, + }), + webServerStarter: async ({ port }) => { + events.push(`web:${port}`); + return { + close: async () => events.push("web:closed"), + }; + }, + }, + ); + + assert.deepEqual(events, [ + "api:5511", + "web:5510", + "browser:http://127.0.0.1:5510/index.html", + ]); + assert.equal(runtime.diagnostics.includes("Browser launch: opened http://127.0.0.1:5510/index.html"), true); + await runtime.close(); + assert.deepEqual(events.slice(-2), ["api:closed", "web:closed"]); +}); + +test("codex role suppresses browser launch even after API and web servers are ready", async () => { + const events = []; + const runtime = await startBootstrapRuntime( + parseBootstrapOptions(["--mode=bootstrap", "--team=alfa", "--role=codex"]), + { + apiServerStarter: async () => { + events.push("api"); + return { + close: async () => {}, + }; + }, + browserLauncher: (url) => { + events.push(`browser:${url}`); + return { + enabled: true, + url, + }; + }, + env: {}, + loadEnv: () => ({ + loaded: false, + loadedKeys: 0, + }), + webServerStarter: async () => { + events.push("web"); + return { + close: async () => {}, + }; + }, + }, + ); + + assert.deepEqual(events, ["api", "web"]); + assert.equal(runtime.diagnostics.includes("Browser launch: suppressed for codex role"), true); + await runtime.close(); +}); + +test("browser launch helper uses platform conventions and team index URLs", () => { + const launches = []; + const result = launchBrowser(teamIndexUrl("http://127.0.0.1:5510/"), { + platform: "win32", + spawnFn: (command, args, options) => { + launches.push({ args, command, options }); + return { + unref: () => launches.push({ unref: true }), + }; + }, + }); + + assert.deepEqual(result, { + args: ["/c", "start", "", "http://127.0.0.1:5510/index.html"], + command: "cmd", + enabled: true, + url: "http://127.0.0.1:5510/index.html", + }); + assert.equal(launches[0].options.detached, true); + assert.equal(launches[0].options.stdio, "ignore"); + assert.deepEqual(launches[1], { unref: true }); +}); + +test("browser launch policy only opens for owner role when API and web are both active", () => { + assert.equal(shouldLaunchBrowser(parseBootstrapOptions(["--mode=bootstrap", "--team=alfa"])), true); + assert.equal(shouldLaunchBrowser(parseBootstrapOptions(["--mode=bootstrap", "--team=alfa", "--role=codex"])), false); + assert.equal(shouldLaunchBrowser(parseBootstrapOptions(["--mode=api", "--team=alfa"])), false); + assert.equal(shouldLaunchBrowser(parseBootstrapOptions(["--mode=web", "--team=alfa"])), false); +}); From 80f732e56fdf919c79f1c7b2c01d67c1cc489e02 Mon Sep 17 00:00:00 2001 From: Alfa Team Date: Sun, 28 Jun 2026 08:09:47 -0400 Subject: [PATCH 3/5] ALFA 009 fix bootstrap team forwarding --- ...aware-bootstrap_manual-validation-notes.md | 8 + ...71_ALFA_009-team-aware-bootstrap_report.md | 3 + ...m-aware-bootstrap_requirement-checklist.md | 3 + ...-team-aware-bootstrap_validation-report.md | 5 +- dev/reports/codex_changed_files.txt | 16 +- dev/reports/codex_review.diff | 473 ++++++------------ dev/scripts/start-dev.mjs | 2 +- dev/scripts/team-port-config.mjs | 29 +- .../dev-runtime/TeamAwareBootstrap.test.mjs | 32 +- 9 files changed, 238 insertions(+), 333 deletions(-) diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md index c40377fb2..eba870a8b 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md @@ -45,6 +45,7 @@ Expected result: npm run dev:bootstrap -- --team owner npm run dev:bootstrap -- --team bravo npm run dev:bootstrap -- --team charlie +npm run dev:bootstrap -- charlie npm run dev:bootstrap -- --team delta npm run dev:bootstrap -- --team echo npm run dev:bootstrap -- --team foxtrot @@ -52,6 +53,13 @@ npm run dev:bootstrap -- --team golf npm run dev:bootstrap -- --team hotel ``` +For Charlie, both forms should show: + +- `Team: charlie` +- `Web URL: http://127.0.0.1:5530` +- `API URL: http://127.0.0.1:5531/api` +- `Browser launch: http://127.0.0.1:5530/index.html` + ## Failure Checks ```powershell diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md index 1690a93ec..95d02d5a3 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md @@ -30,6 +30,7 @@ Per OWNER instruction, those uncommitted changes were discarded with `git reset - Added focused node tests for team resolution, role offsets, invalid team/role validation, script wiring, `.env` loading behavior, and bootstrap diagnostics. - Added automatic browser launch for owner-role `dev:bootstrap` after the API and web servers are both ready. - Suppressed automatic browser launch for the `codex` role. +- Fixed npm argument forwarding/parsing so both `--team charlie` and positional `charlie` select the same team after the package script's `--mode bootstrap` argument. ## Supported Teams @@ -96,6 +97,8 @@ npm run dev:bootstrap -- --team alfa --role codex - Browser launch happens only after both API and web servers are ready. - The Alfa owner launch target is `http://127.0.0.1:5510/index.html`. - The Alfa codex launch target is suppressed while using web `5512` and API `5513`. +- The Charlie launch target is `http://127.0.0.1:5530/index.html` for both `npm run dev:bootstrap -- --team charlie` and `npm run dev:bootstrap -- charlie`. +- Computed team/role ports override any `.env` port values loaded before bootstrap startup. ## Files Changed diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md index 4600ad790..a6c478735 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md @@ -16,6 +16,9 @@ | Do not use single-letter public team commands | PASS | Scan returned no matches in new bootstrap files. | | Validate unknown team names | PASS | `omega` fails clearly with supported team list. | | Validate unknown role names | PASS | `reviewer` fails clearly with supported role list. | +| Support `--team` argument forwarding | PASS | `npm run dev:bootstrap -- --team charlie` resolves Charlie ports. | +| Support positional team forwarding | PASS | `npm run dev:bootstrap -- charlie` resolves Charlie ports. | +| Prevent `.env` port override | PASS | Unit test confirms computed team/role ports overwrite loaded API/site port values. | | Load environment | PASS | Orchestrator loads `.env` values without overriding existing environment values. | | Resolve team ports | PASS | Unit tests cover all teams and both roles. | | Start Local API | PASS | `dev:bootstrap` starts the existing Local API server on resolved API port. | diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md index ea82082a8..5ca1cd0d0 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md @@ -13,10 +13,13 @@ | `npm run validate:canonical-structure` | PASS | Canonical repository structure guardrail passed. | | `npm run dev:bootstrap -- --team alfa` | PASS | Started with team `alfa`, role `owner`, web `5510`, API `5511`, launched browser to `http://127.0.0.1:5510/index.html`, then process tree was stopped. | | `npm run dev:bootstrap -- --team alfa --role codex` | PASS | Started with team `alfa`, role `codex`, web `5512`, API `5513`, suppressed browser launch, then process tree was stopped. | +| `npm run dev:bootstrap -- --team charlie` | PASS | Started with team `charlie`, role `owner`, web `5530`, API `5531`, launched browser to `http://127.0.0.1:5530/index.html`, then process tree was stopped. | +| `npm run dev:bootstrap -- charlie` | PASS | Positional team forwarding selected team `charlie`, web `5530`, API `5531`, and browser target `http://127.0.0.1:5530/index.html`. | +| `npm run dev:bootstrap -- --team bravo` | PASS | Started with team `bravo`, role `owner`, web `5520`, API `5521`, and browser target `http://127.0.0.1:5520/index.html`. | | `npm run dev:bootstrap -- --team omega` | PASS | Failed clearly with supported team list. | | `npm run dev:bootstrap -- --team alfa --role reviewer` | PASS | Failed clearly with supported role list. | | `npm run dev:local-api` | PASS | Legacy command started and printed legacy startup diagnostics, then process tree was stopped. | ## Notes -The startup-command validations used short-lived process launches and `taskkill /T /F` cleanup after expected diagnostics appeared, so no dev servers were left running. The owner-role validation opened the configured browser target after both server diagnostics were available. +The startup-command validations used short-lived process launches and `taskkill /T /F` cleanup after expected diagnostics appeared, so no dev servers were left running. Owner-role validations opened the configured browser target after both server diagnostics were available. Unit coverage verifies loaded `.env` port values are overwritten by computed team/role ports. diff --git a/dev/reports/codex_changed_files.txt b/dev/reports/codex_changed_files.txt index 0143fa0cf..53bb20d20 100644 --- a/dev/reports/codex_changed_files.txt +++ b/dev/reports/codex_changed_files.txt @@ -4,16 +4,18 @@ M dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md M dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md M dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md M dev/scripts/start-dev.mjs + M dev/scripts/team-port-config.mjs M dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs # git ls-files --others --exclude-standard (no output) # git diff --stat -...team-aware-bootstrap_manual-validation-notes.md | 2 + - ...R_26171_ALFA_009-team-aware-bootstrap_report.md | 10 ++ - ...9-team-aware-bootstrap_requirement-checklist.md | 3 + - ...A_009-team-aware-bootstrap_validation-report.md | 8 +- - dev/scripts/start-dev.mjs | 76 +++++++++++-- - dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs | 119 +++++++++++++++++++++ - 6 files changed, 208 insertions(+), 10 deletions(-) \ No newline at end of file +...team-aware-bootstrap_manual-validation-notes.md | 8 ++++++ + ...R_26171_ALFA_009-team-aware-bootstrap_report.md | 3 ++ + ...9-team-aware-bootstrap_requirement-checklist.md | 3 ++ + ...A_009-team-aware-bootstrap_validation-report.md | 5 +++- + dev/scripts/start-dev.mjs | 2 +- + dev/scripts/team-port-config.mjs | 29 +++++++++++++++++++- + dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs | 32 +++++++++++++++++++++- + 7 files changed, 78 insertions(+), 4 deletions(-) \ No newline at end of file diff --git a/dev/reports/codex_review.diff b/dev/reports/codex_review.diff index d6060d413..e4861c7ff 100644 --- a/dev/reports/codex_review.diff +++ b/dev/reports/codex_review.diff @@ -1,371 +1,200 @@ diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md -index 729a5f789..c40377fb2 100644 +index c40377fb2..eba870a8b 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md -@@ -12,6 +12,7 @@ Expected diagnostics: - - `Role: owner` - - `Web URL: http://127.0.0.1:5510` - - `API URL: http://127.0.0.1:5511/api` -+- `Browser launch: opened http://127.0.0.1:5510/index.html` - - ## Primary Alfa Codex Command - -@@ -25,6 +26,7 @@ Expected diagnostics: - - `Role: codex` - - `Web URL: http://127.0.0.1:5512` - - `API URL: http://127.0.0.1:5513/api` -+- `Browser launch: suppressed for codex role` +@@ -45,6 +45,7 @@ Expected result: + npm run dev:bootstrap -- --team owner + npm run dev:bootstrap -- --team bravo + npm run dev:bootstrap -- --team charlie ++npm run dev:bootstrap -- charlie + npm run dev:bootstrap -- --team delta + npm run dev:bootstrap -- --team echo + npm run dev:bootstrap -- --team foxtrot +@@ -52,6 +53,13 @@ npm run dev:bootstrap -- --team golf + npm run dev:bootstrap -- --team hotel + ``` - ## Legacy Command ++For Charlie, both forms should show: ++ ++- `Team: charlie` ++- `Web URL: http://127.0.0.1:5530` ++- `API URL: http://127.0.0.1:5531/api` ++- `Browser launch: http://127.0.0.1:5530/index.html` ++ + ## Failure Checks + ```powershell diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md -index 3aab1164d..1690a93ec 100644 +index 1690a93ec..95d02d5a3 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md -@@ -28,6 +28,8 @@ Per OWNER instruction, those uncommitted changes were discarded with `git reset - - Added `dev/scripts/team-port-config.mjs` for full team name and role-aware port resolution. - - Added `dev/scripts/start-dev.mjs` as the bootstrap orchestrator. +@@ -30,6 +30,7 @@ Per OWNER instruction, those uncommitted changes were discarded with `git reset - Added focused node tests for team resolution, role offsets, invalid team/role validation, script wiring, `.env` loading behavior, and bootstrap diagnostics. -+- Added automatic browser launch for owner-role `dev:bootstrap` after the API and web servers are both ready. -+- Suppressed automatic browser launch for the `codex` role. + - Added automatic browser launch for owner-role `dev:bootstrap` after the API and web servers are both ready. + - Suppressed automatic browser launch for the `codex` role. ++- Fixed npm argument forwarding/parsing so both `--team charlie` and positional `charlie` select the same team after the package script's `--mode bootstrap` argument. ## Supported Teams -@@ -87,6 +89,14 @@ Codex role example: - npm run dev:bootstrap -- --team alfa --role codex - ``` +@@ -96,6 +97,8 @@ npm run dev:bootstrap -- --team alfa --role codex + - Browser launch happens only after both API and web servers are ready. + - The Alfa owner launch target is `http://127.0.0.1:5510/index.html`. + - The Alfa codex launch target is suppressed while using web `5512` and API `5513`. ++- The Charlie launch target is `http://127.0.0.1:5530/index.html` for both `npm run dev:bootstrap -- --team charlie` and `npm run dev:bootstrap -- charlie`. ++- Computed team/role ports override any `.env` port values loaded before bootstrap startup. -+## Browser Launch Behavior -+ -+- `owner` role launches the browser automatically to the selected team's `index.html`. -+- `codex` role suppresses browser launch. -+- Browser launch happens only after both API and web servers are ready. -+- The Alfa owner launch target is `http://127.0.0.1:5510/index.html`. -+- The Alfa codex launch target is suppressed while using web `5512` and API `5513`. -+ ## Files Changed - - `package.json` - updated diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md -index 4d9238295..4600ad790 100644 +index 4600ad790..a6c478735 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md -@@ -21,5 +21,8 @@ +@@ -16,6 +16,9 @@ + | Do not use single-letter public team commands | PASS | Scan returned no matches in new bootstrap files. | + | Validate unknown team names | PASS | `omega` fails clearly with supported team list. | + | Validate unknown role names | PASS | `reviewer` fails clearly with supported role list. | ++| Support `--team` argument forwarding | PASS | `npm run dev:bootstrap -- --team charlie` resolves Charlie ports. | ++| Support positional team forwarding | PASS | `npm run dev:bootstrap -- charlie` resolves Charlie ports. | ++| Prevent `.env` port override | PASS | Unit test confirms computed team/role ports overwrite loaded API/site port values. | + | Load environment | PASS | Orchestrator loads `.env` values without overriding existing environment values. | + | Resolve team ports | PASS | Unit tests cover all teams and both roles. | | Start Local API | PASS | `dev:bootstrap` starts the existing Local API server on resolved API port. | - | Start web server when requested | PASS | `dev:bootstrap` starts web plus API; `dev:web` mode is wired. | - | Print startup diagnostics | PASS | Diagnostics include mode, team, role, web URL, API URL, environment source, supported teams, and supported roles. | -+| Owner role launches browser automatically | PASS | Live startup validation opened `http://127.0.0.1:5510/index.html` after API and web were ready. | -+| Codex role suppresses browser launch | PASS | Live startup validation reported `Browser launch: suppressed for codex role`. | -+| Wait for both servers before browser launch | PASS | Targeted unit test asserts API ready, web ready, then browser launch order. | - | Preserve legacy behavior | PASS | `dev:local-api` command unchanged and startup validation passed. | - | No unrelated cleanup | PASS | Scope limited to package scripts, new dev scripts, targeted tests, and reports. | diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md -index 1ca55c459..ea82082a8 100644 +index ea82082a8..5ca1cd0d0 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md -@@ -8,15 +8,15 @@ - | `node --check dev/scripts/start-dev.mjs` | PASS | Syntax valid. | - | `node --check dev/scripts/start-local-api-server.mjs` | PASS | Legacy startup entrypoint remains valid. | - | `node --check dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs` | PASS | Test syntax valid. | --| `node ./dev/scripts/run-node-test-files.mjs dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs dev/tests/dev-runtime/LocalApiStartupLogging.test.mjs` | PASS | 2 targeted files passed; 10 subtests passed. | -+| `node ./dev/scripts/run-node-test-files.mjs dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs dev/tests/dev-runtime/LocalApiStartupLogging.test.mjs` | PASS | 2 targeted files passed; 14 subtests passed. | - | Unsupported team token scan in new bootstrap files | PASS | No unsupported old team names or single-letter public team commands found. | +@@ -13,10 +13,13 @@ | `npm run validate:canonical-structure` | PASS | Canonical repository structure guardrail passed. | --| `npm run dev:bootstrap -- --team alfa` | PASS | Started with team `alfa`, role `owner`, web `5510`, API `5511`, then process tree was stopped. | --| `npm run dev:bootstrap -- --team alfa --role codex` | PASS | Started with team `alfa`, role `codex`, web `5512`, API `5513`, then process tree was stopped. | -+| `npm run dev:bootstrap -- --team alfa` | PASS | Started with team `alfa`, role `owner`, web `5510`, API `5511`, launched browser to `http://127.0.0.1:5510/index.html`, then process tree was stopped. | -+| `npm run dev:bootstrap -- --team alfa --role codex` | PASS | Started with team `alfa`, role `codex`, web `5512`, API `5513`, suppressed browser launch, then process tree was stopped. | + | `npm run dev:bootstrap -- --team alfa` | PASS | Started with team `alfa`, role `owner`, web `5510`, API `5511`, launched browser to `http://127.0.0.1:5510/index.html`, then process tree was stopped. | + | `npm run dev:bootstrap -- --team alfa --role codex` | PASS | Started with team `alfa`, role `codex`, web `5512`, API `5513`, suppressed browser launch, then process tree was stopped. | ++| `npm run dev:bootstrap -- --team charlie` | PASS | Started with team `charlie`, role `owner`, web `5530`, API `5531`, launched browser to `http://127.0.0.1:5530/index.html`, then process tree was stopped. | ++| `npm run dev:bootstrap -- charlie` | PASS | Positional team forwarding selected team `charlie`, web `5530`, API `5531`, and browser target `http://127.0.0.1:5530/index.html`. | ++| `npm run dev:bootstrap -- --team bravo` | PASS | Started with team `bravo`, role `owner`, web `5520`, API `5521`, and browser target `http://127.0.0.1:5520/index.html`. | | `npm run dev:bootstrap -- --team omega` | PASS | Failed clearly with supported team list. | | `npm run dev:bootstrap -- --team alfa --role reviewer` | PASS | Failed clearly with supported role list. | | `npm run dev:local-api` | PASS | Legacy command started and printed legacy startup diagnostics, then process tree was stopped. | ## Notes --The startup-command validations used short-lived process launches and `taskkill /T /F` cleanup after expected diagnostics appeared, so no dev servers were left running. -+The startup-command validations used short-lived process launches and `taskkill /T /F` cleanup after expected diagnostics appeared, so no dev servers were left running. The owner-role validation opened the configured browser target after both server diagnostics were available. +-The startup-command validations used short-lived process launches and `taskkill /T /F` cleanup after expected diagnostics appeared, so no dev servers were left running. The owner-role validation opened the configured browser target after both server diagnostics were available. ++The startup-command validations used short-lived process launches and `taskkill /T /F` cleanup after expected diagnostics appeared, so no dev servers were left running. Owner-role validations opened the configured browser target after both server diagnostics were available. Unit coverage verifies loaded `.env` port values are overwritten by computed team/role ports. diff --git a/dev/scripts/start-dev.mjs b/dev/scripts/start-dev.mjs -index 625360989..15d3ac1fa 100644 +index 15d3ac1fa..ac3d6242b 100644 --- a/dev/scripts/start-dev.mjs +++ b/dev/scripts/start-dev.mjs -@@ -1,5 +1,6 @@ - import fs from "node:fs/promises"; - import { existsSync, readFileSync } from "node:fs"; -+import { spawn } from "node:child_process"; - import http from "node:http"; - import path from "node:path"; - import process from "node:process"; -@@ -165,6 +166,14 @@ function apiUrl(host, port) { - return `${localUrl(host, port)}/api`; - } - -+export function teamIndexUrl(webBaseUrl) { -+ return `${String(webBaseUrl || "").replace(/\/+$/, "")}/index.html`; -+} -+ -+export function shouldLaunchBrowser(options) { -+ return Boolean(options?.api && options?.web && options?.role === "owner"); -+} -+ - export function applyBootstrapEnvironment({ - env = process.env, - host = DEFAULT_HOST, -@@ -179,6 +188,7 @@ export function applyBootstrapEnvironment({ - - export function formatBootstrapDiagnostics({ - apiBaseUrl, -+ browserLaunch, - mode, - role, - runtimeEnv, -@@ -192,6 +202,9 @@ export function formatBootstrapDiagnostics({ - `Role: ${role}`, +@@ -203,7 +203,7 @@ export function formatBootstrapDiagnostics({ `Web URL: ${webBaseUrl}`, `API URL: ${apiBaseUrl}/api`, -+ browserLaunch?.enabled -+ ? `Browser launch: opened ${browserLaunch.url}` -+ : `Browser launch: ${browserLaunch?.reason || "not requested"}`, + browserLaunch?.enabled +- ? `Browser launch: opened ${browserLaunch.url}` ++ ? `Browser launch: ${browserLaunch.url}` + : `Browser launch: ${browserLaunch?.reason || "not requested"}`, "Environment source: .env + process environment", `.env loaded: ${runtimeEnv.loaded ? `yes (${runtimeEnv.loadedKeys} new key(s))` : "no"}`, - `Supported teams: ${supportedBootstrapTeamsLabel()}`, -@@ -201,6 +214,36 @@ export function formatBootstrapDiagnostics({ - ]; +diff --git a/dev/scripts/team-port-config.mjs b/dev/scripts/team-port-config.mjs +index 2dfa98fc3..9d11f9b7f 100644 +--- a/dev/scripts/team-port-config.mjs ++++ b/dev/scripts/team-port-config.mjs +@@ -80,10 +80,37 @@ function parseNamedArgument(args, name, fallback, supportedLabel) { + return fallback; } -+export function launchBrowser(url, { -+ platform = process.platform, -+ spawnFn = spawn, -+} = {}) { -+ let command; -+ let args; -+ if (platform === "win32") { -+ command = "cmd"; -+ args = ["/c", "start", "", url]; -+ } else if (platform === "darwin") { -+ command = "open"; -+ args = [url]; -+ } else { -+ command = "xdg-open"; -+ args = [url]; ++function firstPositionalArgument(args = []) { ++ const values = Array.from(args); ++ const optionsWithValues = new Set(["mode", "role", "team"]); ++ for (let index = 0; index < values.length; index += 1) { ++ const argument = values[index]; ++ if (!argument) { ++ continue; ++ } ++ if (argument.startsWith("--")) { ++ if (argument.includes("=")) { ++ continue; ++ } ++ const optionName = argument.slice(2); ++ if (optionsWithValues.has(optionName) && values[index + 1] && !values[index + 1].startsWith("--")) { ++ index += 1; ++ } ++ continue; ++ } ++ return argument; + } -+ const child = spawnFn(command, args, { -+ detached: true, -+ stdio: "ignore", -+ windowsHide: true, -+ }); -+ child?.unref?.(); -+ return { -+ args, -+ command, -+ enabled: true, -+ url, -+ }; ++ return ""; +} + - async function readRequestBody(request) { - const chunks = []; - for await (const chunk of request) { -@@ -291,27 +334,48 @@ export async function startStaticWebServer({ - }; + export function parseRoleArgument(args = []) { + return parseNamedArgument(args, "role", DEFAULT_BOOTSTRAP_ROLE, supportedBootstrapRolesLabel); } --export async function startBootstrapRuntime(options = parseBootstrapOptions(process.argv.slice(2))) { -- const runtimeEnv = loadRuntimeEnv(); -- const host = process.env.GAMEFOUNDRY_LOCAL_API_HOST || DEFAULT_HOST; -- applyBootstrapEnvironment({ host, ports: options.ports }); -+export async function startBootstrapRuntime( -+ options = parseBootstrapOptions(process.argv.slice(2)), -+ { -+ apiServerStarter = startLocalApiServer, -+ browserLauncher = launchBrowser, -+ env = process.env, -+ loadEnv = () => loadRuntimeEnv({ env }), -+ webServerStarter = startStaticWebServer, -+ } = {}, -+) { -+ const runtimeEnv = loadEnv(); -+ const host = env.GAMEFOUNDRY_LOCAL_API_HOST || DEFAULT_HOST; -+ applyBootstrapEnvironment({ env, host, ports: options.ports }); - const apiBaseUrl = localUrl(host, options.ports.apiPort); - const webBaseUrl = localUrl(host, options.ports.webPort); - const servers = []; - - if (options.api) { -- servers.push(await startLocalApiServer({ host, port: options.ports.apiPort })); -+ servers.push(await apiServerStarter({ host, port: options.ports.apiPort })); - } - if (options.web) { -- servers.push(await startStaticWebServer({ apiBaseUrl, host, port: options.ports.webPort })); -+ servers.push(await webServerStarter({ apiBaseUrl, host, port: options.ports.webPort })); - } - -+ const browserUrl = teamIndexUrl(webBaseUrl); -+ const browserLaunch = shouldLaunchBrowser(options) -+ ? browserLauncher(browserUrl) -+ : { -+ enabled: false, -+ reason: options.role === "codex" -+ ? "suppressed for codex role" -+ : "requires bootstrap mode with API and web servers", -+ url: browserUrl, -+ }; -+ - return { - close: async () => { - await Promise.all(servers.map((server) => server.close())); - }, - diagnostics: formatBootstrapDiagnostics({ - apiBaseUrl, -+ browserLaunch, - mode: options.mode, - role: options.role, - runtimeEnv, + export function parseTeamArgument(args = []) { +- return parseNamedArgument(args, "team", DEFAULT_BOOTSTRAP_TEAM, supportedBootstrapTeamsLabel); ++ const explicitTeam = parseNamedArgument(args, "team", "", supportedBootstrapTeamsLabel); ++ if (explicitTeam) { ++ return explicitTeam; ++ } ++ return normalizeToken(firstPositionalArgument(args), DEFAULT_BOOTSTRAP_TEAM); + } diff --git a/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs b/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs -index 0576d4db2..2a4e16fe6 100644 +index 2a4e16fe6..226a7461a 100644 --- a/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs +++ b/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs -@@ -5,8 +5,12 @@ import { fileURLToPath } from "node:url"; - import { - applyBootstrapEnvironment, - formatBootstrapDiagnostics, -+ launchBrowser, - loadRuntimeEnv, - parseBootstrapOptions, -+ shouldLaunchBrowser, -+ startBootstrapRuntime, -+ teamIndexUrl, - } from "../../scripts/start-dev.mjs"; - import { - parseRoleArgument, -@@ -159,6 +163,11 @@ test("bootstrap environment applies resolved team and role ports for browser API - test("startup diagnostics and package scripts expose team-aware bootstrap commands", () => { - const lines = formatBootstrapDiagnostics({ - apiBaseUrl: "http://127.0.0.1:5513", -+ browserLaunch: { -+ enabled: false, -+ reason: "suppressed for codex role", -+ url: "http://127.0.0.1:5512/index.html", -+ }, - mode: "bootstrap", - role: "codex", - runtimeEnv: { -@@ -173,6 +182,7 @@ test("startup diagnostics and package scripts expose team-aware bootstrap comman - assert.equal(lines.includes("Role: codex"), true); - assert.equal(lines.includes("Web URL: http://127.0.0.1:5512"), true); - assert.equal(lines.includes("API URL: http://127.0.0.1:5513/api"), true); -+ assert.equal(lines.includes("Browser launch: suppressed for codex role"), true); - assert.equal(lines.includes(`Supported teams: ${supportedBootstrapTeamsLabel()}`), true); - assert.equal(lines.includes(`Supported roles: ${supportedBootstrapRolesLabel()}`), true); - -@@ -195,3 +205,112 @@ test("runtime env loader applies .env values without overriding existing process - assert.equal(typeof result.loaded, "boolean"); - assert.equal(env.GAMEFOUNDRY_API_URL, "http://127.0.0.1:5999/api"); - }); -+ -+test("browser launch targets team index after API and web servers are ready for owner role", async () => { -+ const events = []; -+ const runtime = await startBootstrapRuntime( -+ parseBootstrapOptions(["--mode=bootstrap", "--team=alfa"]), -+ { -+ apiServerStarter: async ({ port }) => { -+ events.push(`api:${port}`); -+ return { -+ close: async () => events.push("api:closed"), -+ }; -+ }, -+ browserLauncher: (url) => { -+ events.push(`browser:${url}`); -+ return { -+ enabled: true, -+ url, -+ }; -+ }, -+ env: {}, -+ loadEnv: () => ({ -+ loaded: false, -+ loadedKeys: 0, -+ }), -+ webServerStarter: async ({ port }) => { -+ events.push(`web:${port}`); -+ return { -+ close: async () => events.push("web:closed"), -+ }; -+ }, -+ }, -+ ); -+ -+ assert.deepEqual(events, [ -+ "api:5511", -+ "web:5510", -+ "browser:http://127.0.0.1:5510/index.html", -+ ]); -+ assert.equal(runtime.diagnostics.includes("Browser launch: opened http://127.0.0.1:5510/index.html"), true); -+ await runtime.close(); -+ assert.deepEqual(events.slice(-2), ["api:closed", "web:closed"]); -+}); -+ -+test("codex role suppresses browser launch even after API and web servers are ready", async () => { -+ const events = []; -+ const runtime = await startBootstrapRuntime( -+ parseBootstrapOptions(["--mode=bootstrap", "--team=alfa", "--role=codex"]), -+ { -+ apiServerStarter: async () => { -+ events.push("api"); -+ return { -+ close: async () => {}, -+ }; -+ }, -+ browserLauncher: (url) => { -+ events.push(`browser:${url}`); -+ return { -+ enabled: true, -+ url, -+ }; -+ }, -+ env: {}, -+ loadEnv: () => ({ -+ loaded: false, -+ loadedKeys: 0, -+ }), -+ webServerStarter: async () => { -+ events.push("web"); -+ return { -+ close: async () => {}, -+ }; -+ }, -+ }, -+ ); -+ -+ assert.deepEqual(events, ["api", "web"]); -+ assert.equal(runtime.diagnostics.includes("Browser launch: suppressed for codex role"), true); -+ await runtime.close(); -+}); -+ -+test("browser launch helper uses platform conventions and team index URLs", () => { -+ const launches = []; -+ const result = launchBrowser(teamIndexUrl("http://127.0.0.1:5510/"), { -+ platform: "win32", -+ spawnFn: (command, args, options) => { -+ launches.push({ args, command, options }); -+ return { -+ unref: () => launches.push({ unref: true }), -+ }; +@@ -60,6 +60,9 @@ test("team and role parsers accept canonical argument forms", () => { + assert.equal(parseTeamArgument([]), "owner"); + assert.equal(parseTeamArgument(["--team", "alfa"]), "alfa"); + assert.equal(parseTeamArgument(["--team=bravo"]), "bravo"); ++ assert.equal(parseTeamArgument(["--mode", "bootstrap", "charlie"]), "charlie"); ++ assert.equal(parseTeamArgument(["--mode=bootstrap", "charlie"]), "charlie"); ++ assert.equal(parseTeamArgument(["charlie", "--role", "codex"]), "charlie"); + assert.equal(parseRoleArgument([]), "owner"); + assert.equal(parseRoleArgument(["--role", "codex"]), "codex"); + assert.equal(parseRoleArgument(["--role=owner"]), "owner"); +@@ -124,6 +127,32 @@ test("bootstrap option parser maps npm script modes to server startup plan", () + team: "charlie", + web: false, + }); ++ assert.deepEqual(parseBootstrapOptions(["--mode", "bootstrap", "charlie"]), { ++ api: true, ++ mode: "bootstrap", ++ ports: { ++ apiPort: 5531, ++ role: "owner", ++ team: "charlie", ++ webPort: 5530, + }, ++ role: "owner", ++ team: "charlie", ++ web: true, + }); -+ -+ assert.deepEqual(result, { -+ args: ["/c", "start", "", "http://127.0.0.1:5510/index.html"], -+ command: "cmd", -+ enabled: true, -+ url: "http://127.0.0.1:5510/index.html", ++ assert.deepEqual(parseBootstrapOptions(["--mode", "bootstrap", "--team", "bravo"]), { ++ api: true, ++ mode: "bootstrap", ++ ports: { ++ apiPort: 5521, ++ role: "owner", ++ team: "bravo", ++ webPort: 5520, ++ }, ++ role: "owner", ++ team: "bravo", ++ web: true, + }); -+ assert.equal(launches[0].options.detached, true); -+ assert.equal(launches[0].options.stdio, "ignore"); -+ assert.deepEqual(launches[1], { unref: true }); -+}); -+ -+test("browser launch policy only opens for owner role when API and web are both active", () => { -+ assert.equal(shouldLaunchBrowser(parseBootstrapOptions(["--mode=bootstrap", "--team=alfa"])), true); -+ assert.equal(shouldLaunchBrowser(parseBootstrapOptions(["--mode=bootstrap", "--team=alfa", "--role=codex"])), false); -+ assert.equal(shouldLaunchBrowser(parseBootstrapOptions(["--mode=api", "--team=alfa"])), false); -+ assert.equal(shouldLaunchBrowser(parseBootstrapOptions(["--mode=web", "--team=alfa"])), false); -+}); + assert.deepEqual(parseBootstrapOptions(["--mode=web", "--team=owner"]), { + api: false, + mode: "web", +@@ -146,6 +175,7 @@ test("bootstrap option parser maps npm script modes to server startup plan", () + test("bootstrap environment applies resolved team and role ports for browser API routing", () => { + const env = { + GAMEFOUNDRY_API_URL: "http://127.0.0.1:5501/api", ++ GAMEFOUNDRY_LOCAL_API_PORT: "5501", + GAMEFOUNDRY_SITE_URL: "http://127.0.0.1:5500", + }; + applyBootstrapEnvironment({ +@@ -243,7 +273,7 @@ test("browser launch targets team index after API and web servers are ready for + "web:5510", + "browser:http://127.0.0.1:5510/index.html", + ]); +- assert.equal(runtime.diagnostics.includes("Browser launch: opened http://127.0.0.1:5510/index.html"), true); ++ assert.equal(runtime.diagnostics.includes("Browser launch: http://127.0.0.1:5510/index.html"), true); + await runtime.close(); + assert.deepEqual(events.slice(-2), ["api:closed", "web:closed"]); + }); diff --git a/dev/scripts/start-dev.mjs b/dev/scripts/start-dev.mjs index 15d3ac1fa..ac3d6242b 100644 --- a/dev/scripts/start-dev.mjs +++ b/dev/scripts/start-dev.mjs @@ -203,7 +203,7 @@ export function formatBootstrapDiagnostics({ `Web URL: ${webBaseUrl}`, `API URL: ${apiBaseUrl}/api`, browserLaunch?.enabled - ? `Browser launch: opened ${browserLaunch.url}` + ? `Browser launch: ${browserLaunch.url}` : `Browser launch: ${browserLaunch?.reason || "not requested"}`, "Environment source: .env + process environment", `.env loaded: ${runtimeEnv.loaded ? `yes (${runtimeEnv.loadedKeys} new key(s))` : "no"}`, diff --git a/dev/scripts/team-port-config.mjs b/dev/scripts/team-port-config.mjs index 2dfa98fc3..9d11f9b7f 100644 --- a/dev/scripts/team-port-config.mjs +++ b/dev/scripts/team-port-config.mjs @@ -80,10 +80,37 @@ function parseNamedArgument(args, name, fallback, supportedLabel) { return fallback; } +function firstPositionalArgument(args = []) { + const values = Array.from(args); + const optionsWithValues = new Set(["mode", "role", "team"]); + for (let index = 0; index < values.length; index += 1) { + const argument = values[index]; + if (!argument) { + continue; + } + if (argument.startsWith("--")) { + if (argument.includes("=")) { + continue; + } + const optionName = argument.slice(2); + if (optionsWithValues.has(optionName) && values[index + 1] && !values[index + 1].startsWith("--")) { + index += 1; + } + continue; + } + return argument; + } + return ""; +} + export function parseRoleArgument(args = []) { return parseNamedArgument(args, "role", DEFAULT_BOOTSTRAP_ROLE, supportedBootstrapRolesLabel); } export function parseTeamArgument(args = []) { - return parseNamedArgument(args, "team", DEFAULT_BOOTSTRAP_TEAM, supportedBootstrapTeamsLabel); + const explicitTeam = parseNamedArgument(args, "team", "", supportedBootstrapTeamsLabel); + if (explicitTeam) { + return explicitTeam; + } + return normalizeToken(firstPositionalArgument(args), DEFAULT_BOOTSTRAP_TEAM); } diff --git a/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs b/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs index 2a4e16fe6..226a7461a 100644 --- a/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs +++ b/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs @@ -60,6 +60,9 @@ test("team and role parsers accept canonical argument forms", () => { assert.equal(parseTeamArgument([]), "owner"); assert.equal(parseTeamArgument(["--team", "alfa"]), "alfa"); assert.equal(parseTeamArgument(["--team=bravo"]), "bravo"); + assert.equal(parseTeamArgument(["--mode", "bootstrap", "charlie"]), "charlie"); + assert.equal(parseTeamArgument(["--mode=bootstrap", "charlie"]), "charlie"); + assert.equal(parseTeamArgument(["charlie", "--role", "codex"]), "charlie"); assert.equal(parseRoleArgument([]), "owner"); assert.equal(parseRoleArgument(["--role", "codex"]), "codex"); assert.equal(parseRoleArgument(["--role=owner"]), "owner"); @@ -124,6 +127,32 @@ test("bootstrap option parser maps npm script modes to server startup plan", () team: "charlie", web: false, }); + assert.deepEqual(parseBootstrapOptions(["--mode", "bootstrap", "charlie"]), { + api: true, + mode: "bootstrap", + ports: { + apiPort: 5531, + role: "owner", + team: "charlie", + webPort: 5530, + }, + role: "owner", + team: "charlie", + web: true, + }); + assert.deepEqual(parseBootstrapOptions(["--mode", "bootstrap", "--team", "bravo"]), { + api: true, + mode: "bootstrap", + ports: { + apiPort: 5521, + role: "owner", + team: "bravo", + webPort: 5520, + }, + role: "owner", + team: "bravo", + web: true, + }); assert.deepEqual(parseBootstrapOptions(["--mode=web", "--team=owner"]), { api: false, mode: "web", @@ -146,6 +175,7 @@ test("bootstrap option parser maps npm script modes to server startup plan", () test("bootstrap environment applies resolved team and role ports for browser API routing", () => { const env = { GAMEFOUNDRY_API_URL: "http://127.0.0.1:5501/api", + GAMEFOUNDRY_LOCAL_API_PORT: "5501", GAMEFOUNDRY_SITE_URL: "http://127.0.0.1:5500", }; applyBootstrapEnvironment({ @@ -243,7 +273,7 @@ test("browser launch targets team index after API and web servers are ready for "web:5510", "browser:http://127.0.0.1:5510/index.html", ]); - assert.equal(runtime.diagnostics.includes("Browser launch: opened http://127.0.0.1:5510/index.html"), true); + assert.equal(runtime.diagnostics.includes("Browser launch: http://127.0.0.1:5510/index.html"), true); await runtime.close(); assert.deepEqual(events.slice(-2), ["api:closed", "web:closed"]); }); From 7104e83ce7e6ae02b83995a7301d41d4f74fd20c Mon Sep 17 00:00:00 2001 From: Alfa Team Date: Sun, 28 Jun 2026 08:17:54 -0400 Subject: [PATCH 4/5] ALFA 009 fix bootstrap env port overrides --- ...aware-bootstrap_manual-validation-notes.md | 4 +- ...71_ALFA_009-team-aware-bootstrap_report.md | 4 +- ...m-aware-bootstrap_requirement-checklist.md | 2 +- ...-team-aware-bootstrap_validation-report.md | 6 +- dev/reports/codex_changed_files.txt | 16 +- dev/reports/codex_review.diff | 250 ++++++++---------- dev/scripts/start-dev.mjs | 6 +- .../dev-runtime/TeamAwareBootstrap.test.mjs | 14 +- 8 files changed, 136 insertions(+), 166 deletions(-) diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md index eba870a8b..de2ee4a6b 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md @@ -12,7 +12,7 @@ Expected diagnostics: - `Role: owner` - `Web URL: http://127.0.0.1:5510` - `API URL: http://127.0.0.1:5511/api` -- `Browser launch: opened http://127.0.0.1:5510/index.html` +- `Browser launch: http://127.0.0.1:5510/index.html` ## Primary Alfa Codex Command @@ -60,6 +60,8 @@ For Charlie, both forms should show: - `API URL: http://127.0.0.1:5531/api` - `Browser launch: http://127.0.0.1:5530/index.html` +If `.env` or the current shell already has `GAMEFOUNDRY_SITE_URL` or `GAMEFOUNDRY_API_URL`, bootstrap should still show the resolved team/role ports in diagnostics and launch the browser from the computed `GAMEFOUNDRY_SITE_URL`. + ## Failure Checks ```powershell diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md index 95d02d5a3..e0279d948 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md @@ -31,6 +31,7 @@ Per OWNER instruction, those uncommitted changes were discarded with `git reset - Added automatic browser launch for owner-role `dev:bootstrap` after the API and web servers are both ready. - Suppressed automatic browser launch for the `codex` role. - Fixed npm argument forwarding/parsing so both `--team charlie` and positional `charlie` select the same team after the package script's `--mode bootstrap` argument. +- Treats `.env` `GAMEFOUNDRY_SITE_URL` and `GAMEFOUNDRY_API_URL` as legacy/default local values for bootstrap mode, then overwrites runtime URLs from the resolved team/role ports. ## Supported Teams @@ -98,7 +99,8 @@ npm run dev:bootstrap -- --team alfa --role codex - The Alfa owner launch target is `http://127.0.0.1:5510/index.html`. - The Alfa codex launch target is suppressed while using web `5512` and API `5513`. - The Charlie launch target is `http://127.0.0.1:5530/index.html` for both `npm run dev:bootstrap -- --team charlie` and `npm run dev:bootstrap -- charlie`. -- Computed team/role ports override any `.env` port values loaded before bootstrap startup. +- Computed team/role ports override any `.env` or inherited process `GAMEFOUNDRY_SITE_URL` / `GAMEFOUNDRY_API_URL` values loaded before bootstrap startup. +- Bootstrap runtime sets `GAMEFOUNDRY_SITE_URL=http://127.0.0.1:` and `GAMEFOUNDRY_API_URL=http://127.0.0.1:/api` after team/role resolution. ## Files Changed diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md index a6c478735..fc4b68f78 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md @@ -18,7 +18,7 @@ | Validate unknown role names | PASS | `reviewer` fails clearly with supported role list. | | Support `--team` argument forwarding | PASS | `npm run dev:bootstrap -- --team charlie` resolves Charlie ports. | | Support positional team forwarding | PASS | `npm run dev:bootstrap -- charlie` resolves Charlie ports. | -| Prevent `.env` port override | PASS | Unit test confirms computed team/role ports overwrite loaded API/site port values. | +| Prevent `.env` and process URL port override | PASS | Unit and live startup tests confirm computed team/role ports overwrite loaded or inherited API/site URL port values. | | Load environment | PASS | Orchestrator loads `.env` values without overriding existing environment values. | | Resolve team ports | PASS | Unit tests cover all teams and both roles. | | Start Local API | PASS | `dev:bootstrap` starts the existing Local API server on resolved API port. | diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md index 5ca1cd0d0..370accd63 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md @@ -9,11 +9,11 @@ | `node --check dev/scripts/start-local-api-server.mjs` | PASS | Legacy startup entrypoint remains valid. | | `node --check dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs` | PASS | Test syntax valid. | | `node ./dev/scripts/run-node-test-files.mjs dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs dev/tests/dev-runtime/LocalApiStartupLogging.test.mjs` | PASS | 2 targeted files passed; 14 subtests passed. | -| Unsupported team token scan in new bootstrap files | PASS | No unsupported old team names or single-letter public team commands found. | +| Unsupported team token scan in new bootstrap files | PASS | No unsupported old team names or single-letter public team commands found in package scripts, bootstrap scripts, or targeted bootstrap tests. | | `npm run validate:canonical-structure` | PASS | Canonical repository structure guardrail passed. | | `npm run dev:bootstrap -- --team alfa` | PASS | Started with team `alfa`, role `owner`, web `5510`, API `5511`, launched browser to `http://127.0.0.1:5510/index.html`, then process tree was stopped. | | `npm run dev:bootstrap -- --team alfa --role codex` | PASS | Started with team `alfa`, role `codex`, web `5512`, API `5513`, suppressed browser launch, then process tree was stopped. | -| `npm run dev:bootstrap -- --team charlie` | PASS | Started with team `charlie`, role `owner`, web `5530`, API `5531`, launched browser to `http://127.0.0.1:5530/index.html`, then process tree was stopped. | +| `npm run dev:bootstrap -- --team charlie` with inherited stale URL env values | PASS | Started with team `charlie`, role `owner`, web `5530`, API `5531`, launched browser to `http://127.0.0.1:5530/index.html`, and ignored inherited `GAMEFOUNDRY_SITE_URL` / `GAMEFOUNDRY_API_URL` port values before process cleanup. | | `npm run dev:bootstrap -- charlie` | PASS | Positional team forwarding selected team `charlie`, web `5530`, API `5531`, and browser target `http://127.0.0.1:5530/index.html`. | | `npm run dev:bootstrap -- --team bravo` | PASS | Started with team `bravo`, role `owner`, web `5520`, API `5521`, and browser target `http://127.0.0.1:5520/index.html`. | | `npm run dev:bootstrap -- --team omega` | PASS | Failed clearly with supported team list. | @@ -22,4 +22,4 @@ ## Notes -The startup-command validations used short-lived process launches and `taskkill /T /F` cleanup after expected diagnostics appeared, so no dev servers were left running. Owner-role validations opened the configured browser target after both server diagnostics were available. Unit coverage verifies loaded `.env` port values are overwritten by computed team/role ports. +The startup-command validations used short-lived process launches and `taskkill /T /F` cleanup after expected diagnostics appeared, so no dev servers were left running. Owner-role validations opened the configured browser target after both server diagnostics were available. Unit coverage verifies loaded `.env` and inherited process URL values are overwritten by computed team/role ports. diff --git a/dev/reports/codex_changed_files.txt b/dev/reports/codex_changed_files.txt index 53bb20d20..ae24ed62d 100644 --- a/dev/reports/codex_changed_files.txt +++ b/dev/reports/codex_changed_files.txt @@ -4,18 +4,16 @@ M dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md M dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md M dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md M dev/scripts/start-dev.mjs - M dev/scripts/team-port-config.mjs M dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs # git ls-files --others --exclude-standard (no output) # git diff --stat -...team-aware-bootstrap_manual-validation-notes.md | 8 ++++++ - ...R_26171_ALFA_009-team-aware-bootstrap_report.md | 3 ++ - ...9-team-aware-bootstrap_requirement-checklist.md | 3 ++ - ...A_009-team-aware-bootstrap_validation-report.md | 5 +++- - dev/scripts/start-dev.mjs | 2 +- - dev/scripts/team-port-config.mjs | 29 +++++++++++++++++++- - dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs | 32 +++++++++++++++++++++- - 7 files changed, 78 insertions(+), 4 deletions(-) \ No newline at end of file +...LFA_009-team-aware-bootstrap_manual-validation-notes.md | 4 +++- + .../PR_26171_ALFA_009-team-aware-bootstrap_report.md | 4 +++- + ..._ALFA_009-team-aware-bootstrap_requirement-checklist.md | 2 +- + ...6171_ALFA_009-team-aware-bootstrap_validation-report.md | 6 +++--- + dev/scripts/start-dev.mjs | 6 +++--- + dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs | 14 +++++++++++--- + 6 files changed, 24 insertions(+), 12 deletions(-) \ No newline at end of file diff --git a/dev/reports/codex_review.diff b/dev/reports/codex_review.diff index e4861c7ff..ef973d429 100644 --- a/dev/reports/codex_review.diff +++ b/dev/reports/codex_review.diff @@ -1,200 +1,160 @@ diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md -index c40377fb2..eba870a8b 100644 +index eba870a8b..de2ee4a6b 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md -@@ -45,6 +45,7 @@ Expected result: - npm run dev:bootstrap -- --team owner - npm run dev:bootstrap -- --team bravo - npm run dev:bootstrap -- --team charlie -+npm run dev:bootstrap -- charlie - npm run dev:bootstrap -- --team delta - npm run dev:bootstrap -- --team echo - npm run dev:bootstrap -- --team foxtrot -@@ -52,6 +53,13 @@ npm run dev:bootstrap -- --team golf - npm run dev:bootstrap -- --team hotel - ``` +@@ -12,7 +12,7 @@ Expected diagnostics: + - `Role: owner` + - `Web URL: http://127.0.0.1:5510` + - `API URL: http://127.0.0.1:5511/api` +-- `Browser launch: opened http://127.0.0.1:5510/index.html` ++- `Browser launch: http://127.0.0.1:5510/index.html` -+For Charlie, both forms should show: -+ -+- `Team: charlie` -+- `Web URL: http://127.0.0.1:5530` -+- `API URL: http://127.0.0.1:5531/api` -+- `Browser launch: http://127.0.0.1:5530/index.html` + ## Primary Alfa Codex Command + +@@ -60,6 +60,8 @@ For Charlie, both forms should show: + - `API URL: http://127.0.0.1:5531/api` + - `Browser launch: http://127.0.0.1:5530/index.html` + ++If `.env` or the current shell already has `GAMEFOUNDRY_SITE_URL` or `GAMEFOUNDRY_API_URL`, bootstrap should still show the resolved team/role ports in diagnostics and launch the browser from the computed `GAMEFOUNDRY_SITE_URL`. + ## Failure Checks ```powershell diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md -index 1690a93ec..95d02d5a3 100644 +index 95d02d5a3..e0279d948 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md -@@ -30,6 +30,7 @@ Per OWNER instruction, those uncommitted changes were discarded with `git reset - - Added focused node tests for team resolution, role offsets, invalid team/role validation, script wiring, `.env` loading behavior, and bootstrap diagnostics. +@@ -31,6 +31,7 @@ Per OWNER instruction, those uncommitted changes were discarded with `git reset - Added automatic browser launch for owner-role `dev:bootstrap` after the API and web servers are both ready. - Suppressed automatic browser launch for the `codex` role. -+- Fixed npm argument forwarding/parsing so both `--team charlie` and positional `charlie` select the same team after the package script's `--mode bootstrap` argument. + - Fixed npm argument forwarding/parsing so both `--team charlie` and positional `charlie` select the same team after the package script's `--mode bootstrap` argument. ++- Treats `.env` `GAMEFOUNDRY_SITE_URL` and `GAMEFOUNDRY_API_URL` as legacy/default local values for bootstrap mode, then overwrites runtime URLs from the resolved team/role ports. ## Supported Teams -@@ -96,6 +97,8 @@ npm run dev:bootstrap -- --team alfa --role codex - - Browser launch happens only after both API and web servers are ready. +@@ -98,7 +99,8 @@ npm run dev:bootstrap -- --team alfa --role codex - The Alfa owner launch target is `http://127.0.0.1:5510/index.html`. - The Alfa codex launch target is suppressed while using web `5512` and API `5513`. -+- The Charlie launch target is `http://127.0.0.1:5530/index.html` for both `npm run dev:bootstrap -- --team charlie` and `npm run dev:bootstrap -- charlie`. -+- Computed team/role ports override any `.env` port values loaded before bootstrap startup. + - The Charlie launch target is `http://127.0.0.1:5530/index.html` for both `npm run dev:bootstrap -- --team charlie` and `npm run dev:bootstrap -- charlie`. +-- Computed team/role ports override any `.env` port values loaded before bootstrap startup. ++- Computed team/role ports override any `.env` or inherited process `GAMEFOUNDRY_SITE_URL` / `GAMEFOUNDRY_API_URL` values loaded before bootstrap startup. ++- Bootstrap runtime sets `GAMEFOUNDRY_SITE_URL=http://127.0.0.1:` and `GAMEFOUNDRY_API_URL=http://127.0.0.1:/api` after team/role resolution. ## Files Changed diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md -index 4600ad790..a6c478735 100644 +index a6c478735..fc4b68f78 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md -@@ -16,6 +16,9 @@ - | Do not use single-letter public team commands | PASS | Scan returned no matches in new bootstrap files. | - | Validate unknown team names | PASS | `omega` fails clearly with supported team list. | +@@ -18,7 +18,7 @@ | Validate unknown role names | PASS | `reviewer` fails clearly with supported role list. | -+| Support `--team` argument forwarding | PASS | `npm run dev:bootstrap -- --team charlie` resolves Charlie ports. | -+| Support positional team forwarding | PASS | `npm run dev:bootstrap -- charlie` resolves Charlie ports. | -+| Prevent `.env` port override | PASS | Unit test confirms computed team/role ports overwrite loaded API/site port values. | + | Support `--team` argument forwarding | PASS | `npm run dev:bootstrap -- --team charlie` resolves Charlie ports. | + | Support positional team forwarding | PASS | `npm run dev:bootstrap -- charlie` resolves Charlie ports. | +-| Prevent `.env` port override | PASS | Unit test confirms computed team/role ports overwrite loaded API/site port values. | ++| Prevent `.env` and process URL port override | PASS | Unit and live startup tests confirm computed team/role ports overwrite loaded or inherited API/site URL port values. | | Load environment | PASS | Orchestrator loads `.env` values without overriding existing environment values. | | Resolve team ports | PASS | Unit tests cover all teams and both roles. | | Start Local API | PASS | `dev:bootstrap` starts the existing Local API server on resolved API port. | diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md -index ea82082a8..5ca1cd0d0 100644 +index 5ca1cd0d0..370accd63 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md -@@ -13,10 +13,13 @@ +@@ -9,11 +9,11 @@ + | `node --check dev/scripts/start-local-api-server.mjs` | PASS | Legacy startup entrypoint remains valid. | + | `node --check dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs` | PASS | Test syntax valid. | + | `node ./dev/scripts/run-node-test-files.mjs dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs dev/tests/dev-runtime/LocalApiStartupLogging.test.mjs` | PASS | 2 targeted files passed; 14 subtests passed. | +-| Unsupported team token scan in new bootstrap files | PASS | No unsupported old team names or single-letter public team commands found. | ++| Unsupported team token scan in new bootstrap files | PASS | No unsupported old team names or single-letter public team commands found in package scripts, bootstrap scripts, or targeted bootstrap tests. | | `npm run validate:canonical-structure` | PASS | Canonical repository structure guardrail passed. | | `npm run dev:bootstrap -- --team alfa` | PASS | Started with team `alfa`, role `owner`, web `5510`, API `5511`, launched browser to `http://127.0.0.1:5510/index.html`, then process tree was stopped. | | `npm run dev:bootstrap -- --team alfa --role codex` | PASS | Started with team `alfa`, role `codex`, web `5512`, API `5513`, suppressed browser launch, then process tree was stopped. | -+| `npm run dev:bootstrap -- --team charlie` | PASS | Started with team `charlie`, role `owner`, web `5530`, API `5531`, launched browser to `http://127.0.0.1:5530/index.html`, then process tree was stopped. | -+| `npm run dev:bootstrap -- charlie` | PASS | Positional team forwarding selected team `charlie`, web `5530`, API `5531`, and browser target `http://127.0.0.1:5530/index.html`. | -+| `npm run dev:bootstrap -- --team bravo` | PASS | Started with team `bravo`, role `owner`, web `5520`, API `5521`, and browser target `http://127.0.0.1:5520/index.html`. | +-| `npm run dev:bootstrap -- --team charlie` | PASS | Started with team `charlie`, role `owner`, web `5530`, API `5531`, launched browser to `http://127.0.0.1:5530/index.html`, then process tree was stopped. | ++| `npm run dev:bootstrap -- --team charlie` with inherited stale URL env values | PASS | Started with team `charlie`, role `owner`, web `5530`, API `5531`, launched browser to `http://127.0.0.1:5530/index.html`, and ignored inherited `GAMEFOUNDRY_SITE_URL` / `GAMEFOUNDRY_API_URL` port values before process cleanup. | + | `npm run dev:bootstrap -- charlie` | PASS | Positional team forwarding selected team `charlie`, web `5530`, API `5531`, and browser target `http://127.0.0.1:5530/index.html`. | + | `npm run dev:bootstrap -- --team bravo` | PASS | Started with team `bravo`, role `owner`, web `5520`, API `5521`, and browser target `http://127.0.0.1:5520/index.html`. | | `npm run dev:bootstrap -- --team omega` | PASS | Failed clearly with supported team list. | - | `npm run dev:bootstrap -- --team alfa --role reviewer` | PASS | Failed clearly with supported role list. | - | `npm run dev:local-api` | PASS | Legacy command started and printed legacy startup diagnostics, then process tree was stopped. | +@@ -22,4 +22,4 @@ ## Notes --The startup-command validations used short-lived process launches and `taskkill /T /F` cleanup after expected diagnostics appeared, so no dev servers were left running. The owner-role validation opened the configured browser target after both server diagnostics were available. -+The startup-command validations used short-lived process launches and `taskkill /T /F` cleanup after expected diagnostics appeared, so no dev servers were left running. Owner-role validations opened the configured browser target after both server diagnostics were available. Unit coverage verifies loaded `.env` port values are overwritten by computed team/role ports. +-The startup-command validations used short-lived process launches and `taskkill /T /F` cleanup after expected diagnostics appeared, so no dev servers were left running. Owner-role validations opened the configured browser target after both server diagnostics were available. Unit coverage verifies loaded `.env` port values are overwritten by computed team/role ports. ++The startup-command validations used short-lived process launches and `taskkill /T /F` cleanup after expected diagnostics appeared, so no dev servers were left running. Owner-role validations opened the configured browser target after both server diagnostics were available. Unit coverage verifies loaded `.env` and inherited process URL values are overwritten by computed team/role ports. diff --git a/dev/scripts/start-dev.mjs b/dev/scripts/start-dev.mjs -index 15d3ac1fa..ac3d6242b 100644 +index ac3d6242b..e1b58491a 100644 --- a/dev/scripts/start-dev.mjs +++ b/dev/scripts/start-dev.mjs -@@ -203,7 +203,7 @@ export function formatBootstrapDiagnostics({ - `Web URL: ${webBaseUrl}`, - `API URL: ${apiBaseUrl}/api`, - browserLaunch?.enabled -- ? `Browser launch: opened ${browserLaunch.url}` -+ ? `Browser launch: ${browserLaunch.url}` - : `Browser launch: ${browserLaunch?.reason || "not requested"}`, - "Environment source: .env + process environment", - `.env loaded: ${runtimeEnv.loaded ? `yes (${runtimeEnv.loadedKeys} new key(s))` : "no"}`, -diff --git a/dev/scripts/team-port-config.mjs b/dev/scripts/team-port-config.mjs -index 2dfa98fc3..9d11f9b7f 100644 ---- a/dev/scripts/team-port-config.mjs -+++ b/dev/scripts/team-port-config.mjs -@@ -80,10 +80,37 @@ function parseNamedArgument(args, name, fallback, supportedLabel) { - return fallback; - } +@@ -179,7 +179,7 @@ export function applyBootstrapEnvironment({ + host = DEFAULT_HOST, + ports, + }) { +- env.GAMEFOUNDRY_LOCAL_API_HOST = env.GAMEFOUNDRY_LOCAL_API_HOST || host; ++ env.GAMEFOUNDRY_LOCAL_API_HOST = host; + env.GAMEFOUNDRY_LOCAL_API_PORT = String(ports.apiPort); + env.GAMEFOUNDRY_API_URL = apiUrl(host, ports.apiPort); + env.GAMEFOUNDRY_SITE_URL = localUrl(host, ports.webPort); +@@ -345,10 +345,10 @@ export async function startBootstrapRuntime( + } = {}, + ) { + const runtimeEnv = loadEnv(); +- const host = env.GAMEFOUNDRY_LOCAL_API_HOST || DEFAULT_HOST; ++ const host = DEFAULT_HOST; + applyBootstrapEnvironment({ env, host, ports: options.ports }); + const apiBaseUrl = localUrl(host, options.ports.apiPort); +- const webBaseUrl = localUrl(host, options.ports.webPort); ++ const webBaseUrl = env.GAMEFOUNDRY_SITE_URL; + const servers = []; -+function firstPositionalArgument(args = []) { -+ const values = Array.from(args); -+ const optionsWithValues = new Set(["mode", "role", "team"]); -+ for (let index = 0; index < values.length; index += 1) { -+ const argument = values[index]; -+ if (!argument) { -+ continue; -+ } -+ if (argument.startsWith("--")) { -+ if (argument.includes("=")) { -+ continue; -+ } -+ const optionName = argument.slice(2); -+ if (optionsWithValues.has(optionName) && values[index + 1] && !values[index + 1].startsWith("--")) { -+ index += 1; -+ } -+ continue; -+ } -+ return argument; -+ } -+ return ""; -+} -+ - export function parseRoleArgument(args = []) { - return parseNamedArgument(args, "role", DEFAULT_BOOTSTRAP_ROLE, supportedBootstrapRolesLabel); - } - - export function parseTeamArgument(args = []) { -- return parseNamedArgument(args, "team", DEFAULT_BOOTSTRAP_TEAM, supportedBootstrapTeamsLabel); -+ const explicitTeam = parseNamedArgument(args, "team", "", supportedBootstrapTeamsLabel); -+ if (explicitTeam) { -+ return explicitTeam; -+ } -+ return normalizeToken(firstPositionalArgument(args), DEFAULT_BOOTSTRAP_TEAM); - } + if (options.api) { diff --git a/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs b/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs -index 2a4e16fe6..226a7461a 100644 +index 226a7461a..c746f1b3b 100644 --- a/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs +++ b/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs -@@ -60,6 +60,9 @@ test("team and role parsers accept canonical argument forms", () => { - assert.equal(parseTeamArgument([]), "owner"); - assert.equal(parseTeamArgument(["--team", "alfa"]), "alfa"); - assert.equal(parseTeamArgument(["--team=bravo"]), "bravo"); -+ assert.equal(parseTeamArgument(["--mode", "bootstrap", "charlie"]), "charlie"); -+ assert.equal(parseTeamArgument(["--mode=bootstrap", "charlie"]), "charlie"); -+ assert.equal(parseTeamArgument(["charlie", "--role", "codex"]), "charlie"); - assert.equal(parseRoleArgument([]), "owner"); - assert.equal(parseRoleArgument(["--role", "codex"]), "codex"); - assert.equal(parseRoleArgument(["--role=owner"]), "owner"); -@@ -124,6 +127,32 @@ test("bootstrap option parser maps npm script modes to server startup plan", () - team: "charlie", - web: false, - }); -+ assert.deepEqual(parseBootstrapOptions(["--mode", "bootstrap", "charlie"]), { -+ api: true, -+ mode: "bootstrap", -+ ports: { -+ apiPort: 5531, -+ role: "owner", -+ team: "charlie", -+ webPort: 5530, -+ }, -+ role: "owner", -+ team: "charlie", -+ web: true, -+ }); -+ assert.deepEqual(parseBootstrapOptions(["--mode", "bootstrap", "--team", "bravo"]), { -+ api: true, -+ mode: "bootstrap", -+ ports: { -+ apiPort: 5521, -+ role: "owner", -+ team: "bravo", -+ webPort: 5520, -+ }, -+ role: "owner", -+ team: "bravo", -+ web: true, -+ }); - assert.deepEqual(parseBootstrapOptions(["--mode=web", "--team=owner"]), { - api: false, - mode: "web", -@@ -146,6 +175,7 @@ test("bootstrap option parser maps npm script modes to server startup plan", () - test("bootstrap environment applies resolved team and role ports for browser API routing", () => { +@@ -172,15 +172,15 @@ test("bootstrap option parser maps npm script modes to server startup plan", () + ); + }); + +-test("bootstrap environment applies resolved team and role ports for browser API routing", () => { ++test("bootstrap environment overrides legacy env URLs with resolved team and role ports", () => { const env = { ++ GAMEFOUNDRY_LOCAL_API_HOST: "0.0.0.0", GAMEFOUNDRY_API_URL: "http://127.0.0.1:5501/api", -+ GAMEFOUNDRY_LOCAL_API_PORT: "5501", + GAMEFOUNDRY_LOCAL_API_PORT: "5501", GAMEFOUNDRY_SITE_URL: "http://127.0.0.1:5500", }; applyBootstrapEnvironment({ -@@ -243,7 +273,7 @@ test("browser launch targets team index after API and web servers are ready for + env, +- host: "127.0.0.1", + ports: resolveTeamPortConfig({ role: "codex", team: "alfa" }), + }); + +@@ -238,6 +238,11 @@ test("runtime env loader applies .env values without overriding existing process + + test("browser launch targets team index after API and web servers are ready for owner role", async () => { + const events = []; ++ const env = { ++ GAMEFOUNDRY_LOCAL_API_HOST: "0.0.0.0", ++ GAMEFOUNDRY_API_URL: "http://127.0.0.1:5501/api", ++ GAMEFOUNDRY_SITE_URL: "http://127.0.0.1:5500", ++ }; + const runtime = await startBootstrapRuntime( + parseBootstrapOptions(["--mode=bootstrap", "--team=alfa"]), + { +@@ -254,7 +259,7 @@ test("browser launch targets team index after API and web servers are ready for + url, + }; + }, +- env: {}, ++ env, + loadEnv: () => ({ + loaded: false, + loadedKeys: 0, +@@ -273,6 +278,9 @@ test("browser launch targets team index after API and web servers are ready for "web:5510", "browser:http://127.0.0.1:5510/index.html", ]); -- assert.equal(runtime.diagnostics.includes("Browser launch: opened http://127.0.0.1:5510/index.html"), true); -+ assert.equal(runtime.diagnostics.includes("Browser launch: http://127.0.0.1:5510/index.html"), true); ++ assert.equal(env.GAMEFOUNDRY_LOCAL_API_HOST, "127.0.0.1"); ++ assert.equal(env.GAMEFOUNDRY_API_URL, "http://127.0.0.1:5511/api"); ++ assert.equal(env.GAMEFOUNDRY_SITE_URL, "http://127.0.0.1:5510"); + assert.equal(runtime.diagnostics.includes("Browser launch: http://127.0.0.1:5510/index.html"), true); await runtime.close(); assert.deepEqual(events.slice(-2), ["api:closed", "web:closed"]); - }); diff --git a/dev/scripts/start-dev.mjs b/dev/scripts/start-dev.mjs index ac3d6242b..e1b58491a 100644 --- a/dev/scripts/start-dev.mjs +++ b/dev/scripts/start-dev.mjs @@ -179,7 +179,7 @@ export function applyBootstrapEnvironment({ host = DEFAULT_HOST, ports, }) { - env.GAMEFOUNDRY_LOCAL_API_HOST = env.GAMEFOUNDRY_LOCAL_API_HOST || host; + env.GAMEFOUNDRY_LOCAL_API_HOST = host; env.GAMEFOUNDRY_LOCAL_API_PORT = String(ports.apiPort); env.GAMEFOUNDRY_API_URL = apiUrl(host, ports.apiPort); env.GAMEFOUNDRY_SITE_URL = localUrl(host, ports.webPort); @@ -345,10 +345,10 @@ export async function startBootstrapRuntime( } = {}, ) { const runtimeEnv = loadEnv(); - const host = env.GAMEFOUNDRY_LOCAL_API_HOST || DEFAULT_HOST; + const host = DEFAULT_HOST; applyBootstrapEnvironment({ env, host, ports: options.ports }); const apiBaseUrl = localUrl(host, options.ports.apiPort); - const webBaseUrl = localUrl(host, options.ports.webPort); + const webBaseUrl = env.GAMEFOUNDRY_SITE_URL; const servers = []; if (options.api) { diff --git a/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs b/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs index 226a7461a..c746f1b3b 100644 --- a/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs +++ b/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs @@ -172,15 +172,15 @@ test("bootstrap option parser maps npm script modes to server startup plan", () ); }); -test("bootstrap environment applies resolved team and role ports for browser API routing", () => { +test("bootstrap environment overrides legacy env URLs with resolved team and role ports", () => { const env = { + GAMEFOUNDRY_LOCAL_API_HOST: "0.0.0.0", GAMEFOUNDRY_API_URL: "http://127.0.0.1:5501/api", GAMEFOUNDRY_LOCAL_API_PORT: "5501", GAMEFOUNDRY_SITE_URL: "http://127.0.0.1:5500", }; applyBootstrapEnvironment({ env, - host: "127.0.0.1", ports: resolveTeamPortConfig({ role: "codex", team: "alfa" }), }); @@ -238,6 +238,11 @@ test("runtime env loader applies .env values without overriding existing process test("browser launch targets team index after API and web servers are ready for owner role", async () => { const events = []; + const env = { + GAMEFOUNDRY_LOCAL_API_HOST: "0.0.0.0", + GAMEFOUNDRY_API_URL: "http://127.0.0.1:5501/api", + GAMEFOUNDRY_SITE_URL: "http://127.0.0.1:5500", + }; const runtime = await startBootstrapRuntime( parseBootstrapOptions(["--mode=bootstrap", "--team=alfa"]), { @@ -254,7 +259,7 @@ test("browser launch targets team index after API and web servers are ready for url, }; }, - env: {}, + env, loadEnv: () => ({ loaded: false, loadedKeys: 0, @@ -273,6 +278,9 @@ test("browser launch targets team index after API and web servers are ready for "web:5510", "browser:http://127.0.0.1:5510/index.html", ]); + assert.equal(env.GAMEFOUNDRY_LOCAL_API_HOST, "127.0.0.1"); + assert.equal(env.GAMEFOUNDRY_API_URL, "http://127.0.0.1:5511/api"); + assert.equal(env.GAMEFOUNDRY_SITE_URL, "http://127.0.0.1:5510"); assert.equal(runtime.diagnostics.includes("Browser launch: http://127.0.0.1:5510/index.html"), true); await runtime.close(); assert.deepEqual(events.slice(-2), ["api:closed", "web:closed"]); From 69c3fbaaa6a0e3ab191c0181fdfc7bfb8f1d13bd Mon Sep 17 00:00:00 2001 From: Alfa Team Date: Sun, 28 Jun 2026 08:29:40 -0400 Subject: [PATCH 5/5] ALFA 009 harden bootstrap role parsing --- ...aware-bootstrap_manual-validation-notes.md | 2 +- ...71_ALFA_009-team-aware-bootstrap_report.md | 6 +- ...m-aware-bootstrap_requirement-checklist.md | 3 +- ...-team-aware-bootstrap_validation-report.md | 3 +- dev/reports/codex_changed_files.txt | 16 +- dev/reports/codex_review.diff | 351 ++++++++++++------ dev/scripts/start-dev.mjs | 2 +- dev/scripts/team-port-config.mjs | 31 +- .../dev-runtime/TeamAwareBootstrap.test.mjs | 71 +++- 9 files changed, 342 insertions(+), 143 deletions(-) diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md index de2ee4a6b..d416b4c0f 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md @@ -26,7 +26,7 @@ Expected diagnostics: - `Role: codex` - `Web URL: http://127.0.0.1:5512` - `API URL: http://127.0.0.1:5513/api` -- `Browser launch: suppressed for codex role` +- `Browser launch: skipped for codex role` ## Legacy Command diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md index e0279d948..9f8343dfb 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md @@ -29,7 +29,7 @@ Per OWNER instruction, those uncommitted changes were discarded with `git reset - Added `dev/scripts/start-dev.mjs` as the bootstrap orchestrator. - Added focused node tests for team resolution, role offsets, invalid team/role validation, script wiring, `.env` loading behavior, and bootstrap diagnostics. - Added automatic browser launch for owner-role `dev:bootstrap` after the API and web servers are both ready. -- Suppressed automatic browser launch for the `codex` role. +- Skipped automatic browser launch for the `codex` role. - Fixed npm argument forwarding/parsing so both `--team charlie` and positional `charlie` select the same team after the package script's `--mode bootstrap` argument. - Treats `.env` `GAMEFOUNDRY_SITE_URL` and `GAMEFOUNDRY_API_URL` as legacy/default local values for bootstrap mode, then overwrites runtime URLs from the resolved team/role ports. @@ -94,10 +94,10 @@ npm run dev:bootstrap -- --team alfa --role codex ## Browser Launch Behavior - `owner` role launches the browser automatically to the selected team's `index.html`. -- `codex` role suppresses browser launch. +- `codex` role skips browser launch. - Browser launch happens only after both API and web servers are ready. - The Alfa owner launch target is `http://127.0.0.1:5510/index.html`. -- The Alfa codex launch target is suppressed while using web `5512` and API `5513`. +- The Alfa codex browser launch is skipped while using web `5512` and API `5513`. - The Charlie launch target is `http://127.0.0.1:5530/index.html` for both `npm run dev:bootstrap -- --team charlie` and `npm run dev:bootstrap -- charlie`. - Computed team/role ports override any `.env` or inherited process `GAMEFOUNDRY_SITE_URL` / `GAMEFOUNDRY_API_URL` values loaded before bootstrap startup. - Bootstrap runtime sets `GAMEFOUNDRY_SITE_URL=http://127.0.0.1:` and `GAMEFOUNDRY_API_URL=http://127.0.0.1:/api` after team/role resolution. diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md index fc4b68f78..27d45e7ac 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md @@ -18,6 +18,7 @@ | Validate unknown role names | PASS | `reviewer` fails clearly with supported role list. | | Support `--team` argument forwarding | PASS | `npm run dev:bootstrap -- --team charlie` resolves Charlie ports. | | Support positional team forwarding | PASS | `npm run dev:bootstrap -- charlie` resolves Charlie ports. | +| Support positional team/role forwarding | PASS | `npm run dev:bootstrap -- alfa codex` resolves Alfa codex ports and skips browser launch. | | Prevent `.env` and process URL port override | PASS | Unit and live startup tests confirm computed team/role ports overwrite loaded or inherited API/site URL port values. | | Load environment | PASS | Orchestrator loads `.env` values without overriding existing environment values. | | Resolve team ports | PASS | Unit tests cover all teams and both roles. | @@ -25,7 +26,7 @@ | Start web server when requested | PASS | `dev:bootstrap` starts web plus API; `dev:web` mode is wired. | | Print startup diagnostics | PASS | Diagnostics include mode, team, role, web URL, API URL, environment source, supported teams, and supported roles. | | Owner role launches browser automatically | PASS | Live startup validation opened `http://127.0.0.1:5510/index.html` after API and web were ready. | -| Codex role suppresses browser launch | PASS | Live startup validation reported `Browser launch: suppressed for codex role`. | +| Codex role skips browser launch | PASS | Live startup validation reported `Browser launch: skipped for codex role`. | | Wait for both servers before browser launch | PASS | Targeted unit test asserts API ready, web ready, then browser launch order. | | Preserve legacy behavior | PASS | `dev:local-api` command unchanged and startup validation passed. | | No unrelated cleanup | PASS | Scope limited to package scripts, new dev scripts, targeted tests, and reports. | diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md index 370accd63..8881d8d98 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md @@ -12,7 +12,8 @@ | Unsupported team token scan in new bootstrap files | PASS | No unsupported old team names or single-letter public team commands found in package scripts, bootstrap scripts, or targeted bootstrap tests. | | `npm run validate:canonical-structure` | PASS | Canonical repository structure guardrail passed. | | `npm run dev:bootstrap -- --team alfa` | PASS | Started with team `alfa`, role `owner`, web `5510`, API `5511`, launched browser to `http://127.0.0.1:5510/index.html`, then process tree was stopped. | -| `npm run dev:bootstrap -- --team alfa --role codex` | PASS | Started with team `alfa`, role `codex`, web `5512`, API `5513`, suppressed browser launch, then process tree was stopped. | +| `npm run dev:bootstrap -- --team alfa --role codex` | PASS | Started with team `alfa`, role `codex`, web `5512`, API `5513`, skipped browser launch, then process tree was stopped. | +| `npm run dev:bootstrap -- alfa codex` | PASS | Positional team/role forwarding selected team `alfa`, role `codex`, web `5512`, API `5513`, and skipped browser launch. | | `npm run dev:bootstrap -- --team charlie` with inherited stale URL env values | PASS | Started with team `charlie`, role `owner`, web `5530`, API `5531`, launched browser to `http://127.0.0.1:5530/index.html`, and ignored inherited `GAMEFOUNDRY_SITE_URL` / `GAMEFOUNDRY_API_URL` port values before process cleanup. | | `npm run dev:bootstrap -- charlie` | PASS | Positional team forwarding selected team `charlie`, web `5530`, API `5531`, and browser target `http://127.0.0.1:5530/index.html`. | | `npm run dev:bootstrap -- --team bravo` | PASS | Started with team `bravo`, role `owner`, web `5520`, API `5521`, and browser target `http://127.0.0.1:5520/index.html`. | diff --git a/dev/reports/codex_changed_files.txt b/dev/reports/codex_changed_files.txt index ae24ed62d..a5f9b8a5e 100644 --- a/dev/reports/codex_changed_files.txt +++ b/dev/reports/codex_changed_files.txt @@ -4,16 +4,18 @@ M dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md M dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md M dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md M dev/scripts/start-dev.mjs + M dev/scripts/team-port-config.mjs M dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs # git ls-files --others --exclude-standard (no output) # git diff --stat -...LFA_009-team-aware-bootstrap_manual-validation-notes.md | 4 +++- - .../PR_26171_ALFA_009-team-aware-bootstrap_report.md | 4 +++- - ..._ALFA_009-team-aware-bootstrap_requirement-checklist.md | 2 +- - ...6171_ALFA_009-team-aware-bootstrap_validation-report.md | 6 +++--- - dev/scripts/start-dev.mjs | 6 +++--- - dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs | 14 +++++++++++--- - 6 files changed, 24 insertions(+), 12 deletions(-) \ No newline at end of file +...team-aware-bootstrap_manual-validation-notes.md | 2 +- + ...R_26171_ALFA_009-team-aware-bootstrap_report.md | 6 +- + ...9-team-aware-bootstrap_requirement-checklist.md | 3 +- + ...A_009-team-aware-bootstrap_validation-report.md | 3 +- + dev/scripts/start-dev.mjs | 2 +- + dev/scripts/team-port-config.mjs | 31 ++++++++-- + dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs | 71 ++++++++++++++++------ + 7 files changed, 87 insertions(+), 31 deletions(-) \ No newline at end of file diff --git a/dev/reports/codex_review.diff b/dev/reports/codex_review.diff index ef973d429..25b5215b9 100644 --- a/dev/reports/codex_review.diff +++ b/dev/reports/codex_review.diff @@ -1,145 +1,276 @@ diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md -index eba870a8b..de2ee4a6b 100644 +index de2ee4a6b..d416b4c0f 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_manual-validation-notes.md -@@ -12,7 +12,7 @@ Expected diagnostics: - - `Role: owner` - - `Web URL: http://127.0.0.1:5510` - - `API URL: http://127.0.0.1:5511/api` --- `Browser launch: opened http://127.0.0.1:5510/index.html` -+- `Browser launch: http://127.0.0.1:5510/index.html` +@@ -26,7 +26,7 @@ Expected diagnostics: + - `Role: codex` + - `Web URL: http://127.0.0.1:5512` + - `API URL: http://127.0.0.1:5513/api` +-- `Browser launch: suppressed for codex role` ++- `Browser launch: skipped for codex role` - ## Primary Alfa Codex Command + ## Legacy Command -@@ -60,6 +60,8 @@ For Charlie, both forms should show: - - `API URL: http://127.0.0.1:5531/api` - - `Browser launch: http://127.0.0.1:5530/index.html` - -+If `.env` or the current shell already has `GAMEFOUNDRY_SITE_URL` or `GAMEFOUNDRY_API_URL`, bootstrap should still show the resolved team/role ports in diagnostics and launch the browser from the computed `GAMEFOUNDRY_SITE_URL`. -+ - ## Failure Checks - - ```powershell diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md -index 95d02d5a3..e0279d948 100644 +index e0279d948..9f8343dfb 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_report.md -@@ -31,6 +31,7 @@ Per OWNER instruction, those uncommitted changes were discarded with `git reset +@@ -29,7 +29,7 @@ Per OWNER instruction, those uncommitted changes were discarded with `git reset + - Added `dev/scripts/start-dev.mjs` as the bootstrap orchestrator. + - Added focused node tests for team resolution, role offsets, invalid team/role validation, script wiring, `.env` loading behavior, and bootstrap diagnostics. - Added automatic browser launch for owner-role `dev:bootstrap` after the API and web servers are both ready. - - Suppressed automatic browser launch for the `codex` role. +-- Suppressed automatic browser launch for the `codex` role. ++- Skipped automatic browser launch for the `codex` role. - Fixed npm argument forwarding/parsing so both `--team charlie` and positional `charlie` select the same team after the package script's `--mode bootstrap` argument. -+- Treats `.env` `GAMEFOUNDRY_SITE_URL` and `GAMEFOUNDRY_API_URL` as legacy/default local values for bootstrap mode, then overwrites runtime URLs from the resolved team/role ports. + - Treats `.env` `GAMEFOUNDRY_SITE_URL` and `GAMEFOUNDRY_API_URL` as legacy/default local values for bootstrap mode, then overwrites runtime URLs from the resolved team/role ports. - ## Supported Teams +@@ -94,10 +94,10 @@ npm run dev:bootstrap -- --team alfa --role codex + ## Browser Launch Behavior -@@ -98,7 +99,8 @@ npm run dev:bootstrap -- --team alfa --role codex + - `owner` role launches the browser automatically to the selected team's `index.html`. +-- `codex` role suppresses browser launch. ++- `codex` role skips browser launch. + - Browser launch happens only after both API and web servers are ready. - The Alfa owner launch target is `http://127.0.0.1:5510/index.html`. - - The Alfa codex launch target is suppressed while using web `5512` and API `5513`. +-- The Alfa codex launch target is suppressed while using web `5512` and API `5513`. ++- The Alfa codex browser launch is skipped while using web `5512` and API `5513`. - The Charlie launch target is `http://127.0.0.1:5530/index.html` for both `npm run dev:bootstrap -- --team charlie` and `npm run dev:bootstrap -- charlie`. --- Computed team/role ports override any `.env` port values loaded before bootstrap startup. -+- Computed team/role ports override any `.env` or inherited process `GAMEFOUNDRY_SITE_URL` / `GAMEFOUNDRY_API_URL` values loaded before bootstrap startup. -+- Bootstrap runtime sets `GAMEFOUNDRY_SITE_URL=http://127.0.0.1:` and `GAMEFOUNDRY_API_URL=http://127.0.0.1:/api` after team/role resolution. - - ## Files Changed - + - Computed team/role ports override any `.env` or inherited process `GAMEFOUNDRY_SITE_URL` / `GAMEFOUNDRY_API_URL` values loaded before bootstrap startup. + - Bootstrap runtime sets `GAMEFOUNDRY_SITE_URL=http://127.0.0.1:` and `GAMEFOUNDRY_API_URL=http://127.0.0.1:/api` after team/role resolution. diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md -index a6c478735..fc4b68f78 100644 +index fc4b68f78..27d45e7ac 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_requirement-checklist.md -@@ -18,7 +18,7 @@ +@@ -18,6 +18,7 @@ | Validate unknown role names | PASS | `reviewer` fails clearly with supported role list. | | Support `--team` argument forwarding | PASS | `npm run dev:bootstrap -- --team charlie` resolves Charlie ports. | | Support positional team forwarding | PASS | `npm run dev:bootstrap -- charlie` resolves Charlie ports. | --| Prevent `.env` port override | PASS | Unit test confirms computed team/role ports overwrite loaded API/site port values. | -+| Prevent `.env` and process URL port override | PASS | Unit and live startup tests confirm computed team/role ports overwrite loaded or inherited API/site URL port values. | ++| Support positional team/role forwarding | PASS | `npm run dev:bootstrap -- alfa codex` resolves Alfa codex ports and skips browser launch. | + | Prevent `.env` and process URL port override | PASS | Unit and live startup tests confirm computed team/role ports overwrite loaded or inherited API/site URL port values. | | Load environment | PASS | Orchestrator loads `.env` values without overriding existing environment values. | | Resolve team ports | PASS | Unit tests cover all teams and both roles. | - | Start Local API | PASS | `dev:bootstrap` starts the existing Local API server on resolved API port. | +@@ -25,7 +26,7 @@ + | Start web server when requested | PASS | `dev:bootstrap` starts web plus API; `dev:web` mode is wired. | + | Print startup diagnostics | PASS | Diagnostics include mode, team, role, web URL, API URL, environment source, supported teams, and supported roles. | + | Owner role launches browser automatically | PASS | Live startup validation opened `http://127.0.0.1:5510/index.html` after API and web were ready. | +-| Codex role suppresses browser launch | PASS | Live startup validation reported `Browser launch: suppressed for codex role`. | ++| Codex role skips browser launch | PASS | Live startup validation reported `Browser launch: skipped for codex role`. | + | Wait for both servers before browser launch | PASS | Targeted unit test asserts API ready, web ready, then browser launch order. | + | Preserve legacy behavior | PASS | `dev:local-api` command unchanged and startup validation passed. | + | No unrelated cleanup | PASS | Scope limited to package scripts, new dev scripts, targeted tests, and reports. | diff --git a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md -index 5ca1cd0d0..370accd63 100644 +index 370accd63..8881d8d98 100644 --- a/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md +++ b/dev/reports/PR_26171_ALFA_009-team-aware-bootstrap_validation-report.md -@@ -9,11 +9,11 @@ - | `node --check dev/scripts/start-local-api-server.mjs` | PASS | Legacy startup entrypoint remains valid. | - | `node --check dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs` | PASS | Test syntax valid. | - | `node ./dev/scripts/run-node-test-files.mjs dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs dev/tests/dev-runtime/LocalApiStartupLogging.test.mjs` | PASS | 2 targeted files passed; 14 subtests passed. | --| Unsupported team token scan in new bootstrap files | PASS | No unsupported old team names or single-letter public team commands found. | -+| Unsupported team token scan in new bootstrap files | PASS | No unsupported old team names or single-letter public team commands found in package scripts, bootstrap scripts, or targeted bootstrap tests. | +@@ -12,7 +12,8 @@ + | Unsupported team token scan in new bootstrap files | PASS | No unsupported old team names or single-letter public team commands found in package scripts, bootstrap scripts, or targeted bootstrap tests. | | `npm run validate:canonical-structure` | PASS | Canonical repository structure guardrail passed. | | `npm run dev:bootstrap -- --team alfa` | PASS | Started with team `alfa`, role `owner`, web `5510`, API `5511`, launched browser to `http://127.0.0.1:5510/index.html`, then process tree was stopped. | - | `npm run dev:bootstrap -- --team alfa --role codex` | PASS | Started with team `alfa`, role `codex`, web `5512`, API `5513`, suppressed browser launch, then process tree was stopped. | --| `npm run dev:bootstrap -- --team charlie` | PASS | Started with team `charlie`, role `owner`, web `5530`, API `5531`, launched browser to `http://127.0.0.1:5530/index.html`, then process tree was stopped. | -+| `npm run dev:bootstrap -- --team charlie` with inherited stale URL env values | PASS | Started with team `charlie`, role `owner`, web `5530`, API `5531`, launched browser to `http://127.0.0.1:5530/index.html`, and ignored inherited `GAMEFOUNDRY_SITE_URL` / `GAMEFOUNDRY_API_URL` port values before process cleanup. | +-| `npm run dev:bootstrap -- --team alfa --role codex` | PASS | Started with team `alfa`, role `codex`, web `5512`, API `5513`, suppressed browser launch, then process tree was stopped. | ++| `npm run dev:bootstrap -- --team alfa --role codex` | PASS | Started with team `alfa`, role `codex`, web `5512`, API `5513`, skipped browser launch, then process tree was stopped. | ++| `npm run dev:bootstrap -- alfa codex` | PASS | Positional team/role forwarding selected team `alfa`, role `codex`, web `5512`, API `5513`, and skipped browser launch. | + | `npm run dev:bootstrap -- --team charlie` with inherited stale URL env values | PASS | Started with team `charlie`, role `owner`, web `5530`, API `5531`, launched browser to `http://127.0.0.1:5530/index.html`, and ignored inherited `GAMEFOUNDRY_SITE_URL` / `GAMEFOUNDRY_API_URL` port values before process cleanup. | | `npm run dev:bootstrap -- charlie` | PASS | Positional team forwarding selected team `charlie`, web `5530`, API `5531`, and browser target `http://127.0.0.1:5530/index.html`. | | `npm run dev:bootstrap -- --team bravo` | PASS | Started with team `bravo`, role `owner`, web `5520`, API `5521`, and browser target `http://127.0.0.1:5520/index.html`. | - | `npm run dev:bootstrap -- --team omega` | PASS | Failed clearly with supported team list. | -@@ -22,4 +22,4 @@ - - ## Notes - --The startup-command validations used short-lived process launches and `taskkill /T /F` cleanup after expected diagnostics appeared, so no dev servers were left running. Owner-role validations opened the configured browser target after both server diagnostics were available. Unit coverage verifies loaded `.env` port values are overwritten by computed team/role ports. -+The startup-command validations used short-lived process launches and `taskkill /T /F` cleanup after expected diagnostics appeared, so no dev servers were left running. Owner-role validations opened the configured browser target after both server diagnostics were available. Unit coverage verifies loaded `.env` and inherited process URL values are overwritten by computed team/role ports. diff --git a/dev/scripts/start-dev.mjs b/dev/scripts/start-dev.mjs -index ac3d6242b..e1b58491a 100644 +index e1b58491a..a58a53697 100644 --- a/dev/scripts/start-dev.mjs +++ b/dev/scripts/start-dev.mjs -@@ -179,7 +179,7 @@ export function applyBootstrapEnvironment({ - host = DEFAULT_HOST, - ports, - }) { -- env.GAMEFOUNDRY_LOCAL_API_HOST = env.GAMEFOUNDRY_LOCAL_API_HOST || host; -+ env.GAMEFOUNDRY_LOCAL_API_HOST = host; - env.GAMEFOUNDRY_LOCAL_API_PORT = String(ports.apiPort); - env.GAMEFOUNDRY_API_URL = apiUrl(host, ports.apiPort); - env.GAMEFOUNDRY_SITE_URL = localUrl(host, ports.webPort); -@@ -345,10 +345,10 @@ export async function startBootstrapRuntime( - } = {}, - ) { - const runtimeEnv = loadEnv(); -- const host = env.GAMEFOUNDRY_LOCAL_API_HOST || DEFAULT_HOST; -+ const host = DEFAULT_HOST; - applyBootstrapEnvironment({ env, host, ports: options.ports }); - const apiBaseUrl = localUrl(host, options.ports.apiPort); -- const webBaseUrl = localUrl(host, options.ports.webPort); -+ const webBaseUrl = env.GAMEFOUNDRY_SITE_URL; - const servers = []; +@@ -364,7 +364,7 @@ export async function startBootstrapRuntime( + : { + enabled: false, + reason: options.role === "codex" +- ? "suppressed for codex role" ++ ? "skipped for codex role" + : "requires bootstrap mode with API and web servers", + url: browserUrl, + }; +diff --git a/dev/scripts/team-port-config.mjs b/dev/scripts/team-port-config.mjs +index 9d11f9b7f..f8b5c3423 100644 +--- a/dev/scripts/team-port-config.mjs ++++ b/dev/scripts/team-port-config.mjs +@@ -80,9 +80,10 @@ function parseNamedArgument(args, name, fallback, supportedLabel) { + return fallback; + } + +-function firstPositionalArgument(args = []) { ++function positionalArguments(args = []) { + const values = Array.from(args); + const optionsWithValues = new Set(["mode", "role", "team"]); ++ const positional = []; + for (let index = 0; index < values.length; index += 1) { + const argument = values[index]; + if (!argument) { +@@ -98,13 +99,21 @@ function firstPositionalArgument(args = []) { + } + continue; + } +- return argument; ++ positional.push(argument); + } +- return ""; ++ return positional; + } + + export function parseRoleArgument(args = []) { +- return parseNamedArgument(args, "role", DEFAULT_BOOTSTRAP_ROLE, supportedBootstrapRolesLabel); ++ const explicitRole = parseNamedArgument(args, "role", "", supportedBootstrapRolesLabel); ++ if (explicitRole) { ++ return explicitRole; ++ } ++ const positionalRole = positionalArguments(args).find((argument) => { ++ const normalizedRole = normalizeToken(argument, ""); ++ return ROLE_PORT_OFFSETS[normalizedRole] !== undefined; ++ }); ++ return normalizeToken(positionalRole, DEFAULT_BOOTSTRAP_ROLE); + } - if (options.api) { + export function parseTeamArgument(args = []) { +@@ -112,5 +121,17 @@ export function parseTeamArgument(args = []) { + if (explicitTeam) { + return explicitTeam; + } +- return normalizeToken(firstPositionalArgument(args), DEFAULT_BOOTSTRAP_TEAM); ++ const positional = positionalArguments(args); ++ const positionalTeam = positional.find((argument) => { ++ const normalizedTeam = normalizeToken(argument, ""); ++ return TEAM_BASE_PORTS[normalizedTeam] !== undefined; ++ }); ++ if (positionalTeam) { ++ return normalizeToken(positionalTeam, DEFAULT_BOOTSTRAP_TEAM); ++ } ++ const unknownTeam = positional.find((argument) => { ++ const normalizedArgument = normalizeToken(argument, ""); ++ return ROLE_PORT_OFFSETS[normalizedArgument] === undefined; ++ }); ++ return normalizeToken(unknownTeam, DEFAULT_BOOTSTRAP_TEAM); + } diff --git a/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs b/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs -index 226a7461a..c746f1b3b 100644 +index c746f1b3b..0f7b91480 100644 --- a/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs +++ b/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs -@@ -172,15 +172,15 @@ test("bootstrap option parser maps npm script modes to server startup plan", () - ); +@@ -62,10 +62,14 @@ test("team and role parsers accept canonical argument forms", () => { + assert.equal(parseTeamArgument(["--team=bravo"]), "bravo"); + assert.equal(parseTeamArgument(["--mode", "bootstrap", "charlie"]), "charlie"); + assert.equal(parseTeamArgument(["--mode=bootstrap", "charlie"]), "charlie"); ++ assert.equal(parseTeamArgument(["--mode", "bootstrap", "alfa", "codex"]), "alfa"); ++ assert.equal(parseTeamArgument(["--mode", "bootstrap", "codex", "alfa"]), "alfa"); + assert.equal(parseTeamArgument(["charlie", "--role", "codex"]), "charlie"); + assert.equal(parseRoleArgument([]), "owner"); + assert.equal(parseRoleArgument(["--role", "codex"]), "codex"); + assert.equal(parseRoleArgument(["--role=owner"]), "owner"); ++ assert.equal(parseRoleArgument(["--mode", "bootstrap", "alfa", "codex"]), "codex"); ++ assert.equal(parseRoleArgument(["--mode", "bootstrap", "codex", "alfa"]), "codex"); }); --test("bootstrap environment applies resolved team and role ports for browser API routing", () => { -+test("bootstrap environment overrides legacy env URLs with resolved team and role ports", () => { + test("invalid teams and roles fail with supported values", () => { +@@ -114,6 +118,32 @@ test("bootstrap option parser maps npm script modes to server startup plan", () + team: "alfa", + web: true, + }); ++ assert.deepEqual(parseBootstrapOptions(["--mode", "bootstrap", "--team", "alfa", "--role", "codex"]), { ++ api: true, ++ mode: "bootstrap", ++ ports: { ++ apiPort: 5513, ++ role: "codex", ++ team: "alfa", ++ webPort: 5512, ++ }, ++ role: "codex", ++ team: "alfa", ++ web: true, ++ }); ++ assert.deepEqual(parseBootstrapOptions(["--mode", "bootstrap", "alfa", "codex"]), { ++ api: true, ++ mode: "bootstrap", ++ ports: { ++ apiPort: 5513, ++ role: "codex", ++ team: "alfa", ++ webPort: 5512, ++ }, ++ role: "codex", ++ team: "alfa", ++ web: true, ++ }); + assert.deepEqual(parseBootstrapOptions(["--mode=api", "--team=charlie", "--role=codex"]), { + api: true, + mode: "api", +@@ -195,7 +225,7 @@ test("startup diagnostics and package scripts expose team-aware bootstrap comman + apiBaseUrl: "http://127.0.0.1:5513", + browserLaunch: { + enabled: false, +- reason: "suppressed for codex role", ++ reason: "skipped for codex role", + url: "http://127.0.0.1:5512/index.html", + }, + mode: "bootstrap", +@@ -212,7 +242,7 @@ test("startup diagnostics and package scripts expose team-aware bootstrap comman + assert.equal(lines.includes("Role: codex"), true); + assert.equal(lines.includes("Web URL: http://127.0.0.1:5512"), true); + assert.equal(lines.includes("API URL: http://127.0.0.1:5513/api"), true); +- assert.equal(lines.includes("Browser launch: suppressed for codex role"), true); ++ assert.equal(lines.includes("Browser launch: skipped for codex role"), true); + assert.equal(lines.includes(`Supported teams: ${supportedBootstrapTeamsLabel()}`), true); + assert.equal(lines.includes(`Supported roles: ${supportedBootstrapRolesLabel()}`), true); + +@@ -236,7 +266,7 @@ test("runtime env loader applies .env values without overriding existing process + assert.equal(env.GAMEFOUNDRY_API_URL, "http://127.0.0.1:5999/api"); + }); + +-test("browser launch targets team index after API and web servers are ready for owner role", async () => { ++test("browser launch targets team index after API and web servers are ready for bravo owner role", async () => { + const events = []; const env = { -+ GAMEFOUNDRY_LOCAL_API_HOST: "0.0.0.0", - GAMEFOUNDRY_API_URL: "http://127.0.0.1:5501/api", - GAMEFOUNDRY_LOCAL_API_PORT: "5501", + GAMEFOUNDRY_LOCAL_API_HOST: "0.0.0.0", +@@ -244,7 +274,7 @@ test("browser launch targets team index after API and web servers are ready for GAMEFOUNDRY_SITE_URL: "http://127.0.0.1:5500", }; - applyBootstrapEnvironment({ - env, -- host: "127.0.0.1", - ports: resolveTeamPortConfig({ role: "codex", team: "alfa" }), - }); + const runtime = await startBootstrapRuntime( +- parseBootstrapOptions(["--mode=bootstrap", "--team=alfa"]), ++ parseBootstrapOptions(["--mode=bootstrap", "--team=bravo"]), + { + apiServerStarter: async ({ port }) => { + events.push(`api:${port}`); +@@ -274,25 +304,26 @@ test("browser launch targets team index after API and web servers are ready for + ); -@@ -238,6 +238,11 @@ test("runtime env loader applies .env values without overriding existing process + assert.deepEqual(events, [ +- "api:5511", +- "web:5510", +- "browser:http://127.0.0.1:5510/index.html", ++ "api:5521", ++ "web:5520", ++ "browser:http://127.0.0.1:5520/index.html", + ]); + assert.equal(env.GAMEFOUNDRY_LOCAL_API_HOST, "127.0.0.1"); +- assert.equal(env.GAMEFOUNDRY_API_URL, "http://127.0.0.1:5511/api"); +- assert.equal(env.GAMEFOUNDRY_SITE_URL, "http://127.0.0.1:5510"); +- assert.equal(runtime.diagnostics.includes("Browser launch: http://127.0.0.1:5510/index.html"), true); ++ assert.equal(env.GAMEFOUNDRY_API_URL, "http://127.0.0.1:5521/api"); ++ assert.equal(env.GAMEFOUNDRY_SITE_URL, "http://127.0.0.1:5520"); ++ assert.equal(runtime.diagnostics.includes("Browser launch: http://127.0.0.1:5520/index.html"), true); + await runtime.close(); + assert.deepEqual(events.slice(-2), ["api:closed", "web:closed"]); + }); - test("browser launch targets team index after API and web servers are ready for owner role", async () => { +-test("codex role suppresses browser launch even after API and web servers are ready", async () => { ++test("alfa codex role skips browser launch even after API and web servers are ready", async () => { const events = []; -+ const env = { -+ GAMEFOUNDRY_LOCAL_API_HOST: "0.0.0.0", -+ GAMEFOUNDRY_API_URL: "http://127.0.0.1:5501/api", -+ GAMEFOUNDRY_SITE_URL: "http://127.0.0.1:5500", -+ }; ++ const env = {}; const runtime = await startBootstrapRuntime( - parseBootstrapOptions(["--mode=bootstrap", "--team=alfa"]), +- parseBootstrapOptions(["--mode=bootstrap", "--team=alfa", "--role=codex"]), ++ parseBootstrapOptions(["--mode=bootstrap", "--team", "alfa", "--role", "codex"]), { -@@ -254,7 +259,7 @@ test("browser launch targets team index after API and web servers are ready for +- apiServerStarter: async () => { +- events.push("api"); ++ apiServerStarter: async ({ port }) => { ++ events.push(`api:${port}`); + return { + close: async () => {}, + }; +@@ -304,13 +335,13 @@ test("codex role suppresses browser launch even after API and web servers are re url, }; }, @@ -148,13 +279,23 @@ index 226a7461a..c746f1b3b 100644 loadEnv: () => ({ loaded: false, loadedKeys: 0, -@@ -273,6 +278,9 @@ test("browser launch targets team index after API and web servers are ready for - "web:5510", - "browser:http://127.0.0.1:5510/index.html", - ]); -+ assert.equal(env.GAMEFOUNDRY_LOCAL_API_HOST, "127.0.0.1"); -+ assert.equal(env.GAMEFOUNDRY_API_URL, "http://127.0.0.1:5511/api"); -+ assert.equal(env.GAMEFOUNDRY_SITE_URL, "http://127.0.0.1:5510"); - assert.equal(runtime.diagnostics.includes("Browser launch: http://127.0.0.1:5510/index.html"), true); + }), +- webServerStarter: async () => { +- events.push("web"); ++ webServerStarter: async ({ port }) => { ++ events.push(`web:${port}`); + return { + close: async () => {}, + }; +@@ -318,8 +349,10 @@ test("codex role suppresses browser launch even after API and web servers are re + }, + ); + +- assert.deepEqual(events, ["api", "web"]); +- assert.equal(runtime.diagnostics.includes("Browser launch: suppressed for codex role"), true); ++ assert.deepEqual(events, ["api:5513", "web:5512"]); ++ assert.equal(env.GAMEFOUNDRY_API_URL, "http://127.0.0.1:5513/api"); ++ assert.equal(env.GAMEFOUNDRY_SITE_URL, "http://127.0.0.1:5512"); ++ assert.equal(runtime.diagnostics.includes("Browser launch: skipped for codex role"), true); await runtime.close(); - assert.deepEqual(events.slice(-2), ["api:closed", "web:closed"]); + }); diff --git a/dev/scripts/start-dev.mjs b/dev/scripts/start-dev.mjs index e1b58491a..a58a53697 100644 --- a/dev/scripts/start-dev.mjs +++ b/dev/scripts/start-dev.mjs @@ -364,7 +364,7 @@ export async function startBootstrapRuntime( : { enabled: false, reason: options.role === "codex" - ? "suppressed for codex role" + ? "skipped for codex role" : "requires bootstrap mode with API and web servers", url: browserUrl, }; diff --git a/dev/scripts/team-port-config.mjs b/dev/scripts/team-port-config.mjs index 9d11f9b7f..f8b5c3423 100644 --- a/dev/scripts/team-port-config.mjs +++ b/dev/scripts/team-port-config.mjs @@ -80,9 +80,10 @@ function parseNamedArgument(args, name, fallback, supportedLabel) { return fallback; } -function firstPositionalArgument(args = []) { +function positionalArguments(args = []) { const values = Array.from(args); const optionsWithValues = new Set(["mode", "role", "team"]); + const positional = []; for (let index = 0; index < values.length; index += 1) { const argument = values[index]; if (!argument) { @@ -98,13 +99,21 @@ function firstPositionalArgument(args = []) { } continue; } - return argument; + positional.push(argument); } - return ""; + return positional; } export function parseRoleArgument(args = []) { - return parseNamedArgument(args, "role", DEFAULT_BOOTSTRAP_ROLE, supportedBootstrapRolesLabel); + const explicitRole = parseNamedArgument(args, "role", "", supportedBootstrapRolesLabel); + if (explicitRole) { + return explicitRole; + } + const positionalRole = positionalArguments(args).find((argument) => { + const normalizedRole = normalizeToken(argument, ""); + return ROLE_PORT_OFFSETS[normalizedRole] !== undefined; + }); + return normalizeToken(positionalRole, DEFAULT_BOOTSTRAP_ROLE); } export function parseTeamArgument(args = []) { @@ -112,5 +121,17 @@ export function parseTeamArgument(args = []) { if (explicitTeam) { return explicitTeam; } - return normalizeToken(firstPositionalArgument(args), DEFAULT_BOOTSTRAP_TEAM); + const positional = positionalArguments(args); + const positionalTeam = positional.find((argument) => { + const normalizedTeam = normalizeToken(argument, ""); + return TEAM_BASE_PORTS[normalizedTeam] !== undefined; + }); + if (positionalTeam) { + return normalizeToken(positionalTeam, DEFAULT_BOOTSTRAP_TEAM); + } + const unknownTeam = positional.find((argument) => { + const normalizedArgument = normalizeToken(argument, ""); + return ROLE_PORT_OFFSETS[normalizedArgument] === undefined; + }); + return normalizeToken(unknownTeam, DEFAULT_BOOTSTRAP_TEAM); } diff --git a/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs b/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs index c746f1b3b..0f7b91480 100644 --- a/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs +++ b/dev/tests/dev-runtime/TeamAwareBootstrap.test.mjs @@ -62,10 +62,14 @@ test("team and role parsers accept canonical argument forms", () => { assert.equal(parseTeamArgument(["--team=bravo"]), "bravo"); assert.equal(parseTeamArgument(["--mode", "bootstrap", "charlie"]), "charlie"); assert.equal(parseTeamArgument(["--mode=bootstrap", "charlie"]), "charlie"); + assert.equal(parseTeamArgument(["--mode", "bootstrap", "alfa", "codex"]), "alfa"); + assert.equal(parseTeamArgument(["--mode", "bootstrap", "codex", "alfa"]), "alfa"); assert.equal(parseTeamArgument(["charlie", "--role", "codex"]), "charlie"); assert.equal(parseRoleArgument([]), "owner"); assert.equal(parseRoleArgument(["--role", "codex"]), "codex"); assert.equal(parseRoleArgument(["--role=owner"]), "owner"); + assert.equal(parseRoleArgument(["--mode", "bootstrap", "alfa", "codex"]), "codex"); + assert.equal(parseRoleArgument(["--mode", "bootstrap", "codex", "alfa"]), "codex"); }); test("invalid teams and roles fail with supported values", () => { @@ -114,6 +118,32 @@ test("bootstrap option parser maps npm script modes to server startup plan", () team: "alfa", web: true, }); + assert.deepEqual(parseBootstrapOptions(["--mode", "bootstrap", "--team", "alfa", "--role", "codex"]), { + api: true, + mode: "bootstrap", + ports: { + apiPort: 5513, + role: "codex", + team: "alfa", + webPort: 5512, + }, + role: "codex", + team: "alfa", + web: true, + }); + assert.deepEqual(parseBootstrapOptions(["--mode", "bootstrap", "alfa", "codex"]), { + api: true, + mode: "bootstrap", + ports: { + apiPort: 5513, + role: "codex", + team: "alfa", + webPort: 5512, + }, + role: "codex", + team: "alfa", + web: true, + }); assert.deepEqual(parseBootstrapOptions(["--mode=api", "--team=charlie", "--role=codex"]), { api: true, mode: "api", @@ -195,7 +225,7 @@ test("startup diagnostics and package scripts expose team-aware bootstrap comman apiBaseUrl: "http://127.0.0.1:5513", browserLaunch: { enabled: false, - reason: "suppressed for codex role", + reason: "skipped for codex role", url: "http://127.0.0.1:5512/index.html", }, mode: "bootstrap", @@ -212,7 +242,7 @@ test("startup diagnostics and package scripts expose team-aware bootstrap comman assert.equal(lines.includes("Role: codex"), true); assert.equal(lines.includes("Web URL: http://127.0.0.1:5512"), true); assert.equal(lines.includes("API URL: http://127.0.0.1:5513/api"), true); - assert.equal(lines.includes("Browser launch: suppressed for codex role"), true); + assert.equal(lines.includes("Browser launch: skipped for codex role"), true); assert.equal(lines.includes(`Supported teams: ${supportedBootstrapTeamsLabel()}`), true); assert.equal(lines.includes(`Supported roles: ${supportedBootstrapRolesLabel()}`), true); @@ -236,7 +266,7 @@ test("runtime env loader applies .env values without overriding existing process assert.equal(env.GAMEFOUNDRY_API_URL, "http://127.0.0.1:5999/api"); }); -test("browser launch targets team index after API and web servers are ready for owner role", async () => { +test("browser launch targets team index after API and web servers are ready for bravo owner role", async () => { const events = []; const env = { GAMEFOUNDRY_LOCAL_API_HOST: "0.0.0.0", @@ -244,7 +274,7 @@ test("browser launch targets team index after API and web servers are ready for GAMEFOUNDRY_SITE_URL: "http://127.0.0.1:5500", }; const runtime = await startBootstrapRuntime( - parseBootstrapOptions(["--mode=bootstrap", "--team=alfa"]), + parseBootstrapOptions(["--mode=bootstrap", "--team=bravo"]), { apiServerStarter: async ({ port }) => { events.push(`api:${port}`); @@ -274,25 +304,26 @@ test("browser launch targets team index after API and web servers are ready for ); assert.deepEqual(events, [ - "api:5511", - "web:5510", - "browser:http://127.0.0.1:5510/index.html", + "api:5521", + "web:5520", + "browser:http://127.0.0.1:5520/index.html", ]); assert.equal(env.GAMEFOUNDRY_LOCAL_API_HOST, "127.0.0.1"); - assert.equal(env.GAMEFOUNDRY_API_URL, "http://127.0.0.1:5511/api"); - assert.equal(env.GAMEFOUNDRY_SITE_URL, "http://127.0.0.1:5510"); - assert.equal(runtime.diagnostics.includes("Browser launch: http://127.0.0.1:5510/index.html"), true); + assert.equal(env.GAMEFOUNDRY_API_URL, "http://127.0.0.1:5521/api"); + assert.equal(env.GAMEFOUNDRY_SITE_URL, "http://127.0.0.1:5520"); + assert.equal(runtime.diagnostics.includes("Browser launch: http://127.0.0.1:5520/index.html"), true); await runtime.close(); assert.deepEqual(events.slice(-2), ["api:closed", "web:closed"]); }); -test("codex role suppresses browser launch even after API and web servers are ready", async () => { +test("alfa codex role skips browser launch even after API and web servers are ready", async () => { const events = []; + const env = {}; const runtime = await startBootstrapRuntime( - parseBootstrapOptions(["--mode=bootstrap", "--team=alfa", "--role=codex"]), + parseBootstrapOptions(["--mode=bootstrap", "--team", "alfa", "--role", "codex"]), { - apiServerStarter: async () => { - events.push("api"); + apiServerStarter: async ({ port }) => { + events.push(`api:${port}`); return { close: async () => {}, }; @@ -304,13 +335,13 @@ test("codex role suppresses browser launch even after API and web servers are re url, }; }, - env: {}, + env, loadEnv: () => ({ loaded: false, loadedKeys: 0, }), - webServerStarter: async () => { - events.push("web"); + webServerStarter: async ({ port }) => { + events.push(`web:${port}`); return { close: async () => {}, }; @@ -318,8 +349,10 @@ test("codex role suppresses browser launch even after API and web servers are re }, ); - assert.deepEqual(events, ["api", "web"]); - assert.equal(runtime.diagnostics.includes("Browser launch: suppressed for codex role"), true); + assert.deepEqual(events, ["api:5513", "web:5512"]); + assert.equal(env.GAMEFOUNDRY_API_URL, "http://127.0.0.1:5513/api"); + assert.equal(env.GAMEFOUNDRY_SITE_URL, "http://127.0.0.1:5512"); + assert.equal(runtime.diagnostics.includes("Browser launch: skipped for codex role"), true); await runtime.close(); });