gm-skill 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,106 @@
1
+ ---
2
+ name: gm-complete
3
+ description: VERIFY and COMPLETE phase. End-to-end system verification and git enforcement. Any new unknown triggers immediate snake back to planning — restart chain.
4
+ ---
5
+
6
+ # GM COMPLETE — Verify, then close
7
+
8
+ Entry: EMIT gates clear, from `gm-emit`. Exit: `.prd` deleted + test.js green + pushed + CI green → `update-docs`.
9
+
10
+ Cross-cutting dispositions live in `gm` SKILL.md.
11
+
12
+ ## Transitions
13
+
14
+ - `.prd` items remain → `gm-execute`
15
+ - `.prd` empty AND test.js green AND pushed AND CI green → `update-docs`
16
+ - Broken file output → `gm-emit`
17
+ - Wrong logic → `gm-execute`
18
+ - New unknown or wrong requirements → `planning`
19
+
20
+ Failure triage: broken output to EMIT, wrong logic to EXECUTE, new unknown to PLAN. Never patch around surprises.
21
+
22
+ ## Mutables that must resolve before COMPLETE
23
+
24
+ - `witnessed_e2e` — real end-to-end run with witnessed output
25
+ - `browser_validated` — for any change touching client / UI / browser-facing code, see gate below. test.js + node-side imports DO NOT satisfy this gate.
26
+ - `git_clean` — `git status --porcelain` returns empty
27
+ - `git_pushed` — `git log origin/main..HEAD --oneline` returns empty
28
+ - `ci_passed` — every GitHub Actions run reaches `conclusion: success`
29
+ - `mutables_resolved` — `.gm/mutables.yml` deleted OR every entry `status: witnessed`. Stop hook hard-blocks turn-stop while any entry is `status: unknown`.
30
+ - `prd_empty` — `.gm/prd.yml` deleted AFTER residual scan: enumerate every in-spirit reachable residual surfaced this session; any hit re-enters `planning`, appends PRD items, executes. Empty PRD is necessary, not sufficient — done = empty PRD AND zero reachable in-spirit residuals. Out-of-spirit-or-unreachable residuals are named in the response and skipped; everything else is this turn's work.
31
+ - `stress_suite_clear` — change walked through M1–D1 (governance), none flunked
32
+ - `hidden_decision_posture` — open → down_weighted → closed only when CI is green AND stress suite is clear
33
+
34
+ ## End-to-end verification
35
+
36
+ Real system, real data, witness actual output. Doc updates, "saying done", and screenshots alone are not verification. Write the e2e probe to the spool (`.gm/exec-spool/in/nodejs/<N>.js`):
37
+
38
+ ```
39
+ const { fn } = await import('/abs/path/to/module.js');
40
+ console.log(await fn(realInput));
41
+ ```
42
+
43
+ After every success, enumerate what remains — never stop at first green.
44
+
45
+ ## Browser validation gate
46
+
47
+ Required when this session changed any code that runs in a browser: anything under `client/`, UI components, shaders, page-loaded JS, served HTML, gh-pages assets, dev-server endpoints, or any module imported into the page bundle.
48
+
49
+ Trigger detection (any one): `git diff --name-only origin/main..HEAD` includes paths under `client/`, `apps/*/index.js` with client export, `docs/`, `*.html`, shader files, or any file imported by a browser entry; new/changed export consumed by `window.*` or rendered in DOM/canvas/WebGL; visual, layout, animation, input, network-on-page, or shader behavior altered.
50
+
51
+ Protocol: boot the real server (or open the static page) on a known URL — witness HTTP 200. `exec:browser` → `page.goto(url)` → wait for app init by polling for the global the change affects (`window.__app.<system>`). Probe via `page.evaluate(() => …)` asserting the specific invariant the change was supposed to establish — instance counts, scene meshes, DOM nodes, render stats, network frames. Capture witnessed numbers in the response — "looks fine" is not a witness. Failures route to `gm-execute` (logic) or `gm-emit` (output) — never paper over.
52
+
53
+ Long-running probes split into navigate-call → `exec:wait N` → probe-call to stay under the per-call budget. Do not stack multi-second `setTimeout` inside one `exec:browser` invocation.
54
+
55
+ Exempt only when: change is server-only with zero browser-facing surface, OR the repository has no browser surface at all (pure CLI / library). Exemption requires explicit tag in the response: `BROWSER EXEMPT: <reason — must reference diff paths showing zero browser-facing surface>`. Default posture is NOT exempt — burden is on the agent to prove exemption with diff evidence.
56
+
57
+ Pre-flight: run `git diff --name-only origin/main..HEAD` directly via Bash, then dispatch a nodejs spool file that reads the diff list and filters lines matching `client/|docs/|\.html$|\.glsl$|\.frag$|\.vert$`. Any hit AND no `exec:browser` block in this session → mandatory regression to `gm-execute`.
58
+
59
+ ## Integration test gate
60
+
61
+ Write to `.gm/exec-spool/in/nodejs/<N>.js`:
62
+
63
+ ```
64
+ const { execSync } = require('child_process');
65
+ try { execSync('node test.js', { stdio: 'inherit', timeout: 30000 }); console.log('PASS'); }
66
+ catch (e) { console.error('FAIL'); process.exit(1); }
67
+ ```
68
+
69
+ Failure → `gm-execute`. No test.js in a repo with testable surface → `gm-execute` to create it.
70
+
71
+ ## Git enforcement
72
+
73
+ Run directly via Bash:
74
+
75
+ ```
76
+ git status --porcelain
77
+ git log origin/main..HEAD --oneline
78
+ ```
79
+
80
+ Both must return empty. Local commit without push is not complete.
81
+
82
+ ## CI is automated
83
+
84
+ The Stop hook watches Actions for the pushed HEAD. Do not call `gh run list` manually. All-green → Stop approves with CI summary in next-turn context. Failure → Stop blocks with run names + IDs; investigate via `gh run view <id> --log-failed`, fix, push, hook re-watches. Deadline 180s (override `GM_CI_WATCH_SECS`); slow jobs get a "still in progress" approve.
85
+
86
+ ## Hygiene sweep
87
+
88
+ 1. Files >200 lines → split
89
+ 2. Comments in code → remove
90
+ 3. Scattered test files (`.test.js`, `.spec.js`, `__tests__/`, `fixtures/`, `mocks/`) → delete, consolidate into root `test.js`
91
+ 4. Mock / stub / simulation files → delete
92
+ 5. Unnecessary doc files (not CHANGELOG, CLAUDE, README, TODO.md) → delete
93
+ 6. Duplicate concern → regress to `planning` with restructuring instructions
94
+ 7. Hardcoded values → derive from ground truth
95
+ 8. Fallback / demo modes → remove, fail loud
96
+ 9. TODO.md → empty or deleted
97
+ 10. CHANGELOG.md → entries for this session
98
+ 11. Observability gaps → server subsystems expose `/debug/<subsystem>`; client modules register in `window.__debug`
99
+ 12. Memorize → every fact from verification handed off via background `Agent(memorize)` at moment of resolution
100
+ 13. Deploy / publish → if deployable, deploy; if npm package, publish
101
+ 14. GitHub Pages → check `.github/workflows/pages.yml` + `docs/index.html` exist; invoke `pages` skill if absent
102
+ 15. Governance stress-suite → walk change through M1, F1, C1, H1, S1, B1, A1, D1; any flunk regresses to the owning phase
103
+
104
+ ## Completion
105
+
106
+ All true at once: witnessed e2e | browser_validated when client work touched | failure paths exercised | test.js passes | `.prd` deleted | git clean and pushed | CI green | hygiene sweep clean | TODO.md gone | CHANGELOG.md updated.
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: gm-emit
3
+ description: EMIT phase. Pre-emit debug, write files, post-emit verify from disk. Any new unknown triggers immediate snake back to planning — restart chain.
4
+ ---
5
+
6
+ # GM EMIT — Write and verify from disk
7
+
8
+ Entry: every mutable KNOWN, from `gm-execute` or re-entered from VERIFY. Exit: gates clear → `gm-complete`.
9
+
10
+ Cross-cutting dispositions live in `gm` SKILL.md.
11
+
12
+ ## Transitions
13
+
14
+ - All gates clear → `gm-complete`
15
+ - Post-emit variance with known cause → fix in-band, re-verify, stay in EMIT
16
+ - Pre-emit reveals known logic error → `gm-execute`
17
+ - Pre-emit reveals new unknown OR post-emit variance with unknown cause OR scope changed → `planning`
18
+
19
+ ## Legitimacy gate (before pre-emit run)
20
+
21
+ For every claim landing in a file, answer five questions:
22
+
23
+ 1. Earned specificity — does it trace to `authorization=witnessed`, or is it inflated from a weak prior?
24
+ 2. Repair legality — is a local patch dressed as structural repair? Downgrade scope or regress to PLAN.
25
+ 3. Lawful downgrade — can a weaker, true statement replace it? Prefer the downgrade.
26
+ 4. Alternative-route suppression — is a live competing route being silenced? Preserve it.
27
+ 5. Strongest objection — what would the sharpest reviewer pushback be? Articulate it. Cannot articulate = have not understood the alternatives → `gm-execute`.
28
+
29
+ Any failure regresses to `gm-execute` to witness what was missing, or `planning` if the gap is structural.
30
+
31
+ ## Pre-emit run
32
+
33
+ Mandatory before writing any file. Write the probe to the spool (`.gm/exec-spool/in/nodejs/<N>.js`):
34
+
35
+ ```
36
+ const { fn } = await import('/abs/path/to/module.js');
37
+ console.log(await fn(realInput));
38
+ ```
39
+
40
+ Import the actual module from disk to witness current behavior as the baseline. Run the proposed logic in isolation without writing — witness with real inputs and with real error inputs. Match expected → write. Unexpected → new unknown → `planning`.
41
+
42
+ ## Writing
43
+
44
+ Use the Write tool, or a nodejs spool file with `require('fs')`. Write only when every gate mutable resolves simultaneously.
45
+
46
+ ## Post-emit verification
47
+
48
+ Re-import from disk — in-memory state is stale and inadmissible. Run identical inputs as pre-emit; output must match the baseline exactly. Known variance → fix and re-verify (self-loop). Unknown variance → `planning`.
49
+
50
+ ## Mutables gate
51
+
52
+ Before pre-emit run, read `.gm/mutables.yml`. Any entry with `status: unknown` → regress to `gm-execute`. The pre-tool-use hook hard-blocks Write/Edit/NotebookEdit while unresolved entries exist; trying to emit anyway returns deny. Zero unresolved is the precondition for every legitimacy question below.
53
+
54
+ ## Gate (all true at once)
55
+
56
+ - `.gm/mutables.yml` empty/absent OR every entry `status: witnessed` with filled `witness_evidence`
57
+ - Legitimacy gate passed; no refused collapse
58
+ - Pre-emit passed with real inputs and real error inputs
59
+ - Post-emit matches pre-emit exactly
60
+ - Hot-reloadable; errors throw with context (no `|| default`, no `catch { return null }`, no fallbacks)
61
+ - No mocks, fakes, stubs, or scattered test files (delete on discovery)
62
+ - Any behavior change has a corresponding assertion in `test.js` — a change no test catches is a change you cannot prove
63
+ - Browser-facing change → post-emit verify includes a live `exec:browser` witness (boot server → `page.goto` → `page.evaluate` asserting the invariant the change established). Node-side import + test.js does not satisfy this — the final gate runs again in `gm-complete`.
64
+ - Files ≤ 200 lines
65
+ - No duplicate concern (run `exec:codesearch` for the primary concern after writing; overlap → `planning`)
66
+ - No comments, no hardcoded values, no adjectives in identifiers, no unnecessary files
67
+ - Observability: new server subsystems expose `/debug/<subsystem>`; new client modules register in `window.__debug`
68
+ - Structure: no if/else where dispatch suffices; no one-liners that obscure; no reinvented APIs
69
+ - Every fact resolved this phase memorized via background `Agent(memorize)`
70
+ - CHANGELOG.md updated; TODO.md cleared or deleted
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: gm-execute
3
+ description: EXECUTE phase AND the foundational execution contract for every skill. Every exec:<lang> run, every witnessed check, every code search, in every phase, follows this skill's discipline. Resolve all mutables via witnessed execution. Any new unknown triggers immediate snake back to planning — restart chain from PLAN.
4
+ ---
5
+
6
+ # GM EXECUTE — Resolve every unknown by witness
7
+
8
+ Entry: `.prd` with named unknowns. Exit: every mutable KNOWN → invoke `gm-emit`.
9
+
10
+ A `@<discipline>` sigil propagates from PLAN through every recall, codesearch, and memorize call; reads without one fan across default plus enabled disciplines, writes without one go to default only.
11
+
12
+ This skill is the execution contract for ALL phases — pre-emit witnesses, post-emit verifies, e2e checks all run on this discipline. Cross-cutting dispositions live in `gm` SKILL.md.
13
+
14
+ ## Transitions
15
+
16
+ - All mutables KNOWN → `gm-emit`
17
+ - Still UNKNOWN → re-run from a different angle (max 2 passes)
18
+ - New unknown OR unresolvable after 2 passes → `planning`
19
+
20
+ ## Mutable discipline
21
+
22
+ Each mutable carries: name, expected, current, resolution method.
23
+
24
+ Resolves to KNOWN only when all four pass:
25
+
26
+ - **ΔS = 0** — witnessed output equals expected
27
+ - **λ ≥ 2** — two independent paths agree
28
+ - **ε intact** — adjacent invariants hold
29
+ - **Coverage ≥ 0.70** — enough corpus inspected to rule out contradiction
30
+
31
+ Unresolved after 2 passes regresses to `planning`. Never narrate past an unresolved mutable.
32
+
33
+ Every witness that resolves a mutable writes back to `.gm/mutables.yml` the same step: set `status: witnessed` and fill `witness_evidence` with concrete proof (file:line, codesearch hit, exec output snippet). No write-back = the mutable stays unknown and the EMIT-gate stays closed. The hook reads this file; the agent's memory of "I resolved it" does not unblock anything.
34
+
35
+ Route candidates from PLAN are `weak_prior` only. Plausibility is the right to test, not the right to believe. A claim with no witness in the current session is a hypothesis — say so when stating it, and say what would settle it. The next reader (you, next turn) needs to know which lines were earned and which were carried forward.
36
+
37
+ ## Verification budget
38
+
39
+ Spend on `.prd` items in descending order of consequence-if-wrong × distance-from-witnessed. Items whose failure would collapse the headline finding must reach witnessed status before EMIT; sub-argument-level items need at minimum a stated fallback path.
40
+
41
+ ## Code execution
42
+
43
+ Code AND utility verbs both run through the file-spool. Write a file to `.gm/exec-spool/in/<lang-or-verb>/<N>.<ext>` — language stems (`in/nodejs/42.js`, `in/python/43.py`, `in/bash/44.sh`, plus typescript, go, rust, c, cpp, java, deno) or verb stems (`in/codesearch/45.txt`, `in/recall/46.txt`, `in/memorize/47.md`, plus wait, sleep, status, close, browser, runner, type, kill-port, forget, feedback, learn-status, learn-debug, learn-build, discipline, pause, health). The spool watcher executes and streams stdout to `out/<N>.out`, stderr to `out/<N>.err`, then writes `out/<N>.json` metadata sidecar at completion (taskId, lang, ok, exitCode, durationMs, timedOut, startedAt, endedAt). Both streams return as systemMessage with `--- stdout ---` / `--- stderr ---` separators. File I/O via a nodejs spool file + `require('fs')`. Only `git` and `gh` run directly in Bash. Never `Bash(node/npm/npx/bun)`, never `Bash(exec:<anything>)`.
44
+
45
+ Pack runs: `Promise.allSettled`, each idea own try/catch, under 12s per call. Runner: write `in/runner/<N>.txt` with body `start` | `stop` | `status`.
46
+
47
+ Every exec daemonizes. The hook tails the task logfile up to 30s wall-clock and returns whatever is there — short tasks complete inside the window and look synchronous; long tasks return a task_id with partial output. Continue with `exec:tail` (drain, bounded), `exec:watch` (resume blocking until match or timeout), or `exec:close` (terminate). Never re-spawn a long task to check on it — that orphans the first one. `exec:wait` is a pure timer; `exec:sleep` blocks on a specific task's output; `exec:watch` is the match-or-timeout primitive. Every execution-platform RPC returns the live list of running tasks for this session — close stragglers via `exec:close\n<id>` so the list stays scannable. Session-end (clear/logout/prompt_input_exit) kills the session's tasks; compaction/handoff preserves them.
48
+
49
+ Every utility verb dispatches via `in/<verb>/<N>.txt`; the body of the file is the verb's argument. There is no inline form and no Bash-prefix form — both are denied by the hook.
50
+
51
+ ## Codebase search
52
+
53
+ `exec:codesearch` only. Grep, Glob, Find, Explore, raw grep/rg/find inside `exec:bash` are all hook-blocked.
54
+
55
+ ```
56
+ exec:codesearch
57
+ <two-word query>
58
+ ```
59
+
60
+ Start two words, change/add one per pass, minimum four attempts before concluding absent. Known absolute path → `Read`. Known directory → `exec:nodejs` + `fs.readdirSync`.
61
+
62
+ ## Utility verb failure handling
63
+
64
+ **Utility verb failures must surface**: exec:memorize, exec:recall, exec:codesearch, and other utility verbs may fail (socket unavailable, timeout, network error). Failures do not block witness completion but must be reported to the user with error context. Fallback mechanisms (AGENTS.md for memorize) ensure memory preservation even when rs-learn is temporarily unavailable.
65
+
66
+ ## Import-based execution
67
+
68
+ Hypotheses become real by importing actual modules from disk. Reimplemented behavior is UNKNOWN. Write the import probe to the spool:
69
+
70
+ ```
71
+ # write .gm/exec-spool/in/nodejs/42.js
72
+ const { fn } = await import('/abs/path/to/module.js');
73
+ console.log(await fn(realInput));
74
+ ```
75
+
76
+ Differential diagnosis: smallest reproduction → compare actual vs expected → name the delta — that delta is the mutable.
77
+
78
+ ## Edits depend on witnesses
79
+
80
+ Hypothesis → run → witness → edit. An edit before a witness is a guess. Scan via `exec:codesearch` before creating or modifying — duplicate concern regresses to `planning`. Code-quality preference: native → library → structure → write.
81
+
82
+ ## Parallel subagents
83
+
84
+ Up to 3 `gm:gm` subagents for independent items in one message. Browser escalation: `exec:browser` → `browser` skill → screenshot only as last resort.
85
+
86
+ ## CI is automated
87
+
88
+ `git push` triggers the Stop hook to watch Actions for the pushed HEAD on the same repo (downstream cascades are not auto-watched). Green → Stop approves with summary; failure → run names + IDs surfaced, investigate via `gh run view <id> --log-failed`. Deadline 180s (override `GM_CI_WATCH_SECS`).
package/gm.SKILL.md ADDED
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: gm
3
+ description: Orchestrator dispatching PLAN→EXECUTE→EMIT→VERIFY→UPDATE-DOCS skill chain; spool-driven task execution with session isolation
4
+ allowed-tools: Skill
5
+ compatible-platforms:
6
+ - gm-cc
7
+ - gm-gc
8
+ - gm-oc
9
+ - gm-kilo
10
+ - gm-codex
11
+ - gm-copilot-cli
12
+ - gm-vscode
13
+ - gm-cursor
14
+ - gm-zed
15
+ - gm-jetbrains
16
+ end-to-end: true
17
+ ---
18
+
19
+ # GM — Orchestrator
20
+
21
+ Invoke `planning` immediately. Phases cascade: PLAN → EXECUTE → EMIT → VERIFY → UPDATE-DOCS.
22
+
23
+ The user's request is authorization. When scope is unclear, pick the maximum reachable shape and declare it — the user can interrupt. Doubts resolve via witnessed probe or recall, never by asking back except for destructive-irreversible actions uncovered by the PRD.
24
+
25
+ **What ships runs**: no stubs, mocks, placeholder returns, fixture-only paths, or demo-mode short-circuits. Real input through real code into real output. A shim is allowed only when delegating to real upstream behavior.
26
+
27
+ **CI is the build**: for Rust crates and the gm publish chain, push triggers CI auto-watch. Green signals authority. Local cargo build is not a witness.
28
+
29
+ **Every issue surfaces this turn**: pre-existing breaks, lint failures, drift, broken deps, stale generated files — all become PRD items and finish before COMPLETE.
30
+
31
+ **LLM provider**: acptoapi (127.0.0.1:4800) is the preferred provider when available. rs-plugkit session_start spawns acptoapi daemon and auto-detects ACP agents (opencode, kilo-code, codex, gemini-cli, qwen-code). All downstream platforms (rs-learn, freddie, gm-skill daemon mode) read OPENAI_BASE_URL environment variable and default to 127.0.0.1:4800. Anthropic SDK is fallback only when acptoapi socket is unavailable (CI, headless mode).
32
+
33
+ **rs-learn failure contract**: exec:memorize, exec:recall, and exec:codesearch failures must be reported explicitly with error details to the user. Fallback to AGENTS.md for memory preservation when socket/network unavailable. Never silently absorb errors because memory preservation requires explicit fallback. This rule applies across all phases (PLAN through UPDATE-DOCS).
34
+
35
+ **Spool dispatch chain**: write to `.gm/exec-spool/in/<lang>/<N>.<ext>` or `in/<verb>/<N>.txt`. Watcher executes and streams `out/<N>.out` + `out/<N>.err` + `out/<N>.json` metadata. Languages: nodejs, python, bash, typescript, go, rust, c, cpp, java, deno. Verbs: codesearch, recall, memorize, wait, sleep, status, close, browser, runner, type, kill-port, forget, feedback, learn-status, learn-debug, learn-build, discipline, pause, health.
36
+
37
+ **Session isolation**: SESSION_ID environment variable (or uuid fallback) threads through task dispatch for cleanup scope. rs-exec RPC handlers verify session_id match on all task-scoped operations.
38
+
39
+ **Code does mechanics; meaning routes through textprocessing skill**: summarize, classify, extract intent, rewrite, translate, semantic dedup, rank, label — all via `Agent(subagent_type='gm:textprocessing', ...)`.
40
+
41
+ **Recall before fresh execution**: before witnessing unknown via execution, recall first. Hits arrive as weak_prior; empty results confirm fresh unknown.
42
+
43
+ **Memorize is the back-half of witness**: resolution incomplete until fact lives outside this context window. Fire `Agent(subagent_type='gm:memorize', model='haiku', run_in_background=true, prompt='## CONTEXT TO MEMORIZE\n<fact>')` alongside witness, in parallel, never blocking.
44
+
45
+ **Parallel independent items**: up to 3 `gm:gm` subagents per message for independent PRD items. Serial for dependent items — no re-asking between them.
46
+
47
+ **Terse response**: fragments OK. `[thing] [action] [reason]. [next step].` Code, commits, PRs use normal prose.
48
+
49
+ ## End-to-End Phase Chaining (Skills-Based Platforms)
50
+
51
+ When `end-to-end: true` is present in SKILL.md frontmatter, skill output includes structured JSON on stdout (final line):
52
+
53
+ ```json
54
+ {"nextSkill": "gm-execute" | "gm-emit" | "gm-complete" | "update-docs" | null, "context": {PRD and state dict}, "phase": "PLAN" | "EXECUTE" | "EMIT" | "COMPLETE"}
55
+ ```
56
+
57
+ Platform adapters (vscode, cursor, zed, jetbrains) that support `end-to-end: true` detection:
58
+ 1. Invoke `Skill(skill="gm:gm")`
59
+ 2. Parse stdout for trailing JSON blob
60
+ 3. If `nextSkill` is non-null, invoke `Skill(skill="gm:<nextSkill>")` with context dict auto-passed
61
+ 4. Repeat until `nextSkill` is null
62
+
63
+ This collapses 5 manual skill invocations into 1 user invocation + 4 transparent auto-dispatches, achieving perceived single-flow parity with gm-cc's subagent orchestration.
package/index.js CHANGED
@@ -1,9 +1 @@
1
- const daemonBootstrap = require('../gm-starter/lib/daemon-bootstrap.js');
2
- const spool = require('../gm-starter/lib/spool.js');
3
- const manifest = require('./lib/manifest.js');
4
-
5
- module.exports = {
6
- ...daemonBootstrap,
7
- spool,
8
- manifest
9
- };
1
+ module.exports = require('./lib/index.js');
@@ -0,0 +1,314 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const net = require('net');
4
+ const crypto = require('crypto');
5
+ const { spawn, execSync } = require('child_process');
6
+ const os = require('os');
7
+
8
+ const LOG_DIR = path.join(os.homedir(), '.claude', 'gm-log');
9
+ const GM_STATE_DIR = path.join(os.homedir(), '.gm');
10
+
11
+ function emitEvent(daemon, severity, message, details = {}) {
12
+ try {
13
+ const date = new Date().toISOString().split('T')[0];
14
+ const logDir = path.join(LOG_DIR, date);
15
+ if (!fs.existsSync(logDir)) {
16
+ fs.mkdirSync(logDir, { recursive: true });
17
+ }
18
+ const logFile = path.join(logDir, 'daemon.jsonl');
19
+ const entry = {
20
+ ts: new Date().toISOString(),
21
+ daemon,
22
+ severity,
23
+ message,
24
+ ...details,
25
+ };
26
+ fs.appendFileSync(logFile, JSON.stringify(entry) + '\n');
27
+ } catch (e) {
28
+ console.error(`[daemon-bootstrap] emit failed: ${e.message}`);
29
+ }
30
+ }
31
+
32
+ function getPlatformKey() {
33
+ const plat = process.platform;
34
+ if (plat === 'win32') return plat;
35
+ if (plat === 'darwin') return plat;
36
+ if (plat === 'linux') return plat;
37
+ throw new Error(`Unsupported platform: ${plat}`);
38
+ }
39
+
40
+ function getSessionId() {
41
+ return process.env.CLAUDE_SESSION_ID || 'unknown';
42
+ }
43
+
44
+ function isDaemonRunning(daemonName) {
45
+ try {
46
+ const plat = getPlatformKey();
47
+ if (plat === 'win32') {
48
+ const output = execSync('tasklist /FO CSV /NH', { encoding: 'utf8' });
49
+ const lines = output.split('\n').filter(Boolean);
50
+ return lines.some(line => {
51
+ const parts = line.split(',').map(p => p.trim().replace(/^"/, '').replace(/"$/, ''));
52
+ return parts[0].includes(daemonName);
53
+ });
54
+ } else {
55
+ try {
56
+ execSync(`pgrep -f "${daemonName}" > /dev/null 2>&1`);
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+ } catch (e) {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ function checkPortReachable(host, port, timeoutMs = 500) {
68
+ return new Promise((resolve) => {
69
+ const socket = new net.Socket();
70
+ const timeoutHandle = setTimeout(() => {
71
+ socket.destroy();
72
+ resolve(false);
73
+ }, timeoutMs);
74
+
75
+ socket.connect(port, host, () => {
76
+ clearTimeout(timeoutHandle);
77
+ socket.destroy();
78
+ resolve(true);
79
+ });
80
+
81
+ socket.on('error', () => {
82
+ clearTimeout(timeoutHandle);
83
+ resolve(false);
84
+ });
85
+ });
86
+ }
87
+
88
+ function writeStatusFile(daemonName, status, details = {}) {
89
+ try {
90
+ fs.mkdirSync(GM_STATE_DIR, { recursive: true });
91
+ const statusFile = path.join(GM_STATE_DIR, `${daemonName}-status.json`);
92
+ const payload = {
93
+ daemon: daemonName,
94
+ status,
95
+ sessionId: getSessionId(),
96
+ timestamp: new Date().toISOString(),
97
+ pid: process.pid,
98
+ ...details,
99
+ };
100
+ fs.writeFileSync(statusFile, JSON.stringify(payload, null, 2));
101
+ emitEvent(daemonName, 'info', 'Status written', { file: statusFile });
102
+ } catch (e) {
103
+ emitEvent(daemonName, 'warn', 'Failed to write status file', { error: e.message });
104
+ }
105
+ }
106
+
107
+ async function checkState(daemonName) {
108
+ const sessionId = getSessionId();
109
+ const startTime = Date.now();
110
+
111
+ try {
112
+ emitEvent(daemonName, 'info', 'checkState initiated', { sessionId });
113
+
114
+ const running = isDaemonRunning(daemonName);
115
+ if (!running) {
116
+ emitEvent(daemonName, 'info', 'Daemon not running', { sessionId });
117
+ writeStatusFile(daemonName, 'not_running', { sessionId });
118
+ return { ok: true, running: false, durationMs: Date.now() - startTime };
119
+ }
120
+
121
+ emitEvent(daemonName, 'info', 'Daemon running', { sessionId });
122
+ writeStatusFile(daemonName, 'running', { sessionId });
123
+ return { ok: true, running: true, durationMs: Date.now() - startTime };
124
+ } catch (e) {
125
+ emitEvent(daemonName, 'error', 'checkState failed', {
126
+ error: e.message,
127
+ sessionId,
128
+ durationMs: Date.now() - startTime,
129
+ });
130
+ return { ok: false, error: e.message, durationMs: Date.now() - startTime };
131
+ }
132
+ }
133
+
134
+ async function spawnDaemon(daemonName, cmd) {
135
+ const sessionId = getSessionId();
136
+ const startTime = Date.now();
137
+
138
+ try {
139
+ emitEvent(daemonName, 'info', 'spawn initiated', { cmd, sessionId });
140
+
141
+ if (isDaemonRunning(daemonName)) {
142
+ emitEvent(daemonName, 'info', 'Already running, skipping spawn', { sessionId });
143
+ writeStatusFile(daemonName, 'running', { sessionId });
144
+ return { ok: true, already_running: true, durationMs: Date.now() - startTime };
145
+ }
146
+
147
+ emitEvent(daemonName, 'info', 'Spawning daemon', { cmd, sessionId });
148
+
149
+ const env = Object.assign({}, process.env, {
150
+ CLAUDE_SESSION_ID: sessionId,
151
+ });
152
+
153
+ const proc = spawn('bun', ['x', cmd], {
154
+ detached: true,
155
+ stdio: 'ignore',
156
+ windowsHide: true,
157
+ env,
158
+ });
159
+
160
+ const pid = proc.pid;
161
+ proc.unref();
162
+
163
+ emitEvent(daemonName, 'info', 'Daemon spawned', { pid, cmd, sessionId });
164
+ writeStatusFile(daemonName, 'spawned', { pid, sessionId });
165
+
166
+ return {
167
+ ok: true,
168
+ pid,
169
+ cmd,
170
+ sessionId,
171
+ durationMs: Date.now() - startTime,
172
+ };
173
+ } catch (e) {
174
+ emitEvent(daemonName, 'error', 'spawn failed', {
175
+ error: e.message,
176
+ cmd,
177
+ sessionId,
178
+ durationMs: Date.now() - startTime,
179
+ });
180
+ writeStatusFile(daemonName, 'spawn_error', { error: e.message, sessionId });
181
+ return { ok: false, error: e.message, durationMs: Date.now() - startTime };
182
+ }
183
+ }
184
+
185
+ async function waitForReady(daemonName, host, port, timeoutMs = 30000) {
186
+ const sessionId = getSessionId();
187
+ const startTime = Date.now();
188
+
189
+ try {
190
+ emitEvent(daemonName, 'info', 'waitForReady initiated', {
191
+ host,
192
+ port,
193
+ timeoutMs,
194
+ sessionId,
195
+ });
196
+
197
+ const deadline = startTime + timeoutMs;
198
+ const pollIntervalMs = 500;
199
+
200
+ while (Date.now() < deadline) {
201
+ const reachable = await checkPortReachable(host, port, 1000);
202
+ if (reachable) {
203
+ emitEvent(daemonName, 'info', 'Ready', {
204
+ host,
205
+ port,
206
+ elapsedMs: Date.now() - startTime,
207
+ sessionId,
208
+ });
209
+ writeStatusFile(daemonName, 'ready', { host, port, sessionId });
210
+ return { ok: true, host, port, elapsedMs: Date.now() - startTime };
211
+ }
212
+
213
+ await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
214
+ }
215
+
216
+ emitEvent(daemonName, 'warn', 'Timeout waiting for readiness', {
217
+ host,
218
+ port,
219
+ timeoutMs,
220
+ sessionId,
221
+ elapsedMs: Date.now() - startTime,
222
+ });
223
+ writeStatusFile(daemonName, 'timeout', { host, port, timeoutMs, sessionId });
224
+ return { ok: false, error: 'Timeout', timeoutMs, elapsedMs: Date.now() - startTime };
225
+ } catch (e) {
226
+ emitEvent(daemonName, 'error', 'waitForReady failed', {
227
+ error: e.message,
228
+ host,
229
+ port,
230
+ sessionId,
231
+ elapsedMs: Date.now() - startTime,
232
+ });
233
+ return { ok: false, error: e.message, elapsedMs: Date.now() - startTime };
234
+ }
235
+ }
236
+
237
+ async function getSocket(daemonName) {
238
+ try {
239
+ emitEvent(daemonName, 'info', 'getSocket initiated', { sessionId: getSessionId() });
240
+
241
+ const statusFile = path.join(GM_STATE_DIR, `${daemonName}-status.json`);
242
+ if (!fs.existsSync(statusFile)) {
243
+ emitEvent(daemonName, 'warn', 'No status file found', { statusFile });
244
+ return { ok: false, error: 'No status file found' };
245
+ }
246
+
247
+ const status = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
248
+ const socket = `${status.host || '127.0.0.1'}:${status.port || 'unknown'}`;
249
+
250
+ emitEvent(daemonName, 'info', 'Socket retrieved', { socket });
251
+ return { ok: true, socket, ...status };
252
+ } catch (e) {
253
+ emitEvent(daemonName, 'error', 'getSocket failed', {
254
+ error: e.message,
255
+ sessionId: getSessionId(),
256
+ });
257
+ return { ok: false, error: e.message };
258
+ }
259
+ }
260
+
261
+ async function shutdown(daemonName) {
262
+ const sessionId = getSessionId();
263
+ const startTime = Date.now();
264
+
265
+ try {
266
+ emitEvent(daemonName, 'info', 'shutdown initiated', { sessionId });
267
+
268
+ const plat = getPlatformKey();
269
+ let killed = false;
270
+
271
+ if (plat === 'win32') {
272
+ try {
273
+ execSync(`taskkill /F /IM ${daemonName}* /T`, { stdio: 'ignore' });
274
+ killed = true;
275
+ } catch {
276
+ killed = false;
277
+ }
278
+ } else {
279
+ try {
280
+ execSync(`pkill -9 -f "${daemonName}"`, { stdio: 'ignore' });
281
+ killed = true;
282
+ } catch {
283
+ killed = false;
284
+ }
285
+ }
286
+
287
+ emitEvent(daemonName, 'info', 'shutdown completed', {
288
+ killed,
289
+ sessionId,
290
+ durationMs: Date.now() - startTime,
291
+ });
292
+ writeStatusFile(daemonName, 'shutdown', { killed, sessionId });
293
+
294
+ return { ok: true, killed, durationMs: Date.now() - startTime };
295
+ } catch (e) {
296
+ emitEvent(daemonName, 'error', 'shutdown failed', {
297
+ error: e.message,
298
+ sessionId,
299
+ durationMs: Date.now() - startTime,
300
+ });
301
+ return { ok: false, error: e.message, durationMs: Date.now() - startTime };
302
+ }
303
+ }
304
+
305
+ module.exports = {
306
+ checkState,
307
+ spawnDaemon,
308
+ waitForReady,
309
+ getSocket,
310
+ shutdown,
311
+ emitEvent,
312
+ isDaemonRunning,
313
+ checkPortReachable,
314
+ };