@agimon-ai/po-runner 0.0.0-alpha.29 → 0.0.0-alpha.30

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 (37) hide show
  1. package/dist/bashTool.cjs +1 -1
  2. package/dist/bashTool.mjs +1 -1
  3. package/dist/bin/cli.cjs +2 -2
  4. package/dist/bin/cli.cjs.map +1 -1
  5. package/dist/bin/cli.mjs +1 -1
  6. package/dist/bin/cli.mjs.map +1 -1
  7. package/dist/bin/logSink.cjs +2 -2
  8. package/dist/bin/logSink.cjs.map +1 -1
  9. package/dist/bin/logSink.mjs +1 -1
  10. package/dist/bin/logSink.mjs.map +1 -1
  11. package/dist/bin/runnerHost.cjs +2 -2
  12. package/dist/bin/runnerHost.cjs.map +1 -1
  13. package/dist/bin/runnerHost.mjs +1 -1
  14. package/dist/bin/runnerHost.mjs.map +1 -1
  15. package/dist/extensions/mcp.cjs +6 -6
  16. package/dist/extensions/mcp.cjs.map +1 -1
  17. package/dist/extensions/mcp.mjs +6 -6
  18. package/dist/extensions/mcp.mjs.map +1 -1
  19. package/dist/extensions/native.mjs +13 -5
  20. package/dist/extensions/native.mjs.map +1 -1
  21. package/dist/extensions/server.cjs +9 -9
  22. package/dist/extensions/server.cjs.map +1 -1
  23. package/dist/extensions/server.mjs +8 -8
  24. package/dist/extensions/server.mjs.map +1 -1
  25. package/dist/extensions/web.mjs +14 -6
  26. package/dist/extensions/web.mjs.map +1 -1
  27. package/dist/{headless-BqYKFHwy.cjs → headless-BxJwr9jq.cjs} +2 -2
  28. package/dist/headless-BxJwr9jq.cjs.map +1 -0
  29. package/dist/{headless-CV-eb5rz.mjs → headless-nu4ufr-y.mjs} +2 -2
  30. package/dist/headless-nu4ufr-y.mjs.map +1 -0
  31. package/dist/index.cjs +1 -1
  32. package/dist/index.mjs +1 -1
  33. package/llms.txt +1 -1
  34. package/package.json +16 -17
  35. package/src/prompts/{po-use-runner → help-how-to-runner}/SKILL.md +1 -1
  36. package/dist/headless-BqYKFHwy.cjs.map +0 -1
  37. package/dist/headless-CV-eb5rz.mjs.map +0 -1
package/dist/bashTool.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_bashResult = require("./bashResult-C2iKhHpD.cjs");
3
3
  const require_logReader = require("./logReader-DHCK2BEr.cjs");
4
- const require_headless = require("./headless-BqYKFHwy.cjs");
4
+ const require_headless = require("./headless-BxJwr9jq.cjs");
5
5
  //#region src/services/runnerResult/index.ts
6
6
  function formatRunnerResult(result, budget = {}) {
7
7
  return require_bashResult.formatRunResult(result, budget, require_logReader.summarizeLog);
package/dist/bashTool.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { c as formatRunResult } from "./bashResult-BqDa7sbC.mjs";
2
2
  import { r as summarizeLog } from "./logReader-mq9uVurh.mjs";
3
- import { t as createHeadlessBashTool } from "./headless-CV-eb5rz.mjs";
3
+ import { t as createHeadlessBashTool } from "./headless-nu4ufr-y.mjs";
4
4
  //#region src/services/runnerResult/index.ts
5
5
  function formatRunnerResult(result, budget = {}) {
6
6
  return formatRunResult(result, budget, summarizeLog);
package/dist/bin/cli.cjs CHANGED
@@ -4,7 +4,7 @@ const require_cliApp = require("../cliApp-T8Fx_-1j.cjs");
4
4
  const require_runnerDependencies = require("../runnerDependencies-HIqMQp9b.cjs");
5
5
  let node_fs = require("node:fs");
6
6
  let node_url = require("node:url");
7
- let _agimon_ai_po_telemetry = require("@agimon-ai/po-telemetry");
7
+ let _agimon_ai_po_core_telemetry = require("@agimon-ai/po-core/telemetry");
8
8
  //#region src/bin/cli.ts
9
9
  async function readStdin() {
10
10
  const chunks = [];
@@ -14,7 +14,7 @@ async function readStdin() {
14
14
  async function main(argv = process.argv.slice(2)) {
15
15
  const startedAt = Date.now();
16
16
  const environment = Object.freeze({ ...process.env });
17
- const telemetry = (0, _agimon_ai_po_telemetry.createPoTelemetry)({
17
+ const telemetry = (0, _agimon_ai_po_core_telemetry.createPoTelemetry)({
18
18
  serviceName: "po-runner-cli",
19
19
  packageName: "@agimon-ai/po-runner",
20
20
  env: environment,
@@ -1 +1 @@
1
- {"version":3,"file":"cli.cjs","names":["createPoTelemetry","createRunnerDependencies","runCli","existsSync","realpathSync","fileURLToPath"],"sources":["../../src/bin/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { existsSync, realpathSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\n\nimport { createPoTelemetry } from '@agimon-ai/po-telemetry';\n\nimport { runCli } from '../services/cliApp';\nimport { createRunnerDependencies } from '../services/runnerDependencies';\n\nasync function readStdin(): Promise<string> {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\n return Buffer.concat(chunks).toString('utf8');\n}\n\nexport async function main(argv: readonly string[] = process.argv.slice(2)): Promise<number> {\n const startedAt = Date.now();\n const environment = Object.freeze({ ...process.env });\n const telemetry = createPoTelemetry({\n serviceName: 'po-runner-cli',\n packageName: '@agimon-ai/po-runner',\n env: environment,\n enableLogs: true,\n enableTraces: true,\n });\n const container = createRunnerDependencies({ environment });\n const registry = container.runnerRegistry;\n let stdoutBytes = 0;\n let stderrBytes = 0;\n try {\n const exitCode = await runCli(argv, {\n registry,\n launcher: container.launcher,\n rmuxBackend: container.rmuxBackend,\n logReader: container.logReader,\n env: environment,\n stdout: (text) => {\n const output = text.endsWith('\\n') ? text : `${text}\\n`;\n stdoutBytes += Buffer.byteLength(output, 'utf8');\n process.stdout.write(output);\n },\n stderr: (text) => {\n const output = text.endsWith('\\n') ? text : `${text}\\n`;\n stderrBytes += Buffer.byteLength(output, 'utf8');\n process.stderr.write(output);\n },\n readStdin,\n });\n await telemetry.recordEvent('po_runner.cli_finished', {\n outcome: exitCode === 0 ? 'completed' : 'failed',\n 'runner.exit_code': exitCode,\n duration_ms: Date.now() - startedAt,\n 'runner.argument_count': argv.length,\n 'runner.stdout_bytes': stdoutBytes,\n 'runner.stderr_bytes': stderrBytes,\n });\n return exitCode;\n } catch (error) {\n await telemetry.recordError('po_runner.cli_failed', error, {\n duration_ms: Date.now() - startedAt,\n 'runner.argument_count': argv.length,\n 'runner.stdout_bytes': stdoutBytes,\n 'runner.stderr_bytes': stderrBytes,\n });\n throw error;\n } finally {\n registry.close();\n await telemetry.shutdown();\n }\n}\n\nif (\n process.argv[1] &&\n existsSync(process.argv[1]) &&\n realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])\n) {\n main().then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n },\n );\n}\n"],"mappings":";;;;;;;;AAUA,eAAe,YAA6B;CAC1C,MAAM,SAAmB,CAAC;CAC1B,WAAW,MAAM,SAAS,QAAQ,OAAO,OAAO,KAAK,OAAO,SAAS,KAAK,IAAI,QAAQ,OAAO,KAAK,KAAK,CAAC;CACxG,OAAO,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,MAAM;AAC9C;AAEA,eAAsB,KAAK,OAA0B,QAAQ,KAAK,MAAM,CAAC,GAAoB;CAC3F,MAAM,YAAY,KAAK,IAAI;CAC3B,MAAM,cAAc,OAAO,OAAO,EAAE,GAAG,QAAQ,IAAI,CAAC;CACpD,MAAM,aAAA,GAAYA,wBAAAA,kBAAAA,CAAkB;EAClC,aAAa;EACb,aAAa;EACb,KAAK;EACL,YAAY;EACZ,cAAc;CAChB,CAAC;CACD,MAAM,YAAYC,2BAAAA,yBAAyB,EAAE,YAAY,CAAC;CAC1D,MAAM,WAAW,UAAU;CAC3B,IAAI,cAAc;CAClB,IAAI,cAAc;CAClB,IAAI;EACF,MAAM,WAAW,MAAMC,eAAAA,OAAO,MAAM;GAClC;GACA,UAAU,UAAU;GACpB,aAAa,UAAU;GACvB,WAAW,UAAU;GACrB,KAAK;GACL,SAAS,SAAS;IAChB,MAAM,SAAS,KAAK,SAAS,IAAI,IAAI,OAAO,GAAG,KAAK;IACpD,eAAe,OAAO,WAAW,QAAQ,MAAM;IAC/C,QAAQ,OAAO,MAAM,MAAM;GAC7B;GACA,SAAS,SAAS;IAChB,MAAM,SAAS,KAAK,SAAS,IAAI,IAAI,OAAO,GAAG,KAAK;IACpD,eAAe,OAAO,WAAW,QAAQ,MAAM;IAC/C,QAAQ,OAAO,MAAM,MAAM;GAC7B;GACA;EACF,CAAC;EACD,MAAM,UAAU,YAAY,0BAA0B;GACpD,SAAS,aAAa,IAAI,cAAc;GACxC,oBAAoB;GACpB,aAAa,KAAK,IAAI,IAAI;GAC1B,yBAAyB,KAAK;GAC9B,uBAAuB;GACvB,uBAAuB;EACzB,CAAC;EACD,OAAO;CACT,SAAS,OAAO;EACd,MAAM,UAAU,YAAY,wBAAwB,OAAO;GACzD,aAAa,KAAK,IAAI,IAAI;GAC1B,yBAAyB,KAAK;GAC9B,uBAAuB;GACvB,uBAAuB;EACzB,CAAC;EACD,MAAM;CACR,UAAU;EACR,SAAS,MAAM;EACf,MAAM,UAAU,SAAS;CAC3B;AACF;AAEA,IACE,QAAQ,KAAK,OAAA,GACbC,QAAAA,WAAAA,CAAW,QAAQ,KAAK,EAAE,MAAA,GAC1BC,QAAAA,aAAAA,EAAAA,GAAaC,SAAAA,cAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,IAA6B,CAAC,OAAA,GAAMD,QAAAA,aAAAA,CAAa,QAAQ,KAAK,EAAE,GAE7E,KAAK,CAAC,CAAC,MACJ,SAAS;CACR,QAAQ,WAAW;AACrB,IACC,UAAmB;CAClB,QAAQ,OAAO,MAAM,GAAG,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,GAAG;CAClF,QAAQ,WAAW;AACrB,CACF"}
1
+ {"version":3,"file":"cli.cjs","names":["createPoTelemetry","createRunnerDependencies","runCli","existsSync","realpathSync","fileURLToPath"],"sources":["../../src/bin/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { existsSync, realpathSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\n\nimport { createPoTelemetry } from '@agimon-ai/po-core/telemetry';\n\nimport { runCli } from '../services/cliApp';\nimport { createRunnerDependencies } from '../services/runnerDependencies';\n\nasync function readStdin(): Promise<string> {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\n return Buffer.concat(chunks).toString('utf8');\n}\n\nexport async function main(argv: readonly string[] = process.argv.slice(2)): Promise<number> {\n const startedAt = Date.now();\n const environment = Object.freeze({ ...process.env });\n const telemetry = createPoTelemetry({\n serviceName: 'po-runner-cli',\n packageName: '@agimon-ai/po-runner',\n env: environment,\n enableLogs: true,\n enableTraces: true,\n });\n const container = createRunnerDependencies({ environment });\n const registry = container.runnerRegistry;\n let stdoutBytes = 0;\n let stderrBytes = 0;\n try {\n const exitCode = await runCli(argv, {\n registry,\n launcher: container.launcher,\n rmuxBackend: container.rmuxBackend,\n logReader: container.logReader,\n env: environment,\n stdout: (text) => {\n const output = text.endsWith('\\n') ? text : `${text}\\n`;\n stdoutBytes += Buffer.byteLength(output, 'utf8');\n process.stdout.write(output);\n },\n stderr: (text) => {\n const output = text.endsWith('\\n') ? text : `${text}\\n`;\n stderrBytes += Buffer.byteLength(output, 'utf8');\n process.stderr.write(output);\n },\n readStdin,\n });\n await telemetry.recordEvent('po_runner.cli_finished', {\n outcome: exitCode === 0 ? 'completed' : 'failed',\n 'runner.exit_code': exitCode,\n duration_ms: Date.now() - startedAt,\n 'runner.argument_count': argv.length,\n 'runner.stdout_bytes': stdoutBytes,\n 'runner.stderr_bytes': stderrBytes,\n });\n return exitCode;\n } catch (error) {\n await telemetry.recordError('po_runner.cli_failed', error, {\n duration_ms: Date.now() - startedAt,\n 'runner.argument_count': argv.length,\n 'runner.stdout_bytes': stdoutBytes,\n 'runner.stderr_bytes': stderrBytes,\n });\n throw error;\n } finally {\n registry.close();\n await telemetry.shutdown();\n }\n}\n\nif (\n process.argv[1] &&\n existsSync(process.argv[1]) &&\n realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])\n) {\n main().then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n },\n );\n}\n"],"mappings":";;;;;;;;AAUA,eAAe,YAA6B;CAC1C,MAAM,SAAmB,CAAC;CAC1B,WAAW,MAAM,SAAS,QAAQ,OAAO,OAAO,KAAK,OAAO,SAAS,KAAK,IAAI,QAAQ,OAAO,KAAK,KAAK,CAAC;CACxG,OAAO,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,MAAM;AAC9C;AAEA,eAAsB,KAAK,OAA0B,QAAQ,KAAK,MAAM,CAAC,GAAoB;CAC3F,MAAM,YAAY,KAAK,IAAI;CAC3B,MAAM,cAAc,OAAO,OAAO,EAAE,GAAG,QAAQ,IAAI,CAAC;CACpD,MAAM,aAAA,GAAYA,6BAAAA,kBAAAA,CAAkB;EAClC,aAAa;EACb,aAAa;EACb,KAAK;EACL,YAAY;EACZ,cAAc;CAChB,CAAC;CACD,MAAM,YAAYC,2BAAAA,yBAAyB,EAAE,YAAY,CAAC;CAC1D,MAAM,WAAW,UAAU;CAC3B,IAAI,cAAc;CAClB,IAAI,cAAc;CAClB,IAAI;EACF,MAAM,WAAW,MAAMC,eAAAA,OAAO,MAAM;GAClC;GACA,UAAU,UAAU;GACpB,aAAa,UAAU;GACvB,WAAW,UAAU;GACrB,KAAK;GACL,SAAS,SAAS;IAChB,MAAM,SAAS,KAAK,SAAS,IAAI,IAAI,OAAO,GAAG,KAAK;IACpD,eAAe,OAAO,WAAW,QAAQ,MAAM;IAC/C,QAAQ,OAAO,MAAM,MAAM;GAC7B;GACA,SAAS,SAAS;IAChB,MAAM,SAAS,KAAK,SAAS,IAAI,IAAI,OAAO,GAAG,KAAK;IACpD,eAAe,OAAO,WAAW,QAAQ,MAAM;IAC/C,QAAQ,OAAO,MAAM,MAAM;GAC7B;GACA;EACF,CAAC;EACD,MAAM,UAAU,YAAY,0BAA0B;GACpD,SAAS,aAAa,IAAI,cAAc;GACxC,oBAAoB;GACpB,aAAa,KAAK,IAAI,IAAI;GAC1B,yBAAyB,KAAK;GAC9B,uBAAuB;GACvB,uBAAuB;EACzB,CAAC;EACD,OAAO;CACT,SAAS,OAAO;EACd,MAAM,UAAU,YAAY,wBAAwB,OAAO;GACzD,aAAa,KAAK,IAAI,IAAI;GAC1B,yBAAyB,KAAK;GAC9B,uBAAuB;GACvB,uBAAuB;EACzB,CAAC;EACD,MAAM;CACR,UAAU;EACR,SAAS,MAAM;EACf,MAAM,UAAU,SAAS;CAC3B;AACF;AAEA,IACE,QAAQ,KAAK,OAAA,GACbC,QAAAA,WAAAA,CAAW,QAAQ,KAAK,EAAE,MAAA,GAC1BC,QAAAA,aAAAA,EAAAA,GAAaC,SAAAA,cAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,IAA6B,CAAC,OAAA,GAAMD,QAAAA,aAAAA,CAAa,QAAQ,KAAK,EAAE,GAE7E,KAAK,CAAC,CAAC,MACJ,SAAS;CACR,QAAQ,WAAW;AACrB,IACC,UAAmB;CAClB,QAAQ,OAAO,MAAM,GAAG,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,GAAG;CAClF,QAAQ,WAAW;AACrB,CACF"}
package/dist/bin/cli.mjs CHANGED
@@ -3,7 +3,7 @@ import { t as runCli } from "../cliApp-q97eVh4E.mjs";
3
3
  import { t as createRunnerDependencies } from "../runnerDependencies-CamKwphq.mjs";
4
4
  import { existsSync, realpathSync } from "node:fs";
5
5
  import { fileURLToPath } from "node:url";
6
- import { createPoTelemetry } from "@agimon-ai/po-telemetry";
6
+ import { createPoTelemetry } from "@agimon-ai/po-core/telemetry";
7
7
  //#region src/bin/cli.ts
8
8
  async function readStdin() {
9
9
  const chunks = [];
@@ -1 +1 @@
1
- {"version":3,"file":"cli.mjs","names":[],"sources":["../../src/bin/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { existsSync, realpathSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\n\nimport { createPoTelemetry } from '@agimon-ai/po-telemetry';\n\nimport { runCli } from '../services/cliApp';\nimport { createRunnerDependencies } from '../services/runnerDependencies';\n\nasync function readStdin(): Promise<string> {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\n return Buffer.concat(chunks).toString('utf8');\n}\n\nexport async function main(argv: readonly string[] = process.argv.slice(2)): Promise<number> {\n const startedAt = Date.now();\n const environment = Object.freeze({ ...process.env });\n const telemetry = createPoTelemetry({\n serviceName: 'po-runner-cli',\n packageName: '@agimon-ai/po-runner',\n env: environment,\n enableLogs: true,\n enableTraces: true,\n });\n const container = createRunnerDependencies({ environment });\n const registry = container.runnerRegistry;\n let stdoutBytes = 0;\n let stderrBytes = 0;\n try {\n const exitCode = await runCli(argv, {\n registry,\n launcher: container.launcher,\n rmuxBackend: container.rmuxBackend,\n logReader: container.logReader,\n env: environment,\n stdout: (text) => {\n const output = text.endsWith('\\n') ? text : `${text}\\n`;\n stdoutBytes += Buffer.byteLength(output, 'utf8');\n process.stdout.write(output);\n },\n stderr: (text) => {\n const output = text.endsWith('\\n') ? text : `${text}\\n`;\n stderrBytes += Buffer.byteLength(output, 'utf8');\n process.stderr.write(output);\n },\n readStdin,\n });\n await telemetry.recordEvent('po_runner.cli_finished', {\n outcome: exitCode === 0 ? 'completed' : 'failed',\n 'runner.exit_code': exitCode,\n duration_ms: Date.now() - startedAt,\n 'runner.argument_count': argv.length,\n 'runner.stdout_bytes': stdoutBytes,\n 'runner.stderr_bytes': stderrBytes,\n });\n return exitCode;\n } catch (error) {\n await telemetry.recordError('po_runner.cli_failed', error, {\n duration_ms: Date.now() - startedAt,\n 'runner.argument_count': argv.length,\n 'runner.stdout_bytes': stdoutBytes,\n 'runner.stderr_bytes': stderrBytes,\n });\n throw error;\n } finally {\n registry.close();\n await telemetry.shutdown();\n }\n}\n\nif (\n process.argv[1] &&\n existsSync(process.argv[1]) &&\n realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])\n) {\n main().then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n },\n );\n}\n"],"mappings":";;;;;;;AAUA,eAAe,YAA6B;CAC1C,MAAM,SAAmB,CAAC;CAC1B,WAAW,MAAM,SAAS,QAAQ,OAAO,OAAO,KAAK,OAAO,SAAS,KAAK,IAAI,QAAQ,OAAO,KAAK,KAAK,CAAC;CACxG,OAAO,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,MAAM;AAC9C;AAEA,eAAsB,KAAK,OAA0B,QAAQ,KAAK,MAAM,CAAC,GAAoB;CAC3F,MAAM,YAAY,KAAK,IAAI;CAC3B,MAAM,cAAc,OAAO,OAAO,EAAE,GAAG,QAAQ,IAAI,CAAC;CACpD,MAAM,YAAY,kBAAkB;EAClC,aAAa;EACb,aAAa;EACb,KAAK;EACL,YAAY;EACZ,cAAc;CAChB,CAAC;CACD,MAAM,YAAY,yBAAyB,EAAE,YAAY,CAAC;CAC1D,MAAM,WAAW,UAAU;CAC3B,IAAI,cAAc;CAClB,IAAI,cAAc;CAClB,IAAI;EACF,MAAM,WAAW,MAAM,OAAO,MAAM;GAClC;GACA,UAAU,UAAU;GACpB,aAAa,UAAU;GACvB,WAAW,UAAU;GACrB,KAAK;GACL,SAAS,SAAS;IAChB,MAAM,SAAS,KAAK,SAAS,IAAI,IAAI,OAAO,GAAG,KAAK;IACpD,eAAe,OAAO,WAAW,QAAQ,MAAM;IAC/C,QAAQ,OAAO,MAAM,MAAM;GAC7B;GACA,SAAS,SAAS;IAChB,MAAM,SAAS,KAAK,SAAS,IAAI,IAAI,OAAO,GAAG,KAAK;IACpD,eAAe,OAAO,WAAW,QAAQ,MAAM;IAC/C,QAAQ,OAAO,MAAM,MAAM;GAC7B;GACA;EACF,CAAC;EACD,MAAM,UAAU,YAAY,0BAA0B;GACpD,SAAS,aAAa,IAAI,cAAc;GACxC,oBAAoB;GACpB,aAAa,KAAK,IAAI,IAAI;GAC1B,yBAAyB,KAAK;GAC9B,uBAAuB;GACvB,uBAAuB;EACzB,CAAC;EACD,OAAO;CACT,SAAS,OAAO;EACd,MAAM,UAAU,YAAY,wBAAwB,OAAO;GACzD,aAAa,KAAK,IAAI,IAAI;GAC1B,yBAAyB,KAAK;GAC9B,uBAAuB;GACvB,uBAAuB;EACzB,CAAC;EACD,MAAM;CACR,UAAU;EACR,SAAS,MAAM;EACf,MAAM,UAAU,SAAS;CAC3B;AACF;AAEA,IACE,QAAQ,KAAK,MACb,WAAW,QAAQ,KAAK,EAAE,KAC1B,aAAa,cAAc,YAAY,GAAG,CAAC,MAAM,aAAa,QAAQ,KAAK,EAAE,GAE7E,KAAK,CAAC,CAAC,MACJ,SAAS;CACR,QAAQ,WAAW;AACrB,IACC,UAAmB;CAClB,QAAQ,OAAO,MAAM,GAAG,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,GAAG;CAClF,QAAQ,WAAW;AACrB,CACF"}
1
+ {"version":3,"file":"cli.mjs","names":[],"sources":["../../src/bin/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { existsSync, realpathSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\n\nimport { createPoTelemetry } from '@agimon-ai/po-core/telemetry';\n\nimport { runCli } from '../services/cliApp';\nimport { createRunnerDependencies } from '../services/runnerDependencies';\n\nasync function readStdin(): Promise<string> {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));\n return Buffer.concat(chunks).toString('utf8');\n}\n\nexport async function main(argv: readonly string[] = process.argv.slice(2)): Promise<number> {\n const startedAt = Date.now();\n const environment = Object.freeze({ ...process.env });\n const telemetry = createPoTelemetry({\n serviceName: 'po-runner-cli',\n packageName: '@agimon-ai/po-runner',\n env: environment,\n enableLogs: true,\n enableTraces: true,\n });\n const container = createRunnerDependencies({ environment });\n const registry = container.runnerRegistry;\n let stdoutBytes = 0;\n let stderrBytes = 0;\n try {\n const exitCode = await runCli(argv, {\n registry,\n launcher: container.launcher,\n rmuxBackend: container.rmuxBackend,\n logReader: container.logReader,\n env: environment,\n stdout: (text) => {\n const output = text.endsWith('\\n') ? text : `${text}\\n`;\n stdoutBytes += Buffer.byteLength(output, 'utf8');\n process.stdout.write(output);\n },\n stderr: (text) => {\n const output = text.endsWith('\\n') ? text : `${text}\\n`;\n stderrBytes += Buffer.byteLength(output, 'utf8');\n process.stderr.write(output);\n },\n readStdin,\n });\n await telemetry.recordEvent('po_runner.cli_finished', {\n outcome: exitCode === 0 ? 'completed' : 'failed',\n 'runner.exit_code': exitCode,\n duration_ms: Date.now() - startedAt,\n 'runner.argument_count': argv.length,\n 'runner.stdout_bytes': stdoutBytes,\n 'runner.stderr_bytes': stderrBytes,\n });\n return exitCode;\n } catch (error) {\n await telemetry.recordError('po_runner.cli_failed', error, {\n duration_ms: Date.now() - startedAt,\n 'runner.argument_count': argv.length,\n 'runner.stdout_bytes': stdoutBytes,\n 'runner.stderr_bytes': stderrBytes,\n });\n throw error;\n } finally {\n registry.close();\n await telemetry.shutdown();\n }\n}\n\nif (\n process.argv[1] &&\n existsSync(process.argv[1]) &&\n realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])\n) {\n main().then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n },\n );\n}\n"],"mappings":";;;;;;;AAUA,eAAe,YAA6B;CAC1C,MAAM,SAAmB,CAAC;CAC1B,WAAW,MAAM,SAAS,QAAQ,OAAO,OAAO,KAAK,OAAO,SAAS,KAAK,IAAI,QAAQ,OAAO,KAAK,KAAK,CAAC;CACxG,OAAO,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,MAAM;AAC9C;AAEA,eAAsB,KAAK,OAA0B,QAAQ,KAAK,MAAM,CAAC,GAAoB;CAC3F,MAAM,YAAY,KAAK,IAAI;CAC3B,MAAM,cAAc,OAAO,OAAO,EAAE,GAAG,QAAQ,IAAI,CAAC;CACpD,MAAM,YAAY,kBAAkB;EAClC,aAAa;EACb,aAAa;EACb,KAAK;EACL,YAAY;EACZ,cAAc;CAChB,CAAC;CACD,MAAM,YAAY,yBAAyB,EAAE,YAAY,CAAC;CAC1D,MAAM,WAAW,UAAU;CAC3B,IAAI,cAAc;CAClB,IAAI,cAAc;CAClB,IAAI;EACF,MAAM,WAAW,MAAM,OAAO,MAAM;GAClC;GACA,UAAU,UAAU;GACpB,aAAa,UAAU;GACvB,WAAW,UAAU;GACrB,KAAK;GACL,SAAS,SAAS;IAChB,MAAM,SAAS,KAAK,SAAS,IAAI,IAAI,OAAO,GAAG,KAAK;IACpD,eAAe,OAAO,WAAW,QAAQ,MAAM;IAC/C,QAAQ,OAAO,MAAM,MAAM;GAC7B;GACA,SAAS,SAAS;IAChB,MAAM,SAAS,KAAK,SAAS,IAAI,IAAI,OAAO,GAAG,KAAK;IACpD,eAAe,OAAO,WAAW,QAAQ,MAAM;IAC/C,QAAQ,OAAO,MAAM,MAAM;GAC7B;GACA;EACF,CAAC;EACD,MAAM,UAAU,YAAY,0BAA0B;GACpD,SAAS,aAAa,IAAI,cAAc;GACxC,oBAAoB;GACpB,aAAa,KAAK,IAAI,IAAI;GAC1B,yBAAyB,KAAK;GAC9B,uBAAuB;GACvB,uBAAuB;EACzB,CAAC;EACD,OAAO;CACT,SAAS,OAAO;EACd,MAAM,UAAU,YAAY,wBAAwB,OAAO;GACzD,aAAa,KAAK,IAAI,IAAI;GAC1B,yBAAyB,KAAK;GAC9B,uBAAuB;GACvB,uBAAuB;EACzB,CAAC;EACD,MAAM;CACR,UAAU;EACR,SAAS,MAAM;EACf,MAAM,UAAU,SAAS;CAC3B;AACF;AAEA,IACE,QAAQ,KAAK,MACb,WAAW,QAAQ,KAAK,EAAE,KAC1B,aAAa,cAAc,YAAY,GAAG,CAAC,MAAM,aAAa,QAAQ,KAAK,EAAE,GAE7E,KAAK,CAAC,CAAC,MACJ,SAAS;CACR,QAAQ,WAAW;AACrB,IACC,UAAmB;CAClB,QAAQ,OAAO,MAAM,GAAG,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,GAAG;CAClF,QAAQ,WAAW;AACrB,CACF"}
@@ -5,11 +5,11 @@ let node_fs = require("node:fs");
5
5
  node_fs = require_rolldown_runtime.__toESM(node_fs, 1);
6
6
  let node_string_decoder = require("node:string_decoder");
7
7
  let node_url = require("node:url");
8
- let _agimon_ai_po_telemetry = require("@agimon-ai/po-telemetry");
8
+ let _agimon_ai_po_core_telemetry = require("@agimon-ai/po-core/telemetry");
9
9
  //#region src/bin/logSink.ts
10
10
  async function main(argv = process.argv.slice(2)) {
11
11
  const startedAt = Date.now();
12
- const telemetry = (0, _agimon_ai_po_telemetry.createPoTelemetry)({
12
+ const telemetry = (0, _agimon_ai_po_core_telemetry.createPoTelemetry)({
13
13
  serviceName: "po-runner-log-sink",
14
14
  packageName: "@agimon-ai/po-runner",
15
15
  env: process.env,
@@ -1 +1 @@
1
- {"version":3,"file":"logSink.cjs","names":["createPoTelemetry","fs","StringDecoder","scrubTerminalOutput","pathToFileURL"],"sources":["../../src/bin/logSink.ts"],"sourcesContent":["import fs from 'node:fs';\nimport { StringDecoder } from 'node:string_decoder';\nimport { pathToFileURL } from 'node:url';\n\nimport { createPoTelemetry } from '@agimon-ai/po-telemetry';\n\nimport { scrubTerminalOutput } from '../services/ansiScrub';\n\nexport async function main(argv: readonly string[] = process.argv.slice(2)): Promise<number> {\n const startedAt = Date.now();\n const telemetry = createPoTelemetry({\n serviceName: 'po-runner-log-sink',\n packageName: '@agimon-ai/po-runner',\n env: process.env,\n enableLogs: true,\n enableTraces: true,\n });\n const [logPath, rotatedPath, maxBytesRaw, donePath, rawPath] = argv;\n try {\n // Keep the legacy argument shape for the published executable subpath. The\n // scrubbed log is append-only, so the former rotation path is unused; the\n // ceiling now bounds the raw copy, which only interactive runs ask for.\n if (!logPath || !rotatedPath || !maxBytesRaw || !donePath) {\n throw new Error('logSink requires log, rotated log, max bytes, and completion paths');\n }\n } catch (error) {\n await telemetry.recordError('po_runner.file_sink_failed', error, { duration_ms: Date.now() - startedAt });\n await telemetry.shutdown();\n throw error;\n }\n\n let handle: number;\n let written: number;\n try {\n handle = fs.openSync(logPath, 'a');\n written = fs.statSync(logPath).size;\n } catch (error) {\n await telemetry.recordError('po_runner.file_sink_failed', error, { duration_ms: Date.now() - startedAt });\n await telemetry.shutdown();\n throw error;\n }\n let chunks = 0;\n // The pane's bytes before anything is taken out of them. Scrubbing exists so\n // the log stays worth grepping, and it is exactly what makes the log useless\n // to a terminal, so an attached view needs its own faithful copy.\n const rawCeiling = Number.parseInt(maxBytesRaw, 10);\n let raw = rawPath === undefined || rawPath === '' ? undefined : openRaw(rawPath);\n let rawWritten = 0;\n const decoder = new StringDecoder('utf8');\n try {\n for await (const chunk of process.stdin) {\n const rawBytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));\n const source = decoder.write(rawBytes);\n if (raw !== undefined) {\n // A terminal that scrolls all day must not fill the disk. Past the\n // ceiling the copy starts over: a reader attaching later wants the\n // recent screen, and no part of this is the record of the run.\n if (rawWritten + rawBytes.byteLength > rawCeiling) {\n const previous = raw;\n raw = openRaw(rawPath as string);\n fs.closeSync(previous);\n rawWritten = 0;\n }\n fs.writeSync(raw, rawBytes);\n rawWritten += rawBytes.byteLength;\n }\n const text = scrubTerminalOutput(source);\n if (!text) continue;\n const bytes = Buffer.from(text, 'utf8');\n fs.writeSync(handle, bytes);\n written += bytes.byteLength;\n chunks += 1;\n }\n const remaining = scrubTerminalOutput(decoder.end());\n if (remaining) {\n const bytes = Buffer.from(remaining);\n fs.writeSync(handle, bytes);\n written += bytes.byteLength;\n }\n fs.closeSync(handle);\n if (raw !== undefined) fs.closeSync(raw);\n writeDone(donePath);\n await telemetry.recordEvent('po_runner.file_sink_finished', {\n outcome: 'completed',\n 'runner.chunk_count': chunks,\n 'runner.bytes_written': written,\n duration_ms: Date.now() - startedAt,\n });\n return 0;\n } catch (error) {\n await telemetry.recordError('po_runner.file_sink_failed', error, { duration_ms: Date.now() - startedAt });\n throw error;\n } finally {\n await telemetry.shutdown();\n }\n}\n\n/** Publish a fresh inode so readers can identify rollover from the descriptor they read. */\nfunction openRaw(target: string): number {\n const temporary = `${target}.${process.pid}.tmp`;\n const descriptor = fs.openSync(temporary, 'wx', 0o600);\n try {\n fs.renameSync(temporary, target);\n return descriptor;\n } catch (error) {\n fs.closeSync(descriptor);\n fs.unlinkSync(temporary);\n throw error;\n }\n}\n\nfunction writeDone(target: string): void {\n const temporary = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(temporary, '', { mode: 0o600 });\n fs.renameSync(temporary, target);\n}\n\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n main().then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n },\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"logSink.cjs","names":["createPoTelemetry","fs","StringDecoder","scrubTerminalOutput","pathToFileURL"],"sources":["../../src/bin/logSink.ts"],"sourcesContent":["import fs from 'node:fs';\nimport { StringDecoder } from 'node:string_decoder';\nimport { pathToFileURL } from 'node:url';\n\nimport { createPoTelemetry } from '@agimon-ai/po-core/telemetry';\n\nimport { scrubTerminalOutput } from '../services/ansiScrub';\n\nexport async function main(argv: readonly string[] = process.argv.slice(2)): Promise<number> {\n const startedAt = Date.now();\n const telemetry = createPoTelemetry({\n serviceName: 'po-runner-log-sink',\n packageName: '@agimon-ai/po-runner',\n env: process.env,\n enableLogs: true,\n enableTraces: true,\n });\n const [logPath, rotatedPath, maxBytesRaw, donePath, rawPath] = argv;\n try {\n // Keep the legacy argument shape for the published executable subpath. The\n // scrubbed log is append-only, so the former rotation path is unused; the\n // ceiling now bounds the raw copy, which only interactive runs ask for.\n if (!logPath || !rotatedPath || !maxBytesRaw || !donePath) {\n throw new Error('logSink requires log, rotated log, max bytes, and completion paths');\n }\n } catch (error) {\n await telemetry.recordError('po_runner.file_sink_failed', error, { duration_ms: Date.now() - startedAt });\n await telemetry.shutdown();\n throw error;\n }\n\n let handle: number;\n let written: number;\n try {\n handle = fs.openSync(logPath, 'a');\n written = fs.statSync(logPath).size;\n } catch (error) {\n await telemetry.recordError('po_runner.file_sink_failed', error, { duration_ms: Date.now() - startedAt });\n await telemetry.shutdown();\n throw error;\n }\n let chunks = 0;\n // The pane's bytes before anything is taken out of them. Scrubbing exists so\n // the log stays worth grepping, and it is exactly what makes the log useless\n // to a terminal, so an attached view needs its own faithful copy.\n const rawCeiling = Number.parseInt(maxBytesRaw, 10);\n let raw = rawPath === undefined || rawPath === '' ? undefined : openRaw(rawPath);\n let rawWritten = 0;\n const decoder = new StringDecoder('utf8');\n try {\n for await (const chunk of process.stdin) {\n const rawBytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));\n const source = decoder.write(rawBytes);\n if (raw !== undefined) {\n // A terminal that scrolls all day must not fill the disk. Past the\n // ceiling the copy starts over: a reader attaching later wants the\n // recent screen, and no part of this is the record of the run.\n if (rawWritten + rawBytes.byteLength > rawCeiling) {\n const previous = raw;\n raw = openRaw(rawPath as string);\n fs.closeSync(previous);\n rawWritten = 0;\n }\n fs.writeSync(raw, rawBytes);\n rawWritten += rawBytes.byteLength;\n }\n const text = scrubTerminalOutput(source);\n if (!text) continue;\n const bytes = Buffer.from(text, 'utf8');\n fs.writeSync(handle, bytes);\n written += bytes.byteLength;\n chunks += 1;\n }\n const remaining = scrubTerminalOutput(decoder.end());\n if (remaining) {\n const bytes = Buffer.from(remaining);\n fs.writeSync(handle, bytes);\n written += bytes.byteLength;\n }\n fs.closeSync(handle);\n if (raw !== undefined) fs.closeSync(raw);\n writeDone(donePath);\n await telemetry.recordEvent('po_runner.file_sink_finished', {\n outcome: 'completed',\n 'runner.chunk_count': chunks,\n 'runner.bytes_written': written,\n duration_ms: Date.now() - startedAt,\n });\n return 0;\n } catch (error) {\n await telemetry.recordError('po_runner.file_sink_failed', error, { duration_ms: Date.now() - startedAt });\n throw error;\n } finally {\n await telemetry.shutdown();\n }\n}\n\n/** Publish a fresh inode so readers can identify rollover from the descriptor they read. */\nfunction openRaw(target: string): number {\n const temporary = `${target}.${process.pid}.tmp`;\n const descriptor = fs.openSync(temporary, 'wx', 0o600);\n try {\n fs.renameSync(temporary, target);\n return descriptor;\n } catch (error) {\n fs.closeSync(descriptor);\n fs.unlinkSync(temporary);\n throw error;\n }\n}\n\nfunction writeDone(target: string): void {\n const temporary = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(temporary, '', { mode: 0o600 });\n fs.renameSync(temporary, target);\n}\n\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n main().then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n },\n );\n}\n"],"mappings":"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"}
@@ -2,7 +2,7 @@ import { n as scrubTerminalOutput } from "../ansiScrub-CsqTSaVT.mjs";
2
2
  import fs from "node:fs";
3
3
  import { StringDecoder } from "node:string_decoder";
4
4
  import { pathToFileURL } from "node:url";
5
- import { createPoTelemetry } from "@agimon-ai/po-telemetry";
5
+ import { createPoTelemetry } from "@agimon-ai/po-core/telemetry";
6
6
  //#region src/bin/logSink.ts
7
7
  async function main(argv = process.argv.slice(2)) {
8
8
  const startedAt = Date.now();
@@ -1 +1 @@
1
- {"version":3,"file":"logSink.mjs","names":[],"sources":["../../src/bin/logSink.ts"],"sourcesContent":["import fs from 'node:fs';\nimport { StringDecoder } from 'node:string_decoder';\nimport { pathToFileURL } from 'node:url';\n\nimport { createPoTelemetry } from '@agimon-ai/po-telemetry';\n\nimport { scrubTerminalOutput } from '../services/ansiScrub';\n\nexport async function main(argv: readonly string[] = process.argv.slice(2)): Promise<number> {\n const startedAt = Date.now();\n const telemetry = createPoTelemetry({\n serviceName: 'po-runner-log-sink',\n packageName: '@agimon-ai/po-runner',\n env: process.env,\n enableLogs: true,\n enableTraces: true,\n });\n const [logPath, rotatedPath, maxBytesRaw, donePath, rawPath] = argv;\n try {\n // Keep the legacy argument shape for the published executable subpath. The\n // scrubbed log is append-only, so the former rotation path is unused; the\n // ceiling now bounds the raw copy, which only interactive runs ask for.\n if (!logPath || !rotatedPath || !maxBytesRaw || !donePath) {\n throw new Error('logSink requires log, rotated log, max bytes, and completion paths');\n }\n } catch (error) {\n await telemetry.recordError('po_runner.file_sink_failed', error, { duration_ms: Date.now() - startedAt });\n await telemetry.shutdown();\n throw error;\n }\n\n let handle: number;\n let written: number;\n try {\n handle = fs.openSync(logPath, 'a');\n written = fs.statSync(logPath).size;\n } catch (error) {\n await telemetry.recordError('po_runner.file_sink_failed', error, { duration_ms: Date.now() - startedAt });\n await telemetry.shutdown();\n throw error;\n }\n let chunks = 0;\n // The pane's bytes before anything is taken out of them. Scrubbing exists so\n // the log stays worth grepping, and it is exactly what makes the log useless\n // to a terminal, so an attached view needs its own faithful copy.\n const rawCeiling = Number.parseInt(maxBytesRaw, 10);\n let raw = rawPath === undefined || rawPath === '' ? undefined : openRaw(rawPath);\n let rawWritten = 0;\n const decoder = new StringDecoder('utf8');\n try {\n for await (const chunk of process.stdin) {\n const rawBytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));\n const source = decoder.write(rawBytes);\n if (raw !== undefined) {\n // A terminal that scrolls all day must not fill the disk. Past the\n // ceiling the copy starts over: a reader attaching later wants the\n // recent screen, and no part of this is the record of the run.\n if (rawWritten + rawBytes.byteLength > rawCeiling) {\n const previous = raw;\n raw = openRaw(rawPath as string);\n fs.closeSync(previous);\n rawWritten = 0;\n }\n fs.writeSync(raw, rawBytes);\n rawWritten += rawBytes.byteLength;\n }\n const text = scrubTerminalOutput(source);\n if (!text) continue;\n const bytes = Buffer.from(text, 'utf8');\n fs.writeSync(handle, bytes);\n written += bytes.byteLength;\n chunks += 1;\n }\n const remaining = scrubTerminalOutput(decoder.end());\n if (remaining) {\n const bytes = Buffer.from(remaining);\n fs.writeSync(handle, bytes);\n written += bytes.byteLength;\n }\n fs.closeSync(handle);\n if (raw !== undefined) fs.closeSync(raw);\n writeDone(donePath);\n await telemetry.recordEvent('po_runner.file_sink_finished', {\n outcome: 'completed',\n 'runner.chunk_count': chunks,\n 'runner.bytes_written': written,\n duration_ms: Date.now() - startedAt,\n });\n return 0;\n } catch (error) {\n await telemetry.recordError('po_runner.file_sink_failed', error, { duration_ms: Date.now() - startedAt });\n throw error;\n } finally {\n await telemetry.shutdown();\n }\n}\n\n/** Publish a fresh inode so readers can identify rollover from the descriptor they read. */\nfunction openRaw(target: string): number {\n const temporary = `${target}.${process.pid}.tmp`;\n const descriptor = fs.openSync(temporary, 'wx', 0o600);\n try {\n fs.renameSync(temporary, target);\n return descriptor;\n } catch (error) {\n fs.closeSync(descriptor);\n fs.unlinkSync(temporary);\n throw error;\n }\n}\n\nfunction writeDone(target: string): void {\n const temporary = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(temporary, '', { mode: 0o600 });\n fs.renameSync(temporary, target);\n}\n\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n main().then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n },\n );\n}\n"],"mappings":";;;;;;AAQA,eAAsB,KAAK,OAA0B,QAAQ,KAAK,MAAM,CAAC,GAAoB;CAC3F,MAAM,YAAY,KAAK,IAAI;CAC3B,MAAM,YAAY,kBAAkB;EAClC,aAAa;EACb,aAAa;EACb,KAAK,QAAQ;EACb,YAAY;EACZ,cAAc;CAChB,CAAC;CACD,MAAM,CAAC,SAAS,aAAa,aAAa,UAAU,WAAW;CAC/D,IAAI;EAIF,IAAI,CAAC,WAAW,CAAC,eAAe,CAAC,eAAe,CAAC,UAC/C,MAAM,IAAI,MAAM,oEAAoE;CAExF,SAAS,OAAO;EACd,MAAM,UAAU,YAAY,8BAA8B,OAAO,EAAE,aAAa,KAAK,IAAI,IAAI,UAAU,CAAC;EACxG,MAAM,UAAU,SAAS;EACzB,MAAM;CACR;CAEA,IAAI;CACJ,IAAI;CACJ,IAAI;EACF,SAAS,GAAG,SAAS,SAAS,GAAG;EACjC,UAAU,GAAG,SAAS,OAAO,CAAC,CAAC;CACjC,SAAS,OAAO;EACd,MAAM,UAAU,YAAY,8BAA8B,OAAO,EAAE,aAAa,KAAK,IAAI,IAAI,UAAU,CAAC;EACxG,MAAM,UAAU,SAAS;EACzB,MAAM;CACR;CACA,IAAI,SAAS;CAIb,MAAM,aAAa,OAAO,SAAS,aAAa,EAAE;CAClD,IAAI,MAAM,YAAY,KAAA,KAAa,YAAY,KAAK,KAAA,IAAY,QAAQ,OAAO;CAC/E,IAAI,aAAa;CACjB,MAAM,UAAU,IAAI,cAAc,MAAM;CACxC,IAAI;EACF,WAAW,MAAM,SAAS,QAAQ,OAAO;GACvC,MAAM,WAAW,OAAO,SAAS,KAAK,IAAI,QAAQ,OAAO,KAAK,OAAO,KAAK,CAAC;GAC3E,MAAM,SAAS,QAAQ,MAAM,QAAQ;GACrC,IAAI,QAAQ,KAAA,GAAW;IAIrB,IAAI,aAAa,SAAS,aAAa,YAAY;KACjD,MAAM,WAAW;KACjB,MAAM,QAAQ,OAAiB;KAC/B,GAAG,UAAU,QAAQ;KACrB,aAAa;IACf;IACA,GAAG,UAAU,KAAK,QAAQ;IAC1B,cAAc,SAAS;GACzB;GACA,MAAM,OAAO,oBAAoB,MAAM;GACvC,IAAI,CAAC,MAAM;GACX,MAAM,QAAQ,OAAO,KAAK,MAAM,MAAM;GACtC,GAAG,UAAU,QAAQ,KAAK;GAC1B,WAAW,MAAM;GACjB,UAAU;EACZ;EACA,MAAM,YAAY,oBAAoB,QAAQ,IAAI,CAAC;EACnD,IAAI,WAAW;GACb,MAAM,QAAQ,OAAO,KAAK,SAAS;GACnC,GAAG,UAAU,QAAQ,KAAK;GAC1B,WAAW,MAAM;EACnB;EACA,GAAG,UAAU,MAAM;EACnB,IAAI,QAAQ,KAAA,GAAW,GAAG,UAAU,GAAG;EACvC,UAAU,QAAQ;EAClB,MAAM,UAAU,YAAY,gCAAgC;GAC1D,SAAS;GACT,sBAAsB;GACtB,wBAAwB;GACxB,aAAa,KAAK,IAAI,IAAI;EAC5B,CAAC;EACD,OAAO;CACT,SAAS,OAAO;EACd,MAAM,UAAU,YAAY,8BAA8B,OAAO,EAAE,aAAa,KAAK,IAAI,IAAI,UAAU,CAAC;EACxG,MAAM;CACR,UAAU;EACR,MAAM,UAAU,SAAS;CAC3B;AACF;;AAGA,SAAS,QAAQ,QAAwB;CACvC,MAAM,YAAY,GAAG,OAAO,GAAG,QAAQ,IAAI;CAC3C,MAAM,aAAa,GAAG,SAAS,WAAW,MAAM,GAAK;CACrD,IAAI;EACF,GAAG,WAAW,WAAW,MAAM;EAC/B,OAAO;CACT,SAAS,OAAO;EACd,GAAG,UAAU,UAAU;EACvB,GAAG,WAAW,SAAS;EACvB,MAAM;CACR;AACF;AAEA,SAAS,UAAU,QAAsB;CACvC,MAAM,YAAY,GAAG,OAAO,GAAG,QAAQ,IAAI;CAC3C,GAAG,cAAc,WAAW,IAAI,EAAE,MAAM,IAAM,CAAC;CAC/C,GAAG,WAAW,WAAW,MAAM;AACjC;AAEA,IAAI,QAAQ,KAAK,MAAM,YAAY,QAAQ,cAAc,QAAQ,KAAK,EAAE,CAAC,CAAC,MACxE,KAAK,CAAC,CAAC,MACJ,SAAS;CACR,QAAQ,WAAW;AACrB,IACC,UAAmB;CAClB,QAAQ,OAAO,MAAM,GAAG,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,GAAG;CAClF,QAAQ,WAAW;AACrB,CACF"}
1
+ {"version":3,"file":"logSink.mjs","names":[],"sources":["../../src/bin/logSink.ts"],"sourcesContent":["import fs from 'node:fs';\nimport { StringDecoder } from 'node:string_decoder';\nimport { pathToFileURL } from 'node:url';\n\nimport { createPoTelemetry } from '@agimon-ai/po-core/telemetry';\n\nimport { scrubTerminalOutput } from '../services/ansiScrub';\n\nexport async function main(argv: readonly string[] = process.argv.slice(2)): Promise<number> {\n const startedAt = Date.now();\n const telemetry = createPoTelemetry({\n serviceName: 'po-runner-log-sink',\n packageName: '@agimon-ai/po-runner',\n env: process.env,\n enableLogs: true,\n enableTraces: true,\n });\n const [logPath, rotatedPath, maxBytesRaw, donePath, rawPath] = argv;\n try {\n // Keep the legacy argument shape for the published executable subpath. The\n // scrubbed log is append-only, so the former rotation path is unused; the\n // ceiling now bounds the raw copy, which only interactive runs ask for.\n if (!logPath || !rotatedPath || !maxBytesRaw || !donePath) {\n throw new Error('logSink requires log, rotated log, max bytes, and completion paths');\n }\n } catch (error) {\n await telemetry.recordError('po_runner.file_sink_failed', error, { duration_ms: Date.now() - startedAt });\n await telemetry.shutdown();\n throw error;\n }\n\n let handle: number;\n let written: number;\n try {\n handle = fs.openSync(logPath, 'a');\n written = fs.statSync(logPath).size;\n } catch (error) {\n await telemetry.recordError('po_runner.file_sink_failed', error, { duration_ms: Date.now() - startedAt });\n await telemetry.shutdown();\n throw error;\n }\n let chunks = 0;\n // The pane's bytes before anything is taken out of them. Scrubbing exists so\n // the log stays worth grepping, and it is exactly what makes the log useless\n // to a terminal, so an attached view needs its own faithful copy.\n const rawCeiling = Number.parseInt(maxBytesRaw, 10);\n let raw = rawPath === undefined || rawPath === '' ? undefined : openRaw(rawPath);\n let rawWritten = 0;\n const decoder = new StringDecoder('utf8');\n try {\n for await (const chunk of process.stdin) {\n const rawBytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));\n const source = decoder.write(rawBytes);\n if (raw !== undefined) {\n // A terminal that scrolls all day must not fill the disk. Past the\n // ceiling the copy starts over: a reader attaching later wants the\n // recent screen, and no part of this is the record of the run.\n if (rawWritten + rawBytes.byteLength > rawCeiling) {\n const previous = raw;\n raw = openRaw(rawPath as string);\n fs.closeSync(previous);\n rawWritten = 0;\n }\n fs.writeSync(raw, rawBytes);\n rawWritten += rawBytes.byteLength;\n }\n const text = scrubTerminalOutput(source);\n if (!text) continue;\n const bytes = Buffer.from(text, 'utf8');\n fs.writeSync(handle, bytes);\n written += bytes.byteLength;\n chunks += 1;\n }\n const remaining = scrubTerminalOutput(decoder.end());\n if (remaining) {\n const bytes = Buffer.from(remaining);\n fs.writeSync(handle, bytes);\n written += bytes.byteLength;\n }\n fs.closeSync(handle);\n if (raw !== undefined) fs.closeSync(raw);\n writeDone(donePath);\n await telemetry.recordEvent('po_runner.file_sink_finished', {\n outcome: 'completed',\n 'runner.chunk_count': chunks,\n 'runner.bytes_written': written,\n duration_ms: Date.now() - startedAt,\n });\n return 0;\n } catch (error) {\n await telemetry.recordError('po_runner.file_sink_failed', error, { duration_ms: Date.now() - startedAt });\n throw error;\n } finally {\n await telemetry.shutdown();\n }\n}\n\n/** Publish a fresh inode so readers can identify rollover from the descriptor they read. */\nfunction openRaw(target: string): number {\n const temporary = `${target}.${process.pid}.tmp`;\n const descriptor = fs.openSync(temporary, 'wx', 0o600);\n try {\n fs.renameSync(temporary, target);\n return descriptor;\n } catch (error) {\n fs.closeSync(descriptor);\n fs.unlinkSync(temporary);\n throw error;\n }\n}\n\nfunction writeDone(target: string): void {\n const temporary = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(temporary, '', { mode: 0o600 });\n fs.renameSync(temporary, target);\n}\n\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n main().then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n },\n );\n}\n"],"mappings":"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"}
@@ -7,7 +7,7 @@ let node_url = require("node:url");
7
7
  let node_child_process = require("node:child_process");
8
8
  let node_os = require("node:os");
9
9
  node_os = require_rolldown_runtime.__toESM(node_os, 1);
10
- let _agimon_ai_po_telemetry = require("@agimon-ai/po-telemetry");
10
+ let _agimon_ai_po_core_telemetry = require("@agimon-ai/po-core/telemetry");
11
11
  //#region src/bin/runnerHost.ts
12
12
  const POLL_MS = 10;
13
13
  const FORWARDED_SIGNAL_GRACE_MS = 2e3;
@@ -15,7 +15,7 @@ const HOST_FAILED_EVENT = "po_runner.host_failed";
15
15
  const INVALID_COMMAND_SPEC_ERROR = "invalid runner command spec";
16
16
  async function main(argv = process.argv.slice(2)) {
17
17
  const startedAt = Date.now();
18
- const telemetry = (0, _agimon_ai_po_telemetry.createPoTelemetry)({
18
+ const telemetry = (0, _agimon_ai_po_core_telemetry.createPoTelemetry)({
19
19
  serviceName: "po-runner-host",
20
20
  packageName: "@agimon-ai/po-runner",
21
21
  env: process.env,
@@ -1 +1 @@
1
- {"version":3,"file":"runnerHost.cjs","names":["createPoTelemetry","spawn","watchOwner","LIFELINE_ENV","os","fs","pathToFileURL"],"sources":["../../src/bin/runnerHost.ts"],"sourcesContent":["import { type ChildProcess, spawn } from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport { pathToFileURL } from 'node:url';\n\nimport { createPoTelemetry } from '@agimon-ai/po-telemetry';\n\nimport { LIFELINE_ENV, watchOwner } from '../services/lifeline/client';\n\ninterface CommandSpec {\n command: string;\n cwd: string;\n env: Record<string, string>;\n}\n\nconst POLL_MS = 10;\nconst FORWARDED_SIGNAL_GRACE_MS = 2_000;\nconst HOST_FAILED_EVENT = 'po_runner.host_failed';\nconst INVALID_COMMAND_SPEC_ERROR = 'invalid runner command spec';\n\nexport async function main(argv: readonly string[] = process.argv.slice(2)): Promise<number> {\n const startedAt = Date.now();\n const telemetry = createPoTelemetry({\n serviceName: 'po-runner-host',\n packageName: '@agimon-ai/po-runner',\n env: process.env,\n enableLogs: true,\n enableTraces: true,\n });\n const [specPath, gatePath, exitPath] = argv;\n let spec: CommandSpec;\n try {\n if (!specPath || !gatePath || !exitPath) throw new Error('runnerHost requires spec, gate, and exit paths');\n spec = readSpec(specPath);\n await waitForGate(gatePath);\n } catch (error) {\n await telemetry.recordError(HOST_FAILED_EVENT, error, { duration_ms: Date.now() - startedAt });\n await telemetry.shutdown();\n throw error;\n }\n let child: ChildProcess | undefined;\n let forwardedSignal: NodeJS.Signals | null = null;\n let escalationTimer: NodeJS.Timeout | undefined;\n let ownerLost = false;\n const signalChild = (signal: NodeJS.Signals): void => {\n if (!child?.pid) return;\n try {\n process.kill(-child.pid, signal);\n } catch {\n child.kill(signal);\n }\n };\n const forward = (signal: NodeJS.Signals): void => {\n forwardedSignal = signal;\n signalChild(signal);\n escalationTimer ??= setTimeout(() => signalChild('SIGKILL'), FORWARDED_SIGNAL_GRACE_MS);\n };\n const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT', 'SIGHUP'];\n for (const signal of signals) process.once(signal, () => forward(signal));\n\n child = spawn(spec.command, {\n cwd: spec.cwd,\n env: spec.env,\n shell: '/bin/bash',\n stdio: 'inherit',\n detached: true,\n });\n if (forwardedSignal) forward(forwardedSignal);\n\n // This process supervises the command for its whole life, so it is also the\n // right place to notice an owner that died without stopping it.\n //\n // The lifeline comes from the spec rather than this process's environment: an\n // rmux server outlives the session that started it, so its own environment can\n // still name a previous session's socket.\n const detachLifeline = watchOwner(() => {\n ownerLost = true;\n forward('SIGTERM');\n }, spec.env[LIFELINE_ENV]);\n\n try {\n const result = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {\n child?.once('error', reject);\n child?.once('exit', (code, signal) => resolve({ code, signal: signal ?? forwardedSignal }));\n });\n writeAtomic(exitPath, {\n ...result,\n ...(ownerLost ? { reason: 'stopped', terminationReason: 'owner_lost', stopReason: 'owner session ended' } : {}),\n });\n const exitCode = result.code !== null ? result.code : result.signal ? 128 + os.constants.signals[result.signal] : 1;\n await telemetry.recordEvent('po_runner.host_finished', {\n outcome: result.signal || forwardedSignal ? 'signaled' : exitCode === 0 ? 'completed' : 'failed',\n 'runner.exit_code': exitCode,\n ...(result.signal || forwardedSignal ? { 'runner.signal': result.signal ?? forwardedSignal ?? '' } : {}),\n duration_ms: Date.now() - startedAt,\n });\n return exitCode;\n } catch (error) {\n await telemetry.recordError(HOST_FAILED_EVENT, error, { duration_ms: Date.now() - startedAt });\n throw error;\n } finally {\n detachLifeline();\n if (escalationTimer) clearTimeout(escalationTimer);\n await telemetry.shutdown();\n }\n}\n\nfunction readSpec(target: string): CommandSpec {\n const value: unknown = JSON.parse(fs.readFileSync(target, 'utf8'));\n if (typeof value !== 'object' || value === null) throw new Error(INVALID_COMMAND_SPEC_ERROR);\n const spec = value as Partial<CommandSpec>;\n if (typeof spec.command !== 'string' || typeof spec.cwd !== 'string' || !spec.env) {\n throw new Error(INVALID_COMMAND_SPEC_ERROR);\n }\n return { command: spec.command, cwd: spec.cwd, env: spec.env };\n}\n\nasync function waitForGate(target: string): Promise<void> {\n while (!fs.existsSync(target)) await new Promise((resolve) => setTimeout(resolve, POLL_MS));\n}\n\nfunction writeAtomic(target: string, value: unknown): void {\n const temporary = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(temporary, `${JSON.stringify(value)}\\n`, { mode: 0o600 });\n fs.renameSync(temporary, target);\n}\n\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n main().then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n },\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"runnerHost.cjs","names":["createPoTelemetry","spawn","watchOwner","LIFELINE_ENV","os","fs","pathToFileURL"],"sources":["../../src/bin/runnerHost.ts"],"sourcesContent":["import { type ChildProcess, spawn } from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport { pathToFileURL } from 'node:url';\n\nimport { createPoTelemetry } from '@agimon-ai/po-core/telemetry';\n\nimport { LIFELINE_ENV, watchOwner } from '../services/lifeline/client';\n\ninterface CommandSpec {\n command: string;\n cwd: string;\n env: Record<string, string>;\n}\n\nconst POLL_MS = 10;\nconst FORWARDED_SIGNAL_GRACE_MS = 2_000;\nconst HOST_FAILED_EVENT = 'po_runner.host_failed';\nconst INVALID_COMMAND_SPEC_ERROR = 'invalid runner command spec';\n\nexport async function main(argv: readonly string[] = process.argv.slice(2)): Promise<number> {\n const startedAt = Date.now();\n const telemetry = createPoTelemetry({\n serviceName: 'po-runner-host',\n packageName: '@agimon-ai/po-runner',\n env: process.env,\n enableLogs: true,\n enableTraces: true,\n });\n const [specPath, gatePath, exitPath] = argv;\n let spec: CommandSpec;\n try {\n if (!specPath || !gatePath || !exitPath) throw new Error('runnerHost requires spec, gate, and exit paths');\n spec = readSpec(specPath);\n await waitForGate(gatePath);\n } catch (error) {\n await telemetry.recordError(HOST_FAILED_EVENT, error, { duration_ms: Date.now() - startedAt });\n await telemetry.shutdown();\n throw error;\n }\n let child: ChildProcess | undefined;\n let forwardedSignal: NodeJS.Signals | null = null;\n let escalationTimer: NodeJS.Timeout | undefined;\n let ownerLost = false;\n const signalChild = (signal: NodeJS.Signals): void => {\n if (!child?.pid) return;\n try {\n process.kill(-child.pid, signal);\n } catch {\n child.kill(signal);\n }\n };\n const forward = (signal: NodeJS.Signals): void => {\n forwardedSignal = signal;\n signalChild(signal);\n escalationTimer ??= setTimeout(() => signalChild('SIGKILL'), FORWARDED_SIGNAL_GRACE_MS);\n };\n const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT', 'SIGHUP'];\n for (const signal of signals) process.once(signal, () => forward(signal));\n\n child = spawn(spec.command, {\n cwd: spec.cwd,\n env: spec.env,\n shell: '/bin/bash',\n stdio: 'inherit',\n detached: true,\n });\n if (forwardedSignal) forward(forwardedSignal);\n\n // This process supervises the command for its whole life, so it is also the\n // right place to notice an owner that died without stopping it.\n //\n // The lifeline comes from the spec rather than this process's environment: an\n // rmux server outlives the session that started it, so its own environment can\n // still name a previous session's socket.\n const detachLifeline = watchOwner(() => {\n ownerLost = true;\n forward('SIGTERM');\n }, spec.env[LIFELINE_ENV]);\n\n try {\n const result = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {\n child?.once('error', reject);\n child?.once('exit', (code, signal) => resolve({ code, signal: signal ?? forwardedSignal }));\n });\n writeAtomic(exitPath, {\n ...result,\n ...(ownerLost ? { reason: 'stopped', terminationReason: 'owner_lost', stopReason: 'owner session ended' } : {}),\n });\n const exitCode = result.code !== null ? result.code : result.signal ? 128 + os.constants.signals[result.signal] : 1;\n await telemetry.recordEvent('po_runner.host_finished', {\n outcome: result.signal || forwardedSignal ? 'signaled' : exitCode === 0 ? 'completed' : 'failed',\n 'runner.exit_code': exitCode,\n ...(result.signal || forwardedSignal ? { 'runner.signal': result.signal ?? forwardedSignal ?? '' } : {}),\n duration_ms: Date.now() - startedAt,\n });\n return exitCode;\n } catch (error) {\n await telemetry.recordError(HOST_FAILED_EVENT, error, { duration_ms: Date.now() - startedAt });\n throw error;\n } finally {\n detachLifeline();\n if (escalationTimer) clearTimeout(escalationTimer);\n await telemetry.shutdown();\n }\n}\n\nfunction readSpec(target: string): CommandSpec {\n const value: unknown = JSON.parse(fs.readFileSync(target, 'utf8'));\n if (typeof value !== 'object' || value === null) throw new Error(INVALID_COMMAND_SPEC_ERROR);\n const spec = value as Partial<CommandSpec>;\n if (typeof spec.command !== 'string' || typeof spec.cwd !== 'string' || !spec.env) {\n throw new Error(INVALID_COMMAND_SPEC_ERROR);\n }\n return { command: spec.command, cwd: spec.cwd, env: spec.env };\n}\n\nasync function waitForGate(target: string): Promise<void> {\n while (!fs.existsSync(target)) await new Promise((resolve) => setTimeout(resolve, POLL_MS));\n}\n\nfunction writeAtomic(target: string, value: unknown): void {\n const temporary = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(temporary, `${JSON.stringify(value)}\\n`, { mode: 0o600 });\n fs.renameSync(temporary, target);\n}\n\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n main().then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n },\n );\n}\n"],"mappings":"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"}
@@ -3,7 +3,7 @@ import fs from "node:fs";
3
3
  import { pathToFileURL } from "node:url";
4
4
  import { spawn } from "node:child_process";
5
5
  import os from "node:os";
6
- import { createPoTelemetry } from "@agimon-ai/po-telemetry";
6
+ import { createPoTelemetry } from "@agimon-ai/po-core/telemetry";
7
7
  //#region src/bin/runnerHost.ts
8
8
  const POLL_MS = 10;
9
9
  const FORWARDED_SIGNAL_GRACE_MS = 2e3;
@@ -1 +1 @@
1
- {"version":3,"file":"runnerHost.mjs","names":[],"sources":["../../src/bin/runnerHost.ts"],"sourcesContent":["import { type ChildProcess, spawn } from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport { pathToFileURL } from 'node:url';\n\nimport { createPoTelemetry } from '@agimon-ai/po-telemetry';\n\nimport { LIFELINE_ENV, watchOwner } from '../services/lifeline/client';\n\ninterface CommandSpec {\n command: string;\n cwd: string;\n env: Record<string, string>;\n}\n\nconst POLL_MS = 10;\nconst FORWARDED_SIGNAL_GRACE_MS = 2_000;\nconst HOST_FAILED_EVENT = 'po_runner.host_failed';\nconst INVALID_COMMAND_SPEC_ERROR = 'invalid runner command spec';\n\nexport async function main(argv: readonly string[] = process.argv.slice(2)): Promise<number> {\n const startedAt = Date.now();\n const telemetry = createPoTelemetry({\n serviceName: 'po-runner-host',\n packageName: '@agimon-ai/po-runner',\n env: process.env,\n enableLogs: true,\n enableTraces: true,\n });\n const [specPath, gatePath, exitPath] = argv;\n let spec: CommandSpec;\n try {\n if (!specPath || !gatePath || !exitPath) throw new Error('runnerHost requires spec, gate, and exit paths');\n spec = readSpec(specPath);\n await waitForGate(gatePath);\n } catch (error) {\n await telemetry.recordError(HOST_FAILED_EVENT, error, { duration_ms: Date.now() - startedAt });\n await telemetry.shutdown();\n throw error;\n }\n let child: ChildProcess | undefined;\n let forwardedSignal: NodeJS.Signals | null = null;\n let escalationTimer: NodeJS.Timeout | undefined;\n let ownerLost = false;\n const signalChild = (signal: NodeJS.Signals): void => {\n if (!child?.pid) return;\n try {\n process.kill(-child.pid, signal);\n } catch {\n child.kill(signal);\n }\n };\n const forward = (signal: NodeJS.Signals): void => {\n forwardedSignal = signal;\n signalChild(signal);\n escalationTimer ??= setTimeout(() => signalChild('SIGKILL'), FORWARDED_SIGNAL_GRACE_MS);\n };\n const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT', 'SIGHUP'];\n for (const signal of signals) process.once(signal, () => forward(signal));\n\n child = spawn(spec.command, {\n cwd: spec.cwd,\n env: spec.env,\n shell: '/bin/bash',\n stdio: 'inherit',\n detached: true,\n });\n if (forwardedSignal) forward(forwardedSignal);\n\n // This process supervises the command for its whole life, so it is also the\n // right place to notice an owner that died without stopping it.\n //\n // The lifeline comes from the spec rather than this process's environment: an\n // rmux server outlives the session that started it, so its own environment can\n // still name a previous session's socket.\n const detachLifeline = watchOwner(() => {\n ownerLost = true;\n forward('SIGTERM');\n }, spec.env[LIFELINE_ENV]);\n\n try {\n const result = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {\n child?.once('error', reject);\n child?.once('exit', (code, signal) => resolve({ code, signal: signal ?? forwardedSignal }));\n });\n writeAtomic(exitPath, {\n ...result,\n ...(ownerLost ? { reason: 'stopped', terminationReason: 'owner_lost', stopReason: 'owner session ended' } : {}),\n });\n const exitCode = result.code !== null ? result.code : result.signal ? 128 + os.constants.signals[result.signal] : 1;\n await telemetry.recordEvent('po_runner.host_finished', {\n outcome: result.signal || forwardedSignal ? 'signaled' : exitCode === 0 ? 'completed' : 'failed',\n 'runner.exit_code': exitCode,\n ...(result.signal || forwardedSignal ? { 'runner.signal': result.signal ?? forwardedSignal ?? '' } : {}),\n duration_ms: Date.now() - startedAt,\n });\n return exitCode;\n } catch (error) {\n await telemetry.recordError(HOST_FAILED_EVENT, error, { duration_ms: Date.now() - startedAt });\n throw error;\n } finally {\n detachLifeline();\n if (escalationTimer) clearTimeout(escalationTimer);\n await telemetry.shutdown();\n }\n}\n\nfunction readSpec(target: string): CommandSpec {\n const value: unknown = JSON.parse(fs.readFileSync(target, 'utf8'));\n if (typeof value !== 'object' || value === null) throw new Error(INVALID_COMMAND_SPEC_ERROR);\n const spec = value as Partial<CommandSpec>;\n if (typeof spec.command !== 'string' || typeof spec.cwd !== 'string' || !spec.env) {\n throw new Error(INVALID_COMMAND_SPEC_ERROR);\n }\n return { command: spec.command, cwd: spec.cwd, env: spec.env };\n}\n\nasync function waitForGate(target: string): Promise<void> {\n while (!fs.existsSync(target)) await new Promise((resolve) => setTimeout(resolve, POLL_MS));\n}\n\nfunction writeAtomic(target: string, value: unknown): void {\n const temporary = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(temporary, `${JSON.stringify(value)}\\n`, { mode: 0o600 });\n fs.renameSync(temporary, target);\n}\n\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n main().then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n },\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"runnerHost.mjs","names":[],"sources":["../../src/bin/runnerHost.ts"],"sourcesContent":["import { type ChildProcess, spawn } from 'node:child_process';\nimport fs from 'node:fs';\nimport os from 'node:os';\nimport { pathToFileURL } from 'node:url';\n\nimport { createPoTelemetry } from '@agimon-ai/po-core/telemetry';\n\nimport { LIFELINE_ENV, watchOwner } from '../services/lifeline/client';\n\ninterface CommandSpec {\n command: string;\n cwd: string;\n env: Record<string, string>;\n}\n\nconst POLL_MS = 10;\nconst FORWARDED_SIGNAL_GRACE_MS = 2_000;\nconst HOST_FAILED_EVENT = 'po_runner.host_failed';\nconst INVALID_COMMAND_SPEC_ERROR = 'invalid runner command spec';\n\nexport async function main(argv: readonly string[] = process.argv.slice(2)): Promise<number> {\n const startedAt = Date.now();\n const telemetry = createPoTelemetry({\n serviceName: 'po-runner-host',\n packageName: '@agimon-ai/po-runner',\n env: process.env,\n enableLogs: true,\n enableTraces: true,\n });\n const [specPath, gatePath, exitPath] = argv;\n let spec: CommandSpec;\n try {\n if (!specPath || !gatePath || !exitPath) throw new Error('runnerHost requires spec, gate, and exit paths');\n spec = readSpec(specPath);\n await waitForGate(gatePath);\n } catch (error) {\n await telemetry.recordError(HOST_FAILED_EVENT, error, { duration_ms: Date.now() - startedAt });\n await telemetry.shutdown();\n throw error;\n }\n let child: ChildProcess | undefined;\n let forwardedSignal: NodeJS.Signals | null = null;\n let escalationTimer: NodeJS.Timeout | undefined;\n let ownerLost = false;\n const signalChild = (signal: NodeJS.Signals): void => {\n if (!child?.pid) return;\n try {\n process.kill(-child.pid, signal);\n } catch {\n child.kill(signal);\n }\n };\n const forward = (signal: NodeJS.Signals): void => {\n forwardedSignal = signal;\n signalChild(signal);\n escalationTimer ??= setTimeout(() => signalChild('SIGKILL'), FORWARDED_SIGNAL_GRACE_MS);\n };\n const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT', 'SIGHUP'];\n for (const signal of signals) process.once(signal, () => forward(signal));\n\n child = spawn(spec.command, {\n cwd: spec.cwd,\n env: spec.env,\n shell: '/bin/bash',\n stdio: 'inherit',\n detached: true,\n });\n if (forwardedSignal) forward(forwardedSignal);\n\n // This process supervises the command for its whole life, so it is also the\n // right place to notice an owner that died without stopping it.\n //\n // The lifeline comes from the spec rather than this process's environment: an\n // rmux server outlives the session that started it, so its own environment can\n // still name a previous session's socket.\n const detachLifeline = watchOwner(() => {\n ownerLost = true;\n forward('SIGTERM');\n }, spec.env[LIFELINE_ENV]);\n\n try {\n const result = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {\n child?.once('error', reject);\n child?.once('exit', (code, signal) => resolve({ code, signal: signal ?? forwardedSignal }));\n });\n writeAtomic(exitPath, {\n ...result,\n ...(ownerLost ? { reason: 'stopped', terminationReason: 'owner_lost', stopReason: 'owner session ended' } : {}),\n });\n const exitCode = result.code !== null ? result.code : result.signal ? 128 + os.constants.signals[result.signal] : 1;\n await telemetry.recordEvent('po_runner.host_finished', {\n outcome: result.signal || forwardedSignal ? 'signaled' : exitCode === 0 ? 'completed' : 'failed',\n 'runner.exit_code': exitCode,\n ...(result.signal || forwardedSignal ? { 'runner.signal': result.signal ?? forwardedSignal ?? '' } : {}),\n duration_ms: Date.now() - startedAt,\n });\n return exitCode;\n } catch (error) {\n await telemetry.recordError(HOST_FAILED_EVENT, error, { duration_ms: Date.now() - startedAt });\n throw error;\n } finally {\n detachLifeline();\n if (escalationTimer) clearTimeout(escalationTimer);\n await telemetry.shutdown();\n }\n}\n\nfunction readSpec(target: string): CommandSpec {\n const value: unknown = JSON.parse(fs.readFileSync(target, 'utf8'));\n if (typeof value !== 'object' || value === null) throw new Error(INVALID_COMMAND_SPEC_ERROR);\n const spec = value as Partial<CommandSpec>;\n if (typeof spec.command !== 'string' || typeof spec.cwd !== 'string' || !spec.env) {\n throw new Error(INVALID_COMMAND_SPEC_ERROR);\n }\n return { command: spec.command, cwd: spec.cwd, env: spec.env };\n}\n\nasync function waitForGate(target: string): Promise<void> {\n while (!fs.existsSync(target)) await new Promise((resolve) => setTimeout(resolve, POLL_MS));\n}\n\nfunction writeAtomic(target: string, value: unknown): void {\n const temporary = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(temporary, `${JSON.stringify(value)}\\n`, { mode: 0o600 });\n fs.renameSync(temporary, target);\n}\n\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n main().then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n process.stderr.write(`${error instanceof Error ? error.message : String(error)}\\n`);\n process.exitCode = 1;\n },\n );\n}\n"],"mappings":";;;;;;;AAeA,MAAM,UAAU;AAChB,MAAM,4BAA4B;AAClC,MAAM,oBAAoB;AAC1B,MAAM,6BAA6B;AAEnC,eAAsB,KAAK,OAA0B,QAAQ,KAAK,MAAM,CAAC,GAAoB;CAC3F,MAAM,YAAY,KAAK,IAAI;CAC3B,MAAM,YAAY,kBAAkB;EAClC,aAAa;EACb,aAAa;EACb,KAAK,QAAQ;EACb,YAAY;EACZ,cAAc;CAChB,CAAC;CACD,MAAM,CAAC,UAAU,UAAU,YAAY;CACvC,IAAI;CACJ,IAAI;EACF,IAAI,CAAC,YAAY,CAAC,YAAY,CAAC,UAAU,MAAM,IAAI,MAAM,gDAAgD;EACzG,OAAO,SAAS,QAAQ;EACxB,MAAM,YAAY,QAAQ;CAC5B,SAAS,OAAO;EACd,MAAM,UAAU,YAAY,mBAAmB,OAAO,EAAE,aAAa,KAAK,IAAI,IAAI,UAAU,CAAC;EAC7F,MAAM,UAAU,SAAS;EACzB,MAAM;CACR;CACA,IAAI;CACJ,IAAI,kBAAyC;CAC7C,IAAI;CACJ,IAAI,YAAY;CAChB,MAAM,eAAe,WAAiC;EACpD,IAAI,CAAC,OAAO,KAAK;EACjB,IAAI;GACF,QAAQ,KAAK,CAAC,MAAM,KAAK,MAAM;EACjC,QAAQ;GACN,MAAM,KAAK,MAAM;EACnB;CACF;CACA,MAAM,WAAW,WAAiC;EAChD,kBAAkB;EAClB,YAAY,MAAM;EAClB,oBAAoB,iBAAiB,YAAY,SAAS,GAAG,yBAAyB;CACxF;CAEA,KAAK,MAAM,UAAU;EADc;EAAW;EAAU;CAC7B,GAAG,QAAQ,KAAK,cAAc,QAAQ,MAAM,CAAC;CAExE,QAAQ,MAAM,KAAK,SAAS;EAC1B,KAAK,KAAK;EACV,KAAK,KAAK;EACV,OAAO;EACP,OAAO;EACP,UAAU;CACZ,CAAC;CACD,IAAI,iBAAiB,QAAQ,eAAe;CAQ5C,MAAM,iBAAiB,iBAAiB;EACtC,YAAY;EACZ,QAAQ,SAAS;CACnB,GAAG,KAAK,IAAI,aAAa;CAEzB,IAAI;EACF,MAAM,SAAS,MAAM,IAAI,SAAiE,SAAS,WAAW;GAC5G,OAAO,KAAK,SAAS,MAAM;GAC3B,OAAO,KAAK,SAAS,MAAM,WAAW,QAAQ;IAAE;IAAM,QAAQ,UAAU;GAAgB,CAAC,CAAC;EAC5F,CAAC;EACD,YAAY,UAAU;GACpB,GAAG;GACH,GAAI,YAAY;IAAE,QAAQ;IAAW,mBAAmB;IAAc,YAAY;GAAsB,IAAI,CAAC;EAC/G,CAAC;EACD,MAAM,WAAW,OAAO,SAAS,OAAO,OAAO,OAAO,OAAO,SAAS,MAAM,GAAG,UAAU,QAAQ,OAAO,UAAU;EAClH,MAAM,UAAU,YAAY,2BAA2B;GACrD,SAAS,OAAO,UAAU,kBAAkB,aAAa,aAAa,IAAI,cAAc;GACxF,oBAAoB;GACpB,GAAI,OAAO,UAAU,kBAAkB,EAAE,iBAAiB,OAAO,UAAU,mBAAmB,GAAG,IAAI,CAAC;GACtG,aAAa,KAAK,IAAI,IAAI;EAC5B,CAAC;EACD,OAAO;CACT,SAAS,OAAO;EACd,MAAM,UAAU,YAAY,mBAAmB,OAAO,EAAE,aAAa,KAAK,IAAI,IAAI,UAAU,CAAC;EAC7F,MAAM;CACR,UAAU;EACR,eAAe;EACf,IAAI,iBAAiB,aAAa,eAAe;EACjD,MAAM,UAAU,SAAS;CAC3B;AACF;AAEA,SAAS,SAAS,QAA6B;CAC7C,MAAM,QAAiB,KAAK,MAAM,GAAG,aAAa,QAAQ,MAAM,CAAC;CACjE,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,MAAM,IAAI,MAAM,0BAA0B;CAC3F,MAAM,OAAO;CACb,IAAI,OAAO,KAAK,YAAY,YAAY,OAAO,KAAK,QAAQ,YAAY,CAAC,KAAK,KAC5E,MAAM,IAAI,MAAM,0BAA0B;CAE5C,OAAO;EAAE,SAAS,KAAK;EAAS,KAAK,KAAK;EAAK,KAAK,KAAK;CAAI;AAC/D;AAEA,eAAe,YAAY,QAA+B;CACxD,OAAO,CAAC,GAAG,WAAW,MAAM,GAAG,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,OAAO,CAAC;AAC5F;AAEA,SAAS,YAAY,QAAgB,OAAsB;CACzD,MAAM,YAAY,GAAG,OAAO,GAAG,QAAQ,IAAI;CAC3C,GAAG,cAAc,WAAW,GAAG,KAAK,UAAU,KAAK,EAAE,KAAK,EAAE,MAAM,IAAM,CAAC;CACzE,GAAG,WAAW,WAAW,MAAM;AACjC;AAEA,IAAI,QAAQ,KAAK,MAAM,YAAY,QAAQ,cAAc,QAAQ,KAAK,EAAE,CAAC,CAAC,MACxE,KAAK,CAAC,CAAC,MACJ,SAAS;CACR,QAAQ,WAAW;AACrB,IACC,UAAmB;CAClB,QAAQ,OAAO,MAAM,GAAG,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,GAAG;CAClF,QAAQ,WAAW;AACrB,CACF"}
@@ -39,13 +39,13 @@ require("@agimon-ai/foundation-process-registry");
39
39
  require("@agimon-ai/po-core/childProcess");
40
40
  require("@agimon-ai/po-core/backgroundWork");
41
41
  require("@agimon-ai/po-core/extensionFile");
42
- require("@agimon-ai/po-telemetry");
42
+ require("@agimon-ai/po-core/telemetry");
43
43
  require("@earendil-works/pi-coding-agent");
44
- //#region src/extensions/workspaces/sessions/(backend)/skill/po-use-runner.mcp.ts
45
- var po_use_runner_mcp_default = (0, _agimon_ai_po_core_mcpFacet.defineMcpSkill)({
46
- name: "po-use-runner",
44
+ //#region src/extensions/workspaces/sessions/(backend)/skill/help-how-to-runner.mcp.ts
45
+ var help_how_to_runner_mcp_default = (0, _agimon_ai_po_core_mcpFacet.defineMcpSkill)({
46
+ name: "help-how-to-runner",
47
47
  description: "Use Po Pi Runner to supervise shell commands, inspect durable logs, provide interactive input, and stop background runs.",
48
- read: () => (0, _agimon_ai_po_core_serverPlugin.readPackageResource)(require("url").pathToFileURL(__filename).href, "src/prompts/po-use-runner/SKILL.md")
48
+ read: () => (0, _agimon_ai_po_core_serverPlugin.readPackageResource)(require("url").pathToFileURL(__filename).href, "src/prompts/help-how-to-runner/SKILL.md")
49
49
  });
50
50
  //#endregion
51
51
  //#region src/constants/runnerSettings.ts
@@ -130,7 +130,7 @@ const mcp = (0, _agimon_ai_po_core_mcpFacet.defineMcpPlugin)({
130
130
  name: "@agimon-ai/po-runner",
131
131
  session: (context) => ({
132
132
  get skills() {
133
- return [at(po_use_runner_mcp_default, context)];
133
+ return [at(help_how_to_runner_mcp_default, context)];
134
134
  },
135
135
  get tools() {
136
136
  return [withWidget("@agimon-ai/po-runner/bash", via({ name: "bash" }, at(bash_mcp_default, context)))];
@@ -1 +1 @@
1
- {"version":3,"file":"mcp.cjs","names":["defineMcpSkill","readPackageResource","Type","headless","promisify","execFile","defineMcpTool","defineMcpPlugin","skillPoUseRunner","toolBash"],"sources":["../../src/extensions/workspaces/sessions/(backend)/skill/po-use-runner.mcp.ts","../../src/constants/runnerSettings.ts","../../src/schemas/bashTool.ts","../../src/services/ansiScrub/index.ts","../../src/services/ptyHost/index.ts","../../src/services/runnerRegistry/index.ts","../../src/services/runnerSettings/index.ts","../../src/extensions/workspaces/sessions/(backend)/_lib/serverRoot.ts","../../src/extensions/workspaces/sessions/(backend)/tool/bash.mcp.ts","../../generated/mcp.ts"],"sourcesContent":["import { defineMcpSkill } from '@agimon-ai/po-core/mcpFacet';\nimport { readPackageResource } from '@agimon-ai/po-core/serverPlugin';\n\nexport default defineMcpSkill({\n name: 'po-use-runner',\n description:\n 'Use Po Pi Runner to supervise shell commands, inspect durable logs, provide interactive input, and stop background runs.',\n read: () => readPackageResource(import.meta.url, 'src/prompts/po-use-runner/SKILL.md'),\n});\n","export const RUNNER_SETTINGS_FILE = 'po-runner.json';\nexport const RATIO_KEYS = ['headRatio', 'errorBudgetRatio'] as const;\nexport const MAX_RESULT_BYTES_CEILING = 262_144;\nexport const MAX_RESULT_LINES_CEILING = 5_000;\nexport const MAX_RESULT_TOKENS_CEILING = 100_000;\nexport const MAX_ERROR_ENTRIES_CEILING = 100;\nexport const MAX_PATTERNS = 32;\nexport const MAX_PATTERN_LENGTH = 512;\n","import { type Static, Type } from 'typebox';\n\n/** Replaces pi's built-in tool of the same name, so hooks keyed on `bash` keep working. */\n\n/**\n * Built-in bash parameters plus the backgrounding controls.\n *\n * `command` and `timeout` keep their original names and wording so the tool is\n * a drop-in replacement. Every `description` is prompt copy the model reads.\n */\nexport const BashParamsSchema = Type.Object({\n command: Type.String({ description: 'Bash command to execute' }),\n timeout: Type.Optional(Type.Number({ description: 'Timeout in seconds (optional, no default timeout)' })),\n background: Type.Optional(\n Type.Boolean({\n description:\n 'Start the command in the background immediately instead of waiting. Use for dev servers, watchers and other commands you already know will not finish.',\n }),\n ),\n interactive: Type.Optional(\n Type.Boolean({\n description:\n 'Run under a pseudo terminal so the command can prompt. Use only when the command asks for confirmation or input; answer it with po-runner input. Logs from interactive runs are noisier.',\n }),\n ),\n name: Type.Optional(\n Type.String({\n description: 'Preferred runner name if the command ends up in the background. A name in use gets a suffix.',\n }),\n ),\n});\n\nexport type BashParams = Static<typeof BashParamsSchema>;\n","import { CONTROL_PATTERN } from '../../constants/ansiScrub';\n/**\n * Turns terminal output into text worth grepping.\n *\n * PTY output is written for a screen, not a file: colours, cursor moves and\n * carriage-return redraws all end up in the byte stream. Without this a single\n * progress bar becomes thousands of near-identical log lines.\n */\n\n/**\n * CSI sequences (colours, cursor moves), OSC strings, the two-byte Fe escapes,\n * and the keypad-mode escapes shells emit around every prompt.\n */\nconst ANSI_PATTERN = new RegExp(\n [\n '\\\\u001B\\\\[[0-9;?]*[ -/]*[@-~]',\n '\\\\u001B\\\\][^\\\\u0007\\\\u001B]*(?:\\\\u0007|\\\\u001B\\\\\\\\)',\n '\\\\u001B[@-Z\\\\\\\\-_]',\n '\\\\u001B[=><]',\n ].join('|'),\n 'g',\n);\n\n/** Bell and backspace, which a terminal consumes rather than displays. */\n// oxlint-disable-next-line no-control-regex -- matching terminal control bytes is the whole job\n\n/**\n * The same set minus SGR, the sequence that only sets colour and weight.\n *\n * The final byte class omits `m`, so `ESC[32m` survives while cursor moves,\n * OSC strings and keypad escapes are still removed.\n */\nconst ANSI_PATTERN_KEEPING_COLOUR = new RegExp(\n [\n '\\\\u001B\\\\[[0-9;?]*[ -/]*[@-ln-~]',\n '\\\\u001B\\\\][^\\\\u0007\\\\u001B]*(?:\\\\u0007|\\\\u001B\\\\\\\\)',\n '\\\\u001B[@-Z\\\\\\\\-_]',\n '\\\\u001B[=><]',\n ].join('|'),\n 'g',\n);\n\n/** Removes escape sequences, leaving the characters a user would have seen. */\nexport function stripAnsi(text: string): string {\n return text.replace(ANSI_PATTERN, '').replace(CONTROL_PATTERN, '');\n}\n\n/**\n * Removes everything `stripAnsi` does except colour.\n *\n * Logs are read back by a terminal renderer as well as by grep, and a command\n * that colours its own output has already said how it should look.\n */\nexport function stripAnsiKeepingColour(text: string): string {\n return text.replace(ANSI_PATTERN_KEEPING_COLOUR, '').replace(CONTROL_PATTERN, '');\n}\n\n/**\n * Collapses carriage-return redraws.\n *\n * A bare `\\r` returns the cursor to the start of the current line, so whatever\n * follows overwrites it. Only the final state of each line is kept. The `\\r` of\n * a CRLF pair is a line terminator rather than a redraw, so it is normalised\n * away first: a pseudo terminal ends every line that way, and treating it as a\n * redraw would blank the whole log.\n */\nexport function collapseCarriageReturns(text: string): string {\n return text\n .replace(/\\r\\n/g, '\\n')\n .split('\\n')\n .map((line) => (line.includes('\\r') ? (line.split('\\r').at(-1) ?? '') : line))\n .join('\\n');\n}\n\n/** Full scrub applied to PTY output before it reaches the log file. */\nexport function scrubTerminalOutput(text: string): string {\n return collapseCarriageReturns(stripAnsiKeepingColour(text));\n}\n\n/** Plain-text scrub, for the copy the model reads. */\nexport function scrubTerminalOutputToPlainText(text: string): string {\n return collapseCarriageReturns(stripAnsi(text));\n}\n","import headless, { type Terminal } from '@xterm/headless';\n\nimport {\n DEFAULT_COLS,\n DEFAULT_ROWS,\n LIVENESS_POLL_MS,\n MEMORY_BUFFER_FACTOR,\n NEWLINE,\n SCROLLBACK,\n TERM_GRACE_MS,\n} from '../../constants/ptyHost';\nimport { PI_SESSION_ID_ENV } from '../../constants/session';\nimport type { IClock } from '../../types/clock';\nimport type { ILogFile } from '../../types/logFile';\nimport type { IProcessControl } from '../../types/processControl';\nimport type { IPtyHost, PtyLaunchRequest, PtyRun } from '../../types/ptyHost';\nimport type { IPtySpawner, PtyProcess } from '../../types/ptySpawner';\nimport type { RunnerSettings } from '../../types/runnerSettings';\nimport type { ExitResult } from '../../types/spawner';\nimport { scrubTerminalOutput } from '../ansiScrub';\nimport { getResultMaxBytes } from '../runnerConfig';\nimport { NO_TERMINAL_INPUT_ENV } from '../runnerSupervisor';\n\n/** Enough history for the overlay to scroll without holding a whole build. */\n\n// Use the CommonJS default export: Node cannot detect this package's named exports.\n// A static import also lets server bundles include it without a live node_modules lookup.\nconst { Terminal: HeadlessTerminal } = headless;\n\nexport class PtyHost implements IPtyHost {\n private readonly runs = new Map<string, PtyRun>();\n\n constructor(\n private readonly spawner: IPtySpawner,\n private readonly logFile: ILogFile,\n private readonly processControl: IProcessControl,\n private readonly clock: IClock,\n private readonly environment: Readonly<Record<string, string | undefined>> = process.env,\n private readonly settings?: RunnerSettings,\n ) {}\n\n async launch(request: PtyLaunchRequest): Promise<PtyRun> {\n const cols = request.cols ?? DEFAULT_COLS;\n const rows = request.rows ?? DEFAULT_ROWS;\n const writer = this.logFile.open(request.id);\n const terminal = new HeadlessTerminal({ cols, rows, scrollback: SCROLLBACK, allowProposedApi: true });\n const memoryLimit = getResultMaxBytes(this.environment, this.settings) * MEMORY_BUFFER_FACTOR;\n\n const child = await this.spawner.spawn({\n command: request.command,\n cwd: request.cwd,\n env: { ...this.environment, ...NO_TERMINAL_INPUT_ENV, [PI_SESSION_ID_ENV]: request.sessionId },\n cols,\n rows,\n });\n\n let buffer = '';\n let buffering = true;\n let settled = false;\n let resolveCompletion: (result: ExitResult) => void = () => undefined;\n const completion = new Promise<ExitResult>((resolve) => {\n resolveCompletion = resolve;\n });\n const dataHandlers = new Set<(data: string) => void>();\n\n child.onData((data) => {\n // The terminal keeps the screen; the log keeps text worth grepping.\n terminal.write(data);\n const scrubbed = scrubTerminalOutput(data);\n writer.append(scrubbed);\n if (buffering) {\n buffer += scrubbed;\n if (buffer.length > memoryLimit) buffer = buffer.slice(-memoryLimit);\n }\n for (const handler of dataHandlers) handler(data);\n });\n\n child.onExit((result) => {\n if (settled) return;\n settled = true;\n this.runs.delete(request.name);\n writer.close();\n resolveCompletion({ code: result.exitCode, signal: null });\n });\n\n const run: PtyRun = {\n id: request.id,\n name: request.name,\n pid: child.pid,\n logPath: writer.path,\n backend: 'native',\n output: () => buffer,\n completion: () => completion,\n detach: () => {\n buffering = false;\n buffer = '';\n },\n stop: () => this.stopChild(child, request.name),\n write: (text) => child.write(text),\n screen: () => renderScreen(terminal),\n onData: (handler) => {\n dataHandlers.add(handler);\n return () => dataHandlers.delete(handler);\n },\n resize: (nextCols, nextRows) => {\n child.resize(nextCols, nextRows);\n terminal.resize(nextCols, nextRows);\n },\n };\n\n this.runs.set(request.name, run);\n return run;\n }\n\n get(name: string): PtyRun | undefined {\n return this.runs.get(name);\n }\n\n write(name: string, text: string): boolean {\n const run = this.runs.get(name);\n if (!run) return false;\n run.write(text.endsWith(NEWLINE) ? text : `${text}${NEWLINE}`);\n return true;\n }\n\n list(): PtyRun[] {\n return [...this.runs.values()];\n }\n\n async disposeAll(): Promise<void> {\n const runs = [...this.runs.values()];\n this.runs.clear();\n for (const run of runs) await run.stop();\n }\n\n private async stopChild(child: PtyProcess, name: string): Promise<boolean> {\n this.runs.delete(name);\n if (!this.processControl.isAlive(child.pid)) return false;\n\n child.kill('SIGTERM');\n const deadline = this.clock.now() + TERM_GRACE_MS;\n while (this.clock.now() < deadline) {\n if (!this.processControl.isAlive(child.pid)) return true;\n await new Promise<void>((resolve) => {\n this.clock.after(LIVENESS_POLL_MS, resolve);\n });\n }\n child.kill('SIGKILL');\n return true;\n }\n}\n\n/** The visible rows, trimmed of trailing blanks, as plain text. */\nfunction renderScreen(terminal: Terminal): string {\n const buffer = terminal.buffer.active;\n const lines: string[] = [];\n for (let row = buffer.baseY; row < buffer.baseY + terminal.rows; row += 1) {\n lines.push(buffer.getLine(row)?.translateToString(true) ?? '');\n }\n while (lines.length > 0 && lines.at(-1) === '') lines.pop();\n return lines.join(NEWLINE);\n}\n","import { execFile } from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\n\nimport {\n createProcessRegistryService,\n normalizeRepositoryPath,\n type ProcessRegistryRecord,\n type ProcessRegistryService,\n resolveProcessTags,\n} from '@agimon-ai/foundation-process-registry';\nimport { resolveRootSessionId } from '@agimon-ai/po-core/childProcess';\n\nimport {\n COMMAND_SIDECAR_SUFFIX,\n EXIT_SIDECAR_SUFFIX,\n JSON_EXTENSION,\n NOT_FOUND_ERROR_CODE,\n REGISTRY_PATH_ENV,\n RUNNER_TAG,\n SERVICE_PREFIX,\n SERVICE_TYPE,\n} from '../../constants/runnerRegistry';\nimport type { IProcessControl } from '../../types/processControl';\nimport type {\n CompleteRunnerInput,\n IRunnerRegistry,\n RegisterRunnerInput,\n RunnerRecord,\n RunnerTerminationIntent,\n} from '../../types/runnerRegistry';\nimport type { IRunnerPaths } from '../runnerPaths/type';\nimport { isRunnerRecord } from '../runnerRecord';\n\n/** The slice of the shared registry service this wrapper depends on. */\nexport type ProcessRegistryPort = Pick<\n ProcessRegistryService,\n 'registerProcess' | 'releaseProcess' | 'listProcesses' | 'close'\n>;\n\n/** Opens the SQLite-backed registry the rest of the monorepo already shares. */\nexport function createDefaultProcessRegistry(\n env: Readonly<Record<string, string | undefined>> = process.env,\n): ProcessRegistryPort {\n return createProcessRegistryService(env[REGISTRY_PATH_ENV]);\n}\n\nfunction sessionTag(sessionId: string): string {\n return `session:${sessionId}`;\n}\n\nfunction readString(record: ProcessRegistryRecord, key: string): string {\n const value = record.metadata?.[key];\n return typeof value === 'string' ? value : '';\n}\n\nfunction readNumber(record: ProcessRegistryRecord, key: string): number {\n const value = record.metadata?.[key];\n return typeof value === 'number' ? value : 0;\n}\n\nconst runCommand = promisify(execFile);\n\n/**\n * The process start time the registry stores to prove a pid is still the\n * process it registered.\n *\n * The registry resolves it itself when the caller omits it, and it does so with\n * `execFileSync`, which stops this process for the length of a `ps` on every\n * runner that starts. Reading it here costs the same subprocess and blocks\n * nothing: the command is already running by the time a runner is registered.\n *\n * The value has to match what the registry would have read, so the command and\n * the trimming are deliberately identical to its own.\n */\nasync function processStartedAt(pid: number): Promise<string | undefined> {\n if (process.platform === 'win32') return undefined;\n try {\n const { stdout } = await runCommand('ps', ['-o', 'lstart=', '-p', String(pid)], { encoding: 'utf8' });\n return stdout.trim() || undefined;\n } catch {\n return undefined;\n }\n}\n\ntype ListenerRegistration = Readonly<{ key: string; listener: () => void }>;\n\n// Server plugin replacement can leave an in-flight tool and its publisher on different registry instances.\nconst listenersBySession = new Map<string, Set<() => void>>();\n\nexport class RunnerRegistry implements IRunnerRegistry {\n private readonly subscriptions = new Set<ListenerRegistration>();\n\n constructor(\n private readonly paths: IRunnerPaths,\n private readonly processControl: IProcessControl,\n private readonly registry: ProcessRegistryPort,\n private readonly environment: Readonly<Record<string, string | undefined>>,\n ) {}\n\n async register(input: RegisterRunnerInput): Promise<RunnerRecord> {\n const startedAt = new Date().toISOString();\n const rootSessionId = resolveRootSessionId(input.sessionId, this.environment);\n const identity = await processStartedAt(input.pid);\n const response = await this.registry.registerProcess({\n repositoryPath: this.repositoryPath(),\n serviceName: `${SERVICE_PREFIX}${input.id}`,\n serviceType: SERVICE_TYPE,\n pid: input.pid,\n command: input.command,\n ...(identity === undefined ? {} : { processStartedAt: identity }),\n tags: resolveProcessTags([RUNNER_TAG, `runner:${input.id}`, sessionTag(input.sessionId)]),\n metadata: {\n id: input.id,\n name: input.name,\n command: input.command,\n cwd: input.cwd,\n logPath: input.logPath,\n interactive: input.interactive,\n sessionId: input.sessionId,\n rootSessionId,\n backend: input.backend,\n ...(input.backendTarget ? { backendTarget: input.backendTarget } : {}),\n startedAt,\n hostPid: process.pid,\n },\n // The caller has already resolved name collisions against live records,\n // so anything still holding this name is stale.\n force: true,\n });\n if (!response.success) throw new Error(response.error ?? `Failed to register runner ${input.name}`);\n const record: RunnerRecord = {\n id: input.id,\n name: input.name,\n pid: input.pid,\n command: input.command,\n cwd: input.cwd,\n logPath: input.logPath,\n interactive: input.interactive,\n sessionId: input.sessionId,\n rootSessionId,\n startedAt,\n state: 'running',\n promoted: false,\n backend: input.backend,\n ...(input.backendTarget ? { backendTarget: input.backendTarget } : {}),\n hostPid: process.pid,\n };\n this.writeRecord(record);\n this.notify(record.sessionId);\n return record;\n }\n\n async list(): Promise<RunnerRecord[]> {\n const records = await this.registry.listProcesses({\n repositoryPath: this.repositoryPath(),\n serviceType: SERVICE_TYPE,\n tags: resolveProcessTags([RUNNER_TAG]),\n });\n return records.map(toRunnerRecord);\n }\n\n async listBySession(sessionId: string): Promise<RunnerRecord[]> {\n const records = await this.list();\n return records.filter((record) => record.sessionId === sessionId);\n }\n\n async listByRootSession(rootSessionId: string): Promise<RunnerRecord[]> {\n const records = await this.list();\n return records.filter((record) => (record.rootSessionId ?? record.sessionId) === rootSessionId);\n }\n\n async listAll(sessionId?: string): Promise<RunnerRecord[]> {\n this.paths.ensureDirectories(sessionId);\n const records: RunnerRecord[] = [];\n const directory = this.paths.stateDirectory(sessionId);\n for (const entry of fs.readdirSync(directory)) {\n if (!isPrimaryMetadataEntry(entry)) continue;\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(`${directory}/${entry}`, 'utf8'));\n if (isRunnerRecord(parsed)) records.push(parsed);\n } catch (error) {\n process.emitWarning(`Could not read runner metadata ${entry}: ${String(error)}`);\n }\n }\n return records.sort((left, right) => right.startedAt.localeCompare(left.startedAt));\n }\n\n async get(id: string, sessionId?: string): Promise<RunnerRecord | undefined> {\n const target = this.paths.statePathFor(id, sessionId);\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(target, 'utf8'));\n if (!isRunnerRecord(parsed) || parsed.id !== id) {\n process.emitWarning(`Could not read runner metadata ${path.basename(target)}: invalid runner record`);\n return undefined;\n }\n return parsed;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === NOT_FOUND_ERROR_CODE) return undefined;\n process.emitWarning(`Could not read runner metadata ${path.basename(target)}: ${String(error)}`);\n return undefined;\n }\n }\n\n async markPromoted(id: string): Promise<RunnerRecord | undefined> {\n const record = await this.get(id);\n if (!record) return undefined;\n const promoted = { ...record, promoted: true };\n this.writeRecord(promoted);\n this.notify(promoted.sessionId);\n return promoted;\n }\n\n async requestTermination(\n id: string,\n intent: RunnerTerminationIntent | undefined,\n sessionId?: string,\n ): Promise<RunnerRecord | undefined> {\n const record = await this.get(id, sessionId);\n if (!record) return undefined;\n if (record.state === 'completed') return record;\n if (intent !== undefined && record.terminationIntent !== undefined) return record;\n const { terminationIntent: _previous, ...rest } = record;\n const updated: RunnerRecord = { ...rest, ...(intent ? { terminationIntent: intent } : {}) };\n this.writeRecord(updated);\n return updated;\n }\n\n async complete(id: string, outcome: CompleteRunnerInput, sessionId?: string): Promise<RunnerRecord | undefined> {\n const persisted = await this.get(id, sessionId);\n if (persisted?.state === 'completed') {\n await this.release(id, persisted.sessionId);\n return persisted;\n }\n const active = await this.list();\n const record =\n persisted ??\n active.find((candidate) => candidate.id === id && (sessionId === undefined || candidate.sessionId === sessionId));\n if (!record) {\n const belongsToAnotherSession =\n sessionId !== undefined && active.some((candidate) => candidate.id === id && candidate.sessionId !== sessionId);\n if (!belongsToAnotherSession) await this.release(id, sessionId);\n return undefined;\n }\n const completed: RunnerRecord = {\n ...record,\n state: 'completed',\n exit: {\n ...outcome,\n ...(outcome.signal !== null && outcome.terminationReason === undefined && record.terminationIntent === undefined\n ? { terminationReason: 'external_signal' as const }\n : {}),\n ...record.terminationIntent,\n finishedAt: new Date().toISOString(),\n },\n };\n this.writeRecord(completed);\n await this.release(id, completed.sessionId);\n return completed;\n }\n\n async release(id: string, sessionId?: string): Promise<void> {\n const activeSessionId = sessionId ?? (await this.list()).find((record) => record.id === id)?.sessionId;\n const ownedSessionId = activeSessionId ?? (await this.get(id))?.sessionId;\n const response = await this.registry.releaseProcess({\n repositoryPath: this.repositoryPath(),\n serviceName: `${SERVICE_PREFIX}${id}`,\n serviceType: SERVICE_TYPE,\n kill: false,\n releasePort: false,\n force: true,\n });\n if (!response.success && !response.error?.includes('No matching process entry')) {\n throw new Error(response.error ?? `Failed to release runner ${id}`);\n }\n if (ownedSessionId !== undefined) this.notify(ownedSessionId);\n }\n\n async pruneDead(): Promise<string[]> {\n const dead = (await this.list()).filter((record) => !this.processControl.isAlive(record.pid));\n for (const record of dead) {\n await this.complete(record.id, { reason: 'backend_lost', code: null, signal: null }, record.sessionId);\n }\n return dead.map((record) => record.id);\n }\n\n subscribe(listener: () => void, sessionId: string): () => void {\n const subscribed = (): void => listener();\n const registration = { key: this.subscriptionKey(sessionId), listener: subscribed };\n const listeners = listenersBySession.get(registration.key) ?? new Set<() => void>();\n listeners.add(subscribed);\n listenersBySession.set(registration.key, listeners);\n this.subscriptions.add(registration);\n return () => this.removeSubscription(registration);\n }\n\n close(): void {\n for (const registration of this.subscriptions) this.removeSubscription(registration);\n this.registry.close();\n }\n\n private notify(sessionId: string): void {\n for (const listener of listenersBySession.get(this.subscriptionKey(sessionId)) ?? []) {\n try {\n listener();\n } catch (error) {\n process.emitWarning(`Runner registry listener failed: ${String(error)}`);\n }\n }\n }\n\n private removeSubscription(registration: ListenerRegistration): void {\n const listeners = listenersBySession.get(registration.key);\n listeners?.delete(registration.listener);\n if (listeners?.size === 0) listenersBySession.delete(registration.key);\n this.subscriptions.delete(registration);\n }\n\n private subscriptionKey(sessionId: string): string {\n return JSON.stringify([path.resolve(this.paths.stateDirectory(sessionId)), sessionId]);\n }\n\n private repositoryPath(): string {\n return normalizeRepositoryPath(this.paths.repositoryPath());\n }\n\n private writeRecord(record: RunnerRecord): void {\n this.paths.ensureDirectories(record.sessionId);\n const target = this.paths.statePathFor(record.id, record.sessionId);\n const temporary = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\\n`, 'utf8');\n fs.renameSync(temporary, target);\n }\n}\n\n/** Every field is read back from metadata this class wrote on registration. */\nfunction toRunnerRecord(record: ProcessRegistryRecord): RunnerRecord {\n return {\n id: readString(record, 'id'),\n name: readString(record, 'name'),\n pid: record.pid,\n command: readString(record, 'command'),\n cwd: readString(record, 'cwd'),\n logPath: readString(record, 'logPath'),\n interactive: record.metadata?.interactive === true,\n sessionId: readString(record, 'sessionId'),\n ...(readString(record, 'rootSessionId') ? { rootSessionId: readString(record, 'rootSessionId') } : {}),\n startedAt: readString(record, 'startedAt'),\n state: 'running',\n promoted: false,\n backend: readBackend(record.metadata?.backend),\n ...(readString(record, 'backendTarget') ? { backendTarget: readString(record, 'backendTarget') } : {}),\n hostPid: readNumber(record, 'hostPid'),\n };\n}\n\nfunction readBackend(value: unknown): RunnerRecord['backend'] {\n return value === 'rmux' || value === 'tmux' ? value : 'native';\n}\n\nfunction isPrimaryMetadataEntry(entry: string): boolean {\n return (\n entry.endsWith(JSON_EXTENSION) && !entry.endsWith(COMMAND_SIDECAR_SUFFIX) && !entry.endsWith(EXIT_SIDECAR_SUFFIX)\n );\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nimport { CONFIG_DIR_NAME } from '@earendil-works/pi-coding-agent';\n\nimport {\n MAX_ERROR_ENTRIES_CEILING,\n MAX_PATTERNS,\n MAX_PATTERN_LENGTH,\n MAX_RESULT_BYTES_CEILING,\n MAX_RESULT_LINES_CEILING,\n MAX_RESULT_TOKENS_CEILING,\n RATIO_KEYS,\n RUNNER_SETTINGS_FILE,\n} from '../../constants/runnerSettings';\nimport type { IRunnerSettingsLoader, RunnerSettings, RunnerSettingsLoad } from '../../types/runnerSettings';\n\n/** Pi's convention is one JSON file per extension under the config directory. */\n\nconst EMPTY: RunnerSettingsLoad = { settings: {}, issues: [] };\nconst COUNT_KEYS = [\n 'maxResultBytes',\n 'maxResultLines',\n 'successMaxResultBytes',\n 'maxResultTokens',\n 'successMaxResultTokens',\n 'errorMaxEntries',\n 'errorMaxVariantsJoined',\n] as const;\nconst KNOWN_KEYS = new Set<string>([...RATIO_KEYS, ...COUNT_KEYS, 'errorPatterns']);\n/** Ceilings so a project file cannot flood the model's context. */\nconst CEILINGS: Readonly<Record<string, number>> = {\n maxResultBytes: MAX_RESULT_BYTES_CEILING,\n successMaxResultBytes: MAX_RESULT_BYTES_CEILING,\n maxResultTokens: MAX_RESULT_TOKENS_CEILING,\n successMaxResultTokens: MAX_RESULT_TOKENS_CEILING,\n maxResultLines: MAX_RESULT_LINES_CEILING,\n errorMaxEntries: MAX_ERROR_ENTRIES_CEILING,\n errorMaxVariantsJoined: MAX_ERROR_ENTRIES_CEILING,\n};\n\nexport class RunnerSettingsLoader implements IRunnerSettingsLoader {\n load(cwd: string, trusted: boolean): RunnerSettingsLoad {\n // Project-local configuration is untrusted input until the project is.\n if (!trusted) return EMPTY;\n\n let raw: string;\n try {\n raw = fs.readFileSync(path.join(cwd, CONFIG_DIR_NAME, RUNNER_SETTINGS_FILE), 'utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return EMPTY;\n return { settings: {}, issues: [`${RUNNER_SETTINGS_FILE} could not be read`] };\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return { settings: {}, issues: [`${RUNNER_SETTINGS_FILE} is not valid JSON`] };\n }\n return validate(parsed);\n }\n}\n\nfunction validate(parsed: unknown): RunnerSettingsLoad {\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n return { settings: {}, issues: [`${RUNNER_SETTINGS_FILE} must contain a JSON object`] };\n }\n\n const source = parsed as Record<string, unknown>;\n const settings: Record<string, unknown> = {};\n const issues: string[] = [];\n // A rejected key is reported rather than dropped: a silent typo reads as a\n // setting that simply does not work.\n for (const key of Object.keys(source)) if (!KNOWN_KEYS.has(key)) issues.push(`unknown key ${key}`);\n\n for (const key of COUNT_KEYS) {\n const value = source[key];\n if (value === undefined) continue;\n if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {\n issues.push(`${key} must be a positive integer`);\n continue;\n }\n settings[key] = Math.min(value, CEILINGS[key] ?? value);\n }\n\n for (const key of RATIO_KEYS) {\n const value = source[key];\n if (value === undefined) continue;\n if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value >= 1) {\n issues.push(`${key} must be a number above 0 and below 1`);\n continue;\n }\n settings[key] = value;\n }\n\n const patterns = source.errorPatterns;\n if (patterns !== undefined) {\n if (!Array.isArray(patterns) || patterns.some((entry) => typeof entry !== 'string')) {\n issues.push('errorPatterns must be an array of strings');\n } else {\n const accepted: string[] = [];\n for (const pattern of patterns.slice(0, MAX_PATTERNS) as string[]) {\n if (pattern.length > MAX_PATTERN_LENGTH) {\n issues.push('errorPatterns entry is too long');\n continue;\n }\n try {\n new RegExp(pattern, 'iu');\n accepted.push(pattern);\n } catch {\n issues.push(`errorPatterns entry is not a valid regular expression: ${pattern}`);\n }\n }\n if (accepted.length > 0) settings.errorPatterns = accepted;\n }\n }\n\n return { settings: settings as RunnerSettings, issues };\n}\n","import { readPoFs } from '@agimon-ai/po-core/platform';\nimport type { PoServerPluginContext } from '@agimon-ai/po-core/serverPlugin';\nimport type { Context } from '@deepseek-ai/cordis';\n\nimport { REGISTRY_PATH_ENV } from '../../../../../constants/runnerRegistry';\nimport {\n HISTORY_SWEEP_INTERVAL_MS,\n RUNNER_FINISHED_MESSAGE,\n RUNNER_STATUS_POLL_MS,\n} from '../../../../../constants/runnerRuntime';\nimport { RUNNER_SETTINGS_FILE } from '../../../../../constants/runnerSettings';\nimport { SystemClock } from '../../../../../services/clock';\nimport { createHeadlessBashTool, createHeadlessRunnersCommand } from '../../../../../services/headless';\nimport { summarizeLog } from '../../../../../services/logReader';\nimport { createRunnerDependencies } from '../../../../../services/runnerDependencies';\nimport { createRunnerHost } from '../../../../../services/runnerHost';\nimport { createDefaultProcessRegistry } from '../../../../../services/runnerRegistry';\nimport { createRunnerServerRuntime } from '../../../../../services/runnerServerRuntime';\nimport { RunnerSettingsLoader } from '../../../../../services/runnerSettings';\n\nexport const RUNNER_SERVER_SCOPE_SERVICE = 'po/runner-server-scope';\n\ndeclare module '@deepseek-ai/cordis' {\n interface Context {\n 'po/runner-server-scope': RunnerServerScope;\n }\n}\n\nexport const createRunnerServerRoot = async ({ context, host, agent, signal, resources }: PoServerPluginContext) => {\n if (!host.context.directEvents) throw new Error('Runner server plugin requires the session direct event bus.');\n if (!host.context.cwd || !host.context.environment) {\n throw new Error('Runner server plugin requires session cwd and environment.');\n }\n // Inherit the current headless Pi bridge's isProjectTrusted: () => true.\n // This preserves runner configuration parity, not a new general trust policy.\n const loaded = new RunnerSettingsLoader().load(host.context.cwd, true);\n const settings = Object.freeze({\n ...loaded.settings,\n ...(loaded.settings.errorPatterns ? { errorPatterns: Object.freeze([...loaded.settings.errorPatterns]) } : {}),\n });\n for (const issue of loaded.issues) process.emitWarning(`${RUNNER_SETTINGS_FILE}: ${issue}`);\n const environment = host.context.environment;\n // One per registry for this build in the hub: every session shares the connection and the loop that\n // keeps house for it, and the last session to close disposes them.\n const runnerHost = await resources.acquire(\n ['runner-host', environment[REGISTRY_PATH_ENV] ?? null],\n () =>\n createRunnerHost({\n processRegistry: createDefaultProcessRegistry(environment),\n clock: new SystemClock(),\n statusPollMs: RUNNER_STATUS_POLL_MS,\n historySweepMs: HISTORY_SWEEP_INTERVAL_MS,\n onWarning: (message) => process.emitWarning(message),\n }),\n (shared) => shared.dispose(),\n );\n const dependencies = createRunnerDependencies({\n settings,\n cwd: host.context.cwd,\n environment,\n processRegistry: runnerHost.processRegistry,\n ...(host.context.executionBudget === undefined ? {} : { executionBudget: host.context.executionBudget }),\n });\n const execution = agent?.context;\n const wakeAgent =\n execution &&\n (async (content: string, requestId: string, notice: string): Promise<void> => {\n try {\n // 'steer' joins a running turn and starts one on an idle session.\n // Reuse completion identity across retries and acknowledge admission, not settlement.\n // A custom message, not a prompt: the model reads it as input, while the\n // transcript shows the notice as an event rather than as the person's words.\n if (!execution.session.admitMessage) throw new Error('The session cannot admit a runner notification.');\n await execution.session.admitMessage(\n { type: RUNNER_FINISHED_MESSAGE, text: content, display: true, details: { notice } },\n 'steer',\n requestId,\n );\n } catch (error) {\n // Keep completion ownership with the watcher until admission succeeds.\n await Promise.resolve()\n .then(() => execution.client.notify({ body: String(error), level: 'warning' }))\n .catch(() => undefined);\n throw error;\n }\n });\n const runtime = createRunnerServerRuntime(dependencies, runnerHost, host.context.directEvents, wakeAgent, execution);\n const bash = (wakeOnExit: boolean) =>\n createHeadlessBashTool(\n {\n run: async (request) => {\n await runtime.ensureSession(request.sessionId);\n const result = await dependencies.bashRunService.run({\n ...request,\n ...(signal ? { signal: request.signal ? AbortSignal.any([signal, request.signal]) : signal } : {}),\n });\n // Without this, nothing tells the agent the runner ended.\n if (wakeOnExit && result.kind === 'promoted') runtime.watchRunner(result.id);\n return result;\n },\n },\n (path, lines, bytes, tokens) => summarizeLog(path, lines, bytes, tokens, settings),\n settings,\n dependencies.environment,\n );\n const value = {\n tool: bash(true),\n // Remote MCP runners must not wake the local agent; the remote caller polls instead.\n remoteTool: bash(false),\n command: createHeadlessRunnersCommand(dependencies, {\n // Without the platform fs a missing folder still fails, only at spawn and less plainly.\n fs: readPoFs(context),\n onStart: (startedSessionId, failure) => runtime.reportStart(startedSessionId, failure),\n }),\n recoverCompaction: runtime.recoverCompaction,\n };\n return {\n value,\n services: [\n runtime.backgroundWorkPlugin,\n (context: Context) => {\n context.plugin((providerContext) => providerContext.provide(RUNNER_SERVER_SCOPE_SERVICE, value));\n },\n ],\n activities: [runtime.activity],\n // Package install is on the session-create path; bash awaits supervision itself.\n onStart: execution\n ? () => {\n void runtime\n .ensureSession(execution.sessionId)\n .catch((error: unknown) =>\n process.emitWarning(`Could not initialize headless runner supervision: ${String(error)}`),\n );\n }\n : undefined,\n onStop: runtime.dispose,\n };\n};\n\nexport type RunnerServerScope = Awaited<ReturnType<typeof createRunnerServerRoot>>['value'];\n","import { defineMcpTool, type PoMcpPluginContext } from '@agimon-ai/po-core/mcpFacet';\n\nimport { RUNNER_SERVER_SCOPE_SERVICE, type RunnerServerScope } from '../_lib/serverRoot';\n\nconst pollHint = (id: string): string =>\n `You will not be notified when this runner ends. Check it with \\`po-runner status ${id}\\` and read output with \\`po-runner logs ${id} --lines 100\\`, both through this tool, until State is completed.`;\n\nexport default defineMcpTool((context: PoMcpPluginContext) => {\n const scope = context.services.get<RunnerServerScope>(RUNNER_SERVER_SCOPE_SERVICE);\n if (!scope) throw new Error('Runner session service is unavailable.');\n return {\n ...scope.remoteTool,\n description:\n \"Run a shell command (bash, terminal, CLI) in the Po session's repository on the host machine, not on your local machine. Use it for tests, builds, linters, git, and package scripts. Short commands return bounded output. Long-running ones return a runner id. No completion notice reaches you, so check them with `po-runner status <id>` and `po-runner logs <id> --lines 100` instead of relaunching.\",\n async execute(toolCallId, parameters, signal, onUpdate, execution) {\n const result = await scope.remoteTool.execute(\n toolCallId,\n parameters,\n AbortSignal.any([context.signal, signal ?? context.signal]),\n onUpdate,\n execution,\n );\n const details = result.details as { promoted?: unknown; id?: unknown } | undefined;\n if (details?.promoted !== true || typeof details.id !== 'string') return result;\n return { ...result, content: [...result.content, { type: 'text', text: pollHint(details.id) }] };\n },\n };\n});\n","// Generated by @agimon-ai/po-build. Do not edit by hand.\nimport { defineMcpPlugin, type PoMcpSessionPlugin } from '@agimon-ai/po-core/mcpFacet';\n\nimport skillPoUseRunner from '../src/extensions/workspaces/sessions/(backend)/skill/po-use-runner.mcp';\nimport toolBash from '../src/extensions/workspaces/sessions/(backend)/tool/bash.mcp';\n\ntype Factory<T, C> = (context: C) => T;\nconst at = <T, C>(value: T | Factory<T, C>, context: C): T =>\n typeof value === 'function' ? (value as Factory<T, C>)(context) : value;\nconst via = <T>(identity: Record<string, unknown>, value: unknown): T =>\n (value === undefined ? undefined : { ...identity, ...(value as object) }) as T;\n\nconst withWidget = <T>(key: string, value: T): T => value === undefined ? value : ({\n ...value, _meta: { ...(value as { _meta?: object })._meta, [\"po/widget\"]: key },\n});\n\nexport const mcp = defineMcpPlugin({\n name: '@agimon-ai/po-runner',\n session: (context) => ({\n get skills(): PoMcpSessionPlugin['skills'] { return [at(skillPoUseRunner, context)]; },\n get tools(): PoMcpSessionPlugin['tools'] { return [withWidget(\"@agimon-ai/po-runner/bash\", via({ name: 'bash' }, at(toolBash, context)))]; },\n }) satisfies PoMcpSessionPlugin,\n});\n\nexport default mcp;\n"],"mappings":"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"}
1
+ {"version":3,"file":"mcp.cjs","names":["defineMcpSkill","readPackageResource","Type","headless","promisify","execFile","defineMcpTool","defineMcpPlugin","skillHelpHowToRunner","toolBash"],"sources":["../../src/extensions/workspaces/sessions/(backend)/skill/help-how-to-runner.mcp.ts","../../src/constants/runnerSettings.ts","../../src/schemas/bashTool.ts","../../src/services/ansiScrub/index.ts","../../src/services/ptyHost/index.ts","../../src/services/runnerRegistry/index.ts","../../src/services/runnerSettings/index.ts","../../src/extensions/workspaces/sessions/(backend)/_lib/serverRoot.ts","../../src/extensions/workspaces/sessions/(backend)/tool/bash.mcp.ts","../../generated/mcp.ts"],"sourcesContent":["import { defineMcpSkill } from '@agimon-ai/po-core/mcpFacet';\nimport { readPackageResource } from '@agimon-ai/po-core/serverPlugin';\n\nexport default defineMcpSkill({\n name: 'help-how-to-runner',\n description:\n 'Use Po Pi Runner to supervise shell commands, inspect durable logs, provide interactive input, and stop background runs.',\n read: () => readPackageResource(import.meta.url, 'src/prompts/help-how-to-runner/SKILL.md'),\n});\n","export const RUNNER_SETTINGS_FILE = 'po-runner.json';\nexport const RATIO_KEYS = ['headRatio', 'errorBudgetRatio'] as const;\nexport const MAX_RESULT_BYTES_CEILING = 262_144;\nexport const MAX_RESULT_LINES_CEILING = 5_000;\nexport const MAX_RESULT_TOKENS_CEILING = 100_000;\nexport const MAX_ERROR_ENTRIES_CEILING = 100;\nexport const MAX_PATTERNS = 32;\nexport const MAX_PATTERN_LENGTH = 512;\n","import { type Static, Type } from 'typebox';\n\n/** Replaces pi's built-in tool of the same name, so hooks keyed on `bash` keep working. */\n\n/**\n * Built-in bash parameters plus the backgrounding controls.\n *\n * `command` and `timeout` keep their original names and wording so the tool is\n * a drop-in replacement. Every `description` is prompt copy the model reads.\n */\nexport const BashParamsSchema = Type.Object({\n command: Type.String({ description: 'Bash command to execute' }),\n timeout: Type.Optional(Type.Number({ description: 'Timeout in seconds (optional, no default timeout)' })),\n background: Type.Optional(\n Type.Boolean({\n description:\n 'Start the command in the background immediately instead of waiting. Use for dev servers, watchers and other commands you already know will not finish.',\n }),\n ),\n interactive: Type.Optional(\n Type.Boolean({\n description:\n 'Run under a pseudo terminal so the command can prompt. Use only when the command asks for confirmation or input; answer it with po-runner input. Logs from interactive runs are noisier.',\n }),\n ),\n name: Type.Optional(\n Type.String({\n description: 'Preferred runner name if the command ends up in the background. A name in use gets a suffix.',\n }),\n ),\n});\n\nexport type BashParams = Static<typeof BashParamsSchema>;\n","import { CONTROL_PATTERN } from '../../constants/ansiScrub';\n/**\n * Turns terminal output into text worth grepping.\n *\n * PTY output is written for a screen, not a file: colours, cursor moves and\n * carriage-return redraws all end up in the byte stream. Without this a single\n * progress bar becomes thousands of near-identical log lines.\n */\n\n/**\n * CSI sequences (colours, cursor moves), OSC strings, the two-byte Fe escapes,\n * and the keypad-mode escapes shells emit around every prompt.\n */\nconst ANSI_PATTERN = new RegExp(\n [\n '\\\\u001B\\\\[[0-9;?]*[ -/]*[@-~]',\n '\\\\u001B\\\\][^\\\\u0007\\\\u001B]*(?:\\\\u0007|\\\\u001B\\\\\\\\)',\n '\\\\u001B[@-Z\\\\\\\\-_]',\n '\\\\u001B[=><]',\n ].join('|'),\n 'g',\n);\n\n/** Bell and backspace, which a terminal consumes rather than displays. */\n// oxlint-disable-next-line no-control-regex -- matching terminal control bytes is the whole job\n\n/**\n * The same set minus SGR, the sequence that only sets colour and weight.\n *\n * The final byte class omits `m`, so `ESC[32m` survives while cursor moves,\n * OSC strings and keypad escapes are still removed.\n */\nconst ANSI_PATTERN_KEEPING_COLOUR = new RegExp(\n [\n '\\\\u001B\\\\[[0-9;?]*[ -/]*[@-ln-~]',\n '\\\\u001B\\\\][^\\\\u0007\\\\u001B]*(?:\\\\u0007|\\\\u001B\\\\\\\\)',\n '\\\\u001B[@-Z\\\\\\\\-_]',\n '\\\\u001B[=><]',\n ].join('|'),\n 'g',\n);\n\n/** Removes escape sequences, leaving the characters a user would have seen. */\nexport function stripAnsi(text: string): string {\n return text.replace(ANSI_PATTERN, '').replace(CONTROL_PATTERN, '');\n}\n\n/**\n * Removes everything `stripAnsi` does except colour.\n *\n * Logs are read back by a terminal renderer as well as by grep, and a command\n * that colours its own output has already said how it should look.\n */\nexport function stripAnsiKeepingColour(text: string): string {\n return text.replace(ANSI_PATTERN_KEEPING_COLOUR, '').replace(CONTROL_PATTERN, '');\n}\n\n/**\n * Collapses carriage-return redraws.\n *\n * A bare `\\r` returns the cursor to the start of the current line, so whatever\n * follows overwrites it. Only the final state of each line is kept. The `\\r` of\n * a CRLF pair is a line terminator rather than a redraw, so it is normalised\n * away first: a pseudo terminal ends every line that way, and treating it as a\n * redraw would blank the whole log.\n */\nexport function collapseCarriageReturns(text: string): string {\n return text\n .replace(/\\r\\n/g, '\\n')\n .split('\\n')\n .map((line) => (line.includes('\\r') ? (line.split('\\r').at(-1) ?? '') : line))\n .join('\\n');\n}\n\n/** Full scrub applied to PTY output before it reaches the log file. */\nexport function scrubTerminalOutput(text: string): string {\n return collapseCarriageReturns(stripAnsiKeepingColour(text));\n}\n\n/** Plain-text scrub, for the copy the model reads. */\nexport function scrubTerminalOutputToPlainText(text: string): string {\n return collapseCarriageReturns(stripAnsi(text));\n}\n","import headless, { type Terminal } from '@xterm/headless';\n\nimport {\n DEFAULT_COLS,\n DEFAULT_ROWS,\n LIVENESS_POLL_MS,\n MEMORY_BUFFER_FACTOR,\n NEWLINE,\n SCROLLBACK,\n TERM_GRACE_MS,\n} from '../../constants/ptyHost';\nimport { PI_SESSION_ID_ENV } from '../../constants/session';\nimport type { IClock } from '../../types/clock';\nimport type { ILogFile } from '../../types/logFile';\nimport type { IProcessControl } from '../../types/processControl';\nimport type { IPtyHost, PtyLaunchRequest, PtyRun } from '../../types/ptyHost';\nimport type { IPtySpawner, PtyProcess } from '../../types/ptySpawner';\nimport type { RunnerSettings } from '../../types/runnerSettings';\nimport type { ExitResult } from '../../types/spawner';\nimport { scrubTerminalOutput } from '../ansiScrub';\nimport { getResultMaxBytes } from '../runnerConfig';\nimport { NO_TERMINAL_INPUT_ENV } from '../runnerSupervisor';\n\n/** Enough history for the overlay to scroll without holding a whole build. */\n\n// Use the CommonJS default export: Node cannot detect this package's named exports.\n// A static import also lets server bundles include it without a live node_modules lookup.\nconst { Terminal: HeadlessTerminal } = headless;\n\nexport class PtyHost implements IPtyHost {\n private readonly runs = new Map<string, PtyRun>();\n\n constructor(\n private readonly spawner: IPtySpawner,\n private readonly logFile: ILogFile,\n private readonly processControl: IProcessControl,\n private readonly clock: IClock,\n private readonly environment: Readonly<Record<string, string | undefined>> = process.env,\n private readonly settings?: RunnerSettings,\n ) {}\n\n async launch(request: PtyLaunchRequest): Promise<PtyRun> {\n const cols = request.cols ?? DEFAULT_COLS;\n const rows = request.rows ?? DEFAULT_ROWS;\n const writer = this.logFile.open(request.id);\n const terminal = new HeadlessTerminal({ cols, rows, scrollback: SCROLLBACK, allowProposedApi: true });\n const memoryLimit = getResultMaxBytes(this.environment, this.settings) * MEMORY_BUFFER_FACTOR;\n\n const child = await this.spawner.spawn({\n command: request.command,\n cwd: request.cwd,\n env: { ...this.environment, ...NO_TERMINAL_INPUT_ENV, [PI_SESSION_ID_ENV]: request.sessionId },\n cols,\n rows,\n });\n\n let buffer = '';\n let buffering = true;\n let settled = false;\n let resolveCompletion: (result: ExitResult) => void = () => undefined;\n const completion = new Promise<ExitResult>((resolve) => {\n resolveCompletion = resolve;\n });\n const dataHandlers = new Set<(data: string) => void>();\n\n child.onData((data) => {\n // The terminal keeps the screen; the log keeps text worth grepping.\n terminal.write(data);\n const scrubbed = scrubTerminalOutput(data);\n writer.append(scrubbed);\n if (buffering) {\n buffer += scrubbed;\n if (buffer.length > memoryLimit) buffer = buffer.slice(-memoryLimit);\n }\n for (const handler of dataHandlers) handler(data);\n });\n\n child.onExit((result) => {\n if (settled) return;\n settled = true;\n this.runs.delete(request.name);\n writer.close();\n resolveCompletion({ code: result.exitCode, signal: null });\n });\n\n const run: PtyRun = {\n id: request.id,\n name: request.name,\n pid: child.pid,\n logPath: writer.path,\n backend: 'native',\n output: () => buffer,\n completion: () => completion,\n detach: () => {\n buffering = false;\n buffer = '';\n },\n stop: () => this.stopChild(child, request.name),\n write: (text) => child.write(text),\n screen: () => renderScreen(terminal),\n onData: (handler) => {\n dataHandlers.add(handler);\n return () => dataHandlers.delete(handler);\n },\n resize: (nextCols, nextRows) => {\n child.resize(nextCols, nextRows);\n terminal.resize(nextCols, nextRows);\n },\n };\n\n this.runs.set(request.name, run);\n return run;\n }\n\n get(name: string): PtyRun | undefined {\n return this.runs.get(name);\n }\n\n write(name: string, text: string): boolean {\n const run = this.runs.get(name);\n if (!run) return false;\n run.write(text.endsWith(NEWLINE) ? text : `${text}${NEWLINE}`);\n return true;\n }\n\n list(): PtyRun[] {\n return [...this.runs.values()];\n }\n\n async disposeAll(): Promise<void> {\n const runs = [...this.runs.values()];\n this.runs.clear();\n for (const run of runs) await run.stop();\n }\n\n private async stopChild(child: PtyProcess, name: string): Promise<boolean> {\n this.runs.delete(name);\n if (!this.processControl.isAlive(child.pid)) return false;\n\n child.kill('SIGTERM');\n const deadline = this.clock.now() + TERM_GRACE_MS;\n while (this.clock.now() < deadline) {\n if (!this.processControl.isAlive(child.pid)) return true;\n await new Promise<void>((resolve) => {\n this.clock.after(LIVENESS_POLL_MS, resolve);\n });\n }\n child.kill('SIGKILL');\n return true;\n }\n}\n\n/** The visible rows, trimmed of trailing blanks, as plain text. */\nfunction renderScreen(terminal: Terminal): string {\n const buffer = terminal.buffer.active;\n const lines: string[] = [];\n for (let row = buffer.baseY; row < buffer.baseY + terminal.rows; row += 1) {\n lines.push(buffer.getLine(row)?.translateToString(true) ?? '');\n }\n while (lines.length > 0 && lines.at(-1) === '') lines.pop();\n return lines.join(NEWLINE);\n}\n","import { execFile } from 'node:child_process';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\n\nimport {\n createProcessRegistryService,\n normalizeRepositoryPath,\n type ProcessRegistryRecord,\n type ProcessRegistryService,\n resolveProcessTags,\n} from '@agimon-ai/foundation-process-registry';\nimport { resolveRootSessionId } from '@agimon-ai/po-core/childProcess';\n\nimport {\n COMMAND_SIDECAR_SUFFIX,\n EXIT_SIDECAR_SUFFIX,\n JSON_EXTENSION,\n NOT_FOUND_ERROR_CODE,\n REGISTRY_PATH_ENV,\n RUNNER_TAG,\n SERVICE_PREFIX,\n SERVICE_TYPE,\n} from '../../constants/runnerRegistry';\nimport type { IProcessControl } from '../../types/processControl';\nimport type {\n CompleteRunnerInput,\n IRunnerRegistry,\n RegisterRunnerInput,\n RunnerRecord,\n RunnerTerminationIntent,\n} from '../../types/runnerRegistry';\nimport type { IRunnerPaths } from '../runnerPaths/type';\nimport { isRunnerRecord } from '../runnerRecord';\n\n/** The slice of the shared registry service this wrapper depends on. */\nexport type ProcessRegistryPort = Pick<\n ProcessRegistryService,\n 'registerProcess' | 'releaseProcess' | 'listProcesses' | 'close'\n>;\n\n/** Opens the SQLite-backed registry the rest of the monorepo already shares. */\nexport function createDefaultProcessRegistry(\n env: Readonly<Record<string, string | undefined>> = process.env,\n): ProcessRegistryPort {\n return createProcessRegistryService(env[REGISTRY_PATH_ENV]);\n}\n\nfunction sessionTag(sessionId: string): string {\n return `session:${sessionId}`;\n}\n\nfunction readString(record: ProcessRegistryRecord, key: string): string {\n const value = record.metadata?.[key];\n return typeof value === 'string' ? value : '';\n}\n\nfunction readNumber(record: ProcessRegistryRecord, key: string): number {\n const value = record.metadata?.[key];\n return typeof value === 'number' ? value : 0;\n}\n\nconst runCommand = promisify(execFile);\n\n/**\n * The process start time the registry stores to prove a pid is still the\n * process it registered.\n *\n * The registry resolves it itself when the caller omits it, and it does so with\n * `execFileSync`, which stops this process for the length of a `ps` on every\n * runner that starts. Reading it here costs the same subprocess and blocks\n * nothing: the command is already running by the time a runner is registered.\n *\n * The value has to match what the registry would have read, so the command and\n * the trimming are deliberately identical to its own.\n */\nasync function processStartedAt(pid: number): Promise<string | undefined> {\n if (process.platform === 'win32') return undefined;\n try {\n const { stdout } = await runCommand('ps', ['-o', 'lstart=', '-p', String(pid)], { encoding: 'utf8' });\n return stdout.trim() || undefined;\n } catch {\n return undefined;\n }\n}\n\ntype ListenerRegistration = Readonly<{ key: string; listener: () => void }>;\n\n// Server plugin replacement can leave an in-flight tool and its publisher on different registry instances.\nconst listenersBySession = new Map<string, Set<() => void>>();\n\nexport class RunnerRegistry implements IRunnerRegistry {\n private readonly subscriptions = new Set<ListenerRegistration>();\n\n constructor(\n private readonly paths: IRunnerPaths,\n private readonly processControl: IProcessControl,\n private readonly registry: ProcessRegistryPort,\n private readonly environment: Readonly<Record<string, string | undefined>>,\n ) {}\n\n async register(input: RegisterRunnerInput): Promise<RunnerRecord> {\n const startedAt = new Date().toISOString();\n const rootSessionId = resolveRootSessionId(input.sessionId, this.environment);\n const identity = await processStartedAt(input.pid);\n const response = await this.registry.registerProcess({\n repositoryPath: this.repositoryPath(),\n serviceName: `${SERVICE_PREFIX}${input.id}`,\n serviceType: SERVICE_TYPE,\n pid: input.pid,\n command: input.command,\n ...(identity === undefined ? {} : { processStartedAt: identity }),\n tags: resolveProcessTags([RUNNER_TAG, `runner:${input.id}`, sessionTag(input.sessionId)]),\n metadata: {\n id: input.id,\n name: input.name,\n command: input.command,\n cwd: input.cwd,\n logPath: input.logPath,\n interactive: input.interactive,\n sessionId: input.sessionId,\n rootSessionId,\n backend: input.backend,\n ...(input.backendTarget ? { backendTarget: input.backendTarget } : {}),\n startedAt,\n hostPid: process.pid,\n },\n // The caller has already resolved name collisions against live records,\n // so anything still holding this name is stale.\n force: true,\n });\n if (!response.success) throw new Error(response.error ?? `Failed to register runner ${input.name}`);\n const record: RunnerRecord = {\n id: input.id,\n name: input.name,\n pid: input.pid,\n command: input.command,\n cwd: input.cwd,\n logPath: input.logPath,\n interactive: input.interactive,\n sessionId: input.sessionId,\n rootSessionId,\n startedAt,\n state: 'running',\n promoted: false,\n backend: input.backend,\n ...(input.backendTarget ? { backendTarget: input.backendTarget } : {}),\n hostPid: process.pid,\n };\n this.writeRecord(record);\n this.notify(record.sessionId);\n return record;\n }\n\n async list(): Promise<RunnerRecord[]> {\n const records = await this.registry.listProcesses({\n repositoryPath: this.repositoryPath(),\n serviceType: SERVICE_TYPE,\n tags: resolveProcessTags([RUNNER_TAG]),\n });\n return records.map(toRunnerRecord);\n }\n\n async listBySession(sessionId: string): Promise<RunnerRecord[]> {\n const records = await this.list();\n return records.filter((record) => record.sessionId === sessionId);\n }\n\n async listByRootSession(rootSessionId: string): Promise<RunnerRecord[]> {\n const records = await this.list();\n return records.filter((record) => (record.rootSessionId ?? record.sessionId) === rootSessionId);\n }\n\n async listAll(sessionId?: string): Promise<RunnerRecord[]> {\n this.paths.ensureDirectories(sessionId);\n const records: RunnerRecord[] = [];\n const directory = this.paths.stateDirectory(sessionId);\n for (const entry of fs.readdirSync(directory)) {\n if (!isPrimaryMetadataEntry(entry)) continue;\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(`${directory}/${entry}`, 'utf8'));\n if (isRunnerRecord(parsed)) records.push(parsed);\n } catch (error) {\n process.emitWarning(`Could not read runner metadata ${entry}: ${String(error)}`);\n }\n }\n return records.sort((left, right) => right.startedAt.localeCompare(left.startedAt));\n }\n\n async get(id: string, sessionId?: string): Promise<RunnerRecord | undefined> {\n const target = this.paths.statePathFor(id, sessionId);\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(target, 'utf8'));\n if (!isRunnerRecord(parsed) || parsed.id !== id) {\n process.emitWarning(`Could not read runner metadata ${path.basename(target)}: invalid runner record`);\n return undefined;\n }\n return parsed;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === NOT_FOUND_ERROR_CODE) return undefined;\n process.emitWarning(`Could not read runner metadata ${path.basename(target)}: ${String(error)}`);\n return undefined;\n }\n }\n\n async markPromoted(id: string): Promise<RunnerRecord | undefined> {\n const record = await this.get(id);\n if (!record) return undefined;\n const promoted = { ...record, promoted: true };\n this.writeRecord(promoted);\n this.notify(promoted.sessionId);\n return promoted;\n }\n\n async requestTermination(\n id: string,\n intent: RunnerTerminationIntent | undefined,\n sessionId?: string,\n ): Promise<RunnerRecord | undefined> {\n const record = await this.get(id, sessionId);\n if (!record) return undefined;\n if (record.state === 'completed') return record;\n if (intent !== undefined && record.terminationIntent !== undefined) return record;\n const { terminationIntent: _previous, ...rest } = record;\n const updated: RunnerRecord = { ...rest, ...(intent ? { terminationIntent: intent } : {}) };\n this.writeRecord(updated);\n return updated;\n }\n\n async complete(id: string, outcome: CompleteRunnerInput, sessionId?: string): Promise<RunnerRecord | undefined> {\n const persisted = await this.get(id, sessionId);\n if (persisted?.state === 'completed') {\n await this.release(id, persisted.sessionId);\n return persisted;\n }\n const active = await this.list();\n const record =\n persisted ??\n active.find((candidate) => candidate.id === id && (sessionId === undefined || candidate.sessionId === sessionId));\n if (!record) {\n const belongsToAnotherSession =\n sessionId !== undefined && active.some((candidate) => candidate.id === id && candidate.sessionId !== sessionId);\n if (!belongsToAnotherSession) await this.release(id, sessionId);\n return undefined;\n }\n const completed: RunnerRecord = {\n ...record,\n state: 'completed',\n exit: {\n ...outcome,\n ...(outcome.signal !== null && outcome.terminationReason === undefined && record.terminationIntent === undefined\n ? { terminationReason: 'external_signal' as const }\n : {}),\n ...record.terminationIntent,\n finishedAt: new Date().toISOString(),\n },\n };\n this.writeRecord(completed);\n await this.release(id, completed.sessionId);\n return completed;\n }\n\n async release(id: string, sessionId?: string): Promise<void> {\n const activeSessionId = sessionId ?? (await this.list()).find((record) => record.id === id)?.sessionId;\n const ownedSessionId = activeSessionId ?? (await this.get(id))?.sessionId;\n const response = await this.registry.releaseProcess({\n repositoryPath: this.repositoryPath(),\n serviceName: `${SERVICE_PREFIX}${id}`,\n serviceType: SERVICE_TYPE,\n kill: false,\n releasePort: false,\n force: true,\n });\n if (!response.success && !response.error?.includes('No matching process entry')) {\n throw new Error(response.error ?? `Failed to release runner ${id}`);\n }\n if (ownedSessionId !== undefined) this.notify(ownedSessionId);\n }\n\n async pruneDead(): Promise<string[]> {\n const dead = (await this.list()).filter((record) => !this.processControl.isAlive(record.pid));\n for (const record of dead) {\n await this.complete(record.id, { reason: 'backend_lost', code: null, signal: null }, record.sessionId);\n }\n return dead.map((record) => record.id);\n }\n\n subscribe(listener: () => void, sessionId: string): () => void {\n const subscribed = (): void => listener();\n const registration = { key: this.subscriptionKey(sessionId), listener: subscribed };\n const listeners = listenersBySession.get(registration.key) ?? new Set<() => void>();\n listeners.add(subscribed);\n listenersBySession.set(registration.key, listeners);\n this.subscriptions.add(registration);\n return () => this.removeSubscription(registration);\n }\n\n close(): void {\n for (const registration of this.subscriptions) this.removeSubscription(registration);\n this.registry.close();\n }\n\n private notify(sessionId: string): void {\n for (const listener of listenersBySession.get(this.subscriptionKey(sessionId)) ?? []) {\n try {\n listener();\n } catch (error) {\n process.emitWarning(`Runner registry listener failed: ${String(error)}`);\n }\n }\n }\n\n private removeSubscription(registration: ListenerRegistration): void {\n const listeners = listenersBySession.get(registration.key);\n listeners?.delete(registration.listener);\n if (listeners?.size === 0) listenersBySession.delete(registration.key);\n this.subscriptions.delete(registration);\n }\n\n private subscriptionKey(sessionId: string): string {\n return JSON.stringify([path.resolve(this.paths.stateDirectory(sessionId)), sessionId]);\n }\n\n private repositoryPath(): string {\n return normalizeRepositoryPath(this.paths.repositoryPath());\n }\n\n private writeRecord(record: RunnerRecord): void {\n this.paths.ensureDirectories(record.sessionId);\n const target = this.paths.statePathFor(record.id, record.sessionId);\n const temporary = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\\n`, 'utf8');\n fs.renameSync(temporary, target);\n }\n}\n\n/** Every field is read back from metadata this class wrote on registration. */\nfunction toRunnerRecord(record: ProcessRegistryRecord): RunnerRecord {\n return {\n id: readString(record, 'id'),\n name: readString(record, 'name'),\n pid: record.pid,\n command: readString(record, 'command'),\n cwd: readString(record, 'cwd'),\n logPath: readString(record, 'logPath'),\n interactive: record.metadata?.interactive === true,\n sessionId: readString(record, 'sessionId'),\n ...(readString(record, 'rootSessionId') ? { rootSessionId: readString(record, 'rootSessionId') } : {}),\n startedAt: readString(record, 'startedAt'),\n state: 'running',\n promoted: false,\n backend: readBackend(record.metadata?.backend),\n ...(readString(record, 'backendTarget') ? { backendTarget: readString(record, 'backendTarget') } : {}),\n hostPid: readNumber(record, 'hostPid'),\n };\n}\n\nfunction readBackend(value: unknown): RunnerRecord['backend'] {\n return value === 'rmux' || value === 'tmux' ? value : 'native';\n}\n\nfunction isPrimaryMetadataEntry(entry: string): boolean {\n return (\n entry.endsWith(JSON_EXTENSION) && !entry.endsWith(COMMAND_SIDECAR_SUFFIX) && !entry.endsWith(EXIT_SIDECAR_SUFFIX)\n );\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\n\nimport { CONFIG_DIR_NAME } from '@earendil-works/pi-coding-agent';\n\nimport {\n MAX_ERROR_ENTRIES_CEILING,\n MAX_PATTERNS,\n MAX_PATTERN_LENGTH,\n MAX_RESULT_BYTES_CEILING,\n MAX_RESULT_LINES_CEILING,\n MAX_RESULT_TOKENS_CEILING,\n RATIO_KEYS,\n RUNNER_SETTINGS_FILE,\n} from '../../constants/runnerSettings';\nimport type { IRunnerSettingsLoader, RunnerSettings, RunnerSettingsLoad } from '../../types/runnerSettings';\n\n/** Pi's convention is one JSON file per extension under the config directory. */\n\nconst EMPTY: RunnerSettingsLoad = { settings: {}, issues: [] };\nconst COUNT_KEYS = [\n 'maxResultBytes',\n 'maxResultLines',\n 'successMaxResultBytes',\n 'maxResultTokens',\n 'successMaxResultTokens',\n 'errorMaxEntries',\n 'errorMaxVariantsJoined',\n] as const;\nconst KNOWN_KEYS = new Set<string>([...RATIO_KEYS, ...COUNT_KEYS, 'errorPatterns']);\n/** Ceilings so a project file cannot flood the model's context. */\nconst CEILINGS: Readonly<Record<string, number>> = {\n maxResultBytes: MAX_RESULT_BYTES_CEILING,\n successMaxResultBytes: MAX_RESULT_BYTES_CEILING,\n maxResultTokens: MAX_RESULT_TOKENS_CEILING,\n successMaxResultTokens: MAX_RESULT_TOKENS_CEILING,\n maxResultLines: MAX_RESULT_LINES_CEILING,\n errorMaxEntries: MAX_ERROR_ENTRIES_CEILING,\n errorMaxVariantsJoined: MAX_ERROR_ENTRIES_CEILING,\n};\n\nexport class RunnerSettingsLoader implements IRunnerSettingsLoader {\n load(cwd: string, trusted: boolean): RunnerSettingsLoad {\n // Project-local configuration is untrusted input until the project is.\n if (!trusted) return EMPTY;\n\n let raw: string;\n try {\n raw = fs.readFileSync(path.join(cwd, CONFIG_DIR_NAME, RUNNER_SETTINGS_FILE), 'utf8');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return EMPTY;\n return { settings: {}, issues: [`${RUNNER_SETTINGS_FILE} could not be read`] };\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return { settings: {}, issues: [`${RUNNER_SETTINGS_FILE} is not valid JSON`] };\n }\n return validate(parsed);\n }\n}\n\nfunction validate(parsed: unknown): RunnerSettingsLoad {\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n return { settings: {}, issues: [`${RUNNER_SETTINGS_FILE} must contain a JSON object`] };\n }\n\n const source = parsed as Record<string, unknown>;\n const settings: Record<string, unknown> = {};\n const issues: string[] = [];\n // A rejected key is reported rather than dropped: a silent typo reads as a\n // setting that simply does not work.\n for (const key of Object.keys(source)) if (!KNOWN_KEYS.has(key)) issues.push(`unknown key ${key}`);\n\n for (const key of COUNT_KEYS) {\n const value = source[key];\n if (value === undefined) continue;\n if (typeof value !== 'number' || !Number.isInteger(value) || value <= 0) {\n issues.push(`${key} must be a positive integer`);\n continue;\n }\n settings[key] = Math.min(value, CEILINGS[key] ?? value);\n }\n\n for (const key of RATIO_KEYS) {\n const value = source[key];\n if (value === undefined) continue;\n if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value >= 1) {\n issues.push(`${key} must be a number above 0 and below 1`);\n continue;\n }\n settings[key] = value;\n }\n\n const patterns = source.errorPatterns;\n if (patterns !== undefined) {\n if (!Array.isArray(patterns) || patterns.some((entry) => typeof entry !== 'string')) {\n issues.push('errorPatterns must be an array of strings');\n } else {\n const accepted: string[] = [];\n for (const pattern of patterns.slice(0, MAX_PATTERNS) as string[]) {\n if (pattern.length > MAX_PATTERN_LENGTH) {\n issues.push('errorPatterns entry is too long');\n continue;\n }\n try {\n new RegExp(pattern, 'iu');\n accepted.push(pattern);\n } catch {\n issues.push(`errorPatterns entry is not a valid regular expression: ${pattern}`);\n }\n }\n if (accepted.length > 0) settings.errorPatterns = accepted;\n }\n }\n\n return { settings: settings as RunnerSettings, issues };\n}\n","import { readPoFs } from '@agimon-ai/po-core/platform';\nimport type { PoServerPluginContext } from '@agimon-ai/po-core/serverPlugin';\nimport type { Context } from '@deepseek-ai/cordis';\n\nimport { REGISTRY_PATH_ENV } from '../../../../../constants/runnerRegistry';\nimport {\n HISTORY_SWEEP_INTERVAL_MS,\n RUNNER_FINISHED_MESSAGE,\n RUNNER_STATUS_POLL_MS,\n} from '../../../../../constants/runnerRuntime';\nimport { RUNNER_SETTINGS_FILE } from '../../../../../constants/runnerSettings';\nimport { SystemClock } from '../../../../../services/clock';\nimport { createHeadlessBashTool, createHeadlessRunnersCommand } from '../../../../../services/headless';\nimport { summarizeLog } from '../../../../../services/logReader';\nimport { createRunnerDependencies } from '../../../../../services/runnerDependencies';\nimport { createRunnerHost } from '../../../../../services/runnerHost';\nimport { createDefaultProcessRegistry } from '../../../../../services/runnerRegistry';\nimport { createRunnerServerRuntime } from '../../../../../services/runnerServerRuntime';\nimport { RunnerSettingsLoader } from '../../../../../services/runnerSettings';\n\nexport const RUNNER_SERVER_SCOPE_SERVICE = 'po/runner-server-scope';\n\ndeclare module '@deepseek-ai/cordis' {\n interface Context {\n 'po/runner-server-scope': RunnerServerScope;\n }\n}\n\nexport const createRunnerServerRoot = async ({ context, host, agent, signal, resources }: PoServerPluginContext) => {\n if (!host.context.directEvents) throw new Error('Runner server plugin requires the session direct event bus.');\n if (!host.context.cwd || !host.context.environment) {\n throw new Error('Runner server plugin requires session cwd and environment.');\n }\n // Inherit the current headless Pi bridge's isProjectTrusted: () => true.\n // This preserves runner configuration parity, not a new general trust policy.\n const loaded = new RunnerSettingsLoader().load(host.context.cwd, true);\n const settings = Object.freeze({\n ...loaded.settings,\n ...(loaded.settings.errorPatterns ? { errorPatterns: Object.freeze([...loaded.settings.errorPatterns]) } : {}),\n });\n for (const issue of loaded.issues) process.emitWarning(`${RUNNER_SETTINGS_FILE}: ${issue}`);\n const environment = host.context.environment;\n // One per registry for this build in the hub: every session shares the connection and the loop that\n // keeps house for it, and the last session to close disposes them.\n const runnerHost = await resources.acquire(\n ['runner-host', environment[REGISTRY_PATH_ENV] ?? null],\n () =>\n createRunnerHost({\n processRegistry: createDefaultProcessRegistry(environment),\n clock: new SystemClock(),\n statusPollMs: RUNNER_STATUS_POLL_MS,\n historySweepMs: HISTORY_SWEEP_INTERVAL_MS,\n onWarning: (message) => process.emitWarning(message),\n }),\n (shared) => shared.dispose(),\n );\n const dependencies = createRunnerDependencies({\n settings,\n cwd: host.context.cwd,\n environment,\n processRegistry: runnerHost.processRegistry,\n ...(host.context.executionBudget === undefined ? {} : { executionBudget: host.context.executionBudget }),\n });\n const execution = agent?.context;\n const wakeAgent =\n execution &&\n (async (content: string, requestId: string, notice: string): Promise<void> => {\n try {\n // 'steer' joins a running turn and starts one on an idle session.\n // Reuse completion identity across retries and acknowledge admission, not settlement.\n // A custom message, not a prompt: the model reads it as input, while the\n // transcript shows the notice as an event rather than as the person's words.\n if (!execution.session.admitMessage) throw new Error('The session cannot admit a runner notification.');\n await execution.session.admitMessage(\n { type: RUNNER_FINISHED_MESSAGE, text: content, display: true, details: { notice } },\n 'steer',\n requestId,\n );\n } catch (error) {\n // Keep completion ownership with the watcher until admission succeeds.\n await Promise.resolve()\n .then(() => execution.client.notify({ body: String(error), level: 'warning' }))\n .catch(() => undefined);\n throw error;\n }\n });\n const runtime = createRunnerServerRuntime(dependencies, runnerHost, host.context.directEvents, wakeAgent, execution);\n const bash = (wakeOnExit: boolean) =>\n createHeadlessBashTool(\n {\n run: async (request) => {\n await runtime.ensureSession(request.sessionId);\n const result = await dependencies.bashRunService.run({\n ...request,\n ...(signal ? { signal: request.signal ? AbortSignal.any([signal, request.signal]) : signal } : {}),\n });\n // Without this, nothing tells the agent the runner ended.\n if (wakeOnExit && result.kind === 'promoted') runtime.watchRunner(result.id);\n return result;\n },\n },\n (path, lines, bytes, tokens) => summarizeLog(path, lines, bytes, tokens, settings),\n settings,\n dependencies.environment,\n );\n const value = {\n tool: bash(true),\n // Remote MCP runners must not wake the local agent; the remote caller polls instead.\n remoteTool: bash(false),\n command: createHeadlessRunnersCommand(dependencies, {\n // Without the platform fs a missing folder still fails, only at spawn and less plainly.\n fs: readPoFs(context),\n onStart: (startedSessionId, failure) => runtime.reportStart(startedSessionId, failure),\n }),\n recoverCompaction: runtime.recoverCompaction,\n };\n return {\n value,\n services: [\n runtime.backgroundWorkPlugin,\n (context: Context) => {\n context.plugin((providerContext) => providerContext.provide(RUNNER_SERVER_SCOPE_SERVICE, value));\n },\n ],\n activities: [runtime.activity],\n // Package install is on the session-create path; bash awaits supervision itself.\n onStart: execution\n ? () => {\n void runtime\n .ensureSession(execution.sessionId)\n .catch((error: unknown) =>\n process.emitWarning(`Could not initialize headless runner supervision: ${String(error)}`),\n );\n }\n : undefined,\n onStop: runtime.dispose,\n };\n};\n\nexport type RunnerServerScope = Awaited<ReturnType<typeof createRunnerServerRoot>>['value'];\n","import { defineMcpTool, type PoMcpPluginContext } from '@agimon-ai/po-core/mcpFacet';\n\nimport { RUNNER_SERVER_SCOPE_SERVICE, type RunnerServerScope } from '../_lib/serverRoot';\n\nconst pollHint = (id: string): string =>\n `You will not be notified when this runner ends. Check it with \\`po-runner status ${id}\\` and read output with \\`po-runner logs ${id} --lines 100\\`, both through this tool, until State is completed.`;\n\nexport default defineMcpTool((context: PoMcpPluginContext) => {\n const scope = context.services.get<RunnerServerScope>(RUNNER_SERVER_SCOPE_SERVICE);\n if (!scope) throw new Error('Runner session service is unavailable.');\n return {\n ...scope.remoteTool,\n description:\n \"Run a shell command (bash, terminal, CLI) in the Po session's repository on the host machine, not on your local machine. Use it for tests, builds, linters, git, and package scripts. Short commands return bounded output. Long-running ones return a runner id. No completion notice reaches you, so check them with `po-runner status <id>` and `po-runner logs <id> --lines 100` instead of relaunching.\",\n async execute(toolCallId, parameters, signal, onUpdate, execution) {\n const result = await scope.remoteTool.execute(\n toolCallId,\n parameters,\n AbortSignal.any([context.signal, signal ?? context.signal]),\n onUpdate,\n execution,\n );\n const details = result.details as { promoted?: unknown; id?: unknown } | undefined;\n if (details?.promoted !== true || typeof details.id !== 'string') return result;\n return { ...result, content: [...result.content, { type: 'text', text: pollHint(details.id) }] };\n },\n };\n});\n","// Generated by @agimon-ai/po-build. Do not edit by hand.\nimport { defineMcpPlugin, type PoMcpSessionPlugin } from '@agimon-ai/po-core/mcpFacet';\n\nimport skillHelpHowToRunner from '../src/extensions/workspaces/sessions/(backend)/skill/help-how-to-runner.mcp';\nimport toolBash from '../src/extensions/workspaces/sessions/(backend)/tool/bash.mcp';\n\ntype Factory<T, C> = (context: C) => T;\nconst at = <T, C>(value: T | Factory<T, C>, context: C): T =>\n typeof value === 'function' ? (value as Factory<T, C>)(context) : value;\nconst via = <T>(identity: Record<string, unknown>, value: unknown): T =>\n (value === undefined ? undefined : { ...identity, ...(value as object) }) as T;\n\nconst withWidget = <T>(key: string, value: T): T => value === undefined ? value : ({\n ...value, _meta: { ...(value as { _meta?: object })._meta, [\"po/widget\"]: key },\n});\n\nexport const mcp = defineMcpPlugin({\n name: '@agimon-ai/po-runner',\n session: (context) => ({\n get skills(): PoMcpSessionPlugin['skills'] { return [at(skillHelpHowToRunner, context)]; },\n get tools(): PoMcpSessionPlugin['tools'] { return [withWidget(\"@agimon-ai/po-runner/bash\", via({ name: 'bash' }, at(toolBash, context)))]; },\n }) satisfies PoMcpSessionPlugin,\n});\n\nexport default mcp;\n"],"mappings":"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"}
@@ -12,13 +12,13 @@ import "@agimon-ai/foundation-process-registry";
12
12
  import "@agimon-ai/po-core/childProcess";
13
13
  import "@agimon-ai/po-core/backgroundWork";
14
14
  import "@agimon-ai/po-core/extensionFile";
15
- import "@agimon-ai/po-telemetry";
15
+ import "@agimon-ai/po-core/telemetry";
16
16
  import "@earendil-works/pi-coding-agent";
17
- //#region src/extensions/workspaces/sessions/(backend)/skill/po-use-runner.mcp.ts
18
- var po_use_runner_mcp_default = defineMcpSkill({
19
- name: "po-use-runner",
17
+ //#region src/extensions/workspaces/sessions/(backend)/skill/help-how-to-runner.mcp.ts
18
+ var help_how_to_runner_mcp_default = defineMcpSkill({
19
+ name: "help-how-to-runner",
20
20
  description: "Use Po Pi Runner to supervise shell commands, inspect durable logs, provide interactive input, and stop background runs.",
21
- read: () => readPackageResource(import.meta.url, "src/prompts/po-use-runner/SKILL.md")
21
+ read: () => readPackageResource(import.meta.url, "src/prompts/help-how-to-runner/SKILL.md")
22
22
  });
23
23
  //#endregion
24
24
  //#region src/constants/runnerSettings.ts
@@ -103,7 +103,7 @@ const mcp = defineMcpPlugin({
103
103
  name: "@agimon-ai/po-runner",
104
104
  session: (context) => ({
105
105
  get skills() {
106
- return [at(po_use_runner_mcp_default, context)];
106
+ return [at(help_how_to_runner_mcp_default, context)];
107
107
  },
108
108
  get tools() {
109
109
  return [withWidget("@agimon-ai/po-runner/bash", via({ name: "bash" }, at(bash_mcp_default, context)))];