claude-code-session-manager 0.124.0 → 0.125.0

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.
Files changed (46) hide show
  1. package/dist/assets/{AgentLibrary-tsHX4uUD.js → AgentLibrary-CcnnJg8P.js} +1 -1
  2. package/dist/assets/{Button-BC94OZc0.js → Button-sA4lqAao.js} +1 -1
  3. package/dist/assets/{DataModel-DX3sReg5.js → DataModel-BSTejIBU.js} +1 -1
  4. package/dist/assets/{History-DtscQica.js → History-DuwlZsQ5.js} +1 -1
  5. package/dist/assets/{Hooks-CQERyV2Q.js → Hooks-DHDSoDJ_.js} +1 -1
  6. package/dist/assets/{Library-DGZ1bv-H.js → Library-XA9osRfi.js} +1 -1
  7. package/dist/assets/{ListDetail-C6JB9n9x.js → ListDetail-B3goE3Wc.js} +1 -1
  8. package/dist/assets/{McpServers-C5G8m3j2.js → McpServers-cMZTsPS6.js} +1 -1
  9. package/dist/assets/{Memory-KgM5KHiQ.js → Memory-D6qntpZ0.js} +1 -1
  10. package/dist/assets/{Panel-BsOgpYZG.js → Panel-DIyJXHfp.js} +1 -1
  11. package/dist/assets/{Permissions-DruyM6MH.js → Permissions-DJda4Aii.js} +1 -1
  12. package/dist/assets/{Plugins-BBWfU2sh.js → Plugins-DcdRVBnU.js} +2 -2
  13. package/dist/assets/{ProvenanceBadge-CyzjcuYD.js → ProvenanceBadge-BSI_2wAv.js} +1 -1
  14. package/dist/assets/{SaveBar-D5nZclkV.js → SaveBar-CeL4vXRp.js} +1 -1
  15. package/dist/assets/{Scheduler-BuxUAv-X.js → Scheduler-CTd19y5U.js} +1 -1
  16. package/dist/assets/{ScopeSwitcher-BFTsmyXK.js → ScopeSwitcher-BMiHySnz.js} +1 -1
  17. package/dist/assets/{Settings-CsU2wgPl.js → Settings-D-oIEKZy.js} +1 -1
  18. package/dist/assets/{SkillReferenceGraph-WTwaKQBk.js → SkillReferenceGraph-B_7WfFjn.js} +1 -1
  19. package/dist/assets/{Skills-DooJ8MqG.js → Skills-CKpgxhSS.js} +1 -1
  20. package/dist/assets/{SystemPrompt-DjWMf9Rk.js → SystemPrompt-BjJPTx_a.js} +1 -1
  21. package/dist/assets/{TagLibrary-DN8yO-eg.js → TagLibrary-DJBB4gL0.js} +1 -1
  22. package/dist/assets/{TiptapBody-DlbOZsU-.js → TiptapBody-C9Cu2GBL.js} +1 -1
  23. package/dist/assets/{Toggle-BgPR-zkl.js → Toggle-DbIRTOix.js} +1 -1
  24. package/dist/assets/{ViewTabs-Bsqxjjs8.js → ViewTabs-BDpHur6l.js} +1 -1
  25. package/dist/assets/{historyTotals-Dw4jn69k.js → historyTotals-WCJHsBMq.js} +1 -1
  26. package/dist/assets/{index-DCbAgcJt.js → index-BD0ufZqe.js} +3 -3
  27. package/dist/assets/{settingsSchema-Duk46cGB.js → settingsSchema-DmjBj3q7.js} +1 -1
  28. package/dist/assets/{skillFrontmatter-B8F8a18M.js → skillFrontmatter-Tn_XHajE.js} +1 -1
  29. package/dist/assets/{useAgentsDirWatch-0oP0fhBV.js → useAgentsDirWatch-Vz2iXtsQ.js} +1 -1
  30. package/dist/index.html +1 -1
  31. package/dist-web/assets/app.css +32 -0
  32. package/dist-web/assets/app.js +1479 -0
  33. package/dist-web/index.html +16 -0
  34. package/package.json +3 -3
  35. package/plugins/session-manager-dev/skills/builder/3-publish/SKILL.md +28 -10
  36. package/src/main/build-info.json +4 -4
  37. package/src/main/chatRunner.cjs +11 -5
  38. package/src/main/docEdit.cjs +8 -4
  39. package/src/main/ipcSchemas.cjs +4 -1
  40. package/src/main/lib/classifyPromptTicket.cjs +9 -4
  41. package/src/main/lib/claude/runClaudeP.cjs +7 -4
  42. package/src/main/lib/process/childStdin.cjs +39 -0
  43. package/src/main/lib/process/childWithLog.cjs +15 -1
  44. package/src/main/lib/scheduler/launchFailure.cjs +37 -1
  45. package/src/main/scheduler.cjs +111 -30
  46. package/src/main/supervisor.cjs +6 -2
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Claude Session Manager</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,400&family=Geist:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
10
+ <script type="module" crossorigin src="./assets/app.js"></script>
11
+ <link rel="stylesheet" crossorigin href="./assets/app.css">
12
+ </head>
13
+ <body class="bg-bg text-fg font-sans antialiased">
14
+ <div id="root"></div>
15
+ </body>
16
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-session-manager",
3
- "version": "0.124.0",
3
+ "version": "0.125.0",
4
4
  "description": "Local cockpit for the Claude Code CLI — multi-tab terminal, full config surface, scheduler, voice dictation, and live observability.",
5
5
  "type": "module",
6
6
  "main": "src/main/index.cjs",
@@ -37,7 +37,7 @@
37
37
  "dev": "concurrently -k -n vite,electron -c magenta,cyan \"npm:dev:renderer\" \"npm:dev:electron\"",
38
38
  "dev:renderer": "vite",
39
39
  "dev:electron": "wait-on http://localhost:5173 && SM_DEV=1 electron .",
40
- "build": "vite build",
40
+ "build": "vite build && vite build --config vite.web.config.ts",
41
41
  "build:web": "vite build --config vite.web.config.ts",
42
42
  "dist:stage": "npm run build && node scripts/package/stage.cjs",
43
43
  "dist:mac": "npm run dist:stage && electron-builder --config electron-builder.yml --mac --publish never",
@@ -47,7 +47,7 @@
47
47
  "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.main.json",
48
48
  "start": "electron .",
49
49
  "prepack": "node scripts/write-build-info.cjs",
50
- "prepublishOnly": "node scripts/write-build-info.cjs && vite build",
50
+ "prepublishOnly": "node scripts/write-build-info.cjs && npm run build",
51
51
  "test:unit": "vitest run",
52
52
  "test:unit:stability": "bash scripts/test-unit-stability.sh",
53
53
  "bench": "node scripts/bench/main-bench.cjs",
@@ -40,24 +40,37 @@ to ask for reconfirmation once `builder:gate` has passed.
40
40
 
41
41
  5. **Create the isolated worktree from that tag:**
42
42
  ```
43
- git worktree add /tmp/sm-publish-v<version> v<version>
43
+ mkdir -p "$HOME/.cache/sm-publish"
44
+ git worktree add "$HOME/.cache/sm-publish/v<version>" v<version>
44
45
  ```
46
+ **Never under `/tmp`** (or anywhere under `os.tmpdir()`): the test suite treats such paths as
47
+ test fixtures, so `src/main/__tests__/opsErrorLog.test.cjs` ("a real project root is
48
+ refused") fails inside a `/tmp` worktree.
45
49
 
46
- 6. **Move into the worktree and build clean:**
50
+ 6. **Move into the worktree, build clean, and run the in-tree gate:**
47
51
  ```
48
- cd /tmp/sm-publish-v<version>
52
+ cd "$HOME/.cache/sm-publish/v<version>"
49
53
  npm ci
50
54
  npm run typecheck
55
+ npm run test:unit
51
56
  ```
52
57
  `npm ci` gives a fully clean `node_modules` from the lockfile — no leftover local state
53
- from the dirty main repo can leak into the published artifact.
58
+ from the dirty main repo can leak into the published artifact. Typecheck and unit tests
59
+ must both pass here; stop before `npm publish` on any failure.
60
+
61
+ After the build step (run the project's build, e.g. `npm run build` plus any web build that
62
+ produces `dist-web/`), check that every `files` entry exists:
63
+ ```
64
+ test -f dist-web/index.html && test -f dist/index.html
65
+ ```
66
+ If either is missing, stop before `npm publish` and report.
54
67
 
55
68
  7. **Publish from inside the worktree:**
56
69
  ```
57
70
  npm publish
58
71
  ```
59
72
  This project's `prepublishOnly` script runs `vite build` — because step 6 already `cd`'d
60
- into `/tmp/sm-publish-v<version>`, that build runs **inside the clean worktree**, never
73
+ into `$HOME/.cache/sm-publish/v<version>`, that build runs **inside the clean worktree**, never
61
74
  touching (or being affected by) the live/dirty working directory back in the main repo.
62
75
  This is the entire reason the worktree exists: it decouples "what's uncommitted in my
63
76
  editor right now" from "what gets built and shipped."
@@ -81,16 +94,21 @@ to ask for reconfirmation once `builder:gate` has passed.
81
94
 
82
95
  9. **Clean up the worktree:**
83
96
  ```
84
- git worktree remove /tmp/sm-publish-v<version> --force
97
+ git worktree remove "$HOME/.cache/sm-publish/v<version>" --force
85
98
  ```
86
99
 
87
- 10. **Verify the publish actually landed on the registry:**
100
+ 10. **Verify the publish actually landed on the registry.** `npm view` is cached and lags for
101
+ minutes after a real publish, so check a cache-busted packument and the tarball first:
88
102
  ```
103
+ curl -s "https://registry.npmjs.org/<packageName>?t=$(date +%s)" # dist-tags.latest + versions
104
+ curl -sI <tarball URL from versions["<version>"].dist.tarball> # expect HTTP 200
89
105
  npm view <packageName> version
90
106
  npm view <packageName> dist-tags
91
107
  ```
92
- Confirm the reported version matches `<version>` and `latest` points to it (unless the
93
- project intentionally publishes to a different dist-tag).
108
+ Confirm `dist-tags.latest` is `<version>`, `<version>` is in `versions`, and the tarball
109
+ returns 200 (unless the project intentionally publishes to a different dist-tag). If the
110
+ packument is current, an unchanged `npm view` for a few minutes is normal lag, not a
111
+ failure — do not republish.
94
112
 
95
113
  11. **Post-publish: verify the installer release.** Pushing `v<version>` (step 8) triggers
96
114
  `.github/workflows/release.yml`. Confirm the tag push succeeded, then poll (bounded: at most
@@ -109,7 +127,7 @@ to ask for reconfirmation once `builder:gate` has passed.
109
127
  Stop at that step. Do not proceed. Report which step failed and its output. If the worktree
110
128
  was already created (step 5+), leave it in place if its state is relevant evidence for
111
129
  diagnosing the failure; otherwise remove it (step 9) before reporting, so a retry doesn't
112
- collide with a stale `/tmp/sm-publish-v<version>` path.
130
+ collide with a stale `$HOME/.cache/sm-publish/v<version>` path.
113
131
 
114
132
  ## Output
115
133
 
@@ -1,8 +1,8 @@
1
1
  {
2
- "version": "0.124.0",
3
- "gitSha": "9d159322bc6717eba697e3ff96379bc0876eda3f",
4
- "gitShortSha": "9d159322",
2
+ "version": "0.125.0",
3
+ "gitSha": "3b39163b6942d4b5b636b06dc2d737366b1d78e2",
4
+ "gitShortSha": "3b39163b",
5
5
  "gitBranch": "HEAD",
6
- "builtAt": "2026-10-26T06:06:09.256Z",
6
+ "builtAt": "2026-10-27T02:44:41.942Z",
7
7
  "dirty": false
8
8
  }
@@ -52,6 +52,7 @@ const { resolveEpicEffort, effortArgs } = require('./lib/claude/agentEffortResol
52
52
  const { planEpicSpawn } = require('./lib/epic/epicSpawnPlan.cjs');
53
53
  const epicWorktreeMint = require('./lib/epic/epicWorktreeMint.cjs');
54
54
  const logs = require('./logs.cjs');
55
+ const { feedChildStdin } = require('./lib/process/childStdin.cjs');
55
56
  const { createHeadTailBuffer } = require('./lib/util/headTailBuffer.cjs');
56
57
 
57
58
  // Caps on per-run stream buffers (a run can last hours).
@@ -627,7 +628,7 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
627
628
  // used for the flag-swap retry so both attempts share one construction path.
628
629
  const buildArgs = (resumeFlag) => {
629
630
  const a = [
630
- '-p', fullPrompt,
631
+ '-p',
631
632
  '--model', model,
632
633
  ...effortFlags,
633
634
  '--dangerously-skip-permissions',
@@ -712,9 +713,11 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
712
713
  // One spawn of the CLI. Per-attempt state (buffers, child) lives in here;
713
714
  // everything turn-level (timers, latch, lane) is in the enclosing scope.
714
715
  const startAttempt = (resumeFlag, attemptNo) => {
715
- // Spawn with stdin closed (mirrors scheduler's 'ignore' — prevents the
716
- // "claude -p stdin must be closed" gotcha from kg.cjs). stdout is piped for
717
- // real-time NDJSON streaming; stderr piped for error-message capture.
716
+ // Spawn with a stdin pipe: the prompt is fed on stdin (never argv — E2BIG
717
+ // above 128 KB per argument on Linux, 8191 chars on Windows cmd.exe) and
718
+ // feedChildStdin closes it, satisfying "claude -p stdin must be closed".
719
+ // stdout is piped for real-time NDJSON streaming; stderr piped for
720
+ // error-message capture.
718
721
  let thisChild;
719
722
  try {
720
723
  const chatArgs = buildArgs(resumeFlag);
@@ -724,7 +727,7 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
724
727
  ...(target.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
725
728
  cwd: execCwd,
726
729
  env: childEnv,
727
- stdio: ['ignore', 'pipe', 'pipe'],
730
+ stdio: ['pipe', 'pipe', 'pipe'],
728
731
  ...detachedSpawnOpts(), // own process group so killTree can SIGTERM descendants
729
732
  });
730
733
  } catch (err) {
@@ -739,6 +742,9 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
739
742
  return;
740
743
  }
741
744
  child = thisChild;
745
+ feedChildStdin(thisChild, fullPrompt, {
746
+ log: (m) => logs.writeLine({ scope: 'chatRunner', level: 'warn', message: m }),
747
+ });
742
748
 
743
749
  // ─── Stream parsing ────────────────────────────────────────────────────
744
750
  // stdout is newline-delimited JSON (stream-json format). We buffer partial
@@ -20,6 +20,7 @@ const { spawn } = require('node:child_process');
20
20
  const crypto = require('node:crypto');
21
21
  const { resolveClaudeBin, claudeSpawnTarget } = require('./lib/claude/claudeBin.cjs');
22
22
  const { headlessPermissionArgs, ensureCliCapsProbed } = require('./lib/claude/claudeCliCaps.cjs');
23
+ const { feedChildStdin } = require('./lib/process/childStdin.cjs');
23
24
  const { withProcRole } = require('./lib/process/cleanEnv.cjs');
24
25
  const { extractJson } = require('./lib/util/extractJson.cjs');
25
26
  const { assertInsideHome } = require('./lib/insideHome.cjs');
@@ -94,7 +95,7 @@ async function runClaude(prompt, { model = 'sonnet', timeoutMs = 90_000, systemP
94
95
  let bin;
95
96
  try { bin = resolveClaudeBin(); } catch (e) { resolve({ ok: false, error: `claude not found: ${e?.message}` }); return; }
96
97
  const args = [
97
- '-p', prompt,
98
+ '-p',
98
99
  '--model', model,
99
100
  '--dangerously-skip-permissions',
100
101
  ...headlessPermissionArgs(),
@@ -102,11 +103,13 @@ async function runClaude(prompt, { model = 'sonnet', timeoutMs = 90_000, systemP
102
103
  '--output-format', 'text',
103
104
  ];
104
105
  if (systemPrompt) args.push('--append-system-prompt', systemPrompt);
105
- // stdin MUST be closed ('ignore') — `claude -p` otherwise blocks waiting
106
- // for piped stdin and returns empty. SM_KG_INTERNAL=1 tells the
106
+ // The prompt goes on stdin (argv caps one argument at 128 KB -> E2BIG) and
107
+ // stdin is closed right after the write, never left open — `claude -p`
108
+ // otherwise blocks waiting on it and returns empty. SM_KG_INTERNAL=1 tells the
107
109
  // prompt-logging hook to skip this invocation.
108
110
  const target = claudeSpawnTarget('aux', 'docedit', bin);
109
- const child = spawn(target.command, args, { env: withProcRole({ ...process.env, SM_KG_INTERNAL: '1' }, 'aux'), stdio: ['ignore', 'pipe', 'pipe'], ...(target.argv0 ? { argv0: target.argv0 } : {}) });
111
+ const child = spawn(target.command, args, { env: withProcRole({ ...process.env, SM_KG_INTERNAL: '1' }, 'aux'), stdio: ['pipe', 'pipe', 'pipe'], ...(target.argv0 ? { argv0: target.argv0 } : {}) });
112
+ feedChildStdin(child, prompt);
110
113
  let out = '';
111
114
  let err = '';
112
115
  // Cap accumulated stdout — the model output shouldn't grow `out` without
@@ -250,6 +253,7 @@ module.exports = {
250
253
  parseDocEdit,
251
254
  editPrompt,
252
255
  runDocEdit,
256
+ runClaude,
253
257
  docEditViaSession,
254
258
  MAX_DOC_CONTEXT,
255
259
  truncateDocumentText,
@@ -252,7 +252,10 @@ const promptSessionsMergeToMain = z.object({
252
252
  // integrateEpicBranch can apply the same carried-wip-only skip the job
253
253
  // path already gets. Absent on a worktree created before this field
254
254
  // existed, or one whose base tree was clean at creation.
255
- carriedPaths: z.array(z.string().min(1).max(4096)).max(2000).optional(),
255
+ // The renderer sends the Epic's full carried diff (`git diff HEAD --name-only`),
256
+ // which is uncapped; 3,306 paths were seen on 2026-10-26. A tight cap rejects
257
+ // every merge for such an Epic.
258
+ carriedPaths: z.array(z.string().min(1).max(4096)).max(100000).optional(),
256
259
  });
257
260
 
258
261
  // Renderer entry points for the per-project "disable Epic worktree
@@ -18,6 +18,7 @@
18
18
  const { spawn } = require('node:child_process');
19
19
  const { resolveClaudeBin, claudeSpawnTarget } = require('./claude/claudeBin.cjs');
20
20
  const { cleanChildEnv, pathWithUserBins } = require('./process/cleanEnv.cjs');
21
+ const { feedChildStdin } = require('./process/childStdin.cjs');
21
22
 
22
23
  const CLASSIFY_TIMEOUT_MS = 30_000;
23
24
  const CLASSIFY_MODEL = 'sonnet';
@@ -61,8 +62,10 @@ function classifyPromptTicket(text) {
61
62
  return;
62
63
  }
63
64
  const childEnv = cleanChildEnv({ PATH: pathWithUserBins() });
65
+ // The prompt goes on stdin, never argv: a >128 KB argv element fails spawn with E2BIG.
66
+ const prompt = `${CLASSIFY_PROMPT_PREFIX}${text}`;
64
67
  const args = [
65
- '-p', `${CLASSIFY_PROMPT_PREFIX}${text}`,
68
+ '-p',
66
69
  '--model', CLASSIFY_MODEL,
67
70
  '--strict-mcp-config',
68
71
  '--output-format', 'text',
@@ -78,9 +81,11 @@ function classifyPromptTicket(text) {
78
81
  };
79
82
 
80
83
  try {
81
- // stdin closed — `claude -p` otherwise blocks waiting for piped stdin.
84
+ // stdin is a pipe fed with the prompt and then ended (feedChildStdin), so
85
+ // `claude -p` neither blocks on stdin nor sees an oversized argv element.
82
86
  const target = claudeSpawnTarget('aux', 'classify', claudeBin);
83
- child = spawnImpl(target.command, args, { env: childEnv, stdio: ['ignore', 'pipe', 'pipe'], ...(target.argv0 ? { argv0: target.argv0 } : {}) });
87
+ child = spawnImpl(target.command, args, { env: childEnv, stdio: ['pipe', 'pipe', 'pipe'], ...(target.argv0 ? { argv0: target.argv0 } : {}) });
88
+ feedChildStdin(child, prompt);
84
89
  } catch {
85
90
  finish('inline');
86
91
  return;
@@ -115,4 +120,4 @@ function classifyPromptTicket(text) {
115
120
  });
116
121
  }
117
122
 
118
- module.exports = { classifyPromptTicket, parseVerdict, CLASSIFY_TIMEOUT_MS, __setSpawnForTest };
123
+ module.exports = { classifyPromptTicket, CLASSIFY_PROMPT_PREFIX, parseVerdict, CLASSIFY_TIMEOUT_MS, __setSpawnForTest };
@@ -7,8 +7,9 @@
7
7
  * cost-gated synthesis caller (Memory Clusters, the Project Brief) shares one
8
8
  * spawn/timeout/output-cap implementation instead of forking it per feature.
9
9
  *
10
- * Pattern: stdin closed ('ignore' — `claude -p` blocks forever waiting for
11
- * piped stdin otherwise), model pinned explicitly by the caller, hard
10
+ * Pattern: the prompt is written to stdin (never argv — Linux caps one
11
+ * argument at 128 KB, E2BIG) and stdin is closed right after the write, never
12
+ * left open (`claude -p` blocks forever waiting on an open pipe), model pinned explicitly by the caller, hard
12
13
  * timeout that resolves {ok:false} rather than hanging, SM_KG_INTERNAL=1 so
13
14
  * the prompt-logging hook skips these internal calls, and an output-size cap
14
15
  * so a runaway response can't grow `out` unbounded.
@@ -17,6 +18,7 @@
17
18
  const { spawn } = require('node:child_process');
18
19
  const { resolveClaudeBin, claudeSpawnTarget } = require('./claudeBin.cjs');
19
20
  const { headlessPermissionArgs, ensureCliCapsProbed } = require('./claudeCliCaps.cjs');
21
+ const { feedChildStdin } = require('../process/childStdin.cjs');
20
22
  const { withProcRole } = require('../process/cleanEnv.cjs');
21
23
 
22
24
  const MAX_OUT_BYTES = 8 * 1024 * 1024;
@@ -28,7 +30,7 @@ async function runClaudeP(prompt, { model = 'sonnet', timeoutMs = 180_000, syste
28
30
  let bin;
29
31
  try { bin = resolveClaudeBin(); } catch (e) { resolve({ ok: false, error: `claude not found: ${e?.message}` }); return; }
30
32
  const args = [
31
- '-p', prompt,
33
+ '-p',
32
34
  '--model', model,
33
35
  '--dangerously-skip-permissions',
34
36
  ...headlessPermissionArgs(),
@@ -37,7 +39,8 @@ async function runClaudeP(prompt, { model = 'sonnet', timeoutMs = 180_000, syste
37
39
  ];
38
40
  if (systemPrompt) args.push('--append-system-prompt', systemPrompt);
39
41
  const target = claudeSpawnTarget('aux', 'runclaudep', bin);
40
- const child = spawn(target.command, args, { env: withProcRole({ ...process.env, SM_KG_INTERNAL: '1' }, 'aux'), stdio: ['ignore', 'pipe', 'pipe'], ...(target.argv0 ? { argv0: target.argv0 } : {}) });
42
+ const child = spawn(target.command, args, { env: withProcRole({ ...process.env, SM_KG_INTERNAL: '1' }, 'aux'), stdio: ['pipe', 'pipe', 'pipe'], ...(target.argv0 ? { argv0: target.argv0 } : {}) });
43
+ feedChildStdin(child, prompt);
41
44
  let out = '';
42
45
  let err = '';
43
46
  let killedForSize = false;
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * childStdin.cjs — write a prompt/text to a child's stdin and close it.
5
+ *
6
+ * Why prompts go on stdin, not argv: Linux caps a single argument at
7
+ * MAX_ARG_STRLEN (131072 bytes) and Windows cmd.exe caps a command line at
8
+ * 8191 characters; a larger argv element fails the spawn with E2BIG
9
+ * (incident 2026-10-26, spawn E2BIG). Ending the pipe also satisfies the rule
10
+ * that `claude -p`'s stdin must be closed (claude CLI 2.1.302 read a 204 KB
11
+ * prompt from stdin and exited 0).
12
+ *
13
+ * The EPIPE handling lives here, in one place: a child that exits without
14
+ * reading raises an 'error' on the pipe; it is logged once and swallowed.
15
+ */
16
+
17
+ /**
18
+ * @param {{ stdin?: any } | null | undefined} child
19
+ * @param {string} text
20
+ * @param {{ log?: (msg: string) => void }} [opts]
21
+ * @returns {boolean} true when the text was handed to child.stdin
22
+ */
23
+ function feedChildStdin(child, text, { log } = {}) {
24
+ if (!child || !child.stdin) return false;
25
+ let logged = false;
26
+ // Listener FIRST: the write can fail asynchronously with EPIPE.
27
+ child.stdin.on('error', (err) => {
28
+ if (logged) return;
29
+ logged = true;
30
+ if (typeof log !== 'function') return;
31
+ try {
32
+ log(`[childStdin] stdin write failed (${(err && (err.code || err.message)) || 'unknown'}): the child exited before reading its input\n`);
33
+ } catch { /* logging must never throw */ }
34
+ });
35
+ child.stdin.end(text, 'utf8');
36
+ return true;
37
+ }
38
+
39
+ module.exports = { feedChildStdin };
@@ -53,6 +53,7 @@
53
53
  const fs = require('node:fs');
54
54
  const { spawn, execFile } = require('node:child_process');
55
55
  const { killTree } = require('./killTree.cjs');
56
+ const { feedChildStdin } = require('./childStdin.cjs');
56
57
 
57
58
  // Injectable for tests (enumeration-failure coverage).
58
59
  const deps = { execFile };
@@ -205,6 +206,16 @@ function sweepChildProcessGroup(child, detached, safeLog) {
205
206
  .then((leaked) => { kill(); return leaked; });
206
207
  }
207
208
 
209
+ /**
210
+ * Spawn a child with its stdout/stderr on the log fd and supervise it.
211
+ *
212
+ * `spawn.stdinText` (optional string): when given, the child's stdin is a pipe
213
+ * that receives the text (UTF-8) and is then closed; otherwise stdin is
214
+ * 'ignore'. It exists because argv is a poor channel for large input: Linux
215
+ * caps a single argument at MAX_ARG_STRLEN (131072 bytes, spawn E2BIG) and
216
+ * Windows cmd.exe caps a command line at 8191 characters. A stdin error
217
+ * (EPIPE when the child never reads) is logged once and otherwise ignored.
218
+ */
208
219
  function withChildAndLog({ fd, logPath, safeLog, closeFd, spawn: spawnSpec, watchdogs = [], onExit }) {
209
220
  const startedAt = Date.now();
210
221
 
@@ -296,11 +307,12 @@ function withChildAndLog({ fd, logPath, safeLog, closeFd, spawn: spawnSpec, watc
296
307
 
297
308
  // Attempt synchronous spawn. On failure, call handleDone immediately so the
298
309
  // caller's onExit can resolve any pending Promise before we return null.
310
+ const stdinText = typeof spawnSpec.stdinText === 'string' ? spawnSpec.stdinText : null;
299
311
  let child;
300
312
  try {
301
313
  child = spawn(spawnSpec.command, spawnSpec.args, {
302
314
  ...spawnSpec.options,
303
- stdio: ['ignore', fd, fd],
315
+ stdio: [stdinText !== null ? 'pipe' : 'ignore', fd, fd],
304
316
  });
305
317
  } catch (e) {
306
318
  handleDone(-1, null, e, /* spawnFailed */ true);
@@ -309,6 +321,8 @@ function withChildAndLog({ fd, logPath, safeLog, closeFd, spawn: spawnSpec, watc
309
321
 
310
322
  ctx.child = child;
311
323
 
324
+ if (stdinText !== null) feedChildStdin(child, stdinText, { log: safeLog });
325
+
312
326
  child.on('error', (err) => handleDone(-1, null, err, false));
313
327
  child.on('exit', (code, signal) => handleDone(code, signal, undefined, false));
314
328
 
@@ -57,6 +57,38 @@ const LAUNCH_FAILURE_KINDS = Object.freeze({
57
57
  API_ERROR: 'api_error',
58
58
  });
59
59
 
60
+ /**
61
+ * Argv-size guard (incident 2026-10-26, social-signals-trader): an oversized
62
+ * `-p` argument made `spawn` fail with E2BIG (exit -1 in ~86 ms), which
63
+ * became `impl_failed:exit_-1` and spawned a fix-plan that failed the same
64
+ * way. Linux caps ONE argv string at MAX_ARG_STRLEN = 131072 bytes, counting
65
+ * the NUL terminator. The cause is one job's input, not the persona's
66
+ * environment, so this never arms the per-persona launch breaker.
67
+ */
68
+ const MAX_ARG_BYTES = 131072;
69
+
70
+ /** First argv element whose utf8 bytes + NUL exceed `limit`, else null. */
71
+ function findOversizedArg(args, limit = MAX_ARG_BYTES) {
72
+ if (!Array.isArray(args)) return null;
73
+ for (let index = 0; index < args.length; index++) {
74
+ const a = args[index];
75
+ if (typeof a !== 'string') continue;
76
+ const bytes = Buffer.byteLength(a, 'utf8') + 1;
77
+ if (bytes <= limit) continue;
78
+ let flag = '(positional)';
79
+ for (let j = index - 1; j >= 0; j--) {
80
+ if (typeof args[j] === 'string' && args[j].startsWith('--')) { flag = args[j]; break; }
81
+ }
82
+ return { index, flag, bytes };
83
+ }
84
+ return null;
85
+ }
86
+
87
+ /** True when a spawn error message is the OS refusing an oversized argv. */
88
+ function isArgvTooLargeSpawnError(message) {
89
+ return /E2BIG|ENAMETOOLONG/.test(String(message ?? ''));
90
+ }
91
+
60
92
  /** Tail bytes scanned for the `result` event — same budget classifyRunOutcome uses. */
61
93
  const RESULT_TAIL_BYTES = 65536;
62
94
 
@@ -310,9 +342,10 @@ function evaluateLaunchGate(block, { now, claudeVersion } = {}) {
310
342
  * closed set so operators never have to open a transcript to tell a
311
343
  * non-start from an implementation failure from a verifier downgrade.
312
344
  */
313
- function deriveTerminalReason({ effectiveStatus, exitCode, verifyResult, sigtermOverride, worktreeIntegrationFailure, budgetKill }) {
345
+ function deriveTerminalReason({ effectiveStatus, exitCode, verifyResult, sigtermOverride, worktreeIntegrationFailure, budgetKill, launchError }) {
314
346
  if (worktreeIntegrationFailure) return 'worktree_integration_failed';
315
347
  if (effectiveStatus === 'completed') return 'completed';
348
+ if (launchError === 'argv_too_large') return 'launch_failure:argv_too_large';
316
349
  if (budgetKill) return 'budget_exceeded';
317
350
  if (sigtermOverride) return 'signal_kill_with_commit';
318
351
  if (exitCode === 143 || exitCode === 137) return 'signal_kill';
@@ -338,6 +371,9 @@ function writeOutcomeSidecar(runDir, slug, outcome) {
338
371
  }
339
372
 
340
373
  module.exports = {
374
+ MAX_ARG_BYTES,
375
+ findOversizedArg,
376
+ isArgvTooLargeSpawnError,
341
377
  LAUNCH_FAILURE_KINDS,
342
378
  LAUNCH_BLOCK_MAX_ATTEMPTS,
343
379
  LAUNCH_PROBE_STALE_MS,