@rol1n/pi-kit 0.0.0-stage → 0.1.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 (136) hide show
  1. package/README.md +98 -2
  2. package/extensions/ask-u-questions/README.md +32 -0
  3. package/extensions/ask-u-questions/answers.ts +53 -0
  4. package/extensions/ask-u-questions/dialog-ui.ts +57 -0
  5. package/extensions/ask-u-questions/index.ts +88 -0
  6. package/extensions/ask-u-questions/multi-choice.ts +260 -0
  7. package/extensions/ask-u-questions/results.ts +83 -0
  8. package/extensions/ask-u-questions/schema.ts +17 -0
  9. package/extensions/ask-u-questions/single-choice.ts +182 -0
  10. package/extensions/ask-u-questions/tool-renderers.ts +69 -0
  11. package/extensions/ask-u-questions/types.ts +45 -0
  12. package/extensions/ask-u-questions/ui-lock.ts +24 -0
  13. package/extensions/continue-signal/README.md +28 -0
  14. package/extensions/continue-signal/index.ts +51 -0
  15. package/extensions/continue-signal/renderer.ts +21 -0
  16. package/extensions/custom-providers/README.md +28 -0
  17. package/extensions/custom-providers/asr/handlers/dashscope.ts +154 -0
  18. package/extensions/custom-providers/asr/index.ts +37 -0
  19. package/extensions/custom-providers/asr/models.ts +18 -0
  20. package/extensions/custom-providers/asr/types.ts +20 -0
  21. package/extensions/custom-providers/chat/index.ts +33 -0
  22. package/extensions/custom-providers/chat/magpie.ts +258 -0
  23. package/extensions/custom-providers/chat/models.ts +111 -0
  24. package/extensions/custom-providers/classifier/index.ts +6 -0
  25. package/extensions/custom-providers/classifier/models.ts +1 -0
  26. package/extensions/custom-providers/image/handlers/dashscope.ts +240 -0
  27. package/extensions/custom-providers/image/handlers/openrouter.ts +185 -0
  28. package/extensions/custom-providers/image/index.ts +96 -0
  29. package/extensions/custom-providers/image/models.ts +71 -0
  30. package/extensions/custom-providers/image/types.ts +22 -0
  31. package/extensions/custom-providers/index.ts +50 -0
  32. package/extensions/custom-providers/types.ts +17 -0
  33. package/extensions/image-generate/README.md +29 -0
  34. package/extensions/image-generate/files.ts +49 -0
  35. package/extensions/image-generate/index.ts +200 -0
  36. package/extensions/image-generate/model-selector.ts +150 -0
  37. package/extensions/image-generate/models.ts +19 -0
  38. package/extensions/memory-log/README.md +54 -0
  39. package/extensions/memory-log/agent/consolidator/prompt.ts +47 -0
  40. package/extensions/memory-log/agent/consolidator/tools.ts +160 -0
  41. package/extensions/memory-log/agent/cost.ts +32 -0
  42. package/extensions/memory-log/agent/index.ts +42 -0
  43. package/extensions/memory-log/agent/observer/prompt.ts +73 -0
  44. package/extensions/memory-log/agent/observer/tool.ts +73 -0
  45. package/extensions/memory-log/commands/compact.ts +35 -0
  46. package/extensions/memory-log/commands/consolidate.ts +47 -0
  47. package/extensions/memory-log/commands/status.ts +50 -0
  48. package/extensions/memory-log/config.ts +253 -0
  49. package/extensions/memory-log/debug-log.ts +76 -0
  50. package/extensions/memory-log/hooks/compaction-hook.ts +161 -0
  51. package/extensions/memory-log/hooks/compaction-trigger.ts +160 -0
  52. package/extensions/memory-log/hooks/consolidator-trigger.ts +143 -0
  53. package/extensions/memory-log/hooks/observer-trigger.ts +176 -0
  54. package/extensions/memory-log/ids.ts +78 -0
  55. package/extensions/memory-log/index.ts +149 -0
  56. package/extensions/memory-log/ledger/fold.ts +77 -0
  57. package/extensions/memory-log/ledger/index.ts +7 -0
  58. package/extensions/memory-log/ledger/pool.ts +38 -0
  59. package/extensions/memory-log/ledger/progress.ts +181 -0
  60. package/extensions/memory-log/ledger/projection.ts +115 -0
  61. package/extensions/memory-log/ledger/render.ts +38 -0
  62. package/extensions/memory-log/ledger/serialize.ts +161 -0
  63. package/extensions/memory-log/ledger/types.ts +201 -0
  64. package/extensions/memory-log/memory/index-render.ts +45 -0
  65. package/extensions/memory-log/memory/paths.ts +120 -0
  66. package/extensions/memory-log/memory/session.ts +64 -0
  67. package/extensions/memory-log/runtime.ts +127 -0
  68. package/extensions/memory-log/spawn/launch.ts +133 -0
  69. package/extensions/memory-log/spawn/runs.ts +83 -0
  70. package/extensions/memory-log/tokens.ts +66 -0
  71. package/extensions/memory-log/ui/status-controller.ts +225 -0
  72. package/extensions/memory-log/ui/timeline.ts +103 -0
  73. package/extensions/model-vision/README.md +28 -0
  74. package/extensions/multi-harness/README.md +63 -0
  75. package/extensions/multi-harness/adapters.mjs +72 -0
  76. package/extensions/multi-harness/harness-done.mjs +14 -0
  77. package/extensions/multi-harness/index.ts +375 -0
  78. package/extensions/multi-harness/ipc.mjs +37 -0
  79. package/extensions/multi-harness/opencode-plugin.mjs +99 -0
  80. package/extensions/multi-harness/process-runner.mjs +115 -0
  81. package/extensions/multi-harness/turn-hook.mjs +29 -0
  82. package/extensions/multi-harness/worker-bridge.ts +52 -0
  83. package/extensions/oai-subquota/README.md +30 -0
  84. package/extensions/oai-subquota/api.ts +27 -0
  85. package/extensions/oai-subquota/auth.ts +34 -0
  86. package/extensions/oai-subquota/cache.ts +30 -0
  87. package/extensions/oai-subquota/index.ts +276 -0
  88. package/extensions/oai-subquota/snapshot.ts +167 -0
  89. package/extensions/oai-subquota/types.ts +40 -0
  90. package/extensions/oai-subquota/ui.ts +183 -0
  91. package/extensions/prompt-snipaste/README.md +33 -0
  92. package/extensions/prompt-snipaste/index.ts +143 -0
  93. package/extensions/prompt-snipaste/menu.ts +159 -0
  94. package/extensions/prompt-snipaste/renderers.ts +88 -0
  95. package/extensions/prompt-snipaste/snippets.ts +203 -0
  96. package/extensions/prompt-snipaste/types.ts +19 -0
  97. package/extensions/screenshot/README.md +25 -0
  98. package/extensions/srp-theme/README.md +24 -0
  99. package/extensions/srp-theme/footer.ts +194 -0
  100. package/extensions/srp-theme/index.ts +172 -0
  101. package/extensions/srp-theme/tps-meter.ts +442 -0
  102. package/extensions/subagent/README.md +35 -0
  103. package/extensions/subagent/bridge.ts +171 -0
  104. package/extensions/subagent/index.ts +590 -0
  105. package/extensions/subagent/mux.ts +621 -0
  106. package/extensions/subagent/session.ts +166 -0
  107. package/extensions/subagent/skills/subagent/SKILL.md +70 -0
  108. package/extensions/subagent/ui.ts +100 -0
  109. package/extensions/tui-asr/README.md +28 -0
  110. package/extensions/tui-asr/audio.ts +40 -0
  111. package/extensions/tui-asr/clipboard.ts +30 -0
  112. package/extensions/tui-asr/dictation.ts +205 -0
  113. package/extensions/tui-asr/index.ts +181 -0
  114. package/extensions/tui-asr/input.ts +58 -0
  115. package/extensions/tui-asr/model-selector.ts +163 -0
  116. package/extensions/tui-asr/providers.ts +25 -0
  117. package/extensions/tui-asr/status.ts +79 -0
  118. package/extensions/web-reach/README.md +37 -0
  119. package/extensions/web-reach/html.ts +83 -0
  120. package/extensions/web-reach/index.ts +82 -0
  121. package/extensions/web-reach/page.ts +132 -0
  122. package/extensions/web-reach/pdf.ts +42 -0
  123. package/extensions/web-reach/reader.ts +42 -0
  124. package/extensions/web-reach/rsc.ts +161 -0
  125. package/extensions/web-reach/search.ts +85 -0
  126. package/extensions/web-reach/transport.ts +35 -0
  127. package/extensions/windows-wheel-fix/README.md +21 -0
  128. package/extensions/windows-wheel-fix/index.ts +44 -0
  129. package/package.json +93 -4
  130. package/prompts/ask-questions.md +8 -0
  131. package/prompts/delegate-exploration.md +7 -0
  132. package/prompts/diagnose-report.md +7 -0
  133. package/prompts/frontend-tech-preset.md +18 -0
  134. package/prompts/orchestrator-mode.md +7 -0
  135. package/prompts/session-kickoff.md +7 -0
  136. package/prompts/verify-not-assume.md +7 -0
@@ -0,0 +1,103 @@
1
+ import type { Config } from "../config.ts";
2
+ import {
3
+ entryIndexById,
4
+ foldLedger,
5
+ isObservationsRecordedEntry,
6
+ rawTokensAfterIndex,
7
+ rawTokensSinceObservationCoverage,
8
+ type Entry,
9
+ } from "../ledger/index.ts";
10
+
11
+ /** Glyphs for the timeline strip. */
12
+ const GLYPH = {
13
+ consolidated: "▓", // observations promoted to .memory (long-term)
14
+ partial: "▚", // chunk straddling the pool-target boundary (some obs promoted)
15
+ pool: "▒", // observed, still in the short-term buffer
16
+ raw: "░", // raw history not yet distilled into observations
17
+ cut: "┊", // compaction cutoff (verbatim tail began here)
18
+ tip: "▶", // live branch tip
19
+ } as const;
20
+
21
+ /** A committed observation chunk, in branch order. */
22
+ type Chunk = { coversUpToIndex: number; timestamps: string[] };
23
+
24
+ function fmtK(tokens: number): string {
25
+ if (tokens < 1000) return `${tokens}`;
26
+ return `${(tokens / 1000).toFixed(1)}k`;
27
+ }
28
+
29
+ function collectChunks(branch: Entry[]): Chunk[] {
30
+ const indexById = entryIndexById(branch);
31
+ const chunks: Chunk[] = [];
32
+ for (const entry of branch) {
33
+ if (!isObservationsRecordedEntry(entry)) continue;
34
+ const coversUpToIndex = indexById.get(entry.data.coversUpToId);
35
+ if (coversUpToIndex === undefined) continue;
36
+ chunks.push({ coversUpToIndex, timestamps: entry.data.observations.map((o) => o.timestamp) });
37
+ }
38
+ return chunks;
39
+ }
40
+
41
+ /** Branch indices of every compaction entry's `firstKeptEntryId` (the verbatim-tail cutoffs). */
42
+ function compactionCutIndices(branch: Entry[]): number[] {
43
+ const indexById = entryIndexById(branch);
44
+ const cuts: number[] = [];
45
+ for (const entry of branch) {
46
+ if (entry.type !== "compaction") continue;
47
+ const idx = entry.firstKeptEntryId ? indexById.get(entry.firstKeptEntryId) : undefined;
48
+ if (idx !== undefined) cuts.push(idx);
49
+ }
50
+ return cuts;
51
+ }
52
+
53
+ function chunkGlyph(chunk: Chunk, dropped: Set<string>): string {
54
+ let droppedCount = 0;
55
+ for (const ts of chunk.timestamps) if (dropped.has(ts)) droppedCount++;
56
+ if (droppedCount === 0) return GLYPH.pool;
57
+ if (droppedCount === chunk.timestamps.length) return GLYPH.consolidated;
58
+ return GLYPH.partial;
59
+ }
60
+
61
+ function wrap(cells: string[], width: number): string {
62
+ if (cells.length === 0) return "";
63
+ const rows: string[] = [];
64
+ for (let i = 0; i < cells.length; i += width) rows.push(cells.slice(i, i + width).join(""));
65
+ return rows.join("\n");
66
+ }
67
+
68
+ /**
69
+ * Render the full session as a horizontal strip.
70
+ */
71
+ export function renderTimeline(branch: Entry[], config: Config, width = 60): string {
72
+ const folded = foldLedger(branch);
73
+ const dropped = folded.droppedObservationTimestamps;
74
+ const chunks = collectChunks(branch);
75
+
76
+ const cells: string[] = chunks.map((chunk) => chunkGlyph(chunk, dropped));
77
+ const tailTokens = rawTokensSinceObservationCoverage(branch);
78
+ const tailCells = tailTokens > 0 ? Math.ceil(tailTokens / config.chunkTokens) : 0;
79
+ for (let i = 0; i < tailCells; i++) cells.push(GLYPH.raw);
80
+
81
+ const cutPositions = compactionCutIndices(branch)
82
+ .map((cutIdx) => {
83
+ let pos = 0;
84
+ for (const chunk of chunks) if (chunk.coversUpToIndex < cutIdx) pos++;
85
+ return Math.min(pos, cells.length);
86
+ })
87
+ .sort((a, b) => b - a);
88
+ for (const pos of cutPositions) cells.splice(pos, 0, GLYPH.cut);
89
+
90
+ const consolidatedChunks = chunks.filter((c) => chunkGlyph(c, dropped) === GLYPH.consolidated).length;
91
+ const poolChunks = chunks.filter((c) => chunkGlyph(c, dropped) === GLYPH.pool).length;
92
+ const rawTotal = rawTokensAfterIndex(branch, -1);
93
+ const compactions = cutPositions.length;
94
+
95
+ const strip = cells.length > 0 ? `${wrap(cells, width)}${GLYPH.tip}` : "(timeline empty)";
96
+
97
+ return [
98
+ `memory-log timeline · 1 cell ≈ ${fmtK(config.chunkTokens)} tok · cumulative ${fmtK(rawTotal)} raw · ${compactions} compaction${compactions === 1 ? "" : "s"}`,
99
+ strip,
100
+ ``,
101
+ ` ${GLYPH.consolidated} .memory (${consolidatedChunks}) ${GLYPH.pool} pool (${poolChunks}) ${GLYPH.raw} raw ${GLYPH.cut} compaction cut ${GLYPH.tip} tip`,
102
+ ].join("\n");
103
+ }
@@ -0,0 +1,28 @@
1
+ # model-vision
2
+
3
+ Analyze a local image or public image URL with a vision model. Registers the `model_vision` tool for OCR, UI inspection, and image questions.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pi install npm:@rol1n/model-vision
9
+ ```
10
+
11
+ Available independently; it is not enabled in the aggregate package by default. The extension entrypoint is `extensions/model-vision/index.ts`; `analysis.ts` coordinates image loading (`image.ts`), network validation (`network.ts`), and model requests (`requests.ts`).
12
+
13
+ ## Use
14
+
15
+ ```json
16
+ {
17
+ "image": "./screenshots/error.png",
18
+ "prompt": "Read the error message and describe the relevant UI."
19
+ }
20
+ ```
21
+
22
+ Supported image formats are PNG, JPEG, WebP, GIF, and BMP. Local paths resolve from the working directory. Remote images must use public HTTP(S) addresses; private-network targets are rejected. Images are limited to 10 MiB and requests have a 90-second timeout.
23
+
24
+ ## Model configuration
25
+
26
+ The current fallback chain is declared in `DEFAULT_VISION_MODELS` in `extensions/model-vision/models.ts`: Antigravity `gemini-3.8-flash` through Pi's native API, followed by SHUAI API `gpt-6-luna` through OpenAI completions. Change that source list to choose a different chain; there is currently no settings key or tool argument for choosing the vision model.
27
+
28
+ Install and authenticate the provider integrations you select. Antigravity is an external integration; SHUAI registration is available in [custom-providers](https://github.com/RolinShmily/pi-kit/tree/main/extensions/custom-providers). The tool returns extracted text and the provider/model used.
@@ -0,0 +1,63 @@
1
+ # Multi Harness
2
+
3
+ A small delegation runtime with one tool, `multi_harness`. `pi`, `codex`, `claude`, and `opencode` are equal CLI peers. The original `subagent` extension remains a separate, lean Pi-only tool.
4
+
5
+ The runtime accepts **one named worker and an existing cwd**. Those decisions belong to the caller. An independently installed `worktree-studio` skill can coordinate isolated implementations and their acceptance review.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pi install npm:@rol1n/multi-harness
11
+ ```
12
+
13
+ Also included in `@rol1n/pi-kit`. Enable one copy of the extension.
14
+
15
+ ## Use
16
+
17
+ Load `extensions/multi-harness` through the aggregate package, or run from this repository:
18
+
19
+ ```sh
20
+ pi --no-extensions -e ./extensions/multi-harness/index.ts
21
+ ```
22
+
23
+ Example tool calls:
24
+
25
+ ```json
26
+ {"action":"spawn","name":"reviewer","harness":"claude","cwd":"/existing/project","task":"Review error handling without changing files. Report concrete findings with file paths."}
27
+ {"action":"spawn","name":"implementer","harness":"pi","cwd":"/existing/other-directory","task":"Implement the approved fix and run the specified checks.","model":"provider/model"}
28
+ {"action":"spawn","name":"opencode-reviewer","harness":"opencode","cwd":"/existing/project","task":"Review the changes and report evidence without editing files."}
29
+ {"action":"send","target":"reviewer","task":"Review the revised implementation in the same session."}
30
+ {"action":"status"}
31
+ {"action":"stop","target":"implementer"}
32
+ {"action":"close","target":"reviewer"}
33
+ ```
34
+
35
+ - `spawn` is the default action; `name`, `harness`, and `task` are required. `cwd` defaults to the parent directory and must already exist. Names are reserved for the session, including after closing.
36
+ - `model` is optional and passed to the selected CLI. Pi defaults to the parent model; external CLIs use their installed defaults. Only the selected executable is checked on demand.
37
+ - `spawn` and `send` return after dispatch. **Turn completion is pushed to the real parent Pi session via `pi.sendMessage`**, not by a background wrapper writing a log. Do not poll terminals or run waiting shell loops.
38
+ - With tmux/Zellij, each worker runs its **native interactive TUI**. Turn settlement leaves the application open and idle. `send` continues that same application; `close` exits idle TUIs. `stop` terminates active work and its runtime, preserving files.
39
+ - Without a supported mux, workers use silent child processes. `send` starts another invocation that resumes the same persisted CLI session. Set `PI_MULTI_HARNESS_BACKEND=process` to force this fallback. This is not a live TUI in headless mode.
40
+ - Pi follow-ups use a file inbox and native `pi.sendUserMessage`. OpenCode uses a local server plugin, its native SDK inbox, and the same visible TUI session (no key injection). Codex uses its reported thread ID with `codex queue`; when no ID is available, mux input uses bracketed paste. Claude uses idle-only bracketed paste. Avoid simultaneous human typing and coordinator dispatch; startup trust/permission dialogs may require manual attention. Zellij input transport still requires validation with your installed Zellij/terminal build.
41
+
42
+ ## Notifications and state
43
+
44
+ Artifacts live in `~/.pi/agent/multi-harness/<worker-id>/`: task prompts, CLI session data/IDs, console/event logs in process mode, completion records, and short TUI screen captures. Working files remain in the caller's `cwd`. No automatic Git report is generated.
45
+
46
+ `harness-done "summary"` is injected into the worker PATH; `studio-done` is a compatibility alias. It records a proposed summary, **not an early completion event**. Native Codex/Claude hooks, OpenCode's root-session idle events, Pi's `agent_settled`, or the headless process exit publish the authoritative completion. This avoids racing a still-running CLI or notifying twice. Every dispatch has a task ID; stale and duplicate completions are ignored.
47
+
48
+ The parent subscribes to directory `fs.watch` events, with no periodic terminal capture or completion polling. Screen capture happens once per completed turn. Watcher failures are surfaced rather than silently claiming delivery. A worker's final text is a claim: independently review its files and test results.
49
+
50
+ Default turn timeout: 15 minutes (`maxMinutes`: 1–120). Headless workers are stopped on timeout. A native TUI stays open and cannot receive a new dispatch until it settles or is stopped. Timeouts do not cap tokens or spending; missing usage/pricing is not zero cost.
51
+
52
+ Session entries record only worker artifact locations. On session restore/reload, the extension reattaches notification watchers for saved workers. A `controller.lock` prevents another session from controlling the same worker concurrently. After an unclean crash, verify the recorded owner PID is no longer running before manually removing that lock. Normal shutdown releases watchers/locks and stops active tasks; idle TUIs remain open and can be recovered by resuming the parent session.
53
+
54
+ ## Permissions and platform boundaries
55
+
56
+ - This tool dispatches directly; user confirmation for expensive or risky workflows belongs to the caller/skill. It is not a sandbox.
57
+ - Per the configured workflow, Codex runs with `--sandbox danger-full-access -a never`, and Claude with `--permission-mode bypassPermissions`. Workers have the invoking user's filesystem/network privileges. Headless Claude has a $0.50 CLI API budget; interactive mode does not impose that limit. Use only in trusted environments.
58
+ - OpenCode uses `--auto`: automatically approve requests not explicitly denied by its configuration. Existing deny rules remain effective. Native mode injects the dependency-free local `opencode-plugin.mjs` through a merged `OPENCODE_CONFIG_CONTENT`; it never edits project/global config files or installs packages. Existing content must be valid JSON. Headless mode uses `opencode run --format json` and resumes by the exact reported `--session` ID. The adapter targets the OpenCode 1.x CLI/plugin API (source-checked against v1.19.3); 2.x API compatibility is not assumed. Install and authenticate the CLI yourself. DeepSeek Harness (`dsh`) is not integrated.
59
+ - Pi's project-resource trust flags apply only to loading project resources, not filesystem access. A different cwd does not inherit the parent's project trust automatically.
60
+ - Windows requires PowerShell 7 (`pwsh`) for external CLI invocation. Encoded PowerShell commands preserve Unicode, multiline tasks, JSON, and quoted paths. CLI hooks and `harness-done.cmd`/`.ps1` wrappers use the same native Windows Node installation. A mux must be available for visible TUIs; otherwise process fallback applies.
61
+ - Existing worktrees, reports, and sessions created by the removed `worktree-studio` extension are left untouched. Its old `the_road_not_taken` tool and two-route manifests are not imported into this runtime. Synchronize platform resources/reload before using the new tool; old running workers should be finished before upgrading.
62
+
63
+ Validation scripts are kept outside the repository. Check extension loading from this repository with `pi --no-extensions -e ./extensions/multi-harness/index.ts --list-models '<filter>'`.
@@ -0,0 +1,72 @@
1
+ import { dirname, join } from "node:path";
2
+ import { fileURLToPath, pathToFileURL } from "node:url";
3
+
4
+ export const HARNESSES = Object.freeze(["pi", "codex", "claude", "opencode"]);
5
+ const extensionDir = dirname(fileURLToPath(import.meta.url));
6
+ export const quotePs = (value) => `'${value.replaceAll("'", "''")}'`;
7
+ const quoteSh = (value) => `'${value.replaceAll("'", "'\\''")}'`;
8
+
9
+ /** Avoid cmd/npm shim argument re-parsing and preserve Unicode and JSON on Windows. */
10
+ export function commandSpec(argv, platform = process.platform) {
11
+ if (platform !== "win32") return { command: argv[0], args: argv.slice(1) };
12
+ const script = `$OutputEncoding = [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); & ${argv.map(quotePs).join(" ")}; if ($null -eq $LASTEXITCODE) { exit 1 }; exit $LASTEXITCODE`;
13
+ return { command: "pwsh", args: ["-NoProfile", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")] };
14
+ }
15
+
16
+ /** Add a dependency-free local plugin for native OpenCode only; never rewrite user config. */
17
+ export function buildEnv(config, inherited = process.env) {
18
+ if (config.harness !== "opencode" || !config.interactive) return inherited;
19
+ let content;
20
+ try { content = inherited.OPENCODE_CONFIG_CONTENT ? JSON.parse(inherited.OPENCODE_CONFIG_CONTENT) : {}; }
21
+ catch { throw new Error("OPENCODE_CONFIG_CONTENT must be valid JSON to append the multi-harness plugin."); }
22
+ if (!content || typeof content !== "object" || Array.isArray(content) || (content.plugin !== undefined && !Array.isArray(content.plugin))) {
23
+ throw new Error("OpenCode config must be an object with an optional plugin array.");
24
+ }
25
+ const plugin = pathToFileURL(join(extensionDir, "opencode-plugin.mjs")).href;
26
+ return { ...inherited, PI_MULTI_HARNESS_CWD: config.cwd,
27
+ OPENCODE_CONFIG_CONTENT: JSON.stringify({ ...content, plugin: [...(content.plugin ?? []).filter((item) => item !== plugin), plugin] }) };
28
+ }
29
+
30
+ export function buildArgs(config) {
31
+ const { harness, cwd, prompt, sessionFile, model, trusted, artifactDir, interactive, resume, sessionId } = config;
32
+ const modelArgs = model ? ["--model", model] : [];
33
+ switch (harness) {
34
+ case "pi":
35
+ return ["pi", ...(interactive ? ["--tui-mode", "regular", "-e", join(extensionDir, "worker-bridge.ts")] : ["--print"]),
36
+ trusted ? "--approve" : "--no-approve", "--session", sessionFile, ...modelArgs, prompt];
37
+ case "codex": {
38
+ const base = ["codex", "--sandbox", "danger-full-access", "-a", "never"];
39
+ if (interactive) {
40
+ const hook = JSON.stringify([process.execPath, join(extensionDir, "turn-hook.mjs"), artifactDir, "codex"]);
41
+ return [...base, "--cd", cwd, ...modelArgs, "-c", `notify=${hook}`, prompt];
42
+ }
43
+ return [...base, "exec", ...(resume ? ["resume", sessionId] : ["--cd", cwd]), "--skip-git-repo-check", "--json", ...modelArgs, "-"];
44
+ }
45
+ case "claude": {
46
+ const base = ["claude", "--permission-mode", "bypassPermissions", ...modelArgs];
47
+ if (interactive) {
48
+ const hook = (kind) => {
49
+ const args = [process.execPath, join(extensionDir, "turn-hook.mjs"), artifactDir, kind];
50
+ // Claude command hooks use a shell, not necessarily the interactive pane's shell.
51
+ if (process.platform === "win32") {
52
+ const script = `& ${args.map(quotePs).join(" ")}; exit $LASTEXITCODE`;
53
+ return `pwsh -NoProfile -EncodedCommand ${Buffer.from(script, "utf16le").toString("base64")}`;
54
+ }
55
+ return args.map(quoteSh).join(" ");
56
+ };
57
+ const settings = { hooks: {
58
+ UserPromptSubmit: [{ hooks: [{ type: "command", command: hook("claude-start"), timeout: 10 }] }],
59
+ Stop: [{ hooks: [{ type: "command", command: hook("claude"), timeout: 10 }] }],
60
+ } };
61
+ return [...base, "--session-id", sessionId, "--settings", JSON.stringify(settings), prompt];
62
+ }
63
+ return [...base, "-p", "--verbose", "--output-format", "stream-json", "--max-budget-usd", "0.50",
64
+ ...(resume ? ["--resume", sessionId] : ["--session-id", sessionId])];
65
+ }
66
+ case "opencode":
67
+ return interactive
68
+ ? ["opencode", cwd, "--auto", ...modelArgs, "--prompt", prompt]
69
+ : ["opencode", "run", "--dir", cwd, "--format", "json", "--auto", ...modelArgs, ...(resume ? ["--session", sessionId] : [])];
70
+ default: throw new Error(`Unsupported harness: ${harness}`);
71
+ }
72
+ }
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import { join } from "node:path";
3
+ import { readJson, writeJson } from "./ipc.mjs";
4
+
5
+ const directory = process.env.PI_MULTI_HARNESS_DIR;
6
+ if (!directory) throw new Error("harness-done must run inside a multi_harness worker.");
7
+ const request = readJson(join(directory, "request.json"));
8
+ if (!request) throw new Error("No task is assigned to this worker.");
9
+ writeJson(join(directory, "proposal.json"), {
10
+ id: request.id,
11
+ summary: process.argv.slice(2).join(" ").trim() || "Task finished; inspect the artifacts for verification.",
12
+ at: Date.now(),
13
+ });
14
+ console.log("Summary recorded. The coordinator will be notified when this turn settles; keep the TUI open.");