@kontourai/flow-agents 0.1.2 → 0.2.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/.github/dependabot.yml +23 -0
- package/.github/workflows/release-please.yml +31 -0
- package/.github/workflows/runtime-compat.yml +118 -0
- package/CHANGELOG.md +23 -0
- package/CONTRIBUTING.md +4 -0
- package/README.md +53 -10
- package/build/src/cli/init.js +215 -5
- package/build/src/cli/utterance-check.js +65 -1
- package/build/src/tools/build-universal-bundles.js +268 -0
- package/build/src/tools/filter-installed-packs.js +3 -0
- package/build/src/tools/validate-source-tree.js +5 -1
- package/context/scripts/telemetry/lib/config.sh +5 -1
- package/context/settings/flow-agents-settings.json +7 -0
- package/docs/context-map.md +1 -0
- package/docs/index.md +45 -4
- package/docs/integrations/conformance.md +246 -0
- package/docs/integrations/framework-adapter.md +275 -0
- package/docs/integrations/harness-install.md +213 -0
- package/docs/integrations/index.md +54 -0
- package/docs/north-star.md +2 -2
- package/docs/spec/runtime-hook-surface.md +472 -0
- package/docs/survey-utterance-check.md +211 -94
- package/docs/vision.md +45 -0
- package/evals/acceptance/run.sh +4 -2
- package/evals/acceptance/test_opencode_harness.sh +121 -0
- package/evals/acceptance/test_pi_harness.sh +98 -0
- package/evals/integration/test_bundle_install.sh +226 -1
- package/evals/integration/test_bundle_lifecycle.sh +641 -0
- package/evals/integration/test_utterance_check.sh +291 -44
- package/evals/run.sh +2 -0
- package/evals/static/test_universal_bundles.sh +137 -2
- package/integrations/strands/README.md +256 -0
- package/integrations/strands/example.py +74 -0
- package/integrations/strands/flow_agents_strands/__init__.py +27 -0
- package/integrations/strands/flow_agents_strands/hooks.py +194 -0
- package/integrations/strands/flow_agents_strands/policy.py +348 -0
- package/integrations/strands/flow_agents_strands/steering.py +172 -0
- package/integrations/strands/flow_agents_strands/telemetry.py +238 -0
- package/integrations/strands/pyproject.toml +38 -0
- package/integrations/strands/tests/__init__.py +0 -0
- package/integrations/strands/tests/test_hooks.py +304 -0
- package/integrations/strands/tests/test_policy.py +315 -0
- package/integrations/strands/tests/test_telemetry.py +184 -0
- package/integrations/strands-ts/README.md +224 -0
- package/integrations/strands-ts/bin/conformance-shim.mjs +257 -0
- package/integrations/strands-ts/package.json +53 -0
- package/integrations/strands-ts/src/hooks.ts +208 -0
- package/integrations/strands-ts/src/index.ts +22 -0
- package/integrations/strands-ts/src/policy.ts +345 -0
- package/integrations/strands-ts/src/telemetry.ts +251 -0
- package/integrations/strands-ts/test/test-policy.ts +322 -0
- package/integrations/strands-ts/test/test-telemetry.ts +226 -0
- package/integrations/strands-ts/tsconfig.json +20 -0
- package/package.json +7 -2
- package/packaging/conformance/README.md +142 -0
- package/packaging/conformance/fixtures/config-protection--allow-no-path.json +18 -0
- package/packaging/conformance/fixtures/config-protection--allow-safe-file.json +20 -0
- package/packaging/conformance/fixtures/config-protection--block-biome.json +20 -0
- package/packaging/conformance/fixtures/config-protection--block-eslintrc.json +20 -0
- package/packaging/conformance/fixtures/quality-gate--allow-no-path.json +17 -0
- package/packaging/conformance/fixtures/quality-gate--allow-nonexistent-file.json +19 -0
- package/packaging/conformance/fixtures/stop-goal-fit--allow-clean-cwd.json +17 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-strict-mode.json +23 -0
- package/packaging/conformance/fixtures/stop-goal-fit--warn-active-delivery.json +21 -0
- package/packaging/conformance/fixtures/workflow-steering--allow-no-state.json +16 -0
- package/packaging/conformance/fixtures/workflow-steering--inject-active-state.json +29 -0
- package/packaging/conformance/fixtures/workflow-steering--inject-subagent-steering.json +25 -0
- package/packaging/conformance/package.json +4 -0
- package/packaging/conformance/run-conformance.js +322 -0
- package/packaging/manifest.json +59 -0
- package/schemas/flow-agents-settings.schema.json +48 -0
- package/scripts/README.md +4 -0
- package/scripts/dogfood.js +16 -0
- package/scripts/hooks/opencode-hook-adapter.js +123 -0
- package/scripts/hooks/opencode-telemetry-hook.js +101 -0
- package/scripts/hooks/pi-hook-adapter.js +123 -0
- package/scripts/hooks/pi-telemetry-hook.js +105 -0
- package/scripts/hooks/run-hook.js +8 -0
- package/scripts/hooks/utterance-check.js +124 -22
- package/scripts/telemetry/lib/config.sh +5 -1
- package/src/cli/init.ts +219 -6
- package/src/cli/utterance-check.ts +71 -1
- package/src/tools/build-universal-bundles.ts +266 -0
- package/src/tools/filter-installed-packs.ts +3 -0
- package/src/tools/validate-source-tree.ts +5 -1
package/src/cli/init.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { activateCodexLocal } from "../runtime-adapters.js";
|
|
|
9
9
|
import { main as buildBundles } from "../tools/build-universal-bundles.js";
|
|
10
10
|
import { root } from "../tools/common.js";
|
|
11
11
|
|
|
12
|
-
type Runtime = "base" | "codex" | "claude-code" | "kiro";
|
|
12
|
+
type Runtime = "base" | "codex" | "claude-code" | "kiro" | "opencode" | "pi";
|
|
13
13
|
type TelemetrySink = "local-files" | "local-kontour-console" | "kontour-hosted-console" | "user-hosted-console" | "kontour-cloud" | "hosted-kontour-console";
|
|
14
14
|
|
|
15
15
|
type InitOptions = {
|
|
@@ -30,13 +30,69 @@ const runtimeBundles: Record<Runtime, string> = {
|
|
|
30
30
|
codex: "codex",
|
|
31
31
|
"claude-code": "claude-code",
|
|
32
32
|
kiro: "kiro",
|
|
33
|
+
opencode: "opencode",
|
|
34
|
+
pi: "pi",
|
|
33
35
|
};
|
|
34
36
|
|
|
37
|
+
// Stable marker present in every Flow Agents claude-code hook command.
|
|
38
|
+
// Used by scope-collision detection to identify an existing flow-agents install.
|
|
39
|
+
// Marker must be distinctive to Flow Agents generated settings. Sibling
|
|
40
|
+
// products from the same lineage ship identically named hook scripts such
|
|
41
|
+
// as claude-hook-adapter.js, so script filenames are NOT a safe marker.
|
|
42
|
+
export const COLLISION_MARKER = "Recording Flow Agents telemetry";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check whether a user-level Claude Code settings file already contains
|
|
46
|
+
* Flow Agents hook commands. If it does, print a WARNING explaining that
|
|
47
|
+
* Claude Code merges user-level and project-level settings and runs ALL
|
|
48
|
+
* matching hooks, so having flow-agents in both places causes duplicate
|
|
49
|
+
* hook execution (double telemetry, double policy enforcement).
|
|
50
|
+
*
|
|
51
|
+
* The check does NOT block the install; it is advisory only.
|
|
52
|
+
*
|
|
53
|
+
* @param userSettingsFile Path to inspect (defaults to $HOME/.claude/settings.json;
|
|
54
|
+
* overridable via FLOW_AGENTS_USER_CLAUDE_SETTINGS env var for testability).
|
|
55
|
+
* @returns true if a collision was detected, false otherwise.
|
|
56
|
+
*/
|
|
57
|
+
export function checkScopeCollision(userSettingsFile?: string): boolean {
|
|
58
|
+
const filePath = userSettingsFile
|
|
59
|
+
?? process.env["FLOW_AGENTS_USER_CLAUDE_SETTINGS"]
|
|
60
|
+
?? path.join(os.homedir(), ".claude", "settings.json");
|
|
61
|
+
if (!fs.existsSync(filePath)) return false;
|
|
62
|
+
let text: string;
|
|
63
|
+
try {
|
|
64
|
+
text = fs.readFileSync(filePath, "utf8");
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (!text.includes(COLLISION_MARKER)) return false;
|
|
69
|
+
console.warn(
|
|
70
|
+
`\nWARNING: Flow Agents scope collision detected.\n` +
|
|
71
|
+
` ${filePath}\n` +
|
|
72
|
+
`already contains Flow Agents hook commands (marker: ${COLLISION_MARKER}).\n` +
|
|
73
|
+
`\n` +
|
|
74
|
+
`Claude Code merges user-level (~/.claude/settings.json) and project-level\n` +
|
|
75
|
+
`(.claude/settings.json) settings, then runs ALL matching hooks from both files.\n` +
|
|
76
|
+
`Installing Flow Agents at the project level while it is also present at the\n` +
|
|
77
|
+
`user level will cause duplicate hook execution: telemetry events are recorded\n` +
|
|
78
|
+
`twice and policy hooks (workflow-steering, config-protection, quality-gate,\n` +
|
|
79
|
+
`stop-goal-fit) run twice per event.\n` +
|
|
80
|
+
`\n` +
|
|
81
|
+
`To resolve:\n` +
|
|
82
|
+
` - Remove the hooks section from ${filePath} and rely solely on the\n` +
|
|
83
|
+
` project-level .claude/settings.json installed by flow-agents init, OR\n` +
|
|
84
|
+
` - Remove the project-level install and keep only the user-level one.\n` +
|
|
85
|
+
`\n` +
|
|
86
|
+
`The install will continue; resolve the collision before running Claude Code.\n`
|
|
87
|
+
);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
35
91
|
function usage(): void {
|
|
36
92
|
console.error(`usage: flow-agents init [options]
|
|
37
93
|
|
|
38
94
|
Options:
|
|
39
|
-
--runtime base|codex|claude-code|kiro
|
|
95
|
+
--runtime base|codex|claude-code|kiro|opencode|pi
|
|
40
96
|
--dest PATH
|
|
41
97
|
--telemetry-sink local-files|local-kontour-console|kontour-hosted-console|user-hosted-console
|
|
42
98
|
--console-url URL
|
|
@@ -52,8 +108,8 @@ Options:
|
|
|
52
108
|
function normalizeRuntime(value: string | undefined): Runtime | undefined {
|
|
53
109
|
if (!value) return undefined;
|
|
54
110
|
if (value === "claude") return "claude-code";
|
|
55
|
-
if (value === "base" || value === "codex" || value === "claude-code" || value === "kiro") return value;
|
|
56
|
-
throw new Error(`unknown runtime '${value}'; expected base, codex, claude-code, or
|
|
111
|
+
if (value === "base" || value === "codex" || value === "claude-code" || value === "kiro" || value === "opencode" || value === "pi") return value;
|
|
112
|
+
throw new Error(`unknown runtime '${value}'; expected base, codex, claude-code, kiro, opencode, or pi`);
|
|
57
113
|
}
|
|
58
114
|
|
|
59
115
|
function normalizeTelemetrySink(value: string): TelemetrySink {
|
|
@@ -86,7 +142,7 @@ async function questionHidden(prompt: string): Promise<string> {
|
|
|
86
142
|
let value = "";
|
|
87
143
|
const onData = (buffer: Buffer) => {
|
|
88
144
|
const text = buffer.toString("utf8");
|
|
89
|
-
if (text === "
|
|
145
|
+
if (text === "") {
|
|
90
146
|
stdout.write("\n");
|
|
91
147
|
process.exit(130);
|
|
92
148
|
}
|
|
@@ -97,7 +153,7 @@ async function questionHidden(prompt: string): Promise<string> {
|
|
|
97
153
|
resolve(value);
|
|
98
154
|
return;
|
|
99
155
|
}
|
|
100
|
-
if (text === "
|
|
156
|
+
if (text === "") {
|
|
101
157
|
value = value.slice(0, -1);
|
|
102
158
|
return;
|
|
103
159
|
}
|
|
@@ -235,6 +291,18 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
|
235
291
|
const headless = argv.includes("--yes") || argv.includes("--headless") || !process.stdin.isTTY;
|
|
236
292
|
try {
|
|
237
293
|
const options = headless ? headlessOptions(argv) : await interactiveOptions(argv);
|
|
294
|
+
// Scope-collision check for claude-code: Claude Code merges user-level
|
|
295
|
+
// (~/.claude/settings.json) and project-level (.claude/settings.json) settings
|
|
296
|
+
// and runs ALL matching hooks from both files. If a user-level settings file
|
|
297
|
+
// already contains flow-agents hooks, installing at the project level will
|
|
298
|
+
// cause duplicate hook execution. We warn but do not block.
|
|
299
|
+
//
|
|
300
|
+
// Codex note: Codex hooks live in .codex/hooks.json (project-level only).
|
|
301
|
+
// There is no well-known user-level codex hooks file in our install paths,
|
|
302
|
+
// so no collision check is needed for codex.
|
|
303
|
+
if (options.runtime === "claude-code") {
|
|
304
|
+
checkScopeCollision();
|
|
305
|
+
}
|
|
238
306
|
const bundle = ensureBundle(options.runtime);
|
|
239
307
|
const installed = installBundle(bundle, options);
|
|
240
308
|
if (installed !== 0) return installed;
|
|
@@ -245,4 +313,149 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
|
|
|
245
313
|
}
|
|
246
314
|
}
|
|
247
315
|
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// Dogfood subcommand
|
|
318
|
+
//
|
|
319
|
+
// `flow-agents dogfood --runtime claude-code [--dest PATH]`
|
|
320
|
+
//
|
|
321
|
+
// Writes only the hook-wiring artifacts for the specified runtime into the
|
|
322
|
+
// target directory (default: cwd). Unlike a full install, dogfood:
|
|
323
|
+
// - Does NOT rsync the full bundle (no agents/skills duplication).
|
|
324
|
+
// - Reads the generated settings/config from dist/<runtime>/ so the output
|
|
325
|
+
// cannot drift from what the bundle generates (DRY guarantee).
|
|
326
|
+
// - For claude-code: OMITS permissions.defaultMode and
|
|
327
|
+
// skipDangerousModePermissionPrompt (permissive defaults are for installed
|
|
328
|
+
// workspaces, not source repos).
|
|
329
|
+
// - Runs the same scope-collision warning as init.
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
function dogfoodUsage(): void {
|
|
333
|
+
console.error(`usage: flow-agents dogfood [options]
|
|
334
|
+
|
|
335
|
+
Options:
|
|
336
|
+
--runtime claude-code|codex|opencode|pi (required)
|
|
337
|
+
--dest PATH (default: cwd)
|
|
338
|
+
--yes, --headless
|
|
339
|
+
`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
type DogfoodRuntime = "claude-code" | "codex" | "opencode" | "pi";
|
|
343
|
+
|
|
344
|
+
function normalizeDogfoodRuntime(value: string | undefined): DogfoodRuntime | undefined {
|
|
345
|
+
if (!value) return undefined;
|
|
346
|
+
if (value === "claude" || value === "claude-code") return "claude-code";
|
|
347
|
+
if (value === "codex" || value === "opencode" || value === "pi") return value;
|
|
348
|
+
throw new Error(`dogfood: unsupported runtime '${value}'; expected claude-code, codex, opencode, or pi`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Write the claude-code hook-wiring artifacts into dest.
|
|
353
|
+
* Reads dist/claude-code/.claude/settings.json (generated by build-bundles),
|
|
354
|
+
* strips the permissive-mode permission keys (defaultMode, skipDangerousModePermissionPrompt),
|
|
355
|
+
* and writes .claude/settings.json to dest.
|
|
356
|
+
*/
|
|
357
|
+
function dogfoodClaudeCode(bundleRoot: string, dest: string): void {
|
|
358
|
+
const sourcePath = path.join(bundleRoot, ".claude", "settings.json");
|
|
359
|
+
if (!fs.existsSync(sourcePath)) throw new Error(`dogfood: bundle settings missing: ${sourcePath}`);
|
|
360
|
+
const settings = JSON.parse(fs.readFileSync(sourcePath, "utf8")) as Record<string, unknown>;
|
|
361
|
+
// Remove permissive defaults that are only appropriate for installed workspaces.
|
|
362
|
+
// These keys must not be present in the source repo's .claude/settings.json.
|
|
363
|
+
delete settings["permissions"];
|
|
364
|
+
delete settings["skipDangerousModePermissionPrompt"];
|
|
365
|
+
const outDir = path.join(dest, ".claude");
|
|
366
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
367
|
+
fs.writeFileSync(path.join(outDir, "settings.json"), `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Write the codex hook-wiring artifacts into dest.
|
|
372
|
+
* Reads dist/codex/.codex/hooks.json and writes .codex/hooks.json to dest.
|
|
373
|
+
* The monolithic .codex/config.toml is not written here because it contains
|
|
374
|
+
* workspace settings (approvals_reviewer, features) that would override the
|
|
375
|
+
* developer's existing codex configuration. Only the hooks file is written.
|
|
376
|
+
*/
|
|
377
|
+
function dogfoodCodex(bundleRoot: string, dest: string): void {
|
|
378
|
+
const sourcePath = path.join(bundleRoot, ".codex", "hooks.json");
|
|
379
|
+
if (!fs.existsSync(sourcePath)) throw new Error(`dogfood: bundle hooks.json missing: ${sourcePath}`);
|
|
380
|
+
const hooks = fs.readFileSync(sourcePath, "utf8");
|
|
381
|
+
const outDir = path.join(dest, ".codex");
|
|
382
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
383
|
+
fs.writeFileSync(path.join(outDir, "hooks.json"), hooks, "utf8");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Write the opencode hook-wiring artifacts into dest.
|
|
388
|
+
* Reads dist/opencode/.opencode/plugins/flow-agents.js and opencode.json,
|
|
389
|
+
* and writes them into dest. These are the minimal hook-wiring files; the
|
|
390
|
+
* full skill/agent tree is not copied.
|
|
391
|
+
*/
|
|
392
|
+
function dogfoodOpencode(bundleRoot: string, dest: string): void {
|
|
393
|
+
const pluginSource = path.join(bundleRoot, ".opencode", "plugins", "flow-agents.js");
|
|
394
|
+
const configSource = path.join(bundleRoot, "opencode.json");
|
|
395
|
+
if (!fs.existsSync(pluginSource)) throw new Error(`dogfood: bundle plugin missing: ${pluginSource}`);
|
|
396
|
+
const pluginDir = path.join(dest, ".opencode", "plugins");
|
|
397
|
+
fs.mkdirSync(pluginDir, { recursive: true });
|
|
398
|
+
fs.copyFileSync(pluginSource, path.join(pluginDir, "flow-agents.js"));
|
|
399
|
+
// Write opencode.json only if it does not already exist to avoid clobbering
|
|
400
|
+
// any workspace-specific opencode configuration.
|
|
401
|
+
const destConfig = path.join(dest, "opencode.json");
|
|
402
|
+
if (!fs.existsSync(destConfig) && fs.existsSync(configSource)) {
|
|
403
|
+
fs.copyFileSync(configSource, destConfig);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Write the pi hook-wiring artifacts into dest.
|
|
409
|
+
* Reads dist/pi/.pi/extensions/flow-agents.ts and writes it to dest.
|
|
410
|
+
* The extension is the only hook-wiring file needed for pi.
|
|
411
|
+
*/
|
|
412
|
+
function dogfoodPi(bundleRoot: string, dest: string): void {
|
|
413
|
+
const extSource = path.join(bundleRoot, ".pi", "extensions", "flow-agents.ts");
|
|
414
|
+
if (!fs.existsSync(extSource)) throw new Error(`dogfood: bundle extension missing: ${extSource}`);
|
|
415
|
+
const extDir = path.join(dest, ".pi", "extensions");
|
|
416
|
+
fs.mkdirSync(extDir, { recursive: true });
|
|
417
|
+
fs.copyFileSync(extSource, path.join(extDir, "flow-agents.ts"));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export async function mainDogfood(argv = process.argv.slice(2)): Promise<number> {
|
|
421
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
422
|
+
dogfoodUsage();
|
|
423
|
+
return 0;
|
|
424
|
+
}
|
|
425
|
+
const args = parseArgs(argv);
|
|
426
|
+
try {
|
|
427
|
+
const runtimeRaw = flagString(args.flags, "runtime");
|
|
428
|
+
const runtime = normalizeDogfoodRuntime(runtimeRaw);
|
|
429
|
+
if (!runtime) {
|
|
430
|
+
console.error("dogfood: --runtime is required (claude-code, codex, opencode, or pi)");
|
|
431
|
+
dogfoodUsage();
|
|
432
|
+
return 2;
|
|
433
|
+
}
|
|
434
|
+
const dest = path.resolve(flagString(args.flags, "dest") ?? process.cwd());
|
|
435
|
+
|
|
436
|
+
// Ensure the bundle for the requested runtime is built.
|
|
437
|
+
const bundleRuntime: Runtime = runtime as Runtime;
|
|
438
|
+
const bundleRoot = ensureBundle(bundleRuntime);
|
|
439
|
+
|
|
440
|
+
// Scope-collision check: warn if user-level claude settings already has flow-agents.
|
|
441
|
+
// Codex: no user-level hooks file in our install paths — skip with note above.
|
|
442
|
+
if (runtime === "claude-code") {
|
|
443
|
+
checkScopeCollision();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Write only the hook-wiring artifacts, not the full bundle.
|
|
447
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
448
|
+
if (runtime === "claude-code") dogfoodClaudeCode(bundleRoot, dest);
|
|
449
|
+
else if (runtime === "codex") dogfoodCodex(bundleRoot, dest);
|
|
450
|
+
else if (runtime === "opencode") dogfoodOpencode(bundleRoot, dest);
|
|
451
|
+
else if (runtime === "pi") dogfoodPi(bundleRoot, dest);
|
|
452
|
+
|
|
453
|
+
console.log(`Flow Agents dogfood hooks wired for ${runtime} in ${dest}`);
|
|
454
|
+
return 0;
|
|
455
|
+
} catch (error) {
|
|
456
|
+
console.error(`flow-agents dogfood: ${(error as Error).message}`);
|
|
457
|
+
return 2;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
248
461
|
if (import.meta.url === `file://${process.argv[1]}`) process.exit(await main());
|
|
@@ -60,6 +60,10 @@ interface SurveyMod {
|
|
|
60
60
|
referenceUtteranceExtractor: SurveyExtractor;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
interface AnthropicSurveyMod {
|
|
64
|
+
createAnthropicUtteranceExtractor: (options?: { model?: string; apiKey?: string }) => SurveyExtractor;
|
|
65
|
+
}
|
|
66
|
+
|
|
63
67
|
// ---------------------------------------------------------------------------
|
|
64
68
|
// Helpers
|
|
65
69
|
// ---------------------------------------------------------------------------
|
|
@@ -76,6 +80,9 @@ function usage(): void {
|
|
|
76
80
|
" --utterance TEXT Utterance text to check (required unless --not-configured).",
|
|
77
81
|
" --bundle-path FILE Trust bundle JSON file. Omit for an empty bundle (all unsupported).",
|
|
78
82
|
" --agent-id ID Agent identifier for provenance (default: flow-agents-utterance-check).",
|
|
83
|
+
" --extractor NAME Extractor to use: 'reference' (default, pattern-based) or 'anthropic'",
|
|
84
|
+
" (model-backed, requires ANTHROPIC_API_KEY and @kontourai/survey/anthropic).",
|
|
85
|
+
" --model MODEL Model for the anthropic extractor (e.g. claude-haiku-4-5).",
|
|
79
86
|
" --not-configured Skip survey call; output not_configured without error.",
|
|
80
87
|
" --strict Exit non-zero when any badge is disputed, rejected, or unsupported.",
|
|
81
88
|
" --help Show this help.",
|
|
@@ -116,6 +123,45 @@ async function loadSurvey(): Promise<SurveyMod | undefined> {
|
|
|
116
123
|
}
|
|
117
124
|
}
|
|
118
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Dynamically import @kontourai/survey/anthropic and create the Anthropic extractor.
|
|
128
|
+
* Fails open with a clear not_configured message when the key or peer dep is missing.
|
|
129
|
+
*/
|
|
130
|
+
async function loadAnthropicExtractor(model?: string): Promise<SurveyExtractor | { notConfigured: true; reason: string }> {
|
|
131
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
132
|
+
if (!apiKey) {
|
|
133
|
+
return {
|
|
134
|
+
notConfigured: true,
|
|
135
|
+
reason:
|
|
136
|
+
"anthropic extractor requires ANTHROPIC_API_KEY to be set. " +
|
|
137
|
+
"Set the environment variable or switch extractor to 'reference'.",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const pkg = "@kontourai/survey/anthropic";
|
|
142
|
+
const mod = await (Function("m", "return import(m)")(pkg) as Promise<unknown>) as AnthropicSurveyMod;
|
|
143
|
+
if (typeof mod.createAnthropicUtteranceExtractor !== "function") {
|
|
144
|
+
return {
|
|
145
|
+
notConfigured: true,
|
|
146
|
+
reason:
|
|
147
|
+
"@kontourai/survey/anthropic does not export createAnthropicUtteranceExtractor. " +
|
|
148
|
+
"Update @kontourai/survey to a version that supports the anthropic extractor.",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const opts: { model?: string; apiKey?: string } = { apiKey };
|
|
152
|
+
if (model) opts.model = model;
|
|
153
|
+
return mod.createAnthropicUtteranceExtractor(opts);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
156
|
+
return {
|
|
157
|
+
notConfigured: true,
|
|
158
|
+
reason:
|
|
159
|
+
`@kontourai/survey/anthropic is not available: ${msg}. ` +
|
|
160
|
+
"Install @kontourai/survey with the anthropic subpath export, or switch extractor to 'reference'.",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
119
165
|
// ---------------------------------------------------------------------------
|
|
120
166
|
// Core check logic
|
|
121
167
|
// ---------------------------------------------------------------------------
|
|
@@ -131,6 +177,8 @@ async function runCheck(argv: string[]): Promise<number> {
|
|
|
131
177
|
const agentId = flagString(flags, "agent-id") ?? "flow-agents-utterance-check";
|
|
132
178
|
const notConfigured = flagBool(flags, "not-configured");
|
|
133
179
|
const strict = flagBool(flags, "strict");
|
|
180
|
+
const extractorName = flagString(flags, "extractor") ?? "reference";
|
|
181
|
+
const model = flagString(flags, "model");
|
|
134
182
|
|
|
135
183
|
if (notConfigured) {
|
|
136
184
|
const report: UtteranceReport = {
|
|
@@ -181,9 +229,31 @@ async function runCheck(argv: string[]): Promise<number> {
|
|
|
181
229
|
|
|
182
230
|
const { surveyAgentUtterance, referenceUtteranceExtractor } = survey;
|
|
183
231
|
|
|
232
|
+
// Resolve which extractor to use.
|
|
233
|
+
let extractor: SurveyExtractor;
|
|
234
|
+
if (extractorName === "anthropic") {
|
|
235
|
+
const anthropicResult = await loadAnthropicExtractor(model);
|
|
236
|
+
if ("notConfigured" in anthropicResult) {
|
|
237
|
+
// Fail open: emit not_configured with a clear reason rather than erroring.
|
|
238
|
+
const report: UtteranceReport = {
|
|
239
|
+
status: "not_configured",
|
|
240
|
+
agent_id: agentId,
|
|
241
|
+
utterance_excerpt: excerptText(utterance),
|
|
242
|
+
statements: [],
|
|
243
|
+
summary: anthropicResult.reason,
|
|
244
|
+
};
|
|
245
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
246
|
+
process.stderr.write(`[UtteranceCheck] not_configured: ${anthropicResult.reason}\n`);
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
extractor = anthropicResult;
|
|
250
|
+
} else {
|
|
251
|
+
extractor = referenceUtteranceExtractor;
|
|
252
|
+
}
|
|
253
|
+
|
|
184
254
|
let trustReport: SurveyTrustReport;
|
|
185
255
|
try {
|
|
186
|
-
trustReport = await surveyAgentUtterance(utterance,
|
|
256
|
+
trustReport = await surveyAgentUtterance(utterance, extractor, {
|
|
187
257
|
bundle,
|
|
188
258
|
agentId,
|
|
189
259
|
});
|
|
@@ -332,6 +332,268 @@ function buildCodex(agents: Agent[]): void {
|
|
|
332
332
|
writeText(path.join(bundle, "install.sh"), installScript("Codex", "/path/to/workspace"));
|
|
333
333
|
fs.chmodSync(path.join(bundle, "install.sh"), 0o755);
|
|
334
334
|
}
|
|
335
|
+
function exportOpencodeAgent(spec: Agent): string {
|
|
336
|
+
// Determine agent mode: orchestrator-like agents -> primary, others -> subagent
|
|
337
|
+
const primaryAgents = new Set(["dev"]);
|
|
338
|
+
const mode = primaryAgents.has(spec.name) ? "primary" : "subagent";
|
|
339
|
+
const prompt = appendExportNote(sanitizeText(spec.prompt, "opencode", "<bundle-root>"), "Kiro hook wiring and JSON-only runtime fields were omitted. If this agent mentions Kiro-specific scheduler or hook behavior, treat that as optional operational guidance rather than a hard dependency.");
|
|
340
|
+
const lines: string[] = ["---"];
|
|
341
|
+
lines.push(`description: ${String(spec.description ?? "").trim()}`);
|
|
342
|
+
lines.push(`mode: ${mode}`);
|
|
343
|
+
lines.push(`model: ${mapped("claude_model_map", spec.model)}`);
|
|
344
|
+
lines.push("---");
|
|
345
|
+
lines.push("");
|
|
346
|
+
lines.push(prompt);
|
|
347
|
+
return lines.join("\n");
|
|
348
|
+
}
|
|
349
|
+
function exportOpencodePlugin(): string {
|
|
350
|
+
// Generate the Flow Agents opencode plugin.
|
|
351
|
+
// opencode plugins are auto-loaded from .opencode/plugins/*.js at startup.
|
|
352
|
+
//
|
|
353
|
+
// NOTE: opencode has no direct user-prompt-submit hook. For prompt-submit
|
|
354
|
+
// workflow steering, we wire the steering command behind session.created
|
|
355
|
+
// (for session-start steering context) and tool.execute.before (for
|
|
356
|
+
// policy). This is the closest reasonable approximation — documented here
|
|
357
|
+
// as an honest gap matching the codex live-hook-influence caveat pattern.
|
|
358
|
+
return `/**
|
|
359
|
+
* Flow Agents opencode plugin.
|
|
360
|
+
*
|
|
361
|
+
* Auto-loaded from .opencode/plugins/flow-agents.js at opencode startup.
|
|
362
|
+
* Delegates policy and telemetry decisions to shared scripts in scripts/hooks/
|
|
363
|
+
* using the same payload contract as the claude/codex adapters.
|
|
364
|
+
*
|
|
365
|
+
* EVENT MAPPING NOTE: opencode has no direct user-prompt-submit hook.
|
|
366
|
+
* Workflow steering (workflow-steering.js) is wired to session.created
|
|
367
|
+
* (session-start context) and tool.execute.before (per-tool policy).
|
|
368
|
+
* This approximates the UserPromptSubmit behavior in other runtimes but
|
|
369
|
+
* cannot intercept mid-session user messages before they are processed.
|
|
370
|
+
* This is an accepted gap documented here analogously to the codex
|
|
371
|
+
* live-hook-influence caveat.
|
|
372
|
+
*/
|
|
373
|
+
|
|
374
|
+
import { spawnSync } from 'node:child_process';
|
|
375
|
+
import { join, basename } from 'node:path';
|
|
376
|
+
|
|
377
|
+
// opencode runs plugins inside its own compiled (Bun-based) binary, so
|
|
378
|
+
// process.execPath points at opencode itself — spawning it with a script
|
|
379
|
+
// path silently does nothing (caught by live acceptance smoke 2026-06-11).
|
|
380
|
+
// Resolve a real node binary instead; fall back to PATH lookup.
|
|
381
|
+
const NODE_BIN = basename(process.execPath).startsWith('node') ? process.execPath : 'node';
|
|
382
|
+
|
|
383
|
+
export const FlowAgentsPlugin = async ({ project, client, $, directory, worktree }) => {
|
|
384
|
+
const root = directory || process.cwd();
|
|
385
|
+
|
|
386
|
+
// The hook scripts read the event payload from stdin; an empty stdin makes
|
|
387
|
+
// the telemetry pipeline silently skip the emit (fail-open), so every spawn
|
|
388
|
+
// must pass a payload (caught by live acceptance smoke 2026-06-11).
|
|
389
|
+
function hookPayload(eventName, detail) {
|
|
390
|
+
return JSON.stringify({ hook_event_name: eventName, cwd: root, ...(detail || {}) });
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function runAdapter(adapterScript, eventName, detail, ...args) {
|
|
394
|
+
const adapterPath = join(root, 'scripts', 'hooks', adapterScript);
|
|
395
|
+
const result = spawnSync(NODE_BIN, [adapterPath, eventName, ...args], {
|
|
396
|
+
input: hookPayload(eventName, detail),
|
|
397
|
+
encoding: 'utf8',
|
|
398
|
+
cwd: root,
|
|
399
|
+
env: { ...process.env, FLOW_AGENTS_HOOK_RUNTIME: 'opencode' },
|
|
400
|
+
timeout: 30000,
|
|
401
|
+
});
|
|
402
|
+
try {
|
|
403
|
+
return JSON.parse(result.stdout || '{}');
|
|
404
|
+
} catch {
|
|
405
|
+
return { allow: true };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function runTelemetry(eventName, detail) {
|
|
410
|
+
const telemetryPath = join(root, 'scripts', 'hooks', 'opencode-telemetry-hook.js');
|
|
411
|
+
spawnSync(NODE_BIN, [telemetryPath, eventName, 'dev'], {
|
|
412
|
+
input: hookPayload(eventName, detail),
|
|
413
|
+
encoding: 'utf8',
|
|
414
|
+
cwd: root,
|
|
415
|
+
env: { ...process.env, FLOW_AGENTS_TELEMETRY_RUNTIME: 'opencode' },
|
|
416
|
+
timeout: 10000,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
'session.created': async (_input, _output) => {
|
|
422
|
+
runTelemetry('session.created');
|
|
423
|
+
// Wire workflow steering on session start for context injection
|
|
424
|
+
runAdapter('opencode-hook-adapter.js', 'session.created', null, 'workflow-steering', 'workflow-steering.js', 'default');
|
|
425
|
+
},
|
|
426
|
+
'tool.execute.before': async (input, output) => {
|
|
427
|
+
const detail = { tool: input && input.tool, args: output && output.args };
|
|
428
|
+
runTelemetry('tool.execute.before', detail);
|
|
429
|
+
const policyResult = runAdapter('opencode-hook-adapter.js', 'tool.execute.before', detail, 'config-protection', 'config-protection.js', 'default');
|
|
430
|
+
if (policyResult && policyResult.allow === false) {
|
|
431
|
+
throw new Error(policyResult.reason || 'Blocked by Flow Agents hook policy.');
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
'tool.execute.after': async (input, output) => {
|
|
435
|
+
const detail = { tool: input && input.tool };
|
|
436
|
+
runTelemetry('tool.execute.after', detail);
|
|
437
|
+
runAdapter('opencode-hook-adapter.js', 'tool.execute.after', detail, 'quality-gate', 'quality-gate.js', 'default');
|
|
438
|
+
},
|
|
439
|
+
'session.idle': async (_input, _output) => {
|
|
440
|
+
runTelemetry('session.idle');
|
|
441
|
+
runAdapter('opencode-hook-adapter.js', 'session.idle', null, 'stop-goal-fit', 'stop-goal-fit.js', 'default');
|
|
442
|
+
},
|
|
443
|
+
'session.error': async (_input, _output) => {
|
|
444
|
+
runTelemetry('session.error');
|
|
445
|
+
},
|
|
446
|
+
'session.compacted': async (_input, _output) => {
|
|
447
|
+
runTelemetry('session.compacted');
|
|
448
|
+
},
|
|
449
|
+
'permission.asked': async (_input, _output) => {
|
|
450
|
+
runTelemetry('permission.asked');
|
|
451
|
+
},
|
|
452
|
+
'file.edited': async (_input, _output) => {
|
|
453
|
+
runTelemetry('file.edited');
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
};
|
|
457
|
+
`;
|
|
458
|
+
}
|
|
459
|
+
function exportOpencodeConfig(): string {
|
|
460
|
+
// opencode's config schema requires `instructions` to be an ARRAY of
|
|
461
|
+
// instruction file paths/globs (a bare string fails validation and aborts
|
|
462
|
+
// startup). AGENTS.md is loaded natively by opencode, so the config stays
|
|
463
|
+
// minimal rather than double-including it.
|
|
464
|
+
return `${JSON.stringify({
|
|
465
|
+
$schema: "https://opencode.ai/config.json",
|
|
466
|
+
}, null, 2)}\n`;
|
|
467
|
+
}
|
|
468
|
+
function buildOpencode(agents: Agent[]): void {
|
|
469
|
+
const bundle = path.join(dist, "opencode");
|
|
470
|
+
resetDir(bundle);
|
|
471
|
+
copySharedContent(bundle, "opencode", "<bundle-root>");
|
|
472
|
+
writeText(path.join(bundle, manifest.opencode.task_dir, ".gitkeep"), "");
|
|
473
|
+
for (const spec of agents) {
|
|
474
|
+
writeText(path.join(bundle, ".opencode/agents", `${spec.name}.md`), exportOpencodeAgent(spec));
|
|
475
|
+
}
|
|
476
|
+
for (const skill of fs.readdirSync(path.join(root, "skills"))) {
|
|
477
|
+
const skillPath = path.join(root, "skills", skill, "SKILL.md");
|
|
478
|
+
if (fs.existsSync(skillPath)) writeText(path.join(bundle, ".opencode/skills", skill, "SKILL.md"), sanitizeText(readText(skillPath), "opencode", "<bundle-root>"));
|
|
479
|
+
}
|
|
480
|
+
writeText(path.join(bundle, ".opencode/plugins/flow-agents.js"), exportOpencodePlugin());
|
|
481
|
+
writeText(path.join(bundle, "opencode.json"), exportOpencodeConfig());
|
|
482
|
+
writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("opencode", agents, manifest.opencode.task_dir));
|
|
483
|
+
writeText(path.join(bundle, "README.md"), exportTargetReadme("opencode", "bash install.sh /path/to/workspace"));
|
|
484
|
+
writeText(path.join(bundle, "install.sh"), installScript("opencode", "/path/to/workspace"));
|
|
485
|
+
fs.chmodSync(path.join(bundle, "install.sh"), 0o755);
|
|
486
|
+
}
|
|
487
|
+
function exportPiExtension(): string {
|
|
488
|
+
// Generate the Flow Agents pi extension.
|
|
489
|
+
// pi extensions are auto-discovered from .pi/extensions/*.ts (needs project trust).
|
|
490
|
+
// pi has no named-subagent registry; agents are not exported. The extension
|
|
491
|
+
// provides workflow steering (via before_agent_start context injection),
|
|
492
|
+
// tool-call policy (via tool_call event), and telemetry delegation to shared scripts.
|
|
493
|
+
return `/**
|
|
494
|
+
* Flow Agents pi extension.
|
|
495
|
+
*
|
|
496
|
+
* Auto-discovered from .pi/extensions/flow-agents.ts at startup (needs project trust).
|
|
497
|
+
* Delegates policy and telemetry to shared scripts/hooks/ using spawnSync,
|
|
498
|
+
* mirroring the payload contract used by the claude/codex adapters.
|
|
499
|
+
*
|
|
500
|
+
* NOTE: pi has no named-subagent registry. Agents are not exported for pi.
|
|
501
|
+
* Rely on AGENTS.md + skills + this extension for workflow guidance.
|
|
502
|
+
*/
|
|
503
|
+
|
|
504
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
505
|
+
import { spawnSync } from "node:child_process";
|
|
506
|
+
import { join, basename } from "node:path";
|
|
507
|
+
|
|
508
|
+
// pi may run extensions under a non-node runtime (Bun), where process.execPath
|
|
509
|
+
// is not a node binary and spawning it with a script path silently fails.
|
|
510
|
+
// Same failure class the opencode live smoke caught on 2026-06-11.
|
|
511
|
+
const NODE_BIN = basename(process.execPath).startsWith("node") ? process.execPath : "node";
|
|
512
|
+
|
|
513
|
+
export default function (pi: ExtensionAPI) {
|
|
514
|
+
const root = process.cwd();
|
|
515
|
+
|
|
516
|
+
function runAdapter(adapterScript: string, eventName: string, hookId: string, relScript: string): { allow: boolean; context?: string; reason?: string } {
|
|
517
|
+
const adapterPath = join(root, "scripts", "hooks", adapterScript);
|
|
518
|
+
const payload = JSON.stringify({ hook_event_name: eventName, cwd: root });
|
|
519
|
+
const result = spawnSync(NODE_BIN, [adapterPath, eventName, hookId, relScript, "default"], {
|
|
520
|
+
input: payload,
|
|
521
|
+
encoding: "utf8",
|
|
522
|
+
cwd: root,
|
|
523
|
+
env: { ...process.env, FLOW_AGENTS_HOOK_RUNTIME: "pi" },
|
|
524
|
+
timeout: 30000,
|
|
525
|
+
});
|
|
526
|
+
try {
|
|
527
|
+
return JSON.parse(result.stdout || "{}") as { allow: boolean; context?: string; reason?: string };
|
|
528
|
+
} catch {
|
|
529
|
+
return { allow: true };
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function runTelemetry(eventName: string): void {
|
|
534
|
+
const telemetryPath = join(root, "scripts", "hooks", "pi-telemetry-hook.js");
|
|
535
|
+
const payload = JSON.stringify({ hook_event_name: eventName, cwd: root });
|
|
536
|
+
spawnSync(NODE_BIN, [telemetryPath, eventName, "dev"], {
|
|
537
|
+
input: payload,
|
|
538
|
+
encoding: "utf8",
|
|
539
|
+
cwd: root,
|
|
540
|
+
env: { ...process.env, FLOW_AGENTS_TELEMETRY_RUNTIME: "pi" },
|
|
541
|
+
timeout: 10000,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
pi.on("session_start", async (_event, _ctx) => {
|
|
546
|
+
runTelemetry("session_start");
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
pi.on("before_agent_start", async (event, _ctx) => {
|
|
550
|
+
runTelemetry("before_agent_start");
|
|
551
|
+
// Inject workflow steering context at agent start
|
|
552
|
+
const result = runAdapter("pi-hook-adapter.js", "before_agent_start", "workflow-steering", "workflow-steering.js");
|
|
553
|
+
if (result.context) {
|
|
554
|
+
return {
|
|
555
|
+
systemPrompt: event.systemPrompt + "\\n\\n" + result.context,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
pi.on("tool_call", async (event, _ctx) => {
|
|
561
|
+
runTelemetry("tool_call");
|
|
562
|
+
const result = runAdapter("pi-hook-adapter.js", "tool_call", "config-protection", "config-protection.js");
|
|
563
|
+
if (result && result.allow === false) {
|
|
564
|
+
return { block: true, reason: result.reason || "Blocked by Flow Agents hook policy." };
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
pi.on("tool_result", async (_event, _ctx) => {
|
|
569
|
+
runTelemetry("tool_result");
|
|
570
|
+
runAdapter("pi-hook-adapter.js", "tool_result", "quality-gate", "quality-gate.js");
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
pi.on("session_shutdown", async (_event, _ctx) => {
|
|
574
|
+
runTelemetry("session_shutdown");
|
|
575
|
+
runAdapter("pi-hook-adapter.js", "session_shutdown", "stop-goal-fit", "stop-goal-fit.js");
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
`;
|
|
579
|
+
}
|
|
580
|
+
function buildPi(agents: Agent[]): void {
|
|
581
|
+
const bundle = path.join(dist, "pi");
|
|
582
|
+
resetDir(bundle);
|
|
583
|
+
copySharedContent(bundle, "pi", "<bundle-root>");
|
|
584
|
+
writeText(path.join(bundle, manifest.pi.task_dir, ".gitkeep"), "");
|
|
585
|
+
// pi has no named-subagent registry; agents are left canonical/unexported.
|
|
586
|
+
// Skills are exported to .pi/skills/ (direct .md files supported in that dir).
|
|
587
|
+
for (const skill of fs.readdirSync(path.join(root, "skills"))) {
|
|
588
|
+
const skillPath = path.join(root, "skills", skill, "SKILL.md");
|
|
589
|
+
if (fs.existsSync(skillPath)) writeText(path.join(bundle, ".pi/skills", skill, "SKILL.md"), sanitizeText(readText(skillPath), "pi", "<bundle-root>"));
|
|
590
|
+
}
|
|
591
|
+
writeText(path.join(bundle, ".pi/extensions/flow-agents.ts"), exportPiExtension());
|
|
592
|
+
writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("pi", agents, manifest.pi.task_dir));
|
|
593
|
+
writeText(path.join(bundle, "README.md"), exportTargetReadme("pi", "bash install.sh /path/to/workspace"));
|
|
594
|
+
writeText(path.join(bundle, "install.sh"), installScript("pi", "/path/to/workspace"));
|
|
595
|
+
fs.chmodSync(path.join(bundle, "install.sh"), 0o755);
|
|
596
|
+
}
|
|
335
597
|
function buildCatalog(agents: Agent[]): Record<string, unknown> {
|
|
336
598
|
const kitsCatalog = path.join(root, "kits/catalog.json");
|
|
337
599
|
return {
|
|
@@ -350,6 +612,8 @@ export function main(): number {
|
|
|
350
612
|
buildKiro(agents);
|
|
351
613
|
buildClaudeCode(agents);
|
|
352
614
|
buildCodex(agents);
|
|
615
|
+
buildOpencode(agents);
|
|
616
|
+
buildPi(agents);
|
|
353
617
|
writeText(path.join(dist, "catalog.json"), `${JSON.stringify(buildCatalog(agents), null, 2)}\n`);
|
|
354
618
|
writeText(path.join(dist, "README.md"), "# Universal Bundles\n\nRun `npm run build:bundles` from the repo root to regenerate these bundles.\n");
|
|
355
619
|
console.log("Built bundles:");
|
|
@@ -357,6 +621,8 @@ export function main(): number {
|
|
|
357
621
|
console.log(" - dist/kiro");
|
|
358
622
|
console.log(" - dist/claude-code");
|
|
359
623
|
console.log(" - dist/codex");
|
|
624
|
+
console.log(" - dist/opencode");
|
|
625
|
+
console.log(" - dist/pi");
|
|
360
626
|
if (printDiagnostics && dropDiagnostics.length) {
|
|
361
627
|
console.error("Export sanitization diagnostics:");
|
|
362
628
|
for (const item of dropDiagnostics) console.error(` - ${item}`);
|