gm-skill 2.0.1867 → 2.0.1868

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.
package/README.md CHANGED
@@ -58,15 +58,14 @@ This repo IS the published `gm-skill` npm package. No build step, no factory. Th
58
58
  ```
59
59
  gm/
60
60
  |-- skills/gm/ <- the skill (SKILL.md), installed as /gm
61
- |-- bin/ <- bootstrap + plugkit launcher (gmsniff / ccsniff are separate npm packages, `bun x gmsniff`, `bun x ccsniff`)
62
- |-- lib/ <- runtime: spool dispatch, skill bootstrap, daemon mgmt
63
- |-- agents/ <- subagent prompts (gm, memorize, research-worker, textprocessing)
64
- |-- lang/ <- language packs (browser, ssh)
65
- |-- gm-plugkit/ <- separate npm package that ships the wasm-wrapper
61
+ |-- bin/ <- bootstrap + installer + plugkit wasm pins (gmsniff / ccsniff are separate npm packages, `bun x gmsniff`, `bun x ccsniff`)
62
+ |-- scripts/ <- publish-time helper scripts
63
+ |-- gm-plugkit/ <- separate npm package that ships the wasm-wrapper + supervisor
66
64
  |-- gm.json <- version + plugkit pin
67
65
  |-- package.json <- npm publish manifest
68
66
  |-- AGENTS.md <- architectural rules (present-tense, no history)
69
67
  |-- CHANGELOG.md <- release history
68
+ |-- docs/ <- long-form paper + crate/skill/distribution pages
70
69
  `-- site/ <- flatspace site source (built to dist/ by CI)
71
70
  ```
72
71
 
@@ -92,12 +91,14 @@ Every tool the agent uses is a dispatch verb. No direct shell, no direct file wr
92
91
  - **`git_status` / `branch_status` / `git_push`**: git verbs that gate on porcelain
93
92
  - **`filter`**: in-wasm stdout-compaction (grep/ls/tree/json/diff)
94
93
 
95
- ### hooks
94
+ ### gates
96
95
 
97
- - **session-start**: bootstraps plugkit, seeds `.gm/next-step.md`, sets `needs-gm` marker
98
- - **prompt-submit**: reminds the agent to dispatch instruction first; injects per-prompt auto-recall
99
- - **pre-tool-use**: blocks tool use before the gm skill fires for the turn
100
- - **stop**: blocks session end while `.gm/prd.yml` has open items, mutables are unresolved, residual-scan hasn't fired, or the worktree is dirty
96
+ Orchestration state is tracked via `.gm/` marker files, not hook events. The gate that admits Write/Edit/git pre-execution runs natively inside `plugkit.wasm` (rs-plugkit `gates.rs` + its `hook_pre_tool_use` / `hook_stop` exports), driven off the same markers:
97
+
98
+ - **session-start**: bootstraps plugkit, seeds `.gm/next-step.md`, sets the `needs-gm` marker
99
+ - **turn entry**: the `instruction` verb reminds the agent to dispatch first and attaches the per-prompt auto-recall pack
100
+ - **pre-tool-use**: blocks Write/Edit/git before the gm skill fires for the turn
101
+ - **stop**: blocks session end while `.gm/prd.yml` has open items, mutables are unresolved, residual-scan hasn't fired, or the worktree is dirty or unpushed
101
102
 
102
103
  ### ground truth
103
104
 
@@ -71,7 +71,7 @@ Write `in/<lang>/<N>.<ext>` for language stems, `in/<verb>/<N>.txt` for orchestr
71
71
 
72
72
  ## SESSION_ID
73
73
 
74
- Thread SESSION_ID through every spool body + rs-exec RPC; plugkit rejects empty.
74
+ Thread SESSION_ID through every spool body; plugkit rejects empty.
75
75
 
76
76
  ## Daemonize
77
77
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1867",
3
+ "version": "2.0.1868",
4
4
  "description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform binary, verifies SHA256, and starts the spool watcher daemon. Includes plugkit-wasm-wrapper for WASM-based spool watching.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -12,7 +12,6 @@
12
12
  "index.js",
13
13
  "bootstrap.js",
14
14
  "supervisor.js",
15
- "lang-host-runner.js",
16
15
  "plugkit-wasm-wrapper.js",
17
16
  "plugkit.version",
18
17
  "plugkit.sha256",
@@ -1049,18 +1049,6 @@ function resolveWindowsExeLocal(cmd) {
1049
1049
  }
1050
1050
  }
1051
1051
 
1052
- function isPortReachableSync(host, port, timeoutMs) {
1053
- const r = spawnSync(process.execPath, ['-e', `
1054
- const net = require('net');
1055
- const s = net.connect({ port: ${port}, host: ${JSON.stringify(host)} });
1056
- let done = false;
1057
- s.on('connect', () => { done = true; s.destroy(); process.exit(0); });
1058
- s.on('error', () => { if (!done) process.exit(1); });
1059
- setTimeout(() => { if (!done) { s.destroy(); process.exit(1); } }, ${timeoutMs || 800});
1060
- `], { timeout: (timeoutMs || 800) + 2000, windowsHide: true });
1061
- return r.status === 0;
1062
- }
1063
-
1064
1052
  function findFreePortSync() {
1065
1053
  const r = spawnSync(process.execPath, ['-e', `
1066
1054
  const net = require('net');
@@ -1072,17 +1060,6 @@ function findFreePortSync() {
1072
1060
  return parseInt(r.stdout.trim(), 10);
1073
1061
  }
1074
1062
 
1075
- function isPortAliveSync(port) {
1076
- const r = spawnSync(process.execPath, ['-e', `
1077
- const net = require('net');
1078
- const s = net.connect({ port: ${port}, host: '127.0.0.1' });
1079
- s.on('connect', () => { s.destroy(); process.exit(0); });
1080
- s.on('error', () => process.exit(1));
1081
- setTimeout(() => process.exit(1), 800);
1082
- `], { timeout: 2000 });
1083
- return r.status === 0;
1084
- }
1085
-
1086
1063
  function sleepSync(ms) {
1087
1064
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(0, ms | 0));
1088
1065
  }
@@ -4390,16 +4367,42 @@ async function runSpoolWatcher(instance, spoolDir) {
4390
4367
  // The window must therefore comfortably exceed the worst-case cold embed;
4391
4368
  // it is a one-time cost (the digest persists after a single completion,
4392
4369
  // so subsequent boots' warmup is near-instant).
4393
- _writeStatusBusy(1200000);
4394
- const vb = new TextEncoder().encode('codesearch');
4395
- const bb = new TextEncoder().encode(JSON.stringify({ query: 'index warmup', k: 1 }));
4396
- const vp = writeWasmInput(instance, vb, 'boot-warmup:codesearch.verb');
4397
- const bp = writeWasmInput(instance, bb, 'boot-warmup:codesearch.body');
4370
+ // A single warmup dispatch only advances the index by one wall-budget
4371
+ // slice, so a large repo needs many passes before the digest converges
4372
+ // (deferred_files == 0). A single-shot warmup left the digest permanently
4373
+ // withheld: every later verb and every respawn re-triggered a fresh
4374
+ // partial pass, and the stale-digest boot path re-embedded from scratch
4375
+ // forever. Loop the warmup, refreshing busy_until each pass so the
4376
+ // supervisor's stale-heartbeat check never kills a converging rebuild,
4377
+ // until deferred_files reaches 0 or a bounded pass count is hit.
4398
4378
  const t0 = Date.now();
4399
- const r = dispatch(vp, vb.length, bp, bb.length);
4400
- decodeWasmResult(instance, r, 'boot-warmup:codesearch');
4379
+ let passes = 0;
4380
+ let lastDeferred = -1;
4381
+ let stagnant = 0;
4382
+ const MAX_WARMUP_PASSES = 80;
4383
+ while (passes < MAX_WARMUP_PASSES) {
4384
+ _writeStatusBusy(1200000);
4385
+ const vb = new TextEncoder().encode('codeinsight_index');
4386
+ const bb = new TextEncoder().encode(JSON.stringify({ root: '.', max_files: 500 }));
4387
+ const vp = writeWasmInput(instance, vb, 'boot-warmup:codeinsight_index.verb');
4388
+ const bp = writeWasmInput(instance, bb, 'boot-warmup:codeinsight_index.body');
4389
+ const r = dispatch(vp, vb.length, bp, bb.length);
4390
+ const decoded = decodeWasmResult(instance, r, 'boot-warmup:codeinsight_index');
4391
+ passes++;
4392
+ let deferred = null;
4393
+ try {
4394
+ const parsed = typeof decoded === 'string' ? JSON.parse(decoded) : decoded;
4395
+ const d = parsed && (parsed.data || parsed);
4396
+ if (d && typeof d.deferred_files === 'number') deferred = d.deferred_files;
4397
+ else if (parsed && typeof parsed.deferred_files === 'number') deferred = parsed.deferred_files;
4398
+ } catch (_) {}
4399
+ if (deferred === null) break;
4400
+ if (deferred === 0) break;
4401
+ if (deferred === lastDeferred) { stagnant++; if (stagnant >= 4) break; } else { stagnant = 0; }
4402
+ lastDeferred = deferred;
4403
+ }
4401
4404
  writeStatus();
4402
- logEvent('plugkit', 'boot.index-warmup', { ms: Date.now() - t0 });
4405
+ logEvent('plugkit', 'boot.index-warmup', { ms: Date.now() - t0, passes, converged: lastDeferred === -1 || lastDeferred === 0 });
4403
4406
  } catch (e) {
4404
4407
  try { logEvent('plugkit', 'boot.index-warmup-failed', { error: String(e && e.message || e) }); } catch (_) {}
4405
4408
  }
package/gm.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.1867",
3
+ "version": "2.0.1868",
4
4
  "description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-skill",
3
- "version": "2.0.1867",
3
+ "version": "2.0.1868",
4
4
  "description": "Canonical universal harness — AI-native software engineering via skill-driven orchestration; bootstraps plugkit for task execution and session isolation. Install in any AI coding agent host.",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
@@ -30,14 +30,9 @@
30
30
  },
31
31
  "files": [
32
32
  "skills/",
33
- "agents/",
34
- "lib/",
35
- "lang/",
36
33
  "scripts/",
37
34
  "bin/bootstrap.js",
38
35
  "bin/install.js",
39
- "bin/gm-validate.js",
40
- "bin/gm-shell-validate.js",
41
36
  "bin/plugkit.sha256",
42
37
  "bin/plugkit.version",
43
38
  "bin/plugkit.wasm.sha256",
@@ -45,7 +40,6 @@
45
40
  "gm-plugkit/browser-idle.js",
46
41
  "gm-plugkit/cli.js",
47
42
  "gm-plugkit/index.js",
48
- "gm-plugkit/lang-host-runner.js",
49
43
  "gm-plugkit/package.json",
50
44
  "gm-plugkit/plugkit-wasm-wrapper.js",
51
45
  "gm-plugkit/supervisor.js",
@@ -72,7 +72,7 @@ Spool input from PowerShell must be UTF-8 no-BOM (`-Encoding utf8` or `[System.I
72
72
 
73
73
  **EXECUTE resolves all mutables before EMIT, discovers more, resolves those too, rearchitects immediately on in-spirit discovery.** Any architectural improvement discovered mid-EXECUTE is an immediate `transition to=PLAN`, re-`prd-add` the affected row with its existing id (re-scope, never delete-and-re-add) -- maximal-effort correctness over preservation-for-its-own-sake, no deferral, no "note for later."
74
74
 
75
- **`prd-resolve` is bound by the false-completion rule in every phase, PLAN included -- not only at VERIFY.** A row marked `status: completed` whose witness says "deferred", "pending next session", "pending browser fix", "awaits [X] recovery", "user must refresh", or any equivalent hedge is undone work labeled done -- forbidden, the same class as a mock standing in for real code. A genuinely external blocker (browser environment down, credential missing, another team's repo) keeps the row `status: pending` with `blockedBy: [external, "<specific reason>"]`; it is never grounds to resolve as completed. A blocker that recurs across sessions (the same tool crashing every attempt) becomes its own PRD row naming the tool defect to fix -- diagnose and repair it, never paper over the original row with a completed status while the real work stays undone.
75
+ **`prd-resolve` is bound by the false-completion rule in every phase, PLAN included -- not only at VERIFY.** A row resolved on a hedge ("deferred", "pending next session", "awaits recovery") is undone work labeled done -- forbidden, the same class as a mock standing in for real code. The served PLAN/VERIFY `instruction` prose carries the full hedge taxonomy, the `blockedBy: [external, ...]` handling for genuine outside-session blockers, and the recurring-blocker-becomes-its-own-row rule; dispatch `instruction` for it rather than re-deriving it here.
76
76
 
77
77
  VERIFY is adversarial, never confirmatory: run the real code path and read its actual output via `exec_js` or `browser` -- a finding is only real once witnessed by execution this turn. Never assert a crash, pass, or defect from memory, prior session state, or written prose alone. A gate denial (e.g. `residual.skipped` on dirty worktree) is resolved immediately by the named recovery action (commit or revert the listed files, then re-dispatch the same verb) in the same turn -- it is never grounds to re-enter PLAN, add unrelated PRD rows, or narrate the blocker instead of clearing it.
78
78
 
package/agents/gm.md DELETED
@@ -1,22 +0,0 @@
1
- ---
2
- description: Agent (not skill) - immutable programming state machine. Always invoke for all work coordination.
3
- mode: primary
4
- ---
5
-
6
- # GM -- Skill-First Orchestrator
7
-
8
- **Invoke the `planning` skill immediately.** Use the Skill tool with `skill: "planning"`.
9
-
10
- **CRITICAL: Skills are invoked via the Skill tool ONLY. Do NOT use the Agent tool to load skills. Skills are not agents. Use: `Skill tool` with `skill: "gm"` (or `"planning"`, `"gm-execute"`, `"gm-emit"`, `"gm-complete"`, `"update-docs"`). Using the Agent tool for skills is a violation.**
11
-
12
- All work coordination, planning, execution, and verification happens through the skill tree:
13
- - `planning` skill -> `gm-execute` skill -> `gm-emit` skill -> `gm-complete` skill -> `update-docs` skill
14
- - `memorize` sub-agent -- background only, non-sequential. Invocation: `Agent(subagent_type='memorize', model='haiku', run_in_background=true, prompt='## CONTEXT TO MEMORIZE\n<what was learned>')`
15
-
16
- All code execution uses `exec:<lang>` via the Bash tool -- never direct `Bash(node ...)` or `Bash(npm ...)`.
17
-
18
- To send stdin to a running background task: `exec:type` with task_id on line 1 and input on line 2.
19
-
20
- Do not use `EnterPlanMode`. Do not run code directly via Bash. Invoke `planning` skill first.
21
-
22
- Responses to the user must be two sentences maximum, only when the user needs to know something, and in plain conversational language -- no file paths, filenames, symbols, or technical identifiers.
@@ -1,100 +0,0 @@
1
- ---
2
- name: memorize
3
- description: Background memory agent. Classifies context and writes to AGENTS.md + rs-learn. No memory dir, no MEMORY.md.
4
- ---
5
-
6
- # Memorize -- Background Memory Agent
7
-
8
- Writes facts to two places only: **AGENTS.md** (non-obvious technical caveats) and **rs-learn** (all classified facts via fast ingest).
9
-
10
- Resolve at start of every run:
11
-
12
- - **Project root** = `process.cwd()` when invoked. `AGENTS.md` is `<project root>/AGENTS.md`.
13
- - **Reach check** = run `gh api repos/<owner>/<repo> --jq .permissions.push` on `<project root>`'s `git remote get-url origin`. Cache the answer for the run. If the result is anything other than literal `true` (false, no remote, non-github URL, gh CLI missing, gh not authed, repo private and inaccessible), the project is **out-of-reach**.
14
-
15
- ## STEP 0: SCOPE GUARD -- DO NOT POLLUTE OUT-OF-REACH PROJECTS
16
-
17
- If the reach check returns out-of-reach:
18
-
19
- - **Do** ingest classified facts into rs-learn (Step 2) -- rs-learn is per-user, not per-project, so private notes about a project the user is reading-but-not-owning are safe there.
20
- - **Do not** read or edit `<project root>/AGENTS.md` (Step 3). Skip the file entirely.
21
- - **Do not** run the AGENTS.md <-> rs-learn migration audit (Step 4). The audit edits AGENTS.md.
22
-
23
- Reason: agents running in a cwd that points at a third-party repo (e.g. running Claude inside a checkout of `nousresearch/hermes-agent` while building a downstream port) must not write project-specific notes into the upstream project's AGENTS.md. That AGENTS.md belongs to the upstream maintainers. Personal porting notes belong in the user's downstream repo's AGENTS.md, or -- when the work spans multiple repos and there's no clean home -- in rs-learn only.
24
-
25
- When the reach check returns **in-reach**, proceed normally with all four steps below.
26
-
27
- ## STEP 1: CLASSIFY
28
-
29
- Examine the ## CONTEXT TO MEMORIZE section at the end of this prompt. For each fact, classify as:
30
-
31
- - user: user role, goals, preferences, knowledge
32
- - feedback: guidance on approach -- corrections AND confirmations
33
- - project: ongoing work, goals, bugs, incidents, decisions
34
- - reference: pointers to external systems, URLs, paths
35
-
36
- Discard:
37
- - Obvious facts derivable from reading the code
38
- - Active task state or session progress
39
- - Facts that would not be useful in a future session
40
-
41
- ## STEP 2: INGEST INTO RS-LEARN
42
-
43
- For each classified fact, invoke `exec:memorize` (HTTP-preferred, subprocess fallback -- fast either way):
44
-
45
- ```
46
- exec:memorize
47
- <type>/<slug>
48
- <fact body -- one to three self-contained sentences>
49
- ```
50
-
51
- Line 1 of the body is the source tag (e.g. `feedback/terse-responses`, `project/merge-freeze`). Lines 2+ are the fact itself. Use kebab-case slugs.
52
-
53
- A discipline sigil -- `@<name>` as the first space-token in the invoking prompt, or a trailing `discipline=<name>` line -- routes the write to that discipline's store. Without one, the write lands in the default store. Forward the sigil verbatim to `exec:memorize`; never invent or default a discipline name.
54
-
55
- To invalidate previously-memorized content (correction or retraction):
56
-
57
- ```
58
- exec:forget
59
- by-source <tag>
60
- ```
61
-
62
- Or by content:
63
-
64
- ```
65
- exec:forget
66
- by-query <2-6 search words>
67
- ```
68
-
69
- **CRITICAL: rs-learn failures must be explicit and recoverable.** If `exec:memorize` fails (socket unavailable, network error, timeout):
70
- 1. Report the failure to the user with error details
71
- 2. Fallback immediately to STEP 3 (AGENTS.md) to preserve the fact in the always-on context buffer
72
- 3. Never proceed as if the write succeeded
73
- 4. This contract ensures memory preservation when the rs-learn retrieval store is temporarily unavailable
74
-
75
- ## STEP 3: AGENTS.md
76
-
77
- A non-obvious technical caveat qualifies if it required multiple failed runs to discover and would not be apparent from reading code or docs.
78
-
79
- For each qualifying fact from context:
80
- - Read AGENTS.md first if not already read this run
81
- - If AGENTS.md already covers it -> skip
82
- - If genuinely non-obvious -> append to the appropriate section
83
-
84
- Never add: obvious patterns, active task progress, redundant restatements.
85
-
86
- ## STEP 4: AGENTS.md -> RS-LEARN MIGRATION (BENCHMARK + DRAIN)
87
-
88
- AGENTS.md is the **always-on context buffer** -- every prompt sees it. rs-learn is the **conditional retrieval store** -- only relevant facts surface. The migration loop turns AGENTS.md into a benchmark for rs-learn's recall quality:
89
-
90
- 1. Pick **5 random items** from AGENTS.md (sections, paragraphs, or numbered points). Don't pick the most recent additions -- pick the oldest stable items.
91
- 2. For each item, derive a 2-6 word query that a future agent would naturally use to find this fact.
92
- 3. Run `exec:recall` with that query.
93
- 4. Decide:
94
- - **Recall accurate AND complete** -> the rs-learn store has internalized this fact; **remove it from AGENTS.md**. Frees buffer space and confirms learning.
95
- - **Recall partial / outdated / missing** -> keep the AGENTS.md item AND ingest a refined version of the fact via `exec:memorize` so next round it can pass. Note the outcome in your run log.
96
- 5. Report the audit cycle in the run output (items checked, removed, refined). Do not write the audit result to AGENTS.md -- it is changelog-shaped and AGENTS.md forbids dated audit sections.
97
-
98
- Why: AGENTS.md grows monotonically without this loop. rs-learn already filters by relevance per-prompt, so duplicating stable facts in AGENTS.md just inflates the always-on context. The migration drains AGENTS.md into the retrieval store as the store proves it can recall. Failed migrations leave the fact in AGENTS.md (safe default) and improve the store. Success rate over time = a metric for how well gm is learning this project.
99
-
100
- Don't migrate if the fact is genuinely about agent meta-behavior that must be active every prompt (e.g. "always invoke gm:gm first") -- those stay permanently.
@@ -1,36 +0,0 @@
1
- ---
2
- name: research-worker
3
- description: Focused single-thread web investigator. Spawned in parallel by the research skill. Owns one question, returns one path.
4
- agent: true
5
- allowed-tools: WebFetch, WebSearch, Bash
6
- ---
7
-
8
- # Research Worker
9
-
10
- One question. One context. One file on disk. One-line return.
11
-
12
- Two shapes of brief arrive: a live-web question owning a path under `.gm/research/<slug>/<worker-id>.md`, or a corpus chunk owning `.gm/disciplines/<name>/corpus/concise/<chunk-id>.md`. The corpus shape carries an input chunk on disk and a fact-preservation contract -- every claim, number, name, caveat, and citation from the source survives the rewrite; prose density rises, content does not shrink. No fetching unless the brief asks for it. The output file looks like the live-web one but the `Sources` section points at the input chunk path and any inline citations the chunk already carried.
13
-
14
- ## Brief shape
15
-
16
- The spawning prompt names: the question, the answer shape expected, the explicit out-of-scope boundary, and the destination path `.gm/research/<slug>/<worker-id>.md`. If any of those is missing or ambiguous, treat that as the first finding -- record what was unclear and stop, rather than guessing scope.
17
-
18
- ## Investigation
19
-
20
- Open with a `WebSearch` broad enough to map sources, narrow enough to exclude obviously off-topic results. Pick the two or three highest-quality hits -- primary docs, dated authored posts, RFCs, source repos -- and `WebFetch` each. Aggregator pages, content farms, and undated listicles are last resort, flagged as such when used.
21
-
22
- Stop fetching when the question is answered to the shape requested. Extra fetches past sufficiency burn tokens the orchestrator needs for synthesis.
23
-
24
- ## Output
25
-
26
- Write the findings file with: the question restated, the answer in the requested shape, every non-trivial claim followed by `[source: <url>]` and a quoted span, a `Sources` section listing every URL touched with a one-line quality note, and an `Unresolved` section naming anything the brief asked for that the search did not yield.
27
-
28
- A claim without an inline source URL is a defect; remove it before writing the file.
29
-
30
- ## Return
31
-
32
- Return only: the absolute path to the findings file, and a single sentence summarising the headline answer. Never return the full findings inline -- the orchestrator reads from disk.
33
-
34
- ## Boundary
35
-
36
- Do not chase tangents that surface mid-investigation, however interesting. Note them in `Unresolved` so the orchestrator can decide whether to fan out a new worker. Stretching past the brief is the worker-side equivalent of the orchestrator skipping fan-out.
@@ -1,47 +0,0 @@
1
- ---
2
- name: textprocessing
3
- description: Haiku-backed text processor. Takes a body of text and an instruction, returns the processed output. The required surface for any task whose correctness depends on understanding (summary, classify, extract, rewrite, translate, semantic dedup, score, label).
4
- agent: true
5
- ---
6
-
7
- # Textprocessing -- Haiku Language Processor
8
-
9
- The single surface for intelligent text transforms. Code does mechanics (count, split, regex, sort, dedup-by-equality); this agent does meaning (summary, classify, extract, rewrite, translate, semantic dedup, score, label).
10
-
11
- ## INVOCATION
12
-
13
- ```
14
- Agent(subagent_type='gm:textprocessing', model='haiku', prompt='## INPUT\n<body>\n\n## INSTRUCTION\n<task>')
15
- ```
16
-
17
- `prompt` always carries both halves -- input under `## INPUT`, instruction under `## INSTRUCTION`. The agent reads both, performs the transform, returns the result as plain text or JSON per what the instruction asked for. No preamble, no commentary, no "here is your output" wrapper.
18
-
19
- ## OUTPUT CONTRACT
20
-
21
- - Plain-text instruction -> plain-text output, no fences, no labels.
22
- - JSON instruction (e.g. "return as a JSON array of {id, label}") -> exactly that JSON, parseable by `JSON.parse`, no fences, no surrounding prose.
23
- - Multi-document input requested as a list -> one entry per input doc in the same order, no skips.
24
-
25
- If the instruction is ambiguous about the output shape, default to plain text. If the input is empty, return empty output (empty string or `[]` for JSON).
26
-
27
- ## BATCH PATTERN
28
-
29
- N independent items -> N parallel `Agent(textprocessing)` calls in ONE message, each with its own item under `## INPUT`. Never serialize independent items. The runner collects results and assembles the batch.
30
-
31
- For one large body that exceeds a single-prompt budget: the *caller* chunks the body deterministically (paragraph, section, fixed-token) and fans out one Agent call per chunk in parallel, then merges. The agent itself does not chunk; it processes whatever it receives in one shot.
32
-
33
- ## DISCIPLINE
34
-
35
- Code for mechanics, agent for meaning.
36
-
37
- - Mechanics (use code): char/word/token count, byte length, split on delimiter, exact-string find/replace, regex match/extract, sort, group-by-key, dedup-by-equality, lowercase/uppercase, JSON parse/stringify, base64, URL encode.
38
- - Meaning (use this agent): summarize, classify, extract entities/intents, rewrite for tone/audience, translate, semantic dedup (same meaning, different words), rank/score by quality, label by topic, decide if two texts are "about the same thing", paraphrase, simplify, expand outline -> prose.
39
-
40
- A loop in code that "checks if this string contains certain meaning" via keyword lists is a stub of this agent. Replace it with one Agent call (or N parallel ones) per item.
41
-
42
- ## CONSTRAINTS
43
-
44
- - Model is fixed at `haiku` -- fast, cheap, sufficient for transform tasks. Escalate to opus only when the agent's haiku output fails an acceptance check, never preemptively.
45
- - No tools beyond Read/Write -- the agent processes text it receives, optionally reads/writes chunks for multi-pass jobs. Never spawns subagents.
46
- - Background-spawnable: `run_in_background=true` for fire-and-forget batch processing where the caller polls results later.
47
- - Idempotent: same input + same instruction -> same output (modulo haiku sampling noise). Callers needing deterministic output specify `temperature=0` in the prompt.
@@ -1,143 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
- const cp = require('child_process');
7
- const os = require('os');
8
- const { pathToFileURL } = require('url');
9
-
10
- const ROOT = process.cwd();
11
- const WITNESS_DIR = path.join(ROOT, '.gm', 'witness');
12
-
13
- function freePort() {
14
- const net = require('net');
15
- return new Promise((resolve, reject) => {
16
- const srv = net.createServer();
17
- srv.once('error', reject);
18
- srv.listen(0, '127.0.0.1', () => {
19
- const p = srv.address().port;
20
- srv.close(() => resolve(p));
21
- });
22
- });
23
- }
24
-
25
- function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
26
- function rmrf(p) { try { fs.rmSync(p, { recursive: true, force: true }); } catch (_) {} }
27
- function write(file, text) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, text); }
28
- function which(cmds) {
29
- for (const cmd of cmds) {
30
- try {
31
- const out = cp.execFileSync('where.exe', [cmd], { encoding: 'utf8', windowsHide: true }).split(/\r?\n/).filter(Boolean);
32
- if (out[0]) return out[0];
33
- } catch (_) {}
34
- }
35
- return '';
36
- }
37
-
38
- async function renderPreview() {
39
- const preview = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-shell-preview-'));
40
- fs.mkdirSync(path.join(preview, 'vendor'), { recursive: true });
41
- fs.cpSync(path.join(ROOT, 'site', 'vendor'), path.join(preview, 'vendor'), { recursive: true });
42
-
43
- const renderScript = `
44
- import { writeFileSync } from 'fs';
45
- import { resolve } from 'path';
46
- const mod = await import(${JSON.stringify(pathToFileURL(path.join(ROOT, 'site', 'theme.mjs')).href)});
47
- const ctx = {
48
- readGlobal: (k) => {
49
- if (k === 'site') return { title: 'gm', tagline: "more coushin' for the pushin'", description: 'local browser OS surface', glyph: 'g', accent_from: '#7ee787', accent_to: '#56d364' };
50
- if (k === 'navigation') return { links: [{ label: 'Home', href: '/' }, { label: 'Paper', href: '/paper/' }, { label: 'Stats', href: '/stats/' }, { label: 'Crates', href: '/crates/' }, { label: 'Skills', href: '/skills/' }] };
51
- return null;
52
- },
53
- read: (k) => {
54
- if (k === 'pages') return { docs: [{ id: 'home', title: 'gm', layout: 'landing', hero: { heading: 'gm', body: 'local browser OS surface', subheading: 'predictable panes and local vendors', ctas: [{ label: 'Open docs', href: '/paper/', primary: true }], badges: [{ label: 'local' }, { label: 'xstate' }] }, features: { heading: 'features', items: [{ name: 'A', desc: 'a' }, { name: 'B', desc: 'b' }, { name: 'C', desc: 'c' }, { name: 'D', desc: 'd' }] }, examples: { heading: 'docs', items: [{ name: 'Doc', desc: 'd', href: '/paper/', cta: 'open' }] }, quickstart: { lines: [{ kind: 'cmd', text: 'npm run build' }, { kind: 'cmd', text: 'npm start' }] } }] };
55
- return null;
56
- }
57
- };
58
- const out = await mod.default.render(ctx);
59
- writeFileSync(resolve(process.env.GM_SHELL_PREVIEW, 'index.html'), out[0].html);
60
- `;
61
- const tmp = path.join(os.tmpdir(), `gm-shell-render-${Date.now()}.mjs`);
62
- fs.writeFileSync(tmp, renderScript);
63
- cp.execFileSync('node', [tmp], { stdio: 'inherit', windowsHide: true, env: { ...process.env, GM_SHELL_PREVIEW: preview } });
64
- try { fs.unlinkSync(tmp); } catch (_) {}
65
-
66
- const port = await freePort();
67
- const server = cp.spawn('python', ['-m', 'http.server', String(port), '--directory', preview], { cwd: ROOT, detached: true, stdio: 'ignore', windowsHide: true });
68
- server.unref();
69
- await sleep(1500);
70
- return { preview, port, serverPid: server.pid };
71
- }
72
-
73
- function killServer(pid) {
74
- if (!pid) return;
75
- try {
76
- if (process.platform === 'win32') cp.execFileSync('taskkill', ['/F', '/T', '/PID', String(pid)], { stdio: 'ignore', windowsHide: true });
77
- else process.kill(pid, 'SIGTERM');
78
- } catch (_) {}
79
- }
80
-
81
- async function main() {
82
- const { preview, port, serverPid } = await renderPreview();
83
- try {
84
- const witness = path.join(os.tmpdir(), `gm-shell-witness-${Date.now()}.js`);
85
- const witnessOut = path.join(WITNESS_DIR, `gm-shell-${Date.now()}.json`);
86
- write(witness, `
87
- await page.goto('http://127.0.0.1:${port}/');
88
- await page.waitForLoadState('domcontentloaded');
89
- await page.waitForTimeout(500);
90
- const before = await page.evaluate(() => ({
91
- app: window.__debug.gm.app(),
92
- activeSurface: document.querySelector('[data-app-surface]:not([hidden])')?.dataset.appSurface || null,
93
- activeAppCopy: document.getElementById('active-app-copy')?.textContent || ''
94
- }));
95
- await page.evaluate(() => { const btn = document.querySelectorAll('[data-app]')[1]; if (btn) btn.click(); });
96
- await page.waitForTimeout(300);
97
- const after = await page.evaluate(() => ({
98
- app: window.__debug.gm.app(),
99
- activeSurface: document.querySelector('[data-app-surface]:not([hidden])')?.dataset.appSurface || null,
100
- activeAppCopy: document.getElementById('active-app-copy')?.textContent || ''
101
- }));
102
- const result = { before, after };
103
- return JSON.stringify(result);
104
- `);
105
-
106
- const script = fs.readFileSync(witness, 'utf8');
107
- const relayPort = Number(process.env.PLAYWRITER_PORT) || 19988;
108
- const relayUrl = `http://127.0.0.1:${relayPort}/cli/execute`;
109
- const response = await fetch(relayUrl, {
110
- method: 'POST',
111
- headers: { 'Content-Type': 'application/json' },
112
- body: JSON.stringify({ sessionId: `gm-shell-${process.pid}-${Date.now()}`, code: script, timeout: 60000, cwd: ROOT }),
113
- });
114
- const result = await response.json();
115
- const out = result.text || '';
116
- console.log(out.trim());
117
- if (!response.ok || result.isError) {
118
- throw new Error(out || `browser witness failed (${response.status})`);
119
- }
120
- fs.mkdirSync(WITNESS_DIR, { recursive: true });
121
- let parsed = null;
122
- const m = out.match(/\[return value\]\s*(\{[\s\S]*\})\s*$/);
123
- if (m) {
124
- try { parsed = JSON.parse(m[1]); } catch (_) {}
125
- }
126
- if (!parsed) {
127
- try {
128
- const raw = fs.readFileSync(witnessOut, 'utf8');
129
- parsed = JSON.parse(raw);
130
- } catch (_) {}
131
- }
132
- fs.writeFileSync(witnessOut, JSON.stringify({ preview, port, output: out.trim(), parsed }, null, 2));
133
- try { fs.unlinkSync(witness); } catch (_) {}
134
- rmrf(preview);
135
- } finally {
136
- killServer(serverPid);
137
- }
138
- }
139
-
140
- main().catch((e) => {
141
- console.error(e && e.stack || String(e));
142
- process.exit(1);
143
- });