feat(namepool): Add themed name pools for polecats
Polecats now get themed names from the Mad Max universe by default
(furiosa, nux, slit, etc.) instead of generic polecat-01, polecat-02.
Changes:
- Add NamepoolConfig to config/types.go for per-rig theme configuration
- Update namepool.go with three built-in themes:
- mad-max (default): furiosa, nux, imperator, etc.
- minerals: obsidian, quartz, ruby, etc.
- wasteland: rust, chrome, fury, etc.
- Add gt namepool commands: themes, set, add, reset
- Update manager.go to load namepool config from rig settings
Configuration in .gastown/config.json:
```json
{
"namepool": {
"style": "minerals",
"max_before_numbering": 50
}
}
```
Issue: beads-rs0
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
{"id":"gt-0pc","title":"Document Overseer role (human operator)","description":"Document the Overseer role in Gas Town architecture.\n\n## The Overseer\n\nThe **Overseer** is the human operator of Gas Town. Not an agent - a person.\n\n## Responsibilities\n\n| Area | Overseer Does | Mayor/Agents Do |\n|------|---------------|-----------------|\n| Strategy | Define project goals | Execute toward goals |\n| Priorities | Set priority order | Work in priority order |\n| Escalations | Final decision on stuck work | Escalate to Overseer |\n| Resources | Provision machines | Use allocated resources |\n| Quality | Review \u0026 approve swarm output | Produce output |\n| Operations | Run gt commands, monitor dashboards | Do the work |\n\n## Key Interactions\n\n### Overseer → Mayor\n- Start/stop Mayor sessions\n- Direct Mayor via conversation\n- Review Mayor recommendations\n- Approve cross-rig decisions\n\n### Mayor → Overseer (Escalations)\n- Stuck workers after retries\n- Resource decisions (add machines, polecats)\n- Ambiguous requirements\n- Architecture decisions\n\n## Operating Cadence\n\nTypical Overseer workflow:\n1. Morning: Check status, review overnight work\n2. During day: Monitor, respond to escalations, adjust priorities\n3. End of day: Review progress, plan next batch\n\n## Commands for Overseers\n\n```bash\ngt status # Quick health check\ngt doctor # Detailed diagnostics \ngt doctor --fix # Auto-repair issues\ngt inbox # Messages from agents\ngt stop --all # Emergency halt\n```\n\n## Documentation Updates\n\nAdd to docs/architecture.md:\n- Overseer section under Agent Roles\n- Clarify Mayor reports to Overseer\n- Add Overseer to workflow diagrams","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-15T23:18:03.177633-08:00","updated_at":"2025-12-15T23:22:51.477786-08:00","closed_at":"2025-12-15T23:22:51.477786-08:00"}
|
{"id":"gt-0pc","title":"Document Overseer role (human operator)","description":"Document the Overseer role in Gas Town architecture.\n\n## The Overseer\n\nThe **Overseer** is the human operator of Gas Town. Not an agent - a person.\n\n## Responsibilities\n\n| Area | Overseer Does | Mayor/Agents Do |\n|------|---------------|-----------------|\n| Strategy | Define project goals | Execute toward goals |\n| Priorities | Set priority order | Work in priority order |\n| Escalations | Final decision on stuck work | Escalate to Overseer |\n| Resources | Provision machines | Use allocated resources |\n| Quality | Review \u0026 approve swarm output | Produce output |\n| Operations | Run gt commands, monitor dashboards | Do the work |\n\n## Key Interactions\n\n### Overseer → Mayor\n- Start/stop Mayor sessions\n- Direct Mayor via conversation\n- Review Mayor recommendations\n- Approve cross-rig decisions\n\n### Mayor → Overseer (Escalations)\n- Stuck workers after retries\n- Resource decisions (add machines, polecats)\n- Ambiguous requirements\n- Architecture decisions\n\n## Operating Cadence\n\nTypical Overseer workflow:\n1. Morning: Check status, review overnight work\n2. During day: Monitor, respond to escalations, adjust priorities\n3. End of day: Review progress, plan next batch\n\n## Commands for Overseers\n\n```bash\ngt status # Quick health check\ngt doctor # Detailed diagnostics \ngt doctor --fix # Auto-repair issues\ngt inbox # Messages from agents\ngt stop --all # Emergency halt\n```\n\n## Documentation Updates\n\nAdd to docs/architecture.md:\n- Overseer section under Agent Roles\n- Clarify Mayor reports to Overseer\n- Add Overseer to workflow diagrams","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-15T23:18:03.177633-08:00","updated_at":"2025-12-15T23:22:51.477786-08:00","closed_at":"2025-12-15T23:22:51.477786-08:00"}
|
||||||
{"id":"gt-0pl","title":"Polecat CLAUDE.md: configure auto-approve for bd and gt commands","description":"Polecats get stuck waiting for bash command approval when running\nbd and gt commands. Need to configure Claude Code to auto-approve these.\n\nOptions:\n1. Add allowedTools to polecat CLAUDE.md\n2. Configure .claude/settings.json in polecat directory\n3. Use --dangerously-skip-permissions flag (not recommended)\n\nShould auto-approve:\n- bd (beads commands)\n- gt (gastown commands)\n- go build/test\n- git status/add/commit/push\n\nShould still require approval:\n- rm -rf\n- Arbitrary commands outside project\n\nRelated to polecat prompting (gt-e1y, gt-sd6).","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T14:10:27.611612-08:00","updated_at":"2025-12-17T14:22:00.715979-08:00","closed_at":"2025-12-17T14:22:00.715979-08:00"}
|
{"id":"gt-0pl","title":"Polecat CLAUDE.md: configure auto-approve for bd and gt commands","description":"Polecats get stuck waiting for bash command approval when running\nbd and gt commands. Need to configure Claude Code to auto-approve these.\n\nOptions:\n1. Add allowedTools to polecat CLAUDE.md\n2. Configure .claude/settings.json in polecat directory\n3. Use --dangerously-skip-permissions flag (not recommended)\n\nShould auto-approve:\n- bd (beads commands)\n- gt (gastown commands)\n- go build/test\n- git status/add/commit/push\n\nShould still require approval:\n- rm -rf\n- Arbitrary commands outside project\n\nRelated to polecat prompting (gt-e1y, gt-sd6).","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T14:10:27.611612-08:00","updated_at":"2025-12-17T14:22:00.715979-08:00","closed_at":"2025-12-17T14:22:00.715979-08:00"}
|
||||||
{"id":"gt-0qki","title":"Refinery-Witness communication protocol","description":"Define mail protocol between Refinery and Witness:\n\nFROM Witness → Refinery:\n- 'Polecat ready': polecat X completed work, ready for merge\n- 'Rework complete': polecat Y finished requested rework\n\nFROM Refinery → Witness:\n- 'Merge success': polecat X merged, can be cleaned up\n- 'Merge failed': polecat X needs rework on \u003creason\u003e\n- 'Rework request': please have a polecat rebase X on current main\n\nImplement as structured mail with parseable format.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-19T18:09:27.451344-08:00","updated_at":"2025-12-19T18:09:27.451344-08:00","dependencies":[{"issue_id":"gt-0qki","depends_on_id":"gt-ktal","type":"blocks","created_at":"2025-12-19T18:09:39.58445-08:00","created_by":"daemon"}]}
|
{"id":"gt-0qki","title":"Refinery-Witness communication protocol","description":"Define mail protocol between Refinery and Witness:\n\nFROM Witness → Refinery:\n- 'Polecat ready': polecat X completed work, ready for merge\n- 'Rework complete': polecat Y finished requested rework\n\nFROM Refinery → Witness:\n- 'Merge success': polecat X merged, can be cleaned up\n- 'Merge failed': polecat X needs rework on \u003creason\u003e\n- 'Rework request': please have a polecat rebase X on current main\n\nImplement as structured mail with parseable format.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-19T18:09:27.451344-08:00","updated_at":"2025-12-19T18:09:27.451344-08:00","dependencies":[{"issue_id":"gt-0qki","depends_on_id":"gt-ktal","type":"blocks","created_at":"2025-12-19T18:09:39.58445-08:00","created_by":"daemon"}]}
|
||||||
|
{"id":"gt-14hd","title":"Work on ga-xxp: Define mol-polecat-work standard molecule...","description":"Work on ga-xxp: Define mol-polecat-work standard molecule. See bd show ga-xxp for full details.","status":"in_progress","priority":2,"issue_type":"task","assignee":"gastown/polecat-01","created_at":"2025-12-19T21:49:14.070072-08:00","updated_at":"2025-12-19T21:49:14.140363-08:00"}
|
||||||
{"id":"gt-17r","title":"Doctor check: Zombie session cleanup","description":"Detect and clean up zombie tmux sessions via gt doctor.\n\n## Problem\n\nZombie sessions occur when:\n- Agent crashes without cleanup\n- gt kill fails mid-operation\n- System restart leaves orphan sessions\n- Session naming collision\n\n## Checks\n\n### ZombieSessionCheck\n- List all tmux sessions matching gt-* pattern\n- Cross-reference with known polecats\n- Flag sessions with no corresponding polecat state\n- Flag sessions for removed polecats\n- Check session age vs polecat creation time\n\n### Detection Criteria\n- Session exists but polecat directory doesn't\n- Session name doesn't match any registered polecat\n- Polecat state=idle but session running\n- Multiple sessions for same polecat\n\n## Output\n\n```\n[WARN] Zombie tmux sessions detected:\n - gt-wyvern-OldPolecat (polecat removed)\n - gt-beads-Unknown (no matching polecat)\n - gt-wyvern-Toast (duplicate session)\n\n Run 'gt doctor --fix' to clean up\n```\n\n## Auto-Fix (--fix flag)\n\n- Kill orphan tmux sessions\n- Update polecat state to match reality\n- Log all cleanup actions\n\n## Safety\n\n- Never kill sessions where polecat state=working\n- Prompt before killing if --fix used without --force\n- Create audit log of killed sessions","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-15T23:18:01.446702-08:00","updated_at":"2025-12-15T23:18:39.517644-08:00","dependencies":[{"issue_id":"gt-17r","depends_on_id":"gt-f9x.4","type":"blocks","created_at":"2025-12-15T23:19:05.66301-08:00","created_by":"daemon"},{"issue_id":"gt-17r","depends_on_id":"gt-7ik","type":"blocks","created_at":"2025-12-17T15:44:41.945064-08:00","created_by":"daemon"}]}
|
{"id":"gt-17r","title":"Doctor check: Zombie session cleanup","description":"Detect and clean up zombie tmux sessions via gt doctor.\n\n## Problem\n\nZombie sessions occur when:\n- Agent crashes without cleanup\n- gt kill fails mid-operation\n- System restart leaves orphan sessions\n- Session naming collision\n\n## Checks\n\n### ZombieSessionCheck\n- List all tmux sessions matching gt-* pattern\n- Cross-reference with known polecats\n- Flag sessions with no corresponding polecat state\n- Flag sessions for removed polecats\n- Check session age vs polecat creation time\n\n### Detection Criteria\n- Session exists but polecat directory doesn't\n- Session name doesn't match any registered polecat\n- Polecat state=idle but session running\n- Multiple sessions for same polecat\n\n## Output\n\n```\n[WARN] Zombie tmux sessions detected:\n - gt-wyvern-OldPolecat (polecat removed)\n - gt-beads-Unknown (no matching polecat)\n - gt-wyvern-Toast (duplicate session)\n\n Run 'gt doctor --fix' to clean up\n```\n\n## Auto-Fix (--fix flag)\n\n- Kill orphan tmux sessions\n- Update polecat state to match reality\n- Log all cleanup actions\n\n## Safety\n\n- Never kill sessions where polecat state=working\n- Prompt before killing if --fix used without --force\n- Create audit log of killed sessions","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-15T23:18:01.446702-08:00","updated_at":"2025-12-15T23:18:39.517644-08:00","dependencies":[{"issue_id":"gt-17r","depends_on_id":"gt-f9x.4","type":"blocks","created_at":"2025-12-15T23:19:05.66301-08:00","created_by":"daemon"},{"issue_id":"gt-17r","depends_on_id":"gt-7ik","type":"blocks","created_at":"2025-12-17T15:44:41.945064-08:00","created_by":"daemon"}]}
|
||||||
{"id":"gt-17zr","title":"gt refinery start: doesn't actually start a session","description":"## Problem\n\n`gt refinery start gastown` reports success but doesn't start a tmux session.\n\n## Evidence\n\n```\n$ gt refinery start gastown\nStarting refinery for gastown...\n✓ Refinery started for gastown\n\n$ tmux list-sessions | grep refinery\n(nothing)\n\n$ gt refinery status gastown\nState: ○ stopped\n```\n\n## Expected\n\nShould start a tmux session (e.g., gt-gastown-refinery) with Claude processing the merge queue.\n\n## Related\n\n- gt-kcee: Witness commands also need implementation\n- The refinery 'start' may just be updating state.json without spawning a session","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-18T21:58:57.188389-08:00","updated_at":"2025-12-19T01:33:49.858934-08:00","closed_at":"2025-12-19T01:33:49.858934-08:00"}
|
{"id":"gt-17zr","title":"gt refinery start: doesn't actually start a session","description":"## Problem\n\n`gt refinery start gastown` reports success but doesn't start a tmux session.\n\n## Evidence\n\n```\n$ gt refinery start gastown\nStarting refinery for gastown...\n✓ Refinery started for gastown\n\n$ tmux list-sessions | grep refinery\n(nothing)\n\n$ gt refinery status gastown\nState: ○ stopped\n```\n\n## Expected\n\nShould start a tmux session (e.g., gt-gastown-refinery) with Claude processing the merge queue.\n\n## Related\n\n- gt-kcee: Witness commands also need implementation\n- The refinery 'start' may just be updating state.json without spawning a session","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-18T21:58:57.188389-08:00","updated_at":"2025-12-19T01:33:49.858934-08:00","closed_at":"2025-12-19T01:33:49.858934-08:00"}
|
||||||
{"id":"gt-1cuq","title":"Merge: gt-svi.1","description":"type: merge-request\nbranch: polecat/Max\ntarget: main\nsource_issue: gt-svi.1\nrig: gastown","status":"closed","priority":0,"issue_type":"task","created_at":"2025-12-18T20:15:31.738938-08:00","updated_at":"2025-12-18T20:15:49.759778-08:00","closed_at":"2025-12-18T20:15:49.759778-08:00"}
|
{"id":"gt-1cuq","title":"Merge: gt-svi.1","description":"type: merge-request\nbranch: polecat/Max\ntarget: main\nsource_issue: gt-svi.1\nrig: gastown","status":"closed","priority":0,"issue_type":"task","created_at":"2025-12-18T20:15:31.738938-08:00","updated_at":"2025-12-18T20:15:49.759778-08:00","closed_at":"2025-12-18T20:15:49.759778-08:00"}
|
||||||
@@ -181,6 +182,7 @@
|
|||||||
{"id":"gt-ebl","title":"CLI: names commands for polecat naming pool","description":"Polecat naming pool for auto-generated names.\n\n## Commands\n\n### gt names generate\n```\ngt names generate [--count N]\n```\nGenerate N names from pool.\n\n### gt names add\n```\ngt names add \u003cname\u003e...\n```\nAdd custom names to pool.\n\n### gt names list\n```\ngt names list\n```\nShow available and used names.\n\n### gt names reset\n```\ngt names reset [--keep-used]\n```\nReset pool to defaults.\n\n## Config File\n\u003crig\u003e/town/naming.json:\n```json\n{\n \"enabled\": true,\n \"auto_refill\": true,\n \"refill_threshold\": 5,\n \"pool\": {\n \"available\": [\"Toast\", \"Nux\", \"Capable\", ...],\n \"used\": [\"Alice\", \"Bob\"]\n }\n}\n```\n\n## Default Pool\nMad Max themed: Toast, Nux, Capable, Furiosa, Immortan, etc.\n\n## Integration\ngt polecat add calls naming pool if no name given:\n```go\nif name == \"\" {\n name, err = naming.Generate(rigPath)\n}\n```\n\n## New Package\ninternal/naming/\n├── pool.go # Pool management\n└── defaults.go # Default name lists\n\n## Acceptance Criteria\n- [ ] Auto-generate names on polecat add\n- [ ] Track used vs available\n- [ ] Auto-refill when low\n- [ ] Custom names addable","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-16T14:48:33.592129-08:00","updated_at":"2025-12-16T16:07:13.882465-08:00"}
|
{"id":"gt-ebl","title":"CLI: names commands for polecat naming pool","description":"Polecat naming pool for auto-generated names.\n\n## Commands\n\n### gt names generate\n```\ngt names generate [--count N]\n```\nGenerate N names from pool.\n\n### gt names add\n```\ngt names add \u003cname\u003e...\n```\nAdd custom names to pool.\n\n### gt names list\n```\ngt names list\n```\nShow available and used names.\n\n### gt names reset\n```\ngt names reset [--keep-used]\n```\nReset pool to defaults.\n\n## Config File\n\u003crig\u003e/town/naming.json:\n```json\n{\n \"enabled\": true,\n \"auto_refill\": true,\n \"refill_threshold\": 5,\n \"pool\": {\n \"available\": [\"Toast\", \"Nux\", \"Capable\", ...],\n \"used\": [\"Alice\", \"Bob\"]\n }\n}\n```\n\n## Default Pool\nMad Max themed: Toast, Nux, Capable, Furiosa, Immortan, etc.\n\n## Integration\ngt polecat add calls naming pool if no name given:\n```go\nif name == \"\" {\n name, err = naming.Generate(rigPath)\n}\n```\n\n## New Package\ninternal/naming/\n├── pool.go # Pool management\n└── defaults.go # Default name lists\n\n## Acceptance Criteria\n- [ ] Auto-generate names on polecat add\n- [ ] Track used vs available\n- [ ] Auto-refill when low\n- [ ] Custom names addable","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-16T14:48:33.592129-08:00","updated_at":"2025-12-16T16:07:13.882465-08:00"}
|
||||||
{"id":"gt-egu","title":"gt refinery attach: Attach to refinery session","description":"Add 'gt refinery attach [rig]' command to attach to refinery tmux session.\n\nMirrors 'gt mayor attach' pattern. If rig not specified, use current rig context.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:47:19.164342-08:00","updated_at":"2025-12-17T22:28:45.661097-08:00","closed_at":"2025-12-17T22:28:45.661097-08:00","dependencies":[{"issue_id":"gt-egu","depends_on_id":"gt-hw6","type":"blocks","created_at":"2025-12-17T22:22:47.578871-08:00","created_by":"daemon"}]}
|
{"id":"gt-egu","title":"gt refinery attach: Attach to refinery session","description":"Add 'gt refinery attach [rig]' command to attach to refinery tmux session.\n\nMirrors 'gt mayor attach' pattern. If rig not specified, use current rig context.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-17T21:47:19.164342-08:00","updated_at":"2025-12-17T22:28:45.661097-08:00","closed_at":"2025-12-17T22:28:45.661097-08:00","dependencies":[{"issue_id":"gt-egu","depends_on_id":"gt-hw6","type":"blocks","created_at":"2025-12-17T22:22:47.578871-08:00","created_by":"daemon"}]}
|
||||||
{"id":"gt-eqys","title":"gt spawn: pasted work assignment needs manual Enter to start","description":"## Problem\n\nAfter `gt spawn` pastes the work assignment into Claude, the session waits for Enter.\n\n## Current Behavior\n\n1. `gt spawn gastown/Rictus --issue gt-xxx`\n2. Session starts, work is pasted\n3. Claude shows 'Pasted text #1 +53 lines' but doesn't start\n4. Must manually send Enter or attach and press Enter\n\n## Expected\n\nThe spawn should send Enter after pasting to kick off the work.\n\n## Related\n\nSame debounce issue as tmux notifications (gt-vnp9).","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-18T21:54:14.111101-08:00","updated_at":"2025-12-19T12:01:32.74364-08:00","closed_at":"2025-12-19T12:01:32.74364-08:00"}
|
{"id":"gt-eqys","title":"gt spawn: pasted work assignment needs manual Enter to start","description":"## Problem\n\nAfter `gt spawn` pastes the work assignment into Claude, the session waits for Enter.\n\n## Current Behavior\n\n1. `gt spawn gastown/Rictus --issue gt-xxx`\n2. Session starts, work is pasted\n3. Claude shows 'Pasted text #1 +53 lines' but doesn't start\n4. Must manually send Enter or attach and press Enter\n\n## Expected\n\nThe spawn should send Enter after pasting to kick off the work.\n\n## Related\n\nSame debounce issue as tmux notifications (gt-vnp9).","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-18T21:54:14.111101-08:00","updated_at":"2025-12-19T12:01:32.74364-08:00","closed_at":"2025-12-19T12:01:32.74364-08:00"}
|
||||||
|
{"id":"gt-er0u","title":"Work on ga-yp3: Polecat inbox system for reliable work as...","description":"Work on ga-yp3: Polecat inbox system for reliable work assignment. This is P1 priority. See bd show ga-yp3 for full design spec.","status":"in_progress","priority":2,"issue_type":"task","assignee":"gastown/polecat-02","created_at":"2025-12-19T21:57:34.473056-08:00","updated_at":"2025-12-19T21:57:34.540384-08:00"}
|
||||||
{"id":"gt-eu9","title":"Witness session cycling and handoff","description":"Add session cycling and handoff protocol to Witness CLAUDE.md template.\n\n## Session Cycling Protocol\n\n```markdown\n## Session Cycling\n\nYour context will fill over long swarms. Proactively cycle when:\n- Running for many hours\n- Losing track of which workers you've checked\n- Responses getting slower\n- About to start complex operation\n\n### Handoff Protocol\n\n1. **Capture current state**:\n```bash\ntown list . # Worker states\ntown all beads # Pending verifications \ntown inbox # Unprocessed messages\n```\n\n2. **Compose handoff note**:\n```\n[HANDOFF_TYPE]: witness_cycle\n[TIMESTAMP]: \u003cnow\u003e\n[RIG]: \u003crig\u003e\n\n## Active Workers\n\u003clist workers and status\u003e\n\n## Pending Verifications\n\u003cworkers signaled done but not verified\u003e\n\n## Recent Actions\n\u003clast 3-5 actions\u003e\n\n## Warnings/Notes\n\u003canything next session should know\u003e\n\n## Next Steps\n\u003cwhat should happen next\u003e\n```\n\n3. **Send handoff**:\n```bash\ntown mail send \u003crig\u003e/witness -s \"Session Handoff\" -m \"\u003cnote\u003e\"\n```\n\n4. **Exit cleanly**: End session, daemon spawns fresh one.\n\n### On Fresh Session Start\n\n1. Check for handoff: `town inbox | grep \"Session Handoff\"`\n2. If found, read it and resume from handoff state\n3. If not found, do full status check\n```\n\n## Implementation\n\nAdd to WITNESS_CLAUDE.md template.","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-15T19:48:55.484911-08:00","updated_at":"2025-12-15T20:47:30.768506-08:00","dependencies":[{"issue_id":"gt-eu9","depends_on_id":"gt-82y","type":"blocks","created_at":"2025-12-15T19:49:05.846443-08:00","created_by":"daemon"}]}
|
{"id":"gt-eu9","title":"Witness session cycling and handoff","description":"Add session cycling and handoff protocol to Witness CLAUDE.md template.\n\n## Session Cycling Protocol\n\n```markdown\n## Session Cycling\n\nYour context will fill over long swarms. Proactively cycle when:\n- Running for many hours\n- Losing track of which workers you've checked\n- Responses getting slower\n- About to start complex operation\n\n### Handoff Protocol\n\n1. **Capture current state**:\n```bash\ntown list . # Worker states\ntown all beads # Pending verifications \ntown inbox # Unprocessed messages\n```\n\n2. **Compose handoff note**:\n```\n[HANDOFF_TYPE]: witness_cycle\n[TIMESTAMP]: \u003cnow\u003e\n[RIG]: \u003crig\u003e\n\n## Active Workers\n\u003clist workers and status\u003e\n\n## Pending Verifications\n\u003cworkers signaled done but not verified\u003e\n\n## Recent Actions\n\u003clast 3-5 actions\u003e\n\n## Warnings/Notes\n\u003canything next session should know\u003e\n\n## Next Steps\n\u003cwhat should happen next\u003e\n```\n\n3. **Send handoff**:\n```bash\ntown mail send \u003crig\u003e/witness -s \"Session Handoff\" -m \"\u003cnote\u003e\"\n```\n\n4. **Exit cleanly**: End session, daemon spawns fresh one.\n\n### On Fresh Session Start\n\n1. Check for handoff: `town inbox | grep \"Session Handoff\"`\n2. If found, read it and resume from handoff state\n3. If not found, do full status check\n```\n\n## Implementation\n\nAdd to WITNESS_CLAUDE.md template.","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-15T19:48:55.484911-08:00","updated_at":"2025-12-15T20:47:30.768506-08:00","dependencies":[{"issue_id":"gt-eu9","depends_on_id":"gt-82y","type":"blocks","created_at":"2025-12-15T19:49:05.846443-08:00","created_by":"daemon"}]}
|
||||||
{"id":"gt-f8v","title":"Witness pre-kill verification protocol","description":"Add pre-kill verification protocol to Witness CLAUDE.md template.\n\n## Protocol for Witness Prompting\n\n```markdown\n## Pre-Kill Verification Protocol\n\nBefore killing any worker session, verify workspace is clean.\n\n### Verification Steps\n\nWhen a worker signals done:\n\n1. **Capture worker state**:\n```bash\ntown capture \u003cpolecat\u003e \"git status \u0026\u0026 git stash list \u0026\u0026 bd sync --status\"\n```\n\n2. **Assess the output** (use your judgment):\n- Is working tree clean?\n- Is stash list empty?\n- Is beads synced?\n\n3. **Decision**:\n- **CLEAN**: Proceed to kill session\n- **DIRTY**: Send nudge with specific issues\n\n### Nudge Templates\n\n**Uncommitted Changes**:\n```\ntown inject \u003cpolecat\u003e \"WITNESS CHECK: Uncommitted changes found. Please commit or discard: \u003cfiles\u003e. Signal done when clean.\"\n```\n\n**Beads Not Synced**:\n```\ntown inject \u003cpolecat\u003e \"WITNESS CHECK: Beads not synced. Run 'bd sync' then commit. Signal done when complete.\"\n```\n\n### Kill Sequence\n\nOnly after verification passes:\n```bash\ntown kill \u003cpolecat\u003e\ntown sleep \u003cpolecat\u003e\n```\n\n### Escalation\n\nIf worker fails verification 3+ times:\n```bash\ntown mail send mayor/ -s \"Escalation: \u003cpolecat\u003e stuck\" -m \"Cannot complete cleanup after 3 attempts. Issues: \u003clist\u003e.\"\n```\n```\n\n## Implementation\n\nAdd to WITNESS_CLAUDE.md template.","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-15T19:48:54.065679-08:00","updated_at":"2025-12-15T20:47:30.415244-08:00","dependencies":[{"issue_id":"gt-f8v","depends_on_id":"gt-82y","type":"blocks","created_at":"2025-12-15T19:49:05.763378-08:00","created_by":"daemon"}]}
|
{"id":"gt-f8v","title":"Witness pre-kill verification protocol","description":"Add pre-kill verification protocol to Witness CLAUDE.md template.\n\n## Protocol for Witness Prompting\n\n```markdown\n## Pre-Kill Verification Protocol\n\nBefore killing any worker session, verify workspace is clean.\n\n### Verification Steps\n\nWhen a worker signals done:\n\n1. **Capture worker state**:\n```bash\ntown capture \u003cpolecat\u003e \"git status \u0026\u0026 git stash list \u0026\u0026 bd sync --status\"\n```\n\n2. **Assess the output** (use your judgment):\n- Is working tree clean?\n- Is stash list empty?\n- Is beads synced?\n\n3. **Decision**:\n- **CLEAN**: Proceed to kill session\n- **DIRTY**: Send nudge with specific issues\n\n### Nudge Templates\n\n**Uncommitted Changes**:\n```\ntown inject \u003cpolecat\u003e \"WITNESS CHECK: Uncommitted changes found. Please commit or discard: \u003cfiles\u003e. Signal done when clean.\"\n```\n\n**Beads Not Synced**:\n```\ntown inject \u003cpolecat\u003e \"WITNESS CHECK: Beads not synced. Run 'bd sync' then commit. Signal done when complete.\"\n```\n\n### Kill Sequence\n\nOnly after verification passes:\n```bash\ntown kill \u003cpolecat\u003e\ntown sleep \u003cpolecat\u003e\n```\n\n### Escalation\n\nIf worker fails verification 3+ times:\n```bash\ntown mail send mayor/ -s \"Escalation: \u003cpolecat\u003e stuck\" -m \"Cannot complete cleanup after 3 attempts. Issues: \u003clist\u003e.\"\n```\n```\n\n## Implementation\n\nAdd to WITNESS_CLAUDE.md template.","status":"open","priority":1,"issue_type":"task","created_at":"2025-12-15T19:48:54.065679-08:00","updated_at":"2025-12-15T20:47:30.415244-08:00","dependencies":[{"issue_id":"gt-f8v","depends_on_id":"gt-82y","type":"blocks","created_at":"2025-12-15T19:49:05.763378-08:00","created_by":"daemon"}]}
|
||||||
{"id":"gt-f9x","title":"Town \u0026 Rig Management: install, doctor, federation","description":"Reify the Gas Town installation as a first-class concept.\n\n## Goals\n- Installable: gt install [path] creates complete installation\n- Diagnosable: gt doctor checks and fixes issues\n- Federable: Clone town to VMs with central control\n\n## Architecture Reference\n\nSee docs/architecture.md for full design, especially:\n- Directory structure (Town Level / Rig Level sections)\n- Configuration (town.json, rigs.json schemas)\n- Key design decisions (visible config dir, decentralized agents)","status":"open","priority":1,"issue_type":"epic","created_at":"2025-12-15T16:36:37.344283-08:00","updated_at":"2025-12-15T21:15:13.120038-08:00","dependencies":[{"issue_id":"gt-f9x","depends_on_id":"gt-u1j.1","type":"blocks","created_at":"2025-12-15T16:37:32.3363-08:00","created_by":"daemon"}]}
|
{"id":"gt-f9x","title":"Town \u0026 Rig Management: install, doctor, federation","description":"Reify the Gas Town installation as a first-class concept.\n\n## Goals\n- Installable: gt install [path] creates complete installation\n- Diagnosable: gt doctor checks and fixes issues\n- Federable: Clone town to VMs with central control\n\n## Architecture Reference\n\nSee docs/architecture.md for full design, especially:\n- Directory structure (Town Level / Rig Level sections)\n- Configuration (town.json, rigs.json schemas)\n- Key design decisions (visible config dir, decentralized agents)","status":"open","priority":1,"issue_type":"epic","created_at":"2025-12-15T16:36:37.344283-08:00","updated_at":"2025-12-15T21:15:13.120038-08:00","dependencies":[{"issue_id":"gt-f9x","depends_on_id":"gt-u1j.1","type":"blocks","created_at":"2025-12-15T16:37:32.3363-08:00","created_by":"daemon"}]}
|
||||||
|
|||||||
310
internal/cmd/namepool.go
Normal file
310
internal/cmd/namepool.go
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/steveyegge/gastown/internal/config"
|
||||||
|
"github.com/steveyegge/gastown/internal/polecat"
|
||||||
|
"github.com/steveyegge/gastown/internal/workspace"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
namepoolListFlag bool
|
||||||
|
namepoolThemeFlag string
|
||||||
|
)
|
||||||
|
|
||||||
|
var namepoolCmd = &cobra.Command{
|
||||||
|
Use: "namepool",
|
||||||
|
Short: "Manage polecat name pools",
|
||||||
|
Long: `Manage themed name pools for polecats in Gas Town.
|
||||||
|
|
||||||
|
By default, polecats get themed names from the Mad Max universe
|
||||||
|
(furiosa, nux, slit, etc.). You can change the theme or add custom names.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
gt namepool # Show current pool status
|
||||||
|
gt namepool --list # List available themes
|
||||||
|
gt namepool themes # Show theme names
|
||||||
|
gt namepool set minerals # Set theme to 'minerals'
|
||||||
|
gt namepool add ember # Add custom name to pool
|
||||||
|
gt namepool reset # Reset pool state`,
|
||||||
|
RunE: runNamepool,
|
||||||
|
}
|
||||||
|
|
||||||
|
var namepoolThemesCmd = &cobra.Command{
|
||||||
|
Use: "themes [theme]",
|
||||||
|
Short: "List available themes and their names",
|
||||||
|
RunE: runNamepoolThemes,
|
||||||
|
}
|
||||||
|
|
||||||
|
var namepoolSetCmd = &cobra.Command{
|
||||||
|
Use: "set <theme>",
|
||||||
|
Short: "Set the namepool theme for this rig",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runNamepoolSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
var namepoolAddCmd = &cobra.Command{
|
||||||
|
Use: "add <name>",
|
||||||
|
Short: "Add a custom name to the pool",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runNamepoolAdd,
|
||||||
|
}
|
||||||
|
|
||||||
|
var namepoolResetCmd = &cobra.Command{
|
||||||
|
Use: "reset",
|
||||||
|
Short: "Reset the pool state (release all names)",
|
||||||
|
RunE: runNamepoolReset,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(namepoolCmd)
|
||||||
|
namepoolCmd.AddCommand(namepoolThemesCmd)
|
||||||
|
namepoolCmd.AddCommand(namepoolSetCmd)
|
||||||
|
namepoolCmd.AddCommand(namepoolAddCmd)
|
||||||
|
namepoolCmd.AddCommand(namepoolResetCmd)
|
||||||
|
namepoolCmd.Flags().BoolVarP(&namepoolListFlag, "list", "l", false, "List available themes")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runNamepool(cmd *cobra.Command, args []string) error {
|
||||||
|
// List themes mode
|
||||||
|
if namepoolListFlag {
|
||||||
|
return runNamepoolThemes(cmd, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show current pool status
|
||||||
|
rigName, rigPath := detectCurrentRigWithPath()
|
||||||
|
if rigName == "" {
|
||||||
|
return fmt.Errorf("not in a rig directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load pool
|
||||||
|
pool := polecat.NewNamePool(rigPath, rigName)
|
||||||
|
if err := pool.Load(); err != nil {
|
||||||
|
// Pool doesn't exist yet, show defaults
|
||||||
|
fmt.Printf("Rig: %s\n", rigName)
|
||||||
|
fmt.Printf("Theme: %s (default)\n", polecat.DefaultTheme)
|
||||||
|
fmt.Printf("Active polecats: 0\n")
|
||||||
|
fmt.Printf("Max pool size: %d\n", polecat.DefaultPoolSize)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show pool status
|
||||||
|
fmt.Printf("Rig: %s\n", rigName)
|
||||||
|
fmt.Printf("Theme: %s\n", pool.GetTheme())
|
||||||
|
fmt.Printf("Active polecats: %d\n", pool.ActiveCount())
|
||||||
|
|
||||||
|
activeNames := pool.ActiveNames()
|
||||||
|
if len(activeNames) > 0 {
|
||||||
|
fmt.Printf("In use: %s\n", strings.Join(activeNames, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if configured
|
||||||
|
configPath := filepath.Join(rigPath, ".gastown", "config.json")
|
||||||
|
if cfg, err := config.LoadRigConfig(configPath); err == nil && cfg.Namepool != nil {
|
||||||
|
fmt.Printf("(configured in .gastown/config.json)\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runNamepoolThemes(cmd *cobra.Command, args []string) error {
|
||||||
|
themes := polecat.ListThemes()
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
// List all themes
|
||||||
|
fmt.Println("Available themes:")
|
||||||
|
for _, theme := range themes {
|
||||||
|
names, _ := polecat.GetThemeNames(theme)
|
||||||
|
fmt.Printf("\n %s (%d names):\n", theme, len(names))
|
||||||
|
// Show first 10 names
|
||||||
|
preview := names
|
||||||
|
if len(preview) > 10 {
|
||||||
|
preview = preview[:10]
|
||||||
|
}
|
||||||
|
fmt.Printf(" %s...\n", strings.Join(preview, ", "))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show specific theme names
|
||||||
|
theme := args[0]
|
||||||
|
names, err := polecat.GetThemeNames(theme)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unknown theme: %s (available: %s)", theme, strings.Join(themes, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Theme: %s (%d names)\n\n", theme, len(names))
|
||||||
|
for i, name := range names {
|
||||||
|
if i > 0 && i%5 == 0 {
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
fmt.Printf(" %-12s", name)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runNamepoolSet(cmd *cobra.Command, args []string) error {
|
||||||
|
theme := args[0]
|
||||||
|
|
||||||
|
// Validate theme
|
||||||
|
themes := polecat.ListThemes()
|
||||||
|
valid := false
|
||||||
|
for _, t := range themes {
|
||||||
|
if t == theme {
|
||||||
|
valid = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
return fmt.Errorf("unknown theme: %s (available: %s)", theme, strings.Join(themes, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get rig
|
||||||
|
rigName, rigPath := detectCurrentRigWithPath()
|
||||||
|
if rigName == "" {
|
||||||
|
return fmt.Errorf("not in a rig directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update pool
|
||||||
|
pool := polecat.NewNamePool(rigPath, rigName)
|
||||||
|
if err := pool.Load(); err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("loading pool: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pool.SetTheme(theme); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pool.Save(); err != nil {
|
||||||
|
return fmt.Errorf("saving pool: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also save to rig config
|
||||||
|
if err := saveRigNamepoolConfig(rigPath, theme, nil); err != nil {
|
||||||
|
return fmt.Errorf("saving config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Theme '%s' set for rig '%s'\n", theme, rigName)
|
||||||
|
fmt.Printf("New polecats will use names from this theme.\n")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runNamepoolAdd(cmd *cobra.Command, args []string) error {
|
||||||
|
name := args[0]
|
||||||
|
|
||||||
|
rigName, rigPath := detectCurrentRigWithPath()
|
||||||
|
if rigName == "" {
|
||||||
|
return fmt.Errorf("not in a rig directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load pool
|
||||||
|
pool := polecat.NewNamePool(rigPath, rigName)
|
||||||
|
if err := pool.Load(); err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("loading pool: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pool.AddCustomName(name)
|
||||||
|
|
||||||
|
if err := pool.Save(); err != nil {
|
||||||
|
return fmt.Errorf("saving pool: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Added '%s' to the name pool\n", name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runNamepoolReset(cmd *cobra.Command, args []string) error {
|
||||||
|
rigName, rigPath := detectCurrentRigWithPath()
|
||||||
|
if rigName == "" {
|
||||||
|
return fmt.Errorf("not in a rig directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load pool
|
||||||
|
pool := polecat.NewNamePool(rigPath, rigName)
|
||||||
|
if err := pool.Load(); err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("loading pool: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pool.Reset()
|
||||||
|
|
||||||
|
if err := pool.Save(); err != nil {
|
||||||
|
return fmt.Errorf("saving pool: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Pool reset for rig '%s'\n", rigName)
|
||||||
|
fmt.Printf("All names released and available for reuse.\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectCurrentRigWithPath determines the rig name and path from cwd.
|
||||||
|
func detectCurrentRigWithPath() (string, string) {
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
townRoot, err := workspace.FindFromCwd()
|
||||||
|
if err != nil || townRoot == "" {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get path relative to town root
|
||||||
|
rel, err := filepath.Rel(townRoot, cwd)
|
||||||
|
if err != nil {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract first path component (rig name)
|
||||||
|
parts := strings.Split(rel, string(filepath.Separator))
|
||||||
|
if len(parts) > 0 && parts[0] != "." && parts[0] != "mayor" && parts[0] != "deacon" {
|
||||||
|
return parts[0], filepath.Join(townRoot, parts[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveRigNamepoolConfig saves the namepool config to rig config.
|
||||||
|
func saveRigNamepoolConfig(rigPath, theme string, customNames []string) error {
|
||||||
|
configPath := filepath.Join(rigPath, ".gastown", "config.json")
|
||||||
|
|
||||||
|
// Load existing config or create new
|
||||||
|
var cfg *config.RigConfig
|
||||||
|
cfg, err := config.LoadRigConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
// Create new config if not found
|
||||||
|
if os.IsNotExist(err) || strings.Contains(err.Error(), "not found") {
|
||||||
|
cfg = &config.RigConfig{
|
||||||
|
Type: "rig",
|
||||||
|
Version: config.CurrentRigConfigVersion,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("loading config: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set namepool
|
||||||
|
cfg.Namepool = &config.NamepoolConfig{
|
||||||
|
Style: theme,
|
||||||
|
Names: customNames,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save
|
||||||
|
if err := config.SaveRigConfig(configPath, cfg); err != nil {
|
||||||
|
return fmt.Errorf("saving config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -53,6 +53,7 @@ type RigConfig struct {
|
|||||||
Version int `json:"version"` // schema version
|
Version int `json:"version"` // schema version
|
||||||
MergeQueue *MergeQueueConfig `json:"merge_queue,omitempty"` // merge queue settings
|
MergeQueue *MergeQueueConfig `json:"merge_queue,omitempty"` // merge queue settings
|
||||||
Theme *ThemeConfig `json:"theme,omitempty"` // tmux theme settings
|
Theme *ThemeConfig `json:"theme,omitempty"` // tmux theme settings
|
||||||
|
Namepool *NamepoolConfig `json:"namepool,omitempty"` // polecat name pool settings
|
||||||
}
|
}
|
||||||
|
|
||||||
// ThemeConfig represents tmux theme settings for a rig.
|
// ThemeConfig represents tmux theme settings for a rig.
|
||||||
@@ -125,3 +126,26 @@ func DefaultMergeQueueConfig() *MergeQueueConfig {
|
|||||||
MaxConcurrent: 1,
|
MaxConcurrent: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NamepoolConfig represents namepool settings for themed polecat names.
|
||||||
|
type NamepoolConfig struct {
|
||||||
|
// Style picks from a built-in theme (e.g., "mad-max", "minerals", "wasteland").
|
||||||
|
// If empty, defaults to "mad-max".
|
||||||
|
Style string `json:"style,omitempty"`
|
||||||
|
|
||||||
|
// Names is a custom list of names to use instead of a built-in theme.
|
||||||
|
// If provided, overrides the Style setting.
|
||||||
|
Names []string `json:"names,omitempty"`
|
||||||
|
|
||||||
|
// MaxBeforeNumbering is when to start appending numbers.
|
||||||
|
// Default is 50. After this many polecats, names become name-01, name-02, etc.
|
||||||
|
MaxBeforeNumbering int `json:"max_before_numbering,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultNamepoolConfig returns a NamepoolConfig with sensible defaults.
|
||||||
|
func DefaultNamepoolConfig() *NamepoolConfig {
|
||||||
|
return &NamepoolConfig{
|
||||||
|
Style: "mad-max",
|
||||||
|
MaxBeforeNumbering: 50,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/steveyegge/gastown/internal/beads"
|
"github.com/steveyegge/gastown/internal/beads"
|
||||||
|
"github.com/steveyegge/gastown/internal/config"
|
||||||
"github.com/steveyegge/gastown/internal/git"
|
"github.com/steveyegge/gastown/internal/git"
|
||||||
"github.com/steveyegge/gastown/internal/rig"
|
"github.com/steveyegge/gastown/internal/rig"
|
||||||
)
|
)
|
||||||
@@ -32,8 +33,24 @@ func NewManager(r *rig.Rig, g *git.Git) *Manager {
|
|||||||
// Use the mayor's rig directory for beads operations (rig-level beads)
|
// Use the mayor's rig directory for beads operations (rig-level beads)
|
||||||
mayorRigPath := filepath.Join(r.Path, "mayor", "rig")
|
mayorRigPath := filepath.Join(r.Path, "mayor", "rig")
|
||||||
|
|
||||||
// Initialize name pool
|
// Try to load rig config for namepool settings
|
||||||
pool := NewNamePool(r.Path, r.Name)
|
rigConfigPath := filepath.Join(r.Path, ".gastown", "config.json")
|
||||||
|
var pool *NamePool
|
||||||
|
|
||||||
|
rigConfig, err := config.LoadRigConfig(rigConfigPath)
|
||||||
|
if err == nil && rigConfig.Namepool != nil {
|
||||||
|
// Use configured namepool settings
|
||||||
|
pool = NewNamePoolWithConfig(
|
||||||
|
r.Path,
|
||||||
|
r.Name,
|
||||||
|
rigConfig.Namepool.Style,
|
||||||
|
rigConfig.Namepool.Names,
|
||||||
|
rigConfig.Namepool.MaxBeforeNumbering,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Use defaults
|
||||||
|
pool = NewNamePool(r.Path, r.Name)
|
||||||
|
}
|
||||||
_ = pool.Load() // Load existing state, ignore errors for new rigs
|
_ = pool.Load() // Load existing state, ignore errors for new rigs
|
||||||
|
|
||||||
return &Manager{
|
return &Manager{
|
||||||
|
|||||||
@@ -10,15 +10,55 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// PoolSize is the number of reusable names in the pool.
|
// DefaultPoolSize is the number of reusable names in the pool.
|
||||||
PoolSize = 50
|
DefaultPoolSize = 50
|
||||||
|
|
||||||
// NamePrefix is the prefix for pooled polecat names.
|
// DefaultTheme is the default theme for new rigs.
|
||||||
NamePrefix = "polecat-"
|
DefaultTheme = "mad-max"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Built-in themes with themed polecat names.
|
||||||
|
var BuiltinThemes = map[string][]string{
|
||||||
|
"mad-max": {
|
||||||
|
"furiosa", "nux", "slit", "rictus", "dementus",
|
||||||
|
"capable", "toast", "dag", "cheedo", "valkyrie",
|
||||||
|
"keeper", "morsov", "ace", "warboy", "imperator",
|
||||||
|
"organic", "coma", "splendid", "angharad", "max",
|
||||||
|
"immortan", "bullet", "toecutter", "goose", "nightrider",
|
||||||
|
"glory", "scrotus", "chumbucket", "corpus", "dinki",
|
||||||
|
"prime", "vuvalini", "rockryder", "wretched", "buzzard",
|
||||||
|
"gastown", "bullet-farmer", "citadel", "wasteland", "fury",
|
||||||
|
"road-warrior", "interceptor", "blackfinger", "wraith", "witness",
|
||||||
|
"chrome", "shiny", "mediocre", "guzzoline", "aqua-cola",
|
||||||
|
},
|
||||||
|
"minerals": {
|
||||||
|
"obsidian", "quartz", "jasper", "onyx", "opal",
|
||||||
|
"topaz", "garnet", "ruby", "amber", "jade",
|
||||||
|
"pearl", "flint", "granite", "basalt", "marble",
|
||||||
|
"shale", "slate", "pyrite", "mica", "agate",
|
||||||
|
"malachite", "turquoise", "lapis", "emerald", "sapphire",
|
||||||
|
"diamond", "amethyst", "citrine", "zircon", "peridot",
|
||||||
|
"coral", "jet", "moonstone", "sunstone", "bloodstone",
|
||||||
|
"rhodonite", "sodalite", "hematite", "magnetite", "calcite",
|
||||||
|
"fluorite", "selenite", "kyanite", "labradorite", "amazonite",
|
||||||
|
"chalcedony", "carnelian", "aventurine", "chrysoprase", "heliodor",
|
||||||
|
},
|
||||||
|
"wasteland": {
|
||||||
|
"rust", "chrome", "nitro", "guzzle", "witness",
|
||||||
|
"shiny", "fury", "thunder", "dust", "scavenger",
|
||||||
|
"radrat", "ghoul", "mutant", "raider", "vault",
|
||||||
|
"pipboy", "nuka", "brahmin", "deathclaw", "mirelurk",
|
||||||
|
"synth", "institute", "enclave", "brotherhood", "minuteman",
|
||||||
|
"railroad", "atom", "crater", "foundation", "refuge",
|
||||||
|
"settler", "wanderer", "courier", "lone", "chosen",
|
||||||
|
"tribal", "khan", "legion", "ncr", "ranger",
|
||||||
|
"overseer", "sentinel", "paladin", "scribe", "initiate",
|
||||||
|
"elder", "lancer", "knight", "squire", "proctor",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// NamePool manages a bounded pool of reusable polecat names.
|
// NamePool manages a bounded pool of reusable polecat names.
|
||||||
// Names in the pool are polecat-01 through polecat-50.
|
// Names are drawn from a themed pool (mad-max by default).
|
||||||
// When the pool is exhausted, overflow names use rigname-N format.
|
// When the pool is exhausted, overflow names use rigname-N format.
|
||||||
type NamePool struct {
|
type NamePool struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
@@ -26,14 +66,23 @@ type NamePool struct {
|
|||||||
// RigName is the rig this pool belongs to.
|
// RigName is the rig this pool belongs to.
|
||||||
RigName string `json:"rig_name"`
|
RigName string `json:"rig_name"`
|
||||||
|
|
||||||
// InUse tracks which pool indices are currently in use.
|
// Theme is the current theme name (e.g., "mad-max", "minerals").
|
||||||
// Key is the pool index (1-50), value is true if in use.
|
Theme string `json:"theme"`
|
||||||
InUse map[int]bool `json:"in_use"`
|
|
||||||
|
// CustomNames allows overriding the built-in theme names.
|
||||||
|
CustomNames []string `json:"custom_names,omitempty"`
|
||||||
|
|
||||||
|
// InUse tracks which pool names are currently in use.
|
||||||
|
// Key is the name itself, value is true if in use.
|
||||||
|
InUse map[string]bool `json:"in_use"`
|
||||||
|
|
||||||
// OverflowNext is the next overflow sequence number.
|
// OverflowNext is the next overflow sequence number.
|
||||||
// Starts at PoolSize+1 (51) and increments.
|
// Starts at MaxSize+1 and increments.
|
||||||
OverflowNext int `json:"overflow_next"`
|
OverflowNext int `json:"overflow_next"`
|
||||||
|
|
||||||
|
// MaxSize is the maximum number of themed names before overflow.
|
||||||
|
MaxSize int `json:"max_size"`
|
||||||
|
|
||||||
// stateFile is the path to persist pool state.
|
// stateFile is the path to persist pool state.
|
||||||
stateFile string
|
stateFile string
|
||||||
}
|
}
|
||||||
@@ -42,12 +91,50 @@ type NamePool struct {
|
|||||||
func NewNamePool(rigPath, rigName string) *NamePool {
|
func NewNamePool(rigPath, rigName string) *NamePool {
|
||||||
return &NamePool{
|
return &NamePool{
|
||||||
RigName: rigName,
|
RigName: rigName,
|
||||||
InUse: make(map[int]bool),
|
Theme: DefaultTheme,
|
||||||
OverflowNext: PoolSize + 1,
|
InUse: make(map[string]bool),
|
||||||
|
OverflowNext: DefaultPoolSize + 1,
|
||||||
|
MaxSize: DefaultPoolSize,
|
||||||
stateFile: filepath.Join(rigPath, ".gastown", "namepool.json"),
|
stateFile: filepath.Join(rigPath, ".gastown", "namepool.json"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewNamePoolWithConfig creates a name pool with specific configuration.
|
||||||
|
func NewNamePoolWithConfig(rigPath, rigName, theme string, customNames []string, maxSize int) *NamePool {
|
||||||
|
if theme == "" {
|
||||||
|
theme = DefaultTheme
|
||||||
|
}
|
||||||
|
if maxSize <= 0 {
|
||||||
|
maxSize = DefaultPoolSize
|
||||||
|
}
|
||||||
|
|
||||||
|
return &NamePool{
|
||||||
|
RigName: rigName,
|
||||||
|
Theme: theme,
|
||||||
|
CustomNames: customNames,
|
||||||
|
InUse: make(map[string]bool),
|
||||||
|
OverflowNext: maxSize + 1,
|
||||||
|
MaxSize: maxSize,
|
||||||
|
stateFile: filepath.Join(rigPath, ".gastown", "namepool.json"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getNames returns the list of names to use for the pool.
|
||||||
|
func (p *NamePool) getNames() []string {
|
||||||
|
// Custom names take precedence
|
||||||
|
if len(p.CustomNames) > 0 {
|
||||||
|
return p.CustomNames
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up built-in theme
|
||||||
|
if names, ok := BuiltinThemes[p.Theme]; ok {
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to default theme
|
||||||
|
return BuiltinThemes[DefaultTheme]
|
||||||
|
}
|
||||||
|
|
||||||
// Load loads the pool state from disk.
|
// Load loads the pool state from disk.
|
||||||
func (p *NamePool) Load() error {
|
func (p *NamePool) Load() error {
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
@@ -57,8 +144,8 @@ func (p *NamePool) Load() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
// Initialize with empty state
|
// Initialize with empty state
|
||||||
p.InUse = make(map[int]bool)
|
p.InUse = make(map[string]bool)
|
||||||
p.OverflowNext = PoolSize + 1
|
p.OverflowNext = p.MaxSize + 1
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@@ -69,13 +156,24 @@ func (p *NamePool) Load() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Preserve the theme and custom names if already set
|
||||||
|
if p.Theme == "" && loaded.Theme != "" {
|
||||||
|
p.Theme = loaded.Theme
|
||||||
|
}
|
||||||
|
if len(p.CustomNames) == 0 && len(loaded.CustomNames) > 0 {
|
||||||
|
p.CustomNames = loaded.CustomNames
|
||||||
|
}
|
||||||
|
|
||||||
p.InUse = loaded.InUse
|
p.InUse = loaded.InUse
|
||||||
if p.InUse == nil {
|
if p.InUse == nil {
|
||||||
p.InUse = make(map[int]bool)
|
p.InUse = make(map[string]bool)
|
||||||
}
|
}
|
||||||
p.OverflowNext = loaded.OverflowNext
|
p.OverflowNext = loaded.OverflowNext
|
||||||
if p.OverflowNext < PoolSize+1 {
|
if p.OverflowNext < p.MaxSize+1 {
|
||||||
p.OverflowNext = PoolSize + 1
|
p.OverflowNext = p.MaxSize + 1
|
||||||
|
}
|
||||||
|
if loaded.MaxSize > 0 {
|
||||||
|
p.MaxSize = loaded.MaxSize
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -100,17 +198,20 @@ func (p *NamePool) Save() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Allocate returns a name from the pool.
|
// Allocate returns a name from the pool.
|
||||||
// It prefers lower-numbered pool slots, and falls back to overflow names
|
// It prefers names in order from the theme list, and falls back to overflow names
|
||||||
// when the pool is exhausted.
|
// when the pool is exhausted.
|
||||||
func (p *NamePool) Allocate() (string, error) {
|
func (p *NamePool) Allocate() (string, error) {
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
// Try to find first available slot in pool (prefer low numbers)
|
names := p.getNames()
|
||||||
for i := 1; i <= PoolSize; i++ {
|
|
||||||
if !p.InUse[i] {
|
// Try to find first available name from the theme
|
||||||
p.InUse[i] = true
|
for i := 0; i < len(names) && i < p.MaxSize; i++ {
|
||||||
return p.formatPoolName(i), nil
|
name := names[i]
|
||||||
|
if !p.InUse[name] {
|
||||||
|
p.InUse[name] = true
|
||||||
|
return name, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,17 +227,27 @@ func (p *NamePool) Release(name string) {
|
|||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
idx := p.parsePoolIndex(name)
|
// Check if it's a themed name
|
||||||
if idx > 0 && idx <= PoolSize {
|
if p.isThemedName(name) {
|
||||||
delete(p.InUse, idx)
|
delete(p.InUse, name)
|
||||||
}
|
}
|
||||||
// Overflow names are not reusable, so we don't track them
|
// Overflow names are not reusable, so we don't track them
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsPoolName returns true if the name is a pool name (polecat-NN format).
|
// isThemedName checks if a name is in the theme pool.
|
||||||
|
func (p *NamePool) isThemedName(name string) bool {
|
||||||
|
names := p.getNames()
|
||||||
|
for _, n := range names {
|
||||||
|
if n == name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPoolName returns true if the name is a pool name (themed or numbered).
|
||||||
func (p *NamePool) IsPoolName(name string) bool {
|
func (p *NamePool) IsPoolName(name string) bool {
|
||||||
idx := p.parsePoolIndex(name)
|
return p.isThemedName(name)
|
||||||
return idx > 0 && idx <= PoolSize
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActiveCount returns the number of names currently in use from the pool.
|
// ActiveCount returns the number of names currently in use from the pool.
|
||||||
@@ -152,8 +263,8 @@ func (p *NamePool) ActiveNames() []string {
|
|||||||
defer p.mu.RUnlock()
|
defer p.mu.RUnlock()
|
||||||
|
|
||||||
var names []string
|
var names []string
|
||||||
for idx := range p.InUse {
|
for name := range p.InUse {
|
||||||
names = append(names, p.formatPoolName(idx))
|
names = append(names, name)
|
||||||
}
|
}
|
||||||
sort.Strings(names)
|
sort.Strings(names)
|
||||||
return names
|
return names
|
||||||
@@ -164,9 +275,8 @@ func (p *NamePool) MarkInUse(name string) {
|
|||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
idx := p.parsePoolIndex(name)
|
if p.isThemedName(name) {
|
||||||
if idx > 0 && idx <= PoolSize {
|
p.InUse[name] = true
|
||||||
p.InUse[idx] = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,41 +287,93 @@ func (p *NamePool) Reconcile(existingPolecats []string) {
|
|||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
// Clear current state
|
// Clear current state
|
||||||
p.InUse = make(map[int]bool)
|
p.InUse = make(map[string]bool)
|
||||||
|
|
||||||
// Mark all existing polecats as in use
|
// Mark all existing polecats as in use
|
||||||
for _, name := range existingPolecats {
|
for _, name := range existingPolecats {
|
||||||
idx := p.parsePoolIndex(name)
|
if p.isThemedName(name) {
|
||||||
if idx > 0 && idx <= PoolSize {
|
p.InUse[name] = true
|
||||||
p.InUse[idx] = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatPoolName formats a pool index as a name.
|
|
||||||
func (p *NamePool) formatPoolName(idx int) string {
|
|
||||||
return fmt.Sprintf("%s%02d", NamePrefix, idx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// formatOverflowName formats an overflow sequence number as a name.
|
// formatOverflowName formats an overflow sequence number as a name.
|
||||||
func (p *NamePool) formatOverflowName(seq int) string {
|
func (p *NamePool) formatOverflowName(seq int) string {
|
||||||
return fmt.Sprintf("%s-%d", p.RigName, seq)
|
return fmt.Sprintf("%s-%d", p.RigName, seq)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parsePoolIndex extracts the pool index from a pool name.
|
// GetTheme returns the current theme name.
|
||||||
// Returns 0 if not a valid pool name.
|
func (p *NamePool) GetTheme() string {
|
||||||
func (p *NamePool) parsePoolIndex(name string) int {
|
p.mu.RLock()
|
||||||
if len(name) < len(NamePrefix)+2 {
|
defer p.mu.RUnlock()
|
||||||
return 0
|
return p.Theme
|
||||||
}
|
}
|
||||||
if name[:len(NamePrefix)] != NamePrefix {
|
|
||||||
return 0
|
// SetTheme sets the theme and resets the pool.
|
||||||
|
// Existing in-use names are preserved if they exist in the new theme.
|
||||||
|
func (p *NamePool) SetTheme(theme string) error {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
if _, ok := BuiltinThemes[theme]; !ok {
|
||||||
|
return fmt.Errorf("unknown theme: %s (available: mad-max, minerals, wasteland)", theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
var idx int
|
// Preserve names that exist in both themes
|
||||||
_, err := fmt.Sscanf(name[len(NamePrefix):], "%d", &idx)
|
newNames := BuiltinThemes[theme]
|
||||||
if err != nil {
|
newInUse := make(map[string]bool)
|
||||||
return 0
|
for name := range p.InUse {
|
||||||
|
for _, n := range newNames {
|
||||||
|
if n == name {
|
||||||
|
newInUse[name] = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return idx
|
|
||||||
|
p.Theme = theme
|
||||||
|
p.InUse = newInUse
|
||||||
|
p.CustomNames = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListThemes returns the list of available built-in themes.
|
||||||
|
func ListThemes() []string {
|
||||||
|
themes := make([]string, 0, len(BuiltinThemes))
|
||||||
|
for theme := range BuiltinThemes {
|
||||||
|
themes = append(themes, theme)
|
||||||
|
}
|
||||||
|
sort.Strings(themes)
|
||||||
|
return themes
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetThemeNames returns the names in a specific theme.
|
||||||
|
func GetThemeNames(theme string) ([]string, error) {
|
||||||
|
if names, ok := BuiltinThemes[theme]; ok {
|
||||||
|
return names, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unknown theme: %s", theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCustomName adds a custom name to the pool.
|
||||||
|
func (p *NamePool) AddCustomName(name string) {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
// Check if already in custom names
|
||||||
|
for _, n := range p.CustomNames {
|
||||||
|
if n == name {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.CustomNames = append(p.CustomNames, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset clears the pool state, releasing all names.
|
||||||
|
func (p *NamePool) Reset() {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
p.InUse = make(map[string]bool)
|
||||||
|
p.OverflowNext = p.MaxSize + 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,22 +15,22 @@ func TestNamePool_Allocate(t *testing.T) {
|
|||||||
|
|
||||||
pool := NewNamePool(tmpDir, "testrig")
|
pool := NewNamePool(tmpDir, "testrig")
|
||||||
|
|
||||||
// First allocation should be polecat-01
|
// First allocation should be first themed name (furiosa)
|
||||||
name, err := pool.Allocate()
|
name, err := pool.Allocate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Allocate error: %v", err)
|
t.Fatalf("Allocate error: %v", err)
|
||||||
}
|
}
|
||||||
if name != "polecat-01" {
|
if name != "furiosa" {
|
||||||
t.Errorf("expected polecat-01, got %s", name)
|
t.Errorf("expected furiosa, got %s", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second allocation should be polecat-02
|
// Second allocation should be nux
|
||||||
name, err = pool.Allocate()
|
name, err = pool.Allocate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Allocate error: %v", err)
|
t.Fatalf("Allocate error: %v", err)
|
||||||
}
|
}
|
||||||
if name != "polecat-02" {
|
if name != "nux" {
|
||||||
t.Errorf("expected polecat-02, got %s", name)
|
t.Errorf("expected nux, got %s", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,21 +47,21 @@ func TestNamePool_Release(t *testing.T) {
|
|||||||
name1, _ := pool.Allocate()
|
name1, _ := pool.Allocate()
|
||||||
name2, _ := pool.Allocate()
|
name2, _ := pool.Allocate()
|
||||||
|
|
||||||
if name1 != "polecat-01" || name2 != "polecat-02" {
|
if name1 != "furiosa" || name2 != "nux" {
|
||||||
t.Fatalf("unexpected allocations: %s, %s", name1, name2)
|
t.Fatalf("unexpected allocations: %s, %s", name1, name2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Release first one
|
// Release first one
|
||||||
pool.Release("polecat-01")
|
pool.Release("furiosa")
|
||||||
|
|
||||||
// Next allocation should reuse polecat-01
|
// Next allocation should reuse furiosa
|
||||||
name, _ := pool.Allocate()
|
name, _ := pool.Allocate()
|
||||||
if name != "polecat-01" {
|
if name != "furiosa" {
|
||||||
t.Errorf("expected polecat-01 to be reused, got %s", name)
|
t.Errorf("expected furiosa to be reused, got %s", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNamePool_PrefersLowNumbers(t *testing.T) {
|
func TestNamePool_PrefersOrder(t *testing.T) {
|
||||||
tmpDir, err := os.MkdirTemp("", "namepool-test-*")
|
tmpDir, err := os.MkdirTemp("", "namepool-test-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -75,20 +75,20 @@ func TestNamePool_PrefersLowNumbers(t *testing.T) {
|
|||||||
pool.Allocate()
|
pool.Allocate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Release 03 and 01
|
// Release slit and furiosa
|
||||||
pool.Release("polecat-03")
|
pool.Release("slit")
|
||||||
pool.Release("polecat-01")
|
pool.Release("furiosa")
|
||||||
|
|
||||||
// Next allocation should be 01 (lowest available)
|
// Next allocation should be furiosa (first in theme order)
|
||||||
name, _ := pool.Allocate()
|
name, _ := pool.Allocate()
|
||||||
if name != "polecat-01" {
|
if name != "furiosa" {
|
||||||
t.Errorf("expected polecat-01 (lowest), got %s", name)
|
t.Errorf("expected furiosa (first in order), got %s", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next should be 03
|
// Next should be slit
|
||||||
name, _ = pool.Allocate()
|
name, _ = pool.Allocate()
|
||||||
if name != "polecat-03" {
|
if name != "slit" {
|
||||||
t.Errorf("expected polecat-03, got %s", name)
|
t.Errorf("expected slit, got %s", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,10 +99,10 @@ func TestNamePool_Overflow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||||
|
|
||||||
pool := NewNamePool(tmpDir, "gastown")
|
pool := NewNamePoolWithConfig(tmpDir, "gastown", "mad-max", nil, 5)
|
||||||
|
|
||||||
// Exhaust the pool
|
// Exhaust the small pool
|
||||||
for i := 0; i < PoolSize; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
pool.Allocate()
|
pool.Allocate()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,15 +111,15 @@ func TestNamePool_Overflow(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Allocate error: %v", err)
|
t.Fatalf("Allocate error: %v", err)
|
||||||
}
|
}
|
||||||
expected := "gastown-51"
|
expected := "gastown-6"
|
||||||
if name != expected {
|
if name != expected {
|
||||||
t.Errorf("expected overflow name %s, got %s", expected, name)
|
t.Errorf("expected overflow name %s, got %s", expected, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next overflow
|
// Next overflow
|
||||||
name, _ = pool.Allocate()
|
name, _ = pool.Allocate()
|
||||||
if name != "gastown-52" {
|
if name != "gastown-7" {
|
||||||
t.Errorf("expected gastown-52, got %s", name)
|
t.Errorf("expected gastown-7, got %s", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,26 +130,26 @@ func TestNamePool_OverflowNotReusable(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer func() { _ = os.RemoveAll(tmpDir) }()
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||||
|
|
||||||
pool := NewNamePool(tmpDir, "gastown")
|
pool := NewNamePoolWithConfig(tmpDir, "gastown", "mad-max", nil, 3)
|
||||||
|
|
||||||
// Exhaust the pool
|
// Exhaust the pool
|
||||||
for i := 0; i < PoolSize; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
pool.Allocate()
|
pool.Allocate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get overflow name
|
// Get overflow name
|
||||||
overflow1, _ := pool.Allocate()
|
overflow1, _ := pool.Allocate()
|
||||||
if overflow1 != "gastown-51" {
|
if overflow1 != "gastown-4" {
|
||||||
t.Fatalf("expected gastown-51, got %s", overflow1)
|
t.Fatalf("expected gastown-4, got %s", overflow1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Release it - should not be reused
|
// Release it - should not be reused
|
||||||
pool.Release(overflow1)
|
pool.Release(overflow1)
|
||||||
|
|
||||||
// Next allocation should be gastown-52, not gastown-51
|
// Next allocation should be gastown-5, not gastown-4
|
||||||
name, _ := pool.Allocate()
|
name, _ := pool.Allocate()
|
||||||
if name != "gastown-52" {
|
if name != "gastown-5" {
|
||||||
t.Errorf("expected gastown-52 (overflow increments), got %s", name)
|
t.Errorf("expected gastown-5 (overflow increments), got %s", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,10 +163,10 @@ func TestNamePool_SaveLoad(t *testing.T) {
|
|||||||
pool := NewNamePool(tmpDir, "testrig")
|
pool := NewNamePool(tmpDir, "testrig")
|
||||||
|
|
||||||
// Allocate some names
|
// Allocate some names
|
||||||
pool.Allocate() // 01
|
pool.Allocate() // furiosa
|
||||||
pool.Allocate() // 02
|
pool.Allocate() // nux
|
||||||
pool.Allocate() // 03
|
pool.Allocate() // slit
|
||||||
pool.Release("polecat-02")
|
pool.Release("nux")
|
||||||
|
|
||||||
// Save state
|
// Save state
|
||||||
if err := pool.Save(); err != nil {
|
if err := pool.Save(); err != nil {
|
||||||
@@ -179,15 +179,15 @@ func TestNamePool_SaveLoad(t *testing.T) {
|
|||||||
t.Fatalf("Load error: %v", err)
|
t.Fatalf("Load error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should have 01 and 03 in use
|
// Should have furiosa and slit in use
|
||||||
if pool2.ActiveCount() != 2 {
|
if pool2.ActiveCount() != 2 {
|
||||||
t.Errorf("expected 2 active, got %d", pool2.ActiveCount())
|
t.Errorf("expected 2 active, got %d", pool2.ActiveCount())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next allocation should be 02 (released slot)
|
// Next allocation should be nux (released slot)
|
||||||
name, _ := pool2.Allocate()
|
name, _ := pool2.Allocate()
|
||||||
if name != "polecat-02" {
|
if name != "nux" {
|
||||||
t.Errorf("expected polecat-02, got %s", name)
|
t.Errorf("expected nux, got %s", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ func TestNamePool_Reconcile(t *testing.T) {
|
|||||||
pool := NewNamePool(tmpDir, "testrig")
|
pool := NewNamePool(tmpDir, "testrig")
|
||||||
|
|
||||||
// Simulate existing polecats from filesystem
|
// Simulate existing polecats from filesystem
|
||||||
existing := []string{"polecat-03", "polecat-07", "some-other-name"}
|
existing := []string{"slit", "valkyrie", "some-other-name"}
|
||||||
|
|
||||||
pool.Reconcile(existing)
|
pool.Reconcile(existing)
|
||||||
|
|
||||||
@@ -209,10 +209,10 @@ func TestNamePool_Reconcile(t *testing.T) {
|
|||||||
t.Errorf("expected 2 active after reconcile, got %d", pool.ActiveCount())
|
t.Errorf("expected 2 active after reconcile, got %d", pool.ActiveCount())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should allocate 01 first (not 03 or 07)
|
// Should allocate furiosa first (not slit or valkyrie)
|
||||||
name, _ := pool.Allocate()
|
name, _ := pool.Allocate()
|
||||||
if name != "polecat-01" {
|
if name != "furiosa" {
|
||||||
t.Errorf("expected polecat-01, got %s", name)
|
t.Errorf("expected furiosa, got %s", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,13 +229,12 @@ func TestNamePool_IsPoolName(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
expected bool
|
expected bool
|
||||||
}{
|
}{
|
||||||
{"polecat-01", true},
|
{"furiosa", true},
|
||||||
{"polecat-50", true},
|
{"nux", true},
|
||||||
{"polecat-51", false}, // > PoolSize
|
{"max", true},
|
||||||
{"gastown-51", false}, // overflow format
|
{"gastown-51", false}, // overflow format
|
||||||
{"Nux", false}, // legacy name
|
{"random-name", false},
|
||||||
{"polecat-", false}, // invalid
|
{"polecat-01", false}, // old format
|
||||||
{"polecat-abc", false},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
@@ -255,17 +254,18 @@ func TestNamePool_ActiveNames(t *testing.T) {
|
|||||||
|
|
||||||
pool := NewNamePool(tmpDir, "testrig")
|
pool := NewNamePool(tmpDir, "testrig")
|
||||||
|
|
||||||
pool.Allocate() // 01
|
pool.Allocate() // furiosa
|
||||||
pool.Allocate() // 02
|
pool.Allocate() // nux
|
||||||
pool.Allocate() // 03
|
pool.Allocate() // slit
|
||||||
pool.Release("polecat-02")
|
pool.Release("nux")
|
||||||
|
|
||||||
names := pool.ActiveNames()
|
names := pool.ActiveNames()
|
||||||
if len(names) != 2 {
|
if len(names) != 2 {
|
||||||
t.Errorf("expected 2 active names, got %d", len(names))
|
t.Errorf("expected 2 active names, got %d", len(names))
|
||||||
}
|
}
|
||||||
if names[0] != "polecat-01" || names[1] != "polecat-03" {
|
// Names are sorted
|
||||||
t.Errorf("expected [polecat-01, polecat-03], got %v", names)
|
if names[0] != "furiosa" || names[1] != "slit" {
|
||||||
|
t.Errorf("expected [furiosa, slit], got %v", names)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,17 +279,17 @@ func TestNamePool_MarkInUse(t *testing.T) {
|
|||||||
pool := NewNamePool(tmpDir, "testrig")
|
pool := NewNamePool(tmpDir, "testrig")
|
||||||
|
|
||||||
// Mark some slots as in use
|
// Mark some slots as in use
|
||||||
pool.MarkInUse("polecat-05")
|
pool.MarkInUse("dementus")
|
||||||
pool.MarkInUse("polecat-10")
|
pool.MarkInUse("valkyrie")
|
||||||
|
|
||||||
// Allocate should skip those
|
// Allocate should skip those
|
||||||
name, _ := pool.Allocate()
|
name, _ := pool.Allocate()
|
||||||
if name != "polecat-01" {
|
if name != "furiosa" {
|
||||||
t.Errorf("expected polecat-01, got %s", name)
|
t.Errorf("expected furiosa, got %s", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark more and verify count
|
// Verify count
|
||||||
if pool.ActiveCount() != 3 { // 01, 05, 10
|
if pool.ActiveCount() != 3 { // furiosa, dementus, valkyrie
|
||||||
t.Errorf("expected 3 active, got %d", pool.ActiveCount())
|
t.Errorf("expected 3 active, got %d", pool.ActiveCount())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -313,3 +313,120 @@ func TestNamePool_StateFilePath(t *testing.T) {
|
|||||||
t.Errorf("state file not found at expected path: %v", err)
|
t.Errorf("state file not found at expected path: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNamePool_Themes(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "namepool-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||||
|
|
||||||
|
// Test minerals theme
|
||||||
|
pool := NewNamePoolWithConfig(tmpDir, "testrig", "minerals", nil, 50)
|
||||||
|
|
||||||
|
name, err := pool.Allocate()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Allocate error: %v", err)
|
||||||
|
}
|
||||||
|
if name != "obsidian" {
|
||||||
|
t.Errorf("expected obsidian (first mineral), got %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test theme switching
|
||||||
|
if err := pool.SetTheme("wasteland"); err != nil {
|
||||||
|
t.Fatalf("SetTheme error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// obsidian should be released (not in wasteland theme)
|
||||||
|
name, _ = pool.Allocate()
|
||||||
|
if name != "rust" {
|
||||||
|
t.Errorf("expected rust (first wasteland name), got %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNamePool_CustomNames(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "namepool-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||||
|
|
||||||
|
custom := []string{"alpha", "beta", "gamma", "delta"}
|
||||||
|
pool := NewNamePoolWithConfig(tmpDir, "testrig", "", custom, 4)
|
||||||
|
|
||||||
|
name, _ := pool.Allocate()
|
||||||
|
if name != "alpha" {
|
||||||
|
t.Errorf("expected alpha, got %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
name, _ = pool.Allocate()
|
||||||
|
if name != "beta" {
|
||||||
|
t.Errorf("expected beta, got %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListThemes(t *testing.T) {
|
||||||
|
themes := ListThemes()
|
||||||
|
if len(themes) != 3 {
|
||||||
|
t.Errorf("expected 3 themes, got %d", len(themes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that all expected themes are present
|
||||||
|
expected := map[string]bool{"mad-max": true, "minerals": true, "wasteland": true}
|
||||||
|
for _, theme := range themes {
|
||||||
|
if !expected[theme] {
|
||||||
|
t.Errorf("unexpected theme: %s", theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetThemeNames(t *testing.T) {
|
||||||
|
names, err := GetThemeNames("mad-max")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetThemeNames error: %v", err)
|
||||||
|
}
|
||||||
|
if len(names) != 50 {
|
||||||
|
t.Errorf("expected 50 mad-max names, got %d", len(names))
|
||||||
|
}
|
||||||
|
if names[0] != "furiosa" {
|
||||||
|
t.Errorf("expected first name to be furiosa, got %s", names[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test invalid theme
|
||||||
|
_, err = GetThemeNames("invalid-theme")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for invalid theme")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNamePool_Reset(t *testing.T) {
|
||||||
|
tmpDir, err := os.MkdirTemp("", "namepool-test-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = os.RemoveAll(tmpDir) }()
|
||||||
|
|
||||||
|
pool := NewNamePool(tmpDir, "testrig")
|
||||||
|
|
||||||
|
// Allocate several names
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
pool.Allocate()
|
||||||
|
}
|
||||||
|
|
||||||
|
if pool.ActiveCount() != 10 {
|
||||||
|
t.Errorf("expected 10 active, got %d", pool.ActiveCount())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
pool.Reset()
|
||||||
|
|
||||||
|
if pool.ActiveCount() != 0 {
|
||||||
|
t.Errorf("expected 0 active after reset, got %d", pool.ActiveCount())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should allocate furiosa again
|
||||||
|
name, _ := pool.Allocate()
|
||||||
|
if name != "furiosa" {
|
||||||
|
t.Errorf("expected furiosa after reset, got %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user