@agntn/harnesses 0.0.1

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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -0
  3. package/dist/THIRD-PARTY-LICENSES.md +36 -0
  4. package/dist/_chunks/agents-sync.d.mts +417 -0
  5. package/dist/_chunks/agents-sync.d.mts.map +1 -0
  6. package/dist/_chunks/agents-sync.mjs +2719 -0
  7. package/dist/_chunks/agents-sync.mjs.map +1 -0
  8. package/dist/_chunks/agents.mjs +62 -0
  9. package/dist/_chunks/agents.mjs.map +1 -0
  10. package/dist/_chunks/libs/typebox.mjs +5512 -0
  11. package/dist/_chunks/libs/typebox.mjs.map +1 -0
  12. package/dist/_chunks/mcp-servers.mjs +149 -0
  13. package/dist/_chunks/mcp-servers.mjs.map +1 -0
  14. package/dist/_chunks/mcp.mjs +164 -0
  15. package/dist/_chunks/mcp.mjs.map +1 -0
  16. package/dist/_chunks/mcp2.mjs +17 -0
  17. package/dist/_chunks/mcp2.mjs.map +1 -0
  18. package/dist/_chunks/tool-operations.mjs +361 -0
  19. package/dist/_chunks/tool-operations.mjs.map +1 -0
  20. package/dist/_chunks/tool-schemas.d.mts +94 -0
  21. package/dist/_chunks/tool-schemas.d.mts.map +1 -0
  22. package/dist/_chunks/tool-schemas.mjs +112 -0
  23. package/dist/_chunks/tool-schemas.mjs.map +1 -0
  24. package/dist/_chunks/types.mjs +4 -0
  25. package/dist/_chunks/types.mjs.map +1 -0
  26. package/dist/cli.d.mts +1 -0
  27. package/dist/cli.mjs +350 -0
  28. package/dist/cli.mjs.map +1 -0
  29. package/dist/index.d.mts +300 -0
  30. package/dist/index.d.mts.map +1 -0
  31. package/dist/index.mjs +6 -0
  32. package/dist/index.mjs.map +1 -0
  33. package/dist/mcp.d.mts +15 -0
  34. package/dist/mcp.d.mts.map +1 -0
  35. package/dist/mcp.mjs +2 -0
  36. package/dist/tool-operations.d.mts +222 -0
  37. package/dist/tool-operations.d.mts.map +1 -0
  38. package/dist/tool-operations.mjs +3 -0
  39. package/dist/tool-schemas.d.mts +2 -0
  40. package/dist/tool-schemas.mjs +2 -0
  41. package/package.json +102 -0
  42. package/packages/omp/extensions/AGENTS.md +7 -0
  43. package/packages/omp/extensions/harnesses.ts +253 -0
  44. package/packages/pi/extensions/AGENTS.md +7 -0
  45. package/packages/pi/extensions/harnesses.ts +265 -0
  46. package/packages/shared/AGENTS.md +7 -0
  47. package/packages/shared/tui.ts +346 -0
package/package.json ADDED
@@ -0,0 +1,102 @@
1
+ {
2
+ "name": "@agntn/harnesses",
3
+ "version": "0.0.1",
4
+ "description": "Metadata registry for AI coding harnesses: binaries, configs, sessions, and capabilities.",
5
+ "homepage": "https://github.com/agntn/harnesses#readme",
6
+ "bugs": "https://github.com/agntn/harnesses/issues",
7
+ "license": "MIT",
8
+ "author": "oritwoen (https://github.com/oritwoen)",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/agntn/harnesses.git"
12
+ },
13
+ "bin": {
14
+ "harnesses": "./dist/cli.mjs"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "packages/omp/extensions",
19
+ "packages/pi/extensions",
20
+ "packages/shared"
21
+ ],
22
+ "type": "module",
23
+ "sideEffects": false,
24
+ "main": "./dist/index.mjs",
25
+ "types": "./dist/index.d.mts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.mts",
29
+ "import": "./dist/index.mjs"
30
+ },
31
+ "./mcp": {
32
+ "types": "./dist/mcp.d.mts",
33
+ "import": "./dist/mcp.mjs"
34
+ }
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "1.30.0",
41
+ "@toon-format/toon": "^4.1.1",
42
+ "citty": "^0.2.2",
43
+ "consola": "^3.4.2",
44
+ "smol-toml": "^1.8.0",
45
+ "typebox": "^1.3.19"
46
+ },
47
+ "devDependencies": {
48
+ "@agntn/ox": "^0.1.0",
49
+ "@earendil-works/pi-coding-agent": "0.84.3",
50
+ "@earendil-works/pi-tui": "0.84.3",
51
+ "@oh-my-pi/omptype": "18.0.4",
52
+ "@oh-my-pi/pi-ai": "18.0.4",
53
+ "@oh-my-pi/pi-coding-agent": "18.0.4",
54
+ "@types/node": "latest",
55
+ "@typescript/native-preview": "latest",
56
+ "changelogen": "^0.6.2",
57
+ "obuild": "^0.4.38",
58
+ "oxfmt": "^0.65.0",
59
+ "oxlint": "^1.80.0",
60
+ "oxlint-tsgolint": "^7.0.2001",
61
+ "vitest": "^4.1.11"
62
+ },
63
+ "peerDependencies": {
64
+ "@earendil-works/pi-coding-agent": ">=0.84.2 <1",
65
+ "@earendil-works/pi-tui": ">=0.84.2 <1",
66
+ "@oh-my-pi/pi-coding-agent": ">=18.0.0 <19"
67
+ },
68
+ "peerDependenciesMeta": {
69
+ "@earendil-works/pi-coding-agent": {
70
+ "optional": true
71
+ },
72
+ "@earendil-works/pi-tui": {
73
+ "optional": true
74
+ },
75
+ "@oh-my-pi/pi-coding-agent": {
76
+ "optional": true
77
+ }
78
+ },
79
+ "engines": {
80
+ "node": ">=22"
81
+ },
82
+ "omp": {
83
+ "extensions": [
84
+ "./packages/omp/extensions/harnesses.ts"
85
+ ]
86
+ },
87
+ "pi": {
88
+ "extensions": [
89
+ "./packages/pi/extensions/harnesses.ts"
90
+ ]
91
+ },
92
+ "scripts": {
93
+ "build": "obuild",
94
+ "dev": "obuild --stub",
95
+ "lint": "pnpm build && oxlint . && oxfmt --check .",
96
+ "lint:fix": "pnpm build && oxlint . --fix && oxfmt . && pnpm build",
97
+ "typecheck": "tsgo --noEmit --skipLibCheck && pnpm build && tsgo --noEmit --skipLibCheck -p tsconfig.extensions.json",
98
+ "test": "vitest",
99
+ "test:run": "vitest --run",
100
+ "release": "pnpm test:run && pnpm build && changelogen --release --push"
101
+ }
102
+ }
@@ -0,0 +1,7 @@
1
+ # OMP extension
2
+
3
+ Scope: the distributable OMP extension adapter and its TUI renderers.
4
+
5
+ - Reuse shared schemas and executors; this file owns only OMP registration, approval level, and rendering.
6
+ - Treat spawned harness commands as `exec`, even when their logical result is read-only.
7
+ - Sanitize every external value before rendering it in the terminal.
@@ -0,0 +1,253 @@
1
+ import { existsSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import type { AgentToolResult, ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
5
+ import { Text } from "@oh-my-pi/pi-coding-agent";
6
+ import type { TSchema } from "@oh-my-pi/pi-ai";
7
+ import type { AnySchema } from "@oh-my-pi/omptype/typebox";
8
+
9
+ import type * as HarnessTools from "../../../dist/tool-operations.d.mts";
10
+ import {
11
+ HARNESS_TOOL_APPROVALS,
12
+ HARNESS_TOOL_LABELS,
13
+ type HarnessToolName,
14
+ type RenderedToolResult,
15
+ type RenderOptions,
16
+ renderToolCall,
17
+ renderToolResult,
18
+ type StatusTheme,
19
+ } from "../../shared/tui.ts";
20
+ // Schemas must exist synchronously at registration, so unlike the executors
21
+ // below they load from dist; `pnpm build` keeps it current in the checkout.
22
+ import { harnessToolSchemas } from "../../../dist/tool-schemas.mjs";
23
+ import type * as HarnessSchemas from "../../../dist/tool-schemas.d.mts";
24
+
25
+ const sourceModulePath = fileURLToPath(new URL("../../../src/tool-operations.ts", import.meta.url));
26
+ let toolOperationsPromise: Promise<typeof HarnessTools> | undefined;
27
+
28
+ /**
29
+ * Loads the tool executors shared with the Pi extension, so the tool answers
30
+ * stay identical across surfaces. Both specifiers stay literal on purpose:
31
+ * OMP's compiled loader rewrites bare dependencies only for imports it can
32
+ * see statically.
33
+ *
34
+ * @returns {Promise<typeof HarnessTools>} The shared executor module.
35
+ */
36
+ function loadToolOperations(): Promise<typeof HarnessTools> {
37
+ toolOperationsPromise ??= (
38
+ existsSync(sourceModulePath)
39
+ ? import("../../../src/tool-operations.ts")
40
+ : import("../../../dist/tool-operations.mjs")
41
+ ) as Promise<typeof HarnessTools>;
42
+
43
+ return toolOperationsPromise;
44
+ }
45
+
46
+ function statusRenderers(tool: HarnessToolName) {
47
+ return {
48
+ renderCall(args: unknown, options: RenderOptions, theme: StatusTheme) {
49
+ return new Text(renderToolCall(tool, args, options, theme), 0, 0);
50
+ },
51
+ renderResult(result: RenderedToolResult, options: RenderOptions, theme: StatusTheme) {
52
+ return new Text(
53
+ renderToolResult(tool, result, result.isError === true, options, theme),
54
+ 0,
55
+ 0,
56
+ );
57
+ },
58
+ };
59
+ }
60
+
61
+ export default function harnessesExtension(pi: ExtensionAPI): void {
62
+ // OMP validates tool parameters with its own TypeBox build, so schemas must
63
+ // come from the host-injected facade rather than a standalone typebox import.
64
+ const schemas = harnessToolSchemas<AnySchema, TSchema>(pi.typebox.Type);
65
+ pi.setLabel("Harnesses");
66
+
67
+ pi.registerTool({
68
+ name: "harnesses_detect",
69
+ label: HARNESS_TOOL_LABELS.harnesses_detect,
70
+ description: "List every known AI coding harness with its install state and version",
71
+ parameters: schemas.detect,
72
+ approval: HARNESS_TOOL_APPROVALS.harnesses_detect,
73
+ async execute(_toolCallId, _params): Promise<AgentToolResult<HarnessTools.HarnessListing>> {
74
+ const { detectHarnesses } = await loadToolOperations();
75
+ const { content, details } = detectHarnesses();
76
+ return { content, details };
77
+ },
78
+ ...statusRenderers("harnesses_detect"),
79
+ });
80
+
81
+ pi.registerTool({
82
+ name: "harnesses_info",
83
+ label: HARNESS_TOOL_LABELS.harnesses_info,
84
+ description:
85
+ "Full metadata for one or more AI coding harnesses, including supported invocation and model operations, configuration, sessions, instructions, skills, commands, hooks, and resolved paths",
86
+ parameters: schemas.info,
87
+ approval: HARNESS_TOOL_APPROVALS.harnesses_info,
88
+ async execute(
89
+ _toolCallId,
90
+ params: HarnessSchemas.InfoParams,
91
+ ): Promise<AgentToolResult<HarnessTools.HarnessInfoDetails>> {
92
+ const { harnessInfo } = await loadToolOperations();
93
+ const { content, details } = harnessInfo(params.id);
94
+ return { content, details };
95
+ },
96
+ ...statusRenderers("harnesses_info"),
97
+ });
98
+
99
+ pi.registerTool({
100
+ name: "harnesses_models",
101
+ label: HARNESS_TOOL_LABELS.harnesses_models,
102
+ description:
103
+ "List the models currently available to one AI coding harness through its native CLI",
104
+ parameters: schemas.models,
105
+ approval: HARNESS_TOOL_APPROVALS.harnesses_models,
106
+ async execute(
107
+ _toolCallId,
108
+ params: HarnessSchemas.ModelsParams,
109
+ ): Promise<AgentToolResult<HarnessTools.ModelsOutcome | HarnessTools.RunFailure>> {
110
+ const { listHarnessModels } = await loadToolOperations();
111
+ const { content, details, isError } = await listHarnessModels(params.id, {
112
+ search: params.search,
113
+ cwd: params.cwd,
114
+ timeoutSeconds: params.timeoutSeconds,
115
+ });
116
+ if (isError) {
117
+ const message =
118
+ "error" in details
119
+ ? details.error
120
+ : details.timedOut
121
+ ? `Harness ${details.id} model listing timed out`
122
+ : details.stderr || `Harness ${details.id} exited with code ${details.exitCode}`;
123
+ throw new Error(message);
124
+ }
125
+ return { content, details };
126
+ },
127
+ ...statusRenderers("harnesses_models"),
128
+ });
129
+
130
+ pi.registerTool({
131
+ name: "harnesses_run",
132
+ label: HARNESS_TOOL_LABELS.harnesses_run,
133
+ description:
134
+ "Run one prompt through a harness. Always choose tools explicitly. Add readOnly when tools is true to require native read-only enforcement, or use tools false for an advisor without tools.",
135
+ parameters: schemas.run,
136
+ approval: HARNESS_TOOL_APPROVALS.harnesses_run,
137
+ async execute(
138
+ _toolCallId,
139
+ params: HarnessSchemas.RunParams,
140
+ ): Promise<AgentToolResult<HarnessTools.RunOutcome | HarnessTools.RunFailure>> {
141
+ const { runHarness } = await loadToolOperations();
142
+ const { content, details, isError } = await runHarness(params.id, params.prompt, {
143
+ cwd: params.cwd,
144
+ model: params.model,
145
+ timeoutSeconds: params.timeoutSeconds,
146
+ structured: params.structured,
147
+ tools: params.tools,
148
+ readOnly: params.readOnly,
149
+ });
150
+ if (isError) {
151
+ const message =
152
+ "error" in details
153
+ ? details.error
154
+ : details.timedOut
155
+ ? `Harness ${details.id} timed out`
156
+ : details.stderr || `Harness ${details.id} exited with code ${details.exitCode}`;
157
+ throw new Error(message);
158
+ }
159
+ return { content, details };
160
+ },
161
+ ...statusRenderers("harnesses_run"),
162
+ });
163
+
164
+ pi.registerTool({
165
+ name: "harnesses_mcp_list",
166
+ label: HARNESS_TOOL_LABELS.harnesses_mcp_list,
167
+ description:
168
+ "List the MCP servers configured in each harness's config files, normalized across dialects",
169
+ parameters: schemas.mcpList,
170
+ approval: HARNESS_TOOL_APPROVALS.harnesses_mcp_list,
171
+ async execute(
172
+ _toolCallId,
173
+ params: HarnessSchemas.McpListParams,
174
+ ): Promise<AgentToolResult<HarnessTools.McpListing | HarnessTools.UnknownHarness>> {
175
+ const { mcpList } = await loadToolOperations();
176
+ const { content, details } = mcpList(params.id);
177
+ return { content, details };
178
+ },
179
+ ...statusRenderers("harnesses_mcp_list"),
180
+ });
181
+
182
+ pi.registerTool({
183
+ name: "harnesses_mcp_add",
184
+ label: HARNESS_TOOL_LABELS.harnesses_mcp_add,
185
+ description:
186
+ "Add or replace one MCP server in a harness config; TOML configs get a surgical, comment-preserving edit",
187
+ parameters: schemas.mcpAdd,
188
+ approval: HARNESS_TOOL_APPROVALS.harnesses_mcp_add,
189
+ async execute(
190
+ _toolCallId,
191
+ params: HarnessSchemas.McpAddParams,
192
+ ): Promise<AgentToolResult<HarnessTools.McpMutation | HarnessTools.RunFailure>> {
193
+ const { mcpAdd } = await loadToolOperations();
194
+ const { content, details } = mcpAdd(params.id, params, params.scope ?? "user");
195
+ return { content, details };
196
+ },
197
+ ...statusRenderers("harnesses_mcp_add"),
198
+ });
199
+
200
+ pi.registerTool({
201
+ name: "harnesses_mcp_sync",
202
+ label: HARNESS_TOOL_LABELS.harnesses_mcp_sync,
203
+ description:
204
+ "Reset every harness's user-scope MCP config to exactly the master list from ~/.config/agntn/mcp.jsonc; extras are removed and master-listed names are withdrawn from excluded harnesses",
205
+ parameters: schemas.mcpSync,
206
+ approval: HARNESS_TOOL_APPROVALS.harnesses_mcp_sync,
207
+ async execute(
208
+ _toolCallId,
209
+ params: HarnessSchemas.McpSyncParams,
210
+ ): Promise<AgentToolResult<HarnessTools.SyncReport | HarnessTools.RunFailure>> {
211
+ const { mcpSync } = await loadToolOperations();
212
+ const { content, details } = mcpSync(params.id);
213
+ return { content, details };
214
+ },
215
+ ...statusRenderers("harnesses_mcp_sync"),
216
+ });
217
+
218
+ pi.registerTool({
219
+ name: "harnesses_agents_sync",
220
+ label: HARNESS_TOOL_LABELS.harnesses_agents_sync,
221
+ description:
222
+ "Link every harness's global instructions file and declared companions to the master bundle; diverged copies are backed up and relinked. Pass check to only report",
223
+ parameters: schemas.agentsSync,
224
+ approval: HARNESS_TOOL_APPROVALS.harnesses_agents_sync,
225
+ async execute(
226
+ _toolCallId,
227
+ params: HarnessSchemas.AgentsSyncParams,
228
+ ): Promise<AgentToolResult<HarnessTools.AgentsSyncReport | HarnessTools.RunFailure>> {
229
+ const { agentsSync } = await loadToolOperations();
230
+ const { content, details } = agentsSync(params.id, params.check === true);
231
+ return { content, details };
232
+ },
233
+ ...statusRenderers("harnesses_agents_sync"),
234
+ });
235
+
236
+ pi.registerTool({
237
+ name: "harnesses_mcp_remove",
238
+ label: HARNESS_TOOL_LABELS.harnesses_mcp_remove,
239
+ description:
240
+ "Remove one MCP server from a harness config; TOML configs get a surgical, comment-preserving edit",
241
+ parameters: schemas.mcpRemove,
242
+ approval: HARNESS_TOOL_APPROVALS.harnesses_mcp_remove,
243
+ async execute(
244
+ _toolCallId,
245
+ params: HarnessSchemas.McpRemoveParams,
246
+ ): Promise<AgentToolResult<HarnessTools.McpMutation | HarnessTools.RunFailure>> {
247
+ const { mcpRemove } = await loadToolOperations();
248
+ const { content, details } = mcpRemove(params.id, params.name, params.scope ?? "user");
249
+ return { content, details };
250
+ },
251
+ ...statusRenderers("harnesses_mcp_remove"),
252
+ });
253
+ }
@@ -0,0 +1,7 @@
1
+ # Pi extension
2
+
3
+ Scope: the distributable Pi extension adapter.
4
+
5
+ - Reuse schemas and executors from `src/tool-schemas.ts` and `src/tool-operations.ts`; do not fork business logic here.
6
+ - Register every shared read or execution capability exposed by the package tools.
7
+ - Keep result types sourced from built declarations and preserve lazy executor loading.
@@ -0,0 +1,265 @@
1
+ import { existsSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import type { AgentToolResult, ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
+ import { Text } from "@earendil-works/pi-tui";
6
+ import { Type, type TSchema } from "typebox";
7
+
8
+ import type * as HarnessTools from "../../../dist/tool-operations.d.mts";
9
+ import {
10
+ HARNESS_TOOL_LABELS,
11
+ type HarnessToolName,
12
+ type RenderedToolResult,
13
+ type RenderOptions,
14
+ renderToolCall,
15
+ renderToolResult,
16
+ type StatusTheme,
17
+ } from "../../shared/tui.ts";
18
+ // Schemas must exist synchronously at registration, so unlike the executors
19
+ // below they load from dist; `pnpm build` keeps it current in the checkout.
20
+ import { harnessToolSchemas } from "../../../dist/tool-schemas.mjs";
21
+ import type * as HarnessSchemas from "../../../dist/tool-schemas.d.mts";
22
+
23
+ const sourceModuleUrl = new URL("../../../src/tool-operations.ts", import.meta.url);
24
+ const distributionModuleUrl = new URL("../../../dist/tool-operations.mjs", import.meta.url);
25
+ let toolOperationsPromise: Promise<typeof HarnessTools> | undefined;
26
+
27
+ /**
28
+ * Loads the tool executors shared with the OMP extension, so the tool answers
29
+ * stay identical across surfaces.
30
+ *
31
+ * @returns {Promise<typeof HarnessTools>} The shared executor module.
32
+ */
33
+ function loadToolOperations(): Promise<typeof HarnessTools> {
34
+ toolOperationsPromise ??= import(
35
+ existsSync(fileURLToPath(sourceModuleUrl)) ? sourceModuleUrl.href : distributionModuleUrl.href
36
+ ) as Promise<typeof HarnessTools>;
37
+
38
+ return toolOperationsPromise;
39
+ }
40
+
41
+ function statusRenderers(tool: HarnessToolName) {
42
+ return {
43
+ renderCall(args: unknown, theme: StatusTheme, context: RenderOptions) {
44
+ return new Text(renderToolCall(tool, args, context, theme), 0, 0);
45
+ },
46
+ renderResult(
47
+ result: RenderedToolResult,
48
+ options: RenderOptions,
49
+ theme: StatusTheme,
50
+ context?: Readonly<{ isError?: boolean }>,
51
+ ) {
52
+ return new Text(
53
+ renderToolResult(tool, result, context?.isError === true, options, theme),
54
+ 0,
55
+ 0,
56
+ );
57
+ },
58
+ };
59
+ }
60
+
61
+ export default function harnessesExtension(pi: ExtensionAPI): void {
62
+ const schemas = harnessToolSchemas<TSchema, TSchema>(Type);
63
+
64
+ pi.registerTool({
65
+ name: "harnesses_detect",
66
+ label: HARNESS_TOOL_LABELS.harnesses_detect,
67
+ description: "List every known AI coding harness with its install state and version",
68
+ promptSnippet: "Use harnesses_detect to see which AI coding harnesses are installed.",
69
+ promptGuidelines: ["The scan checks each harness's binaries on PATH and reads their versions."],
70
+ parameters: schemas.detect,
71
+ ...statusRenderers("harnesses_detect"),
72
+ async execute(_toolCallId, _params): Promise<AgentToolResult<HarnessTools.HarnessListing>> {
73
+ const { detectHarnesses } = await loadToolOperations();
74
+ const { content, details } = detectHarnesses();
75
+ return { content, details };
76
+ },
77
+ });
78
+
79
+ pi.registerTool({
80
+ name: "harnesses_info",
81
+ label: HARNESS_TOOL_LABELS.harnesses_info,
82
+ description:
83
+ "Full metadata for one or more AI coding harnesses, including supported invocation and model operations, configuration, sessions, instructions, skills, commands, hooks, and resolved paths",
84
+ promptSnippet: "Use harnesses_info to look up where coding harnesses store their data.",
85
+ promptGuidelines: [
86
+ "Pass one harness id from harnesses_detect, or batch several ids in one call.",
87
+ "Resolved paths are absolute for the current platform and home directory.",
88
+ ],
89
+ parameters: schemas.info,
90
+ ...statusRenderers("harnesses_info"),
91
+ async execute(
92
+ _toolCallId,
93
+ params: HarnessSchemas.InfoParams,
94
+ ): Promise<AgentToolResult<HarnessTools.HarnessInfoDetails>> {
95
+ const { harnessInfo } = await loadToolOperations();
96
+ const { content, details } = harnessInfo(params.id);
97
+ return { content, details };
98
+ },
99
+ });
100
+
101
+ pi.registerTool({
102
+ name: "harnesses_models",
103
+ label: HARNESS_TOOL_LABELS.harnesses_models,
104
+ description:
105
+ "List the models currently available to one AI coding harness through its native CLI",
106
+ promptSnippet:
107
+ "Use harnesses_models to inspect models available to an installed coding harness.",
108
+ promptGuidelines: [
109
+ "An optional search filter is passed only to harnesses that support native filtering.",
110
+ "The harness command may load configuration and extensions from cwd.",
111
+ ],
112
+ parameters: schemas.models,
113
+ ...statusRenderers("harnesses_models"),
114
+ async execute(
115
+ _toolCallId,
116
+ params: HarnessSchemas.ModelsParams,
117
+ ): Promise<AgentToolResult<HarnessTools.ModelsOutcome | HarnessTools.RunFailure>> {
118
+ const { listHarnessModels } = await loadToolOperations();
119
+ const { content, details, isError } = await listHarnessModels(params.id, {
120
+ search: params.search,
121
+ cwd: params.cwd,
122
+ timeoutSeconds: params.timeoutSeconds,
123
+ });
124
+ if (isError) {
125
+ const message =
126
+ "error" in details
127
+ ? details.error
128
+ : details.timedOut
129
+ ? `Harness ${details.id} model listing timed out`
130
+ : details.stderr || `Harness ${details.id} exited with code ${details.exitCode}`;
131
+ throw new Error(message);
132
+ }
133
+ return { content, details };
134
+ },
135
+ });
136
+
137
+ pi.registerTool({
138
+ name: "harnesses_run",
139
+ label: HARNESS_TOOL_LABELS.harnesses_run,
140
+ description:
141
+ "Run one prompt through an AI coding harness's normalized non-interactive invocation, optionally selecting a model, and return its output",
142
+ promptSnippet: "Use harnesses_run to delegate one prompt to another installed coding harness.",
143
+ promptGuidelines: [
144
+ "Always choose tools explicitly. Set true whenever the task needs harness tools, including Grok native X search.",
145
+ "Add readOnly when tools is true to require native read-only enforcement. Unsupported harnesses reject it instead of widening access.",
146
+ "Set tools false only for an advisor without tools. It never silently falls back to a full agent.",
147
+ "An unsupported mode returns an explicit retry when the alternate mode is available.",
148
+ "Output is capped for context; long runs stop at the timeout.",
149
+ ],
150
+ parameters: schemas.run,
151
+ ...statusRenderers("harnesses_run"),
152
+ async execute(
153
+ _toolCallId,
154
+ params: HarnessSchemas.RunParams,
155
+ ): Promise<AgentToolResult<HarnessTools.RunOutcome | HarnessTools.RunFailure>> {
156
+ const { runHarness } = await loadToolOperations();
157
+ const { content, details, isError } = await runHarness(params.id, params.prompt, {
158
+ cwd: params.cwd,
159
+ model: params.model,
160
+ timeoutSeconds: params.timeoutSeconds,
161
+ structured: params.structured,
162
+ tools: params.tools,
163
+ readOnly: params.readOnly,
164
+ });
165
+ if (isError) {
166
+ const message =
167
+ "error" in details
168
+ ? details.error
169
+ : details.timedOut
170
+ ? `Harness ${details.id} timed out`
171
+ : details.stderr || `Harness ${details.id} exited with code ${details.exitCode}`;
172
+ throw new Error(message);
173
+ }
174
+ return { content, details };
175
+ },
176
+ });
177
+
178
+ pi.registerTool({
179
+ name: "harnesses_mcp_list",
180
+ label: HARNESS_TOOL_LABELS.harnesses_mcp_list,
181
+ description:
182
+ "List the MCP servers configured in each harness's config files, normalized across dialects",
183
+ promptSnippet:
184
+ "Use harnesses_mcp_list to see which MCP servers each coding harness has configured.",
185
+ promptGuidelines: ["Omit id to scan every harness."],
186
+ parameters: schemas.mcpList,
187
+ ...statusRenderers("harnesses_mcp_list"),
188
+ async execute(
189
+ _toolCallId,
190
+ params: HarnessSchemas.McpListParams,
191
+ ): Promise<AgentToolResult<HarnessTools.McpListing | HarnessTools.UnknownHarness>> {
192
+ const { mcpList } = await loadToolOperations();
193
+ const { content, details } = mcpList(params.id);
194
+ return { content, details };
195
+ },
196
+ });
197
+
198
+ pi.registerTool({
199
+ name: "harnesses_mcp_add",
200
+ label: HARNESS_TOOL_LABELS.harnesses_mcp_add,
201
+ description:
202
+ "Add or replace one MCP server in a harness config; TOML configs get a surgical, comment-preserving edit",
203
+ parameters: schemas.mcpAdd,
204
+ ...statusRenderers("harnesses_mcp_add"),
205
+ async execute(
206
+ _toolCallId,
207
+ params: HarnessSchemas.McpAddParams,
208
+ ): Promise<AgentToolResult<HarnessTools.McpMutation | HarnessTools.RunFailure>> {
209
+ const { mcpAdd } = await loadToolOperations();
210
+ const { content, details } = mcpAdd(params.id, params, params.scope ?? "user");
211
+ return { content, details };
212
+ },
213
+ });
214
+
215
+ pi.registerTool({
216
+ name: "harnesses_mcp_sync",
217
+ label: HARNESS_TOOL_LABELS.harnesses_mcp_sync,
218
+ description:
219
+ "Reset every harness's user-scope MCP config to exactly the master list from ~/.config/agntn/mcp.jsonc; extras are removed and master-listed names are withdrawn from excluded harnesses",
220
+ parameters: schemas.mcpSync,
221
+ ...statusRenderers("harnesses_mcp_sync"),
222
+ async execute(
223
+ _toolCallId,
224
+ params: HarnessSchemas.McpSyncParams,
225
+ ): Promise<AgentToolResult<HarnessTools.SyncReport | HarnessTools.RunFailure>> {
226
+ const { mcpSync } = await loadToolOperations();
227
+ const { content, details } = mcpSync(params.id);
228
+ return { content, details };
229
+ },
230
+ });
231
+
232
+ pi.registerTool({
233
+ name: "harnesses_agents_sync",
234
+ label: HARNESS_TOOL_LABELS.harnesses_agents_sync,
235
+ description:
236
+ "Link every harness's global instructions file and declared companions to the master bundle; diverged copies are backed up and relinked. Pass check to only report",
237
+ parameters: schemas.agentsSync,
238
+ ...statusRenderers("harnesses_agents_sync"),
239
+ async execute(
240
+ _toolCallId,
241
+ params: HarnessSchemas.AgentsSyncParams,
242
+ ): Promise<AgentToolResult<HarnessTools.AgentsSyncReport | HarnessTools.RunFailure>> {
243
+ const { agentsSync } = await loadToolOperations();
244
+ const { content, details } = agentsSync(params.id, params.check === true);
245
+ return { content, details };
246
+ },
247
+ });
248
+
249
+ pi.registerTool({
250
+ name: "harnesses_mcp_remove",
251
+ label: HARNESS_TOOL_LABELS.harnesses_mcp_remove,
252
+ description:
253
+ "Remove one MCP server from a harness config; TOML configs get a surgical, comment-preserving edit",
254
+ parameters: schemas.mcpRemove,
255
+ ...statusRenderers("harnesses_mcp_remove"),
256
+ async execute(
257
+ _toolCallId,
258
+ params: HarnessSchemas.McpRemoveParams,
259
+ ): Promise<AgentToolResult<HarnessTools.McpMutation | HarnessTools.RunFailure>> {
260
+ const { mcpRemove } = await loadToolOperations();
261
+ const { content, details } = mcpRemove(params.id, params.name, params.scope ?? "user");
262
+ return { content, details };
263
+ },
264
+ });
265
+ }
@@ -0,0 +1,7 @@
1
+ # Shared extension UI
2
+
3
+ Scope: framework-neutral rendering shared by the Pi and OMP extension adapters.
4
+
5
+ - Keep terminal text sanitized before it reaches either harness component.
6
+ - Return plain strings and structural data here. Pi and OMP own their component imports and renderer signatures.
7
+ - Keep collapsed status lines bounded. Tool output still belongs to the model-facing result.