@kal-elsam/kairo-runtime 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -6
- package/global-template/components/catalog.json +4 -2
- package/global-template/components/orchestrator/orchestrator.md +1 -1
- package/global-template/core/orchestrator.md +1 -1
- package/package.json +3 -2
- package/scripts/cockpit-smoke.mjs +1 -1
- package/scripts/install.sh +1 -1
- package/scripts/installer-smoke-test.sh +6 -4
- package/src/cli.js +1 -1
- package/src/global/adapters/pi.js +53 -0
- package/src/global/agent-capabilities/index.js +11 -1
- package/src/global/brand/index.js +4 -2
- package/src/global/dashboard-guidance.js +1 -1
- package/src/global/global-cli.js +1 -1
- package/src/global/integrations/engram-evidence.js +41 -3
- package/src/global/integrations/sdd-destinations.js +2 -2
- package/src/global/registry.js +2 -1
- package/src/global/runtime/execution-adapters/index.js +2 -1
- package/src/global/runtime/execution-adapters/pi.js +183 -0
- package/src/global/runtime/run-cancel-signal.js +3 -2
- package/src/global/runtime/run-store.js +4 -3
- package/src/global/runtime/run-supervisor-lock.js +3 -2
- package/src/global/runtime/write-atomic-json.js +51 -0
package/README.md
CHANGED
|
@@ -261,7 +261,7 @@ node ./bin/kairo.js install --dry-run
|
|
|
261
261
|
|
|
262
262
|
## Supported adapters (agent-global)
|
|
263
263
|
|
|
264
|
-
Kairo Runtime does **not** install Cursor, Codex, OpenCode,
|
|
264
|
+
Kairo Runtime does **not** install Cursor, Codex, OpenCode, Claude Code, or Pi. It detects
|
|
265
265
|
their home-directory roots and writes managed sections into their config files.
|
|
266
266
|
|
|
267
267
|
| Adapter | Label | Root | Config file |
|
|
@@ -270,6 +270,7 @@ their home-directory roots and writes managed sections into their config files.
|
|
|
270
270
|
| `codex` | Codex | `~/.codex` | `~/.codex/AGENTS.md` |
|
|
271
271
|
| `opencode` | OpenCode | `~/.config/opencode` | `~/.config/opencode/AGENTS.md` |
|
|
272
272
|
| `claude` | Claude Code | `~/.claude` | `~/.claude/CLAUDE.md` |
|
|
273
|
+
| `pi` | Pi | `~/.pi/agent` | `~/.pi/agent/AGENTS.md` |
|
|
273
274
|
|
|
274
275
|
Inspect detection and managed state:
|
|
275
276
|
|
|
@@ -281,14 +282,25 @@ kairo adapters --json
|
|
|
281
282
|
Agent selection defaults:
|
|
282
283
|
|
|
283
284
|
- If agent roots are detected → configure detected agents only.
|
|
284
|
-
- If none are detected → safe fallback to all
|
|
285
|
-
- Force all
|
|
285
|
+
- If none are detected → safe fallback to all five supported adapters.
|
|
286
|
+
- Force all five explicitly:
|
|
286
287
|
|
|
287
288
|
```bash
|
|
288
289
|
kairo setup --agents all
|
|
289
290
|
kairo install --agents all
|
|
290
291
|
```
|
|
291
292
|
|
|
293
|
+
### Pi runtime (auditable)
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
kairo run --agent pi --task "Review this repository" --permissions read-only --follow
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Launches `pi --mode json --no-session` (optional `--model`). `read-only` maps to
|
|
300
|
+
`--tools read,grep,find,ls`; other permission aliases are rejected (never translated
|
|
301
|
+
to `--approve`). Custom `PI_CODING_AGENT_DIR` blocks config writes in 0.6.0 but does
|
|
302
|
+
not block runtime. Kairo does not install Pi or assert subscription/entitlement.
|
|
303
|
+
|
|
292
304
|
Primary governance flow:
|
|
293
305
|
|
|
294
306
|
```txt
|
|
@@ -518,9 +530,11 @@ kairo components rollback engram-memory --receipt <id> --dry-run
|
|
|
518
530
|
|
|
519
531
|
Without `--agents`, Kairo uses the intersection of detected agents and
|
|
520
532
|
Kairo-managed Engram agents (`cursor`, `codex`, `opencode`, `claude` → setup slug
|
|
521
|
-
`claude-code`). After setup, status is `restart_required` — restart the agent to
|
|
533
|
+
`claude-code`, `pi` → `pi`). After setup, status is `restart_required` — restart the agent to
|
|
522
534
|
load MCP; configuration evidence is not runtime-active. Receipts live under
|
|
523
|
-
`~/.harness/integrations/engram/`.
|
|
535
|
+
`~/.harness/integrations/engram/`. For Pi, positive evidence requires
|
|
536
|
+
`~/.pi/agent/settings.json` packages (`npm:gentle-engram`, `npm:pi-mcp-adapter`) and
|
|
537
|
+
`~/.pi/agent/mcp.json` with `mcpServers.engram`.
|
|
524
538
|
|
|
525
539
|
### SDD Core skills
|
|
526
540
|
|
|
@@ -537,7 +551,7 @@ same `sdd-core` apply path. Lifecycle keeps persona frozen (`preservePersona`):
|
|
|
537
551
|
it never auto-activates teaching. Explicit persona changes stay on
|
|
538
552
|
`kairo components configure sdd-core`.
|
|
539
553
|
|
|
540
|
-
**Destinations.** Cursor, Codex, and
|
|
554
|
+
**Destinations.** Cursor, Codex, OpenCode, and Pi share `~/.agents/skills/<id>/`.
|
|
541
555
|
Claude uses `~/.claude/skills/<id>/`. One physical tree per root; consumers are
|
|
542
556
|
recorded per destination.
|
|
543
557
|
|
|
@@ -58,7 +58,8 @@
|
|
|
58
58
|
"cursor": "- In repos with workspace harness: also read `.cursor/rules/` and `docs/specs/`.",
|
|
59
59
|
"codex": "- In repos with workspace harness: also read `.codex/skills/sdd/` when present.",
|
|
60
60
|
"claude": "- In repos with workspace harness: repo `AGENTS.md` wins over `CLAUDE.md` pointers.",
|
|
61
|
-
"opencode": "- In repos with workspace harness: also read `.opencode/` SDD assets when present."
|
|
61
|
+
"opencode": "- In repos with workspace harness: also read `.opencode/` SDD assets when present.",
|
|
62
|
+
"pi": "- Skills load from `~/.agents/skills` (shared Agent Skills root). Teaching persona activates only via the managed section of `~/.pi/agent/AGENTS.md`."
|
|
62
63
|
}
|
|
63
64
|
},
|
|
64
65
|
{
|
|
@@ -80,7 +81,8 @@
|
|
|
80
81
|
"cursor": "- Configure Engram via `kairo components configure engram-memory` (official `engram setup cursor`).",
|
|
81
82
|
"codex": "- Configure Engram via `kairo components configure engram-memory` (official `engram setup codex`).",
|
|
82
83
|
"claude": "- Configure Engram via `kairo components configure engram-memory` (official `engram setup claude-code`).",
|
|
83
|
-
"opencode": "- Configure Engram via `kairo components configure engram-memory` (official `engram setup opencode`)."
|
|
84
|
+
"opencode": "- Configure Engram via `kairo components configure engram-memory` (official `engram setup opencode`).",
|
|
85
|
+
"pi": "- Configure Engram via `kairo components configure engram-memory` (official `engram setup pi`). Evidence: `~/.pi/agent/settings.json` packages + `mcp.json` engram server. Restart Pi after setup; Kairo never runs `engram doctor`/`serve` or touches `engram.db`."
|
|
84
86
|
}
|
|
85
87
|
},
|
|
86
88
|
{
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
## Role
|
|
6
6
|
|
|
7
7
|
The Harness Orchestrator (conductor) coordinates work across local coding agents
|
|
8
|
-
(Cursor, Codex, OpenCode, Claude). It does not replace any agent. It defines how
|
|
8
|
+
(Cursor, Codex, OpenCode, Claude, Pi). It does not replace any agent. It defines how
|
|
9
9
|
agents hand off work and which source of truth governs.
|
|
10
10
|
|
|
11
11
|
## Authority order
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
## Role
|
|
6
6
|
|
|
7
7
|
The Harness Orchestrator (conductor) coordinates work across local coding agents
|
|
8
|
-
(Cursor, Codex, OpenCode, Claude). It does not replace any agent. It defines how
|
|
8
|
+
(Cursor, Codex, OpenCode, Claude, Pi). It does not replace any agent. It defines how
|
|
9
9
|
agents hand off work and which source of truth governs.
|
|
10
10
|
|
|
11
11
|
## Authority order
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kal-elsam/kairo-runtime",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Kairo Runtime — local agent operating system for Codex, Cursor, Claude,
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Kairo Runtime — local agent operating system for Codex, Cursor, Claude, Pi, Engram, and Graphify.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/Kal-elSam/harness#readme",
|
|
7
7
|
"repository": {
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"agents",
|
|
46
46
|
"codex",
|
|
47
47
|
"cursor",
|
|
48
|
+
"pi",
|
|
48
49
|
"kairo",
|
|
49
50
|
"kairo-runtime",
|
|
50
51
|
"harness",
|
|
@@ -26,7 +26,7 @@ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
26
26
|
const require = createRequire(import.meta.url);
|
|
27
27
|
const pkg = require(join(root, "package.json"));
|
|
28
28
|
|
|
29
|
-
assert.equal(pkg.version, "0.
|
|
29
|
+
assert.equal(pkg.version, "0.6.0");
|
|
30
30
|
assert.ok(pkg.dependencies["ansi-escapes"]);
|
|
31
31
|
|
|
32
32
|
assert.equal(resolveLayoutMode({ columns: 120, rows: 40 }), LAYOUT_MODES.WIDE);
|
package/scripts/install.sh
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# Safe by design:
|
|
5
5
|
# - no sudo
|
|
6
6
|
# - no shell profile changes
|
|
7
|
-
# - does not install Cursor/Codex/OpenCode/Claude
|
|
7
|
+
# - does not install Cursor/Codex/OpenCode/Claude/Pi
|
|
8
8
|
# - default ends with setup --dry-run (no writes)
|
|
9
9
|
# - explicit apply: pass --yes to run setup --yes
|
|
10
10
|
#
|
|
@@ -88,9 +88,9 @@ if (!payload.ok || payload.overall !== 'ok') {
|
|
|
88
88
|
process.exit(1);
|
|
89
89
|
}
|
|
90
90
|
const managed = payload.agents.filter((agent) => agent.managed).map((agent) => agent.id).sort();
|
|
91
|
-
const expected = ['claude', 'codex', 'cursor', 'opencode'];
|
|
91
|
+
const expected = ['claude', 'codex', 'cursor', 'opencode', 'pi'];
|
|
92
92
|
if (JSON.stringify(managed) !== JSON.stringify(expected)) {
|
|
93
|
-
console.error('Expected all
|
|
93
|
+
console.error('Expected all five agents managed');
|
|
94
94
|
process.exit(1);
|
|
95
95
|
}
|
|
96
96
|
" "$status_json"
|
|
@@ -105,7 +105,8 @@ const targets = [
|
|
|
105
105
|
'.cursor/AGENTS.md',
|
|
106
106
|
'.codex/AGENTS.md',
|
|
107
107
|
'.config/opencode/AGENTS.md',
|
|
108
|
-
'.claude/CLAUDE.md'
|
|
108
|
+
'.claude/CLAUDE.md',
|
|
109
|
+
'.pi/agent/AGENTS.md'
|
|
109
110
|
];
|
|
110
111
|
|
|
111
112
|
for (const relativePath of targets) {
|
|
@@ -143,7 +144,8 @@ mkdir -p \
|
|
|
143
144
|
"$FAKE_HOME/.cursor" \
|
|
144
145
|
"$FAKE_HOME/.codex" \
|
|
145
146
|
"$FAKE_HOME/.config/opencode" \
|
|
146
|
-
"$FAKE_HOME/.claude"
|
|
147
|
+
"$FAKE_HOME/.claude" \
|
|
148
|
+
"$FAKE_HOME/.pi/agent"
|
|
147
149
|
|
|
148
150
|
echo
|
|
149
151
|
echo "== curl install.sh | sh --version ${VERSION} --yes --agents all =="
|
package/src/cli.js
CHANGED
|
@@ -732,7 +732,7 @@ function printHelp() {
|
|
|
732
732
|
console.log(`${BRAND.displayName} (${PACKAGE_NAME})
|
|
733
733
|
|
|
734
734
|
${BRAND.tagline}. ${BRAND.displayName} does not install AI apps — it powers and
|
|
735
|
-
coordinates agents you already have (Cursor, Codex, OpenCode, Claude) with managed
|
|
735
|
+
coordinates agents you already have (Cursor, Codex, OpenCode, Claude, Pi) with managed
|
|
736
736
|
sections, components, backups, and drift repair under ~/.harness.
|
|
737
737
|
|
|
738
738
|
Bootstrap: see README.md (curl install.sh or npx ${PACKAGE_NAME}).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { createManagedConfigAdapter } from "../managed-config-adapter.js";
|
|
3
|
+
|
|
4
|
+
export const PI_DEFAULT_ROOT_DIR = ".pi/agent";
|
|
5
|
+
export const PI_DEFAULT_CONFIG_FILE = ".pi/agent/AGENTS.md";
|
|
6
|
+
export const PI_CODING_AGENT_DIR_ENV = "PI_CODING_AGENT_DIR";
|
|
7
|
+
|
|
8
|
+
const CUSTOM_DIR_MESSAGE =
|
|
9
|
+
`Pi config writes are unsupported when ${PI_CODING_AGENT_DIR_ENV} points away from `
|
|
10
|
+
+ `~/${PI_DEFAULT_ROOT_DIR} (out of scope for Kairo 0.6.0). `
|
|
11
|
+
+ `Unset ${PI_CODING_AGENT_DIR_ENV} to manage the default directory; runtime launches remain available.`;
|
|
12
|
+
|
|
13
|
+
/** True when PI_CODING_AGENT_DIR is set to a non-default absolute/relative path. */
|
|
14
|
+
export function isCustomPiCodingAgentDir(env = process.env, homeDir) {
|
|
15
|
+
const raw = env?.[PI_CODING_AGENT_DIR_ENV];
|
|
16
|
+
if (raw == null || String(raw).trim() === "") return false;
|
|
17
|
+
if (homeDir == null) return true;
|
|
18
|
+
return resolve(String(raw).trim()) !== resolve(homeDir, PI_DEFAULT_ROOT_DIR);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function assertDefaultPiConfigDir(context = {}) {
|
|
22
|
+
const env = context.env ?? process.env;
|
|
23
|
+
if (isCustomPiCodingAgentDir(env, context.homeDir)) {
|
|
24
|
+
throw new Error(CUSTOM_DIR_MESSAGE);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const managed = createManagedConfigAdapter({
|
|
29
|
+
id: "pi",
|
|
30
|
+
label: "Pi",
|
|
31
|
+
rootDir: PI_DEFAULT_ROOT_DIR,
|
|
32
|
+
configFile: PI_DEFAULT_CONFIG_FILE
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/** Managed adapter for ~/.pi/agent/AGENTS.md; custom PI_CODING_AGENT_DIR fails before writes. */
|
|
36
|
+
export default {
|
|
37
|
+
...managed,
|
|
38
|
+
|
|
39
|
+
plan(context) {
|
|
40
|
+
assertDefaultPiConfigDir(context);
|
|
41
|
+
return managed.plan(context);
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
async apply(context, plan) {
|
|
45
|
+
assertDefaultPiConfigDir(context);
|
|
46
|
+
return managed.apply(context, plan);
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
async uninstall(context, stateEntry) {
|
|
50
|
+
assertDefaultPiConfigDir(context);
|
|
51
|
+
return managed.uninstall(context, stateEntry);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
@@ -2,6 +2,7 @@ import cursorManaged from "../adapters/cursor.js";
|
|
|
2
2
|
import codexManaged from "../adapters/codex.js";
|
|
3
3
|
import opencodeManaged from "../adapters/opencode.js";
|
|
4
4
|
import claudeManaged from "../adapters/claude.js";
|
|
5
|
+
import piManaged from "../adapters/pi.js";
|
|
5
6
|
import { createAgentCapabilityAdapter } from "./create-capability-adapter.js";
|
|
6
7
|
|
|
7
8
|
const cursor = createAgentCapabilityAdapter({
|
|
@@ -37,7 +38,16 @@ const claude = createAgentCapabilityAdapter({
|
|
|
37
38
|
runExecutable: "claude"
|
|
38
39
|
});
|
|
39
40
|
|
|
40
|
-
const
|
|
41
|
+
const pi = createAgentCapabilityAdapter({
|
|
42
|
+
id: "pi",
|
|
43
|
+
label: "Pi",
|
|
44
|
+
managedAdapter: piManaged,
|
|
45
|
+
executable: "pi",
|
|
46
|
+
opaqueAuth: true,
|
|
47
|
+
runExecutable: "pi"
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const CAPABILITY_ADAPTERS = [cursor, codex, opencode, claude, pi];
|
|
41
51
|
|
|
42
52
|
export const AGENT_CAPABILITY_IDS = CAPABILITY_ADAPTERS.map((adapter) => adapter.id);
|
|
43
53
|
|
|
@@ -30,14 +30,16 @@ export const AGENT_LABELS = {
|
|
|
30
30
|
cursor: "Cursor",
|
|
31
31
|
codex: "Codex",
|
|
32
32
|
opencode: "OpenCode",
|
|
33
|
-
claude: "Claude Code"
|
|
33
|
+
claude: "Claude Code",
|
|
34
|
+
pi: "Pi"
|
|
34
35
|
};
|
|
35
36
|
|
|
36
37
|
export const AGENT_SYMBOLS = {
|
|
37
38
|
cursor: "◆",
|
|
38
39
|
codex: "◆",
|
|
39
40
|
opencode: "◆",
|
|
40
|
-
claude: "◆"
|
|
41
|
+
claude: "◆",
|
|
42
|
+
pi: "◆"
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
export const TOKENS = {
|
|
@@ -150,7 +150,7 @@ export function resolveDashboardRecommendation({
|
|
|
150
150
|
kind: NEXT_STEP_KINDS.LAUNCH,
|
|
151
151
|
title: "Create a new run",
|
|
152
152
|
message: "Create a new run",
|
|
153
|
-
detail: "Delegate a task to Cursor, Codex or
|
|
153
|
+
detail: "Delegate a task to Cursor, Codex, Claude, or Pi.",
|
|
154
154
|
targetView: RECOMMENDATION_TARGETS.LAUNCH,
|
|
155
155
|
targetAction: "launch"
|
|
156
156
|
};
|
package/src/global/global-cli.js
CHANGED
|
@@ -734,7 +734,7 @@ function printAdapterMatrixReport(report) {
|
|
|
734
734
|
console.log(commandHeader("adapters — supported agent integrations"));
|
|
735
735
|
console.log(`Home: ${report.homeDir}`);
|
|
736
736
|
console.log("");
|
|
737
|
-
console.log(`${BRAND.displayName} does not install Cursor, Codex, OpenCode,
|
|
737
|
+
console.log(`${BRAND.displayName} does not install Cursor, Codex, OpenCode, Claude Code, or Pi.`);
|
|
738
738
|
console.log("It configures managed sections in each agent's config files.");
|
|
739
739
|
console.log("");
|
|
740
740
|
console.log(`Supported: ${report.supportedCount} Detected: ${report.detectedCount} Managed: ${report.managedCount}`);
|
|
@@ -3,12 +3,13 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { probeCommand } from "../cli-probe.js";
|
|
5
5
|
|
|
6
|
-
export const KAIRO_ENGRAM_AGENT_IDS = Object.freeze(["cursor", "codex", "opencode", "claude"]);
|
|
6
|
+
export const KAIRO_ENGRAM_AGENT_IDS = Object.freeze(["cursor", "codex", "opencode", "claude", "pi"]);
|
|
7
7
|
export const ENGRAM_SETUP_SLUG_BY_AGENT = Object.freeze({
|
|
8
8
|
cursor: "cursor",
|
|
9
9
|
codex: "codex",
|
|
10
10
|
opencode: "opencode",
|
|
11
|
-
claude: "claude-code"
|
|
11
|
+
claude: "claude-code",
|
|
12
|
+
pi: "pi"
|
|
12
13
|
});
|
|
13
14
|
export const ENGRAM_MIN_VERSION = "1.19.0";
|
|
14
15
|
export const ENGRAM_MAX_MAJOR = 2;
|
|
@@ -110,7 +111,7 @@ export function inspectEngramAgentConfig(agentId, { homeDir = homedir() } = {})
|
|
|
110
111
|
const conflicts = evidence.filter((item) => item.conflict);
|
|
111
112
|
let status = ENGRAM_INTEGRATION_STATUS.UNCONFIGURED;
|
|
112
113
|
if (conflicts.length > 0) status = ENGRAM_INTEGRATION_STATUS.CONFLICT;
|
|
113
|
-
else if (
|
|
114
|
+
else if (isEngramAgentConfigured(agentId, evidence)) {
|
|
114
115
|
status = ENGRAM_INTEGRATION_STATUS.CONFIGURED;
|
|
115
116
|
}
|
|
116
117
|
return { id: agentId, slug, status, evidence };
|
|
@@ -165,9 +166,46 @@ function collectAgentEvidence(agentId, homeDir) {
|
|
|
165
166
|
fileEvidence(join(homeDir, ".cursor", "rules", "engram.mdc"), "protocol")
|
|
166
167
|
];
|
|
167
168
|
}
|
|
169
|
+
if (agentId === "pi") {
|
|
170
|
+
return [
|
|
171
|
+
piSettingsPackagesEvidence(join(homeDir, ".pi", "agent", "settings.json")),
|
|
172
|
+
jsonKeyEvidence(join(homeDir, ".pi", "agent", "mcp.json"), ["mcpServers", "engram"], "mcp")
|
|
173
|
+
];
|
|
174
|
+
}
|
|
168
175
|
throw new Error(`Unsupported Engram agent "${agentId}".`);
|
|
169
176
|
}
|
|
170
177
|
|
|
178
|
+
function isEngramAgentConfigured(agentId, evidence) {
|
|
179
|
+
if (agentId === "pi") {
|
|
180
|
+
return evidence.length > 0 && evidence.every((item) => item.present && !item.conflict);
|
|
181
|
+
}
|
|
182
|
+
return evidence.some((item) => item.present && item.kind === "mcp");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function piSettingsPackagesEvidence(path) {
|
|
186
|
+
const required = ["npm:gentle-engram", "npm:pi-mcp-adapter"];
|
|
187
|
+
if (!existsSync(path)) {
|
|
188
|
+
return { path, kind: "settings", present: false, conflict: false, required };
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
192
|
+
if (data == null || typeof data !== "object" || Array.isArray(data)) {
|
|
193
|
+
return { path, kind: "settings", present: false, conflict: true, required, detail: "invalid structure" };
|
|
194
|
+
}
|
|
195
|
+
const packages = Array.isArray(data.packages) ? data.packages : null;
|
|
196
|
+
if (packages == null) {
|
|
197
|
+
return { path, kind: "settings", present: false, conflict: true, required, detail: "invalid structure" };
|
|
198
|
+
}
|
|
199
|
+
const normalized = packages.map((entry) => String(entry));
|
|
200
|
+
const present = required.every((req) =>
|
|
201
|
+
normalized.some((entry) => entry === req || entry.startsWith(`${req}@`))
|
|
202
|
+
);
|
|
203
|
+
return { path, kind: "settings", present, conflict: false, required };
|
|
204
|
+
} catch {
|
|
205
|
+
return { path, kind: "settings", present: false, conflict: true, required, detail: "unreadable json" };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
171
209
|
function fileEvidence(path, kind) {
|
|
172
210
|
return { path, kind, present: existsSync(path), conflict: false };
|
|
173
211
|
}
|
|
@@ -12,8 +12,8 @@ export const SDD_SKILL_IDS = Object.freeze([
|
|
|
12
12
|
"sdd-archive"
|
|
13
13
|
]);
|
|
14
14
|
|
|
15
|
-
export const SDD_MANAGED_AGENT_IDS = Object.freeze(["cursor", "codex", "opencode", "claude"]);
|
|
16
|
-
export const SDD_SHARED_AGENT_IDS = Object.freeze(["cursor", "codex", "opencode"]);
|
|
15
|
+
export const SDD_MANAGED_AGENT_IDS = Object.freeze(["cursor", "codex", "opencode", "claude", "pi"]);
|
|
16
|
+
export const SDD_SHARED_AGENT_IDS = Object.freeze(["cursor", "codex", "opencode", "pi"]);
|
|
17
17
|
export const SDD_PERSONA_IDS = Object.freeze(["off", "teaching"]);
|
|
18
18
|
|
|
19
19
|
export function resolveSddAgentSelection({
|
package/src/global/registry.js
CHANGED
|
@@ -4,8 +4,9 @@ import claude from "./adapters/claude.js";
|
|
|
4
4
|
import codex from "./adapters/codex.js";
|
|
5
5
|
import cursor from "./adapters/cursor.js";
|
|
6
6
|
import opencode from "./adapters/opencode.js";
|
|
7
|
+
import pi from "./adapters/pi.js";
|
|
7
8
|
|
|
8
|
-
const ADAPTERS = [cursor, codex, opencode, claude];
|
|
9
|
+
const ADAPTERS = [cursor, codex, opencode, claude, pi];
|
|
9
10
|
|
|
10
11
|
export const GLOBAL_AGENT_IDS = ADAPTERS.map((adapter) => adapter.id);
|
|
11
12
|
|
|
@@ -2,8 +2,9 @@ import cursor from "./cursor.js";
|
|
|
2
2
|
import codex from "./codex.js";
|
|
3
3
|
import claude from "./claude.js";
|
|
4
4
|
import opencode from "./opencode.js";
|
|
5
|
+
import pi from "./pi.js";
|
|
5
6
|
|
|
6
|
-
const EXECUTION_ADAPTERS = [cursor, codex, claude, opencode];
|
|
7
|
+
const EXECUTION_ADAPTERS = [cursor, codex, claude, opencode, pi];
|
|
7
8
|
|
|
8
9
|
export const EXECUTION_ADAPTER_IDS = EXECUTION_ADAPTERS.map((adapter) => adapter.id);
|
|
9
10
|
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { createExecutionAdapter, parseNdjsonLine } from "./create-execution-adapter.js";
|
|
2
|
+
import { isExecutableAvailable, probeCommand } from "../../cli-probe.js";
|
|
3
|
+
|
|
4
|
+
const EXECUTABLE = "pi";
|
|
5
|
+
const READ_ONLY_TOOLS = "read,grep,find,ls";
|
|
6
|
+
|
|
7
|
+
const LIFECYCLE_KINDS = new Set([
|
|
8
|
+
"session",
|
|
9
|
+
"agent_start",
|
|
10
|
+
"agent_end",
|
|
11
|
+
"turn_start",
|
|
12
|
+
"message_start",
|
|
13
|
+
"message_update",
|
|
14
|
+
"compaction_start",
|
|
15
|
+
"compaction_end",
|
|
16
|
+
"auto_retry_start",
|
|
17
|
+
"auto_retry_end",
|
|
18
|
+
"queue_update",
|
|
19
|
+
"summarization_retry_scheduled",
|
|
20
|
+
"summarization_retry_attempt_start",
|
|
21
|
+
"summarization_retry_finished"
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export function buildPiPermissionsArgs(permissions = []) {
|
|
25
|
+
if (!Array.isArray(permissions) || permissions.length === 0) return [];
|
|
26
|
+
|
|
27
|
+
const normalized = [...new Set(permissions.map((entry) => String(entry).toLowerCase()))];
|
|
28
|
+
if (normalized.length === 1 && normalized[0] === "read-only") {
|
|
29
|
+
return ["--tools", READ_ONLY_TOOLS];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Pi permissions only support "read-only" in Kairo 0.6.0 (got: ${permissions.join(", ")}). `
|
|
34
|
+
+ "Other aliases are rejected and are never translated to --approve."
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function buildPiLaunch({ task, cwd, model, permissions = [], env = process.env } = {}) {
|
|
39
|
+
const args = [
|
|
40
|
+
"--mode",
|
|
41
|
+
"json",
|
|
42
|
+
"--no-session",
|
|
43
|
+
...buildPiPermissionsArgs(permissions)
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
if (model) {
|
|
47
|
+
args.push("--model", model);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
args.push(task);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
command: EXECUTABLE,
|
|
54
|
+
args,
|
|
55
|
+
cwd,
|
|
56
|
+
env
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function checkPiAvailability(context = {}) {
|
|
61
|
+
const env = context.env ?? process.env;
|
|
62
|
+
const probe = context.probeImpl ?? probeCommand;
|
|
63
|
+
const availableCheck = context.isAvailableImpl ?? isExecutableAvailable;
|
|
64
|
+
|
|
65
|
+
if (!availableCheck(EXECUTABLE, { env })) {
|
|
66
|
+
return {
|
|
67
|
+
available: false,
|
|
68
|
+
compatible: false,
|
|
69
|
+
launchable: false,
|
|
70
|
+
reason: 'Pi CLI "pi" is not on PATH.'
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const help = probe(EXECUTABLE, ["--help"], { env, timeoutMs: 5000 });
|
|
75
|
+
const text = `${help.stdout ?? ""}\n${help.stderr ?? ""}`;
|
|
76
|
+
const hasMode = /--mode\b/.test(text);
|
|
77
|
+
const hasNoSession = /--no-session\b/.test(text);
|
|
78
|
+
|
|
79
|
+
if (!help.ok || !hasMode || !hasNoSession) {
|
|
80
|
+
return {
|
|
81
|
+
available: true,
|
|
82
|
+
compatible: false,
|
|
83
|
+
launchable: false,
|
|
84
|
+
reason: "Pi CLI is missing required --mode / --no-session flags for auditable JSON runs."
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
available: true,
|
|
90
|
+
compatible: true,
|
|
91
|
+
launchable: true,
|
|
92
|
+
reason: null
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function parsePiEventLine(line) {
|
|
97
|
+
const parsed = parseNdjsonLine(line);
|
|
98
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
99
|
+
|
|
100
|
+
if (parsed.type === "tool_execution_start") {
|
|
101
|
+
return {
|
|
102
|
+
type: "tool_call",
|
|
103
|
+
tool_name: parsed.toolName ?? "unknown",
|
|
104
|
+
status: "started",
|
|
105
|
+
id: parsed.toolCallId ?? null
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (parsed.type === "tool_execution_end") {
|
|
110
|
+
return {
|
|
111
|
+
type: "tool_result",
|
|
112
|
+
tool_name: parsed.toolName ?? "unknown",
|
|
113
|
+
status: parsed.isError ? "error" : "completed",
|
|
114
|
+
id: parsed.toolCallId ?? null
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (parsed.type === "tool_execution_update") {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (parsed.type === "message_end") {
|
|
123
|
+
if (parsed.message?.role !== "assistant") return null;
|
|
124
|
+
const content = Array.isArray(parsed.message.content)
|
|
125
|
+
? parsed.message.content
|
|
126
|
+
.filter((block) => block?.type === "text")
|
|
127
|
+
.map((block) => block.text ?? "")
|
|
128
|
+
.join("")
|
|
129
|
+
: "";
|
|
130
|
+
return {
|
|
131
|
+
type: "assistant",
|
|
132
|
+
role: "assistant",
|
|
133
|
+
content
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (parsed.type === "turn_end") {
|
|
138
|
+
const usage = parsed.message?.usage;
|
|
139
|
+
if (usage && typeof usage === "object") {
|
|
140
|
+
const cost = typeof usage.cost === "number"
|
|
141
|
+
? usage.cost
|
|
142
|
+
: typeof usage.cost?.total === "number" ? usage.cost.total : null;
|
|
143
|
+
return {
|
|
144
|
+
type: "usage",
|
|
145
|
+
inputTokens: usage.input ?? usage.inputTokens ?? usage.input_tokens ?? null,
|
|
146
|
+
outputTokens: usage.output ?? usage.outputTokens ?? usage.output_tokens ?? null,
|
|
147
|
+
totalTokens: usage.totalTokens ?? usage.total ?? usage.total_tokens ?? null,
|
|
148
|
+
cost
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return { type: "system", kind: "turn_end" };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (LIFECYCLE_KINDS.has(parsed.type)) {
|
|
155
|
+
return {
|
|
156
|
+
type: "system",
|
|
157
|
+
kind: parsed.type,
|
|
158
|
+
...(parsed.reason ? { reason: parsed.reason } : {}),
|
|
159
|
+
...(parsed.attempt != null ? { attempt: parsed.attempt } : {})
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
type: "system",
|
|
165
|
+
kind: parsed.type ?? "unknown"
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export default createExecutionAdapter({
|
|
170
|
+
id: "pi",
|
|
171
|
+
label: "Pi",
|
|
172
|
+
executable: EXECUTABLE,
|
|
173
|
+
capabilities: {
|
|
174
|
+
structuredEvents: true,
|
|
175
|
+
tokens: true,
|
|
176
|
+
diff: false,
|
|
177
|
+
cancel: true,
|
|
178
|
+
transcript: true
|
|
179
|
+
},
|
|
180
|
+
checkAvailability: checkPiAvailability,
|
|
181
|
+
buildLaunch: buildPiLaunch,
|
|
182
|
+
parseEventLine: parsePiEventLine
|
|
183
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { mkdir, readFile
|
|
2
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
3
3
|
import { runPaths } from "../paths.js";
|
|
4
|
+
import { writeAtomicJson } from "./write-atomic-json.js";
|
|
4
5
|
|
|
5
6
|
function cancelSignalPath(homeDir, runId) {
|
|
6
7
|
return `${runPaths(homeDir, runId).runDir}/cancel.signal.json`;
|
|
@@ -9,7 +10,7 @@ function cancelSignalPath(homeDir, runId) {
|
|
|
9
10
|
export async function writeCancelSignal(homeDir, runId, payload) {
|
|
10
11
|
const { runDir } = runPaths(homeDir, runId);
|
|
11
12
|
await mkdir(runDir, { recursive: true });
|
|
12
|
-
await
|
|
13
|
+
await writeAtomicJson(cancelSignalPath(homeDir, runId), payload);
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export async function readCancelSignal(homeDir, runId) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { appendFile, mkdir, readdir, readFile
|
|
2
|
+
import { appendFile, mkdir, readdir, readFile } from "node:fs/promises";
|
|
3
3
|
import { harnessHomePaths, runPaths } from "../paths.js";
|
|
4
4
|
import { RUN_STATES, isActiveRunState } from "./run-types.js";
|
|
5
5
|
import { createRunEvent } from "./run-events.js";
|
|
6
|
+
import { writeAtomicJson } from "./write-atomic-json.js";
|
|
6
7
|
|
|
7
8
|
const writeLocks = new Map();
|
|
8
9
|
|
|
@@ -13,7 +14,7 @@ export function getRunsDir(homeDir) {
|
|
|
13
14
|
export async function createRunRecord(homeDir, metadata) {
|
|
14
15
|
const { runDir, statePath } = runPaths(homeDir, metadata.runId);
|
|
15
16
|
await mkdir(runDir, { recursive: true });
|
|
16
|
-
await
|
|
17
|
+
await writeAtomicJson(statePath, metadata);
|
|
17
18
|
return metadata;
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -34,7 +35,7 @@ export async function writeRunState(homeDir, metadata) {
|
|
|
34
35
|
const next = previous.then(async () => {
|
|
35
36
|
const { statePath, runDir } = runPaths(homeDir, metadata.runId);
|
|
36
37
|
await mkdir(runDir, { recursive: true });
|
|
37
|
-
await
|
|
38
|
+
await writeAtomicJson(statePath, metadata);
|
|
38
39
|
return metadata;
|
|
39
40
|
});
|
|
40
41
|
writeLocks.set(key, next.catch(() => {}));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { mkdir, readFile
|
|
2
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
3
3
|
import { runPaths } from "../paths.js";
|
|
4
|
+
import { writeAtomicJson } from "./write-atomic-json.js";
|
|
4
5
|
|
|
5
6
|
function lockPath(homeDir, runId) {
|
|
6
7
|
return `${runPaths(homeDir, runId).runDir}/supervisor.lock.json`;
|
|
@@ -9,7 +10,7 @@ function lockPath(homeDir, runId) {
|
|
|
9
10
|
export async function writeSupervisorLock(homeDir, runId, lock) {
|
|
10
11
|
const { runDir } = runPaths(homeDir, runId);
|
|
11
12
|
await mkdir(runDir, { recursive: true });
|
|
12
|
-
await
|
|
13
|
+
await writeAtomicJson(lockPath(homeDir, runId), lock);
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export async function readSupervisorLock(homeDir, runId) {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { open as fsOpen, rename as fsRename, unlink as fsUnlink } from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
function defaultTempPath(targetPath) {
|
|
7
|
+
const id = randomBytes(8).toString("hex");
|
|
8
|
+
return join(
|
|
9
|
+
dirname(targetPath),
|
|
10
|
+
`.${basename(targetPath)}.${process.pid}.${id}.tmp`
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Atomically replace targetPath with pretty-printed JSON.
|
|
16
|
+
* Creates a unique temp in the same directory (O_EXCL), writes + fsync,
|
|
17
|
+
* renames over the destination, and deletes the temp on any failure.
|
|
18
|
+
*/
|
|
19
|
+
export async function writeAtomicJson(targetPath, value, deps = {}) {
|
|
20
|
+
const open = deps.open ?? fsOpen;
|
|
21
|
+
const rename = deps.rename ?? fsRename;
|
|
22
|
+
const unlink = deps.unlink ?? fsUnlink;
|
|
23
|
+
const createTempPath = deps.createTempPath ?? defaultTempPath;
|
|
24
|
+
|
|
25
|
+
const payload = `${JSON.stringify(value, null, 2)}\n`;
|
|
26
|
+
const tempPath = createTempPath(targetPath);
|
|
27
|
+
let handle;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
handle = await open(tempPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o644);
|
|
31
|
+
await handle.writeFile(payload, "utf8");
|
|
32
|
+
await handle.sync();
|
|
33
|
+
await handle.close();
|
|
34
|
+
handle = undefined;
|
|
35
|
+
await rename(tempPath, targetPath);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (handle) {
|
|
38
|
+
try {
|
|
39
|
+
await handle.close();
|
|
40
|
+
} catch {
|
|
41
|
+
// Best-effort close before temp cleanup.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
await unlink(tempPath);
|
|
46
|
+
} catch {
|
|
47
|
+
// Temp may not exist yet or already renamed.
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|