@agimon-ai/po-runner 0.0.0-alpha.20 → 0.0.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +149 -21
- package/README.md +1 -1
- package/dist/bin/cli.cjs +1 -1
- package/dist/bin/cli.mjs +1 -1
- package/dist/extensions/mcp.cjs +1 -0
- package/dist/extensions/mcp.cjs.map +1 -1
- package/dist/extensions/mcp.mjs +1 -0
- package/dist/extensions/mcp.mjs.map +1 -1
- package/dist/extensions/server.cjs +35 -32
- package/dist/extensions/server.cjs.map +1 -1
- package/dist/extensions/server.mjs +36 -33
- package/dist/extensions/server.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{launcher-DMNpxB3O.mjs → launcher-CCYsFBM9.mjs} +2 -2
- package/dist/{launcher-DMNpxB3O.mjs.map → launcher-CCYsFBM9.mjs.map} +1 -1
- package/dist/{launcher-CWgRDu1f.cjs → launcher-ZgDdyXpq.cjs} +7 -1
- package/dist/{launcher-CWgRDu1f.cjs.map → launcher-ZgDdyXpq.cjs.map} +1 -1
- package/dist/launcher.cjs +1 -1
- package/dist/launcher.mjs +1 -1
- package/dist/{runnerDependencies-B0Vj8ZEc.mjs → runnerDependencies-7h_cxOva.mjs} +2 -2
- package/dist/{runnerDependencies-B0Vj8ZEc.mjs.map → runnerDependencies-7h_cxOva.mjs.map} +1 -1
- package/dist/{runnerDependencies-B7fVlgFs.cjs → runnerDependencies-DSun3xO-.cjs} +2 -2
- package/dist/{runnerDependencies-B7fVlgFs.cjs.map → runnerDependencies-DSun3xO-.cjs.map} +1 -1
- package/dist/runnerDependencies.cjs +1 -1
- package/dist/runnerDependencies.mjs +1 -1
- package/package.json +18 -18
package/LICENSE
CHANGED
|
@@ -1,21 +1,149 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
License text copyright © 2017 MariaDB Corporation Ab, All Rights Reserved.
|
|
2
|
+
"Business Source License" is a trademark of MariaDB Corporation Ab.
|
|
3
|
+
|
|
4
|
+
Parameters
|
|
5
|
+
|
|
6
|
+
Licensor: Agimon Pty Ltd
|
|
7
|
+
Licensed Work: Po, the software in the Po source repository and the
|
|
8
|
+
packages released from it, in each version released by
|
|
9
|
+
the Licensor.
|
|
10
|
+
The Licensed Work is (c) 2026 Agimon Pty Ltd.
|
|
11
|
+
Material identified as third-party in the "Third-party
|
|
12
|
+
components" section at the end of this file or in a
|
|
13
|
+
NOTICE file, and any package or directory whose own
|
|
14
|
+
license file grants a different license, is not part of
|
|
15
|
+
the Licensed Work and remains under its own license.
|
|
16
|
+
Additional Use Grant: You may make production use of the Licensed Work if
|
|
17
|
+
your use is Eligible Use. "Eligible Use" is production use that falls
|
|
18
|
+
within at least one of the following:
|
|
19
|
+
|
|
20
|
+
(a) Personal Use. Use by an individual for their own personal purposes,
|
|
21
|
+
and not on behalf of, or for the benefit of, an employer, client, or
|
|
22
|
+
other Organization. Use by an individual in the course of employment
|
|
23
|
+
or engagement by an Organization is use by that Organization.
|
|
24
|
+
|
|
25
|
+
(b) Small Organization Use. Use by or on behalf of an Organization whose
|
|
26
|
+
Annual Revenue is less than USD 1,000,000.
|
|
27
|
+
|
|
28
|
+
(c) Non-profit, Education and Research Use. Use, regardless of Annual
|
|
29
|
+
Revenue, by (i) an Organization registered or recognized as a
|
|
30
|
+
non-profit or charitable organization under the laws of its
|
|
31
|
+
jurisdiction, for its non-profit purposes; (ii) an accredited school,
|
|
32
|
+
college, or university, for teaching, learning, or academic research;
|
|
33
|
+
or (iii) a publicly funded research institution, for its research.
|
|
34
|
+
|
|
35
|
+
(d) Service Provider Use. Use by a consultant, contractor, agency, or other
|
|
36
|
+
service provider, regardless of its own Annual Revenue, solely to
|
|
37
|
+
build, deploy, host, operate, or support the Licensed Work for a client
|
|
38
|
+
whose own use would be Eligible Use under (a), (b), or (c). A service
|
|
39
|
+
provider's use for its own purposes is Eligible Use only if the service
|
|
40
|
+
provider itself qualifies under (b) or (c).
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
Hosted services. Offering the Licensed Work to third parties as a hosted
|
|
44
|
+
or managed service whose primary value is the Licensed Work is Eligible
|
|
45
|
+
Use only if each such third party's own use would be Eligible Use.
|
|
46
|
+
|
|
47
|
+
Definitions. "Organization" means any company, partnership, sole
|
|
48
|
+
proprietorship, trust, association, government body, or other entity, and
|
|
49
|
+
includes an individual acting in a business or professional capacity.
|
|
50
|
+
"Affiliate" means any entity that directly or indirectly controls, is
|
|
51
|
+
controlled by, or is under common control with an Organization, where
|
|
52
|
+
"control" means ownership of more than 50% of the voting interests of an
|
|
53
|
+
entity or the power to direct its management and policies by contract or
|
|
54
|
+
otherwise. "Annual Revenue" means the total gross revenue of an
|
|
55
|
+
Organization and all of its Affiliates combined, from all sources and
|
|
56
|
+
whether or not related to the Licensed Work, for its most recently
|
|
57
|
+
completed fiscal year, or, if it has not completed a fiscal year, for the
|
|
58
|
+
twelve months before the use (or since its formation, if shorter),
|
|
59
|
+
converted into US dollars at the average exchange rate for that period.
|
|
60
|
+
Proceeds of equity or debt financing are not revenue.
|
|
61
|
+
|
|
62
|
+
Threshold. If an Organization stops qualifying under (b), its existing
|
|
63
|
+
production use remains Eligible Use for 90 days after it stops
|
|
64
|
+
qualifying, after which it must obtain a commercial license or stop
|
|
65
|
+
production use. Use that was Eligible Use when it occurred never requires
|
|
66
|
+
a commercial license after the fact.
|
|
67
|
+
Change Date: Four years from the date a version of the Licensed Work
|
|
68
|
+
is first publicly released by the Licensor.
|
|
69
|
+
Change License: Apache License, Version 2.0
|
|
70
|
+
|
|
71
|
+
For information about alternative licensing arrangements for the Licensed
|
|
72
|
+
Work, please visit: https://agimon.ai/licensing
|
|
73
|
+
|
|
74
|
+
Notice
|
|
75
|
+
|
|
76
|
+
The Business Source License (this document, or the "License") is not an Open
|
|
77
|
+
Source license. However, the Licensed Work will eventually be made available
|
|
78
|
+
under an Open Source License, as stated in this License.
|
|
79
|
+
|
|
80
|
+
-----------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
Business Source License 1.1
|
|
83
|
+
|
|
84
|
+
Terms
|
|
85
|
+
|
|
86
|
+
The Licensor hereby grants you the right to copy, modify, create derivative
|
|
87
|
+
works, redistribute, and make non-production use of the Licensed Work. The
|
|
88
|
+
Licensor may make an Additional Use Grant, above, permitting limited
|
|
89
|
+
production use.
|
|
90
|
+
|
|
91
|
+
Effective on the Change Date, or the fourth anniversary of the first publicly
|
|
92
|
+
available distribution of a specific version of the Licensed Work under this
|
|
93
|
+
License, whichever comes first, the Licensor hereby grants you rights under
|
|
94
|
+
the terms of the Change License, and the rights granted in the paragraph
|
|
95
|
+
above terminate.
|
|
96
|
+
|
|
97
|
+
If your use of the Licensed Work does not comply with the requirements
|
|
98
|
+
currently in effect as described in this License, you must purchase a
|
|
99
|
+
commercial license from the Licensor, its affiliated entities, or authorized
|
|
100
|
+
resellers, or you must refrain from using the Licensed Work.
|
|
101
|
+
|
|
102
|
+
All copies of the original and modified Licensed Work, and derivative works
|
|
103
|
+
of the Licensed Work, are subject to this License. This License applies
|
|
104
|
+
separately for each version of the Licensed Work and the Change Date may vary
|
|
105
|
+
for each version of the Licensed Work released by Licensor.
|
|
106
|
+
|
|
107
|
+
You must conspicuously display this License on each original or modified copy
|
|
108
|
+
of the Licensed Work. If you receive the Licensed Work in original or
|
|
109
|
+
modified form from a third party, the terms and conditions set forth in this
|
|
110
|
+
License apply to your use of that work.
|
|
111
|
+
|
|
112
|
+
Any use of the Licensed Work in violation of this License will automatically
|
|
113
|
+
terminate your rights under this License for the current and all other
|
|
114
|
+
versions of the Licensed Work.
|
|
115
|
+
|
|
116
|
+
This License does not grant you any right in any trademark or logo of
|
|
117
|
+
Licensor or its affiliates (provided that you may use a trademark or logo of
|
|
118
|
+
Licensor as expressly required by this License).
|
|
119
|
+
|
|
120
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
|
121
|
+
AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
|
122
|
+
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
|
123
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
|
124
|
+
TITLE.
|
|
125
|
+
|
|
126
|
+
MariaDB hereby grants you permission to use this License’s text to license
|
|
127
|
+
your works, and to refer to it using the trademark “Business Source License”,
|
|
128
|
+
as long as you comply with the Covenants of Licensor below.
|
|
129
|
+
|
|
130
|
+
Covenants of Licensor
|
|
131
|
+
|
|
132
|
+
In consideration of the right to use this License’s text and the “Business
|
|
133
|
+
Source License” name and trademark, Licensor covenants to MariaDB, and to all
|
|
134
|
+
other recipients of the licensed work to be provided by Licensor:
|
|
135
|
+
|
|
136
|
+
1. To specify as the Change License the GPL Version 2.0 or any later version,
|
|
137
|
+
or a license that is compatible with GPL Version 2.0 or a later version,
|
|
138
|
+
where “compatible” means that software provided under the Change License can
|
|
139
|
+
be included in a program with software provided under GPL Version 2.0 or a
|
|
140
|
+
later version. Licensor may specify additional Change Licenses without
|
|
141
|
+
limitation.
|
|
142
|
+
|
|
143
|
+
2. To either: (a) specify an additional grant of rights to use that does not
|
|
144
|
+
impose any additional restriction on the right granted in this License, as
|
|
145
|
+
the Additional Use Grant; or (b) insert the text “None”.
|
|
146
|
+
|
|
147
|
+
3. To specify a Change Date.
|
|
148
|
+
|
|
149
|
+
4. Not to modify this License in any other way.
|
package/README.md
CHANGED
|
@@ -246,7 +246,7 @@ Maintained by [Agimon](https://agimon.ai/about).
|
|
|
246
246
|
|
|
247
247
|
## License
|
|
248
248
|
|
|
249
|
-
|
|
249
|
+
Business Source License 1.1: free for personal use, organisations under USD 1M revenue, non-profits and education. See [LICENSE](LICENSE) and https://agimon.ai/licensing.
|
|
250
250
|
|
|
251
251
|
## Extension lifecycle and source layout
|
|
252
252
|
|
package/dist/bin/cli.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const require_cliApp = require("../cliApp-T8Fx_-1j.cjs");
|
|
4
|
-
const require_runnerDependencies = require("../runnerDependencies-
|
|
4
|
+
const require_runnerDependencies = require("../runnerDependencies-DSun3xO-.cjs");
|
|
5
5
|
let node_fs = require("node:fs");
|
|
6
6
|
let node_url = require("node:url");
|
|
7
7
|
let _agimon_ai_po_telemetry = require("@agimon-ai/po-telemetry");
|
package/dist/bin/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as runCli } from "../cliApp-q97eVh4E.mjs";
|
|
3
|
-
import { t as createRunnerDependencies } from "../runnerDependencies-
|
|
3
|
+
import { t as createRunnerDependencies } from "../runnerDependencies-7h_cxOva.mjs";
|
|
4
4
|
import { existsSync, realpathSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { createPoTelemetry } from "@agimon-ai/po-telemetry";
|
package/dist/extensions/mcp.cjs
CHANGED
|
@@ -37,6 +37,7 @@ let node_util = require("node:util");
|
|
|
37
37
|
require("@agimon-ai/foundation-process-registry");
|
|
38
38
|
require("@agimon-ai/po-core/childProcess");
|
|
39
39
|
require("@agimon-ai/po-core/backgroundWork");
|
|
40
|
+
require("@agimon-ai/po-core/extensionFile");
|
|
40
41
|
require("@agimon-ai/po-telemetry");
|
|
41
42
|
require("@earendil-works/pi-coding-agent");
|
|
42
43
|
//#region src/extensions/workspaces/sessions/(backend)/skill/po-use-runner.mcp.ts
|
|
@@ -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 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 ({ 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 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGA,IAAA,6BAAA,GAAeA,4BAAAA,eAAAA,CAAe;CAC5B,MAAM;CACN,aACE;CACF,aAAA,GAAYC,gCAAAA,oBAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,MAAqC,oCAAoC;AACvF,CAAC;;;ACPD,MAAa,aAAa,CAAC,aAAa,kBAAkB;ACS1BC,QAAAA,KAAK,OAAO;CAC1C,SAASA,QAAAA,KAAK,OAAO,EAAE,aAAa,0BAA0B,CAAC;CAC/D,SAASA,QAAAA,KAAK,SAASA,QAAAA,KAAK,OAAO,EAAE,aAAa,oDAAoD,CAAC,CAAC;CACxG,YAAYA,QAAAA,KAAK,SACfA,QAAAA,KAAK,QAAQ,EACX,aACE,yJACJ,CAAC,CACH;CACA,aAAaA,QAAAA,KAAK,SAChBA,QAAAA,KAAK,QAAQ,EACX,aACE,2LACJ,CAAC,CACH;CACA,MAAMA,QAAAA,KAAK,SACTA,QAAAA,KAAK,OAAO,EACV,aAAa,+FACf,CAAC,CACH;AACF,CAAC;ACjBoB,IAAI,OACvB;CACE;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,GAAG,GACV,GACF;AAWoC,IAAI,OACtC;CACE;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,GAAG,GACV,GACF;;;;ACbA,MAAM,EAAE,UAAU,qBAAqBC,gBAAAA;CCmCjC,GAAaC,UAAAA,UAAAA,CAAUC,mBAAAA,QAAQ;;;AC1CrC,MAAM,aAAa;CACjB;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AACmC,CAAC,GAAG,YAAY,GAAG,UAA2B;;;ACVjF,MAAa,8BAA8B;;;ACf3C,MAAM,YAAY,OAChB,oFAAoF,GAAG,2CAA2C,GAAG;AAEvI,IAAA,oBAAA,GAAeC,4BAAAA,cAAAA,EAAe,YAAgC;CAC5D,MAAM,QAAQ,QAAQ,SAAS,IAAuB,2BAA2B;CACjF,IAAI,CAAC,OAAO,MAAM,IAAI,MAAM,wCAAwC;CACpE,OAAO;EACL,GAAG,MAAM;EACT,aACE;EACF,MAAM,QAAQ,YAAY,YAAY,QAAQ,UAAU,WAAW;GACjE,MAAM,SAAS,MAAM,MAAM,WAAW,QACpC,YACA,YACA,YAAY,IAAI,CAAC,QAAQ,QAAQ,UAAU,QAAQ,MAAM,CAAC,GAC1D,UACA,SACF;GACA,MAAM,UAAU,OAAO;GACvB,IAAI,SAAS,aAAa,QAAQ,OAAO,QAAQ,OAAO,UAAU,OAAO;GACzE,OAAO;IAAE,GAAG;IAAQ,SAAS,CAAC,GAAG,OAAO,SAAS;KAAE,MAAM;KAAQ,MAAM,SAAS,QAAQ,EAAE;IAAE,CAAC;GAAE;EACjG;CACF;AACF,CAAC;;;ACpBD,MAAM,MAAY,OAA0B,YAC1C,OAAO,UAAU,aAAc,MAAwB,OAAO,IAAI;AACpE,MAAM,OAAU,UAAmC,UAChD,UAAU,KAAA,IAAY,KAAA,IAAY;CAAE,GAAG;CAAU,GAAI;AAAiB;AAEzE,MAAM,cAAiB,KAAa,UAAgB,UAAU,KAAA,IAAY,QAAS;CACjF,GAAG;CAAO,OAAO;EAAE,GAAI,MAA6B;GAAQ,cAAc;CAAI;AAChF;AAEA,MAAa,OAAA,GAAMC,4BAAAA,gBAAAA,CAAgB;CACjC,MAAM;CACN,UAAU,aAAa;EACrB,IAAI,SAAuC;GAAE,OAAO,CAAC,GAAGC,2BAAkB,OAAO,CAAC;EAAG;EACrF,IAAI,QAAqC;GAAE,OAAO,CAAC,WAAW,6BAA6B,IAAI,EAAE,MAAM,OAAO,GAAG,GAAGC,kBAAU,OAAO,CAAC,CAAC,CAAC;EAAG;CAC7I;AACF,CAAC"}
|
|
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 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 ({ 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 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGA,IAAA,6BAAA,GAAeA,4BAAAA,eAAAA,CAAe;CAC5B,MAAM;CACN,aACE;CACF,aAAA,GAAYC,gCAAAA,oBAAAA,CAAAA,QAAAA,KAAAA,CAAAA,CAAAA,cAAAA,UAAAA,CAAAA,CAAAA,MAAqC,oCAAoC;AACvF,CAAC;;;ACPD,MAAa,aAAa,CAAC,aAAa,kBAAkB;ACS1BC,QAAAA,KAAK,OAAO;CAC1C,SAASA,QAAAA,KAAK,OAAO,EAAE,aAAa,0BAA0B,CAAC;CAC/D,SAASA,QAAAA,KAAK,SAASA,QAAAA,KAAK,OAAO,EAAE,aAAa,oDAAoD,CAAC,CAAC;CACxG,YAAYA,QAAAA,KAAK,SACfA,QAAAA,KAAK,QAAQ,EACX,aACE,yJACJ,CAAC,CACH;CACA,aAAaA,QAAAA,KAAK,SAChBA,QAAAA,KAAK,QAAQ,EACX,aACE,2LACJ,CAAC,CACH;CACA,MAAMA,QAAAA,KAAK,SACTA,QAAAA,KAAK,OAAO,EACV,aAAa,+FACf,CAAC,CACH;AACF,CAAC;ACjBoB,IAAI,OACvB;CACE;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,GAAG,GACV,GACF;AAWoC,IAAI,OACtC;CACE;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,GAAG,GACV,GACF;;;;ACbA,MAAM,EAAE,UAAU,qBAAqBC,gBAAAA;CCmCjC,GAAaC,UAAAA,UAAAA,CAAUC,mBAAAA,QAAQ;;;AC1CrC,MAAM,aAAa;CACjB;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AACmC,CAAC,GAAG,YAAY,GAAG,UAA2B;;;ACVjF,MAAa,8BAA8B;;;ACf3C,MAAM,YAAY,OAChB,oFAAoF,GAAG,2CAA2C,GAAG;AAEvI,IAAA,oBAAA,GAAeC,4BAAAA,cAAAA,EAAe,YAAgC;CAC5D,MAAM,QAAQ,QAAQ,SAAS,IAAuB,2BAA2B;CACjF,IAAI,CAAC,OAAO,MAAM,IAAI,MAAM,wCAAwC;CACpE,OAAO;EACL,GAAG,MAAM;EACT,aACE;EACF,MAAM,QAAQ,YAAY,YAAY,QAAQ,UAAU,WAAW;GACjE,MAAM,SAAS,MAAM,MAAM,WAAW,QACpC,YACA,YACA,YAAY,IAAI,CAAC,QAAQ,QAAQ,UAAU,QAAQ,MAAM,CAAC,GAC1D,UACA,SACF;GACA,MAAM,UAAU,OAAO;GACvB,IAAI,SAAS,aAAa,QAAQ,OAAO,QAAQ,OAAO,UAAU,OAAO;GACzE,OAAO;IAAE,GAAG;IAAQ,SAAS,CAAC,GAAG,OAAO,SAAS;KAAE,MAAM;KAAQ,MAAM,SAAS,QAAQ,EAAE;IAAE,CAAC;GAAE;EACjG;CACF;AACF,CAAC;;;ACpBD,MAAM,MAAY,OAA0B,YAC1C,OAAO,UAAU,aAAc,MAAwB,OAAO,IAAI;AACpE,MAAM,OAAU,UAAmC,UAChD,UAAU,KAAA,IAAY,KAAA,IAAY;CAAE,GAAG;CAAU,GAAI;AAAiB;AAEzE,MAAM,cAAiB,KAAa,UAAgB,UAAU,KAAA,IAAY,QAAS;CACjF,GAAG;CAAO,OAAO;EAAE,GAAI,MAA6B;GAAQ,cAAc;CAAI;AAChF;AAEA,MAAa,OAAA,GAAMC,4BAAAA,gBAAAA,CAAgB;CACjC,MAAM;CACN,UAAU,aAAa;EACrB,IAAI,SAAuC;GAAE,OAAO,CAAC,GAAGC,2BAAkB,OAAO,CAAC;EAAG;EACrF,IAAI,QAAqC;GAAE,OAAO,CAAC,WAAW,6BAA6B,IAAI,EAAE,MAAM,OAAO,GAAG,GAAGC,kBAAU,OAAO,CAAC,CAAC,CAAC;EAAG;CAC7I;AACF,CAAC"}
|
package/dist/extensions/mcp.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { promisify } from "node:util";
|
|
|
10
10
|
import "@agimon-ai/foundation-process-registry";
|
|
11
11
|
import "@agimon-ai/po-core/childProcess";
|
|
12
12
|
import "@agimon-ai/po-core/backgroundWork";
|
|
13
|
+
import "@agimon-ai/po-core/extensionFile";
|
|
13
14
|
import "@agimon-ai/po-telemetry";
|
|
14
15
|
import "@earendil-works/pi-coding-agent";
|
|
15
16
|
//#region src/extensions/workspaces/sessions/(backend)/skill/po-use-runner.mcp.ts
|