@kontourai/flow-agents 0.1.1 → 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/publish-npm.yml +1 -1
- package/.github/workflows/release-please.yml +31 -0
- package/.github/workflows/runtime-compat.yml +118 -0
- package/CHANGELOG.md +38 -0
- package/CONTRIBUTING.md +4 -0
- package/README.md +58 -19
- package/build/src/cli/init.js +215 -5
- package/build/src/cli/utterance-check.js +236 -0
- package/build/src/cli.js +3 -0
- 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 +6 -1
- package/context/scripts/telemetry/lib/config.sh +5 -1
- package/context/settings/flow-agents-settings.json +7 -0
- package/docs/agent-system-guidebook.md +4 -5
- package/docs/context-map.md +1 -0
- package/docs/index.md +46 -6
- 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 +3 -3
- package/docs/repository-structure.md +1 -1
- package/docs/skills-map.md +10 -4
- package/docs/spec/runtime-hook-surface.md +472 -0
- package/docs/survey-utterance-check.md +308 -0
- package/docs/vision.md +45 -0
- package/docs/workflow-usage-guide.md +1 -1
- 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 +518 -0
- 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 +5 -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 +327 -0
- package/scripts/telemetry/lib/config.sh +5 -1
- package/skills/idea-to-backlog/SKILL.md +1 -1
- package/src/cli/init.ts +219 -6
- package/src/cli/utterance-check.ts +324 -0
- package/src/cli.ts +3 -0
- 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 +6 -1
- package/build/src/cli/docs-preview.js +0 -39
- package/build/src/cli/export-bookmarks.js +0 -38
- package/build/src/cli/import-bookmarks.js +0 -50
- package/build/src/cli/instinct-cli.js +0 -93
|
@@ -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}`);
|
|
@@ -107,10 +107,13 @@ export function main(argv = process.argv.slice(2)): number {
|
|
|
107
107
|
removed += pruneNamedDirs(rootDir, "skills", allPackMembers(packs, "skills"), selected.skills, dryRun);
|
|
108
108
|
removed += pruneNamedDirs(rootDir, ".claude/skills", allPackMembers(packs, "skills"), selected.skills, dryRun);
|
|
109
109
|
removed += pruneNamedDirs(rootDir, ".codex/skills", allPackMembers(packs, "skills"), selected.skills, dryRun);
|
|
110
|
+
removed += pruneNamedDirs(rootDir, ".opencode/skills", allPackMembers(packs, "skills"), selected.skills, dryRun);
|
|
111
|
+
removed += pruneNamedDirs(rootDir, ".pi/skills", allPackMembers(packs, "skills"), selected.skills, dryRun);
|
|
110
112
|
removed += pruneNamedDirs(rootDir, "powers", allPackMembers(packs, "powers"), selected.powers, dryRun);
|
|
111
113
|
removed += pruneAgentFiles(rootDir, "agents", ".json", allPackMembers(packs, "agents"), selected.agents, dryRun);
|
|
112
114
|
removed += pruneAgentFiles(rootDir, ".claude/agents", ".md", allPackMembers(packs, "agents"), selected.agents, dryRun);
|
|
113
115
|
removed += pruneAgentFiles(rootDir, ".codex/agents", ".toml", allPackMembers(packs, "agents"), selected.agents, dryRun);
|
|
116
|
+
removed += pruneAgentFiles(rootDir, ".opencode/agents", ".md", allPackMembers(packs, "agents"), selected.agents, dryRun);
|
|
114
117
|
const summary = {
|
|
115
118
|
selected_packs: [...selectedNames].sort(),
|
|
116
119
|
removed_entries: removed,
|
|
@@ -62,12 +62,17 @@ const hookFilePolicies = new Map<string, { category: string; requiredNeedles: st
|
|
|
62
62
|
["scripts/hooks/run-hook.js", { category: "hook runner", requiredNeedles: ["isHookEnabled", "Path traversal rejected"] }],
|
|
63
63
|
["scripts/hooks/config-protection.js", { category: "policy hook", requiredNeedles: ["Config Protection Hook"] }],
|
|
64
64
|
["scripts/hooks/governance-audit.sh", { category: "policy hook", requiredNeedles: ["governance-audit.sh", "audit_emit"] }],
|
|
65
|
+
["scripts/hooks/opencode-hook-adapter.js", { category: "runtime adapter", requiredNeedles: ["opencode", "run-hook.js"] }],
|
|
66
|
+
["scripts/hooks/opencode-telemetry-hook.js", { category: "telemetry shim", requiredNeedles: ["opencode", "telemetry"] }],
|
|
67
|
+
["scripts/hooks/pi-hook-adapter.js", { category: "runtime adapter", requiredNeedles: ["pi", "run-hook.js"] }],
|
|
68
|
+
["scripts/hooks/pi-telemetry-hook.js", { category: "telemetry shim", requiredNeedles: ["pi", "telemetry"] }],
|
|
65
69
|
["scripts/hooks/post-edit-accumulator.js", { category: "policy hook", requiredNeedles: ["Post-Edit"] }],
|
|
66
70
|
["scripts/hooks/pre-commit-quality.js", { category: "repo guardrail hook", requiredNeedles: ["staged"] }],
|
|
67
71
|
["scripts/hooks/quality-gate.js", { category: "policy hook", requiredNeedles: ["Quality"] }],
|
|
68
72
|
["scripts/hooks/report-only-guard.js", { category: "policy hook", requiredNeedles: ["Report-Only Guard Hook"] }],
|
|
69
73
|
["scripts/hooks/stop-format-typecheck.js", { category: "policy hook", requiredNeedles: ["Stop Hook", "typecheck"] }],
|
|
70
74
|
["scripts/hooks/stop-goal-fit.js", { category: "policy hook", requiredNeedles: ["Stop Hook", "Goal Fit"] }],
|
|
75
|
+
["scripts/hooks/utterance-check.js", { category: "policy hook", requiredNeedles: ["Utterance Check Hook", "FLOW_AGENTS_UTTERANCE_CHECK_ENABLED"] }],
|
|
71
76
|
["scripts/hooks/workflow-steering.js", { category: "policy hook", requiredNeedles: ["Workflow Steering Hook"] }],
|
|
72
77
|
["scripts/hooks/desktop-notify.sh", { category: "local notification helper", requiredNeedles: ["desktop-notify.sh", "osascript"] }],
|
|
73
78
|
["scripts/hooks/lib/audit-transport.sh", { category: "shared hook library", requiredNeedles: ["audit_emit"] }],
|
|
@@ -94,7 +99,7 @@ const requiredUsageFeedbackFiles = [
|
|
|
94
99
|
const fixtureOwnershipSelfAuditRefs = new Set([
|
|
95
100
|
"evals/integration/test_fixture_retirement_audit.sh",
|
|
96
101
|
]);
|
|
97
|
-
const pythonInventoryExcludes = new Set([".git", ".flow-agents", "node_modules", ".venv", "dist", "__pycache__", ".pytest_cache", ".cache", "build"]);
|
|
102
|
+
const pythonInventoryExcludes = new Set([".git", ".flow-agents", "node_modules", ".venv", "dist", "__pycache__", ".pytest_cache", ".cache", "build", "integrations"]);
|
|
98
103
|
const pythonCommandScanRoots = ["README.md", "docs", "context", "skills", "prompts", "agents", "evals", "scripts", "packaging", "package.json"];
|
|
99
104
|
const allowedPythonCommandFiles = [
|
|
100
105
|
/^agents\/tool-explore-deps\.json$/,
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
function esc(value) {
|
|
4
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
5
|
-
}
|
|
6
|
-
function titleFor(file, text) {
|
|
7
|
-
return text.match(/^title:\s*(.+)$/m)?.[1].trim() ?? text.split(/\r?\n/).find((line) => line.startsWith("# "))?.slice(2).trim() ?? path.basename(file, ".md").replace(/-/g, " ");
|
|
8
|
-
}
|
|
9
|
-
function render(text) {
|
|
10
|
-
return text.replace(/^---\n.*?\n---\n/s, "").split(/\r?\n/).map((line) => {
|
|
11
|
-
if (line.startsWith("# "))
|
|
12
|
-
return `<h1>${esc(line.slice(2).trim())}</h1>`;
|
|
13
|
-
if (line.startsWith("## "))
|
|
14
|
-
return `<h2>${esc(line.slice(3).trim())}</h2>`;
|
|
15
|
-
if (line.startsWith("- "))
|
|
16
|
-
return `<li>${esc(line.slice(2).trim())}</li>`;
|
|
17
|
-
return line.trim() ? `<p>${esc(line.trim())}</p>` : "";
|
|
18
|
-
}).join("\n");
|
|
19
|
-
}
|
|
20
|
-
export function main() {
|
|
21
|
-
const root = path.resolve(path.dirname(process.argv[1]), "..");
|
|
22
|
-
const docs = path.join(root, "docs");
|
|
23
|
-
const out = path.join(root, "_site");
|
|
24
|
-
fs.rmSync(out, { recursive: true, force: true });
|
|
25
|
-
fs.mkdirSync(out, { recursive: true });
|
|
26
|
-
const assets = path.join(docs, "assets");
|
|
27
|
-
if (fs.existsSync(assets))
|
|
28
|
-
fs.cpSync(assets, path.join(out, "assets"), { recursive: true });
|
|
29
|
-
for (const file of fs.readdirSync(docs).filter((name) => name.endsWith(".md")).sort()) {
|
|
30
|
-
const source = path.join(docs, file);
|
|
31
|
-
const text = fs.readFileSync(source, "utf8");
|
|
32
|
-
const title = titleFor(file, text);
|
|
33
|
-
fs.writeFileSync(path.join(out, `${path.basename(file, ".md")}.html`), `<!doctype html><html><head><meta charset="utf-8"><title>${esc(title)}</title><link rel="stylesheet" href="assets/site.css"></head><body>${render(text)}<script src="assets/site.js" defer></script></body></html>`, "utf8");
|
|
34
|
-
}
|
|
35
|
-
console.log(`Built local docs preview: ${out}`);
|
|
36
|
-
return 0;
|
|
37
|
-
}
|
|
38
|
-
if (import.meta.url === `file://${process.argv[1]}`)
|
|
39
|
-
process.exit(main());
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as os from "node:os";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
function esc(value) {
|
|
5
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
6
|
-
}
|
|
7
|
-
export function main(argv = process.argv.slice(2)) {
|
|
8
|
-
if (argv.length < 1) {
|
|
9
|
-
console.log("Usage: export-bookmarks <bookmarks.md> [output.html]");
|
|
10
|
-
return 1;
|
|
11
|
-
}
|
|
12
|
-
const input = argv[0];
|
|
13
|
-
const output = argv[1] ?? path.join(os.homedir(), "Downloads", "bookmarks_organized.html");
|
|
14
|
-
if (!fs.existsSync(input)) {
|
|
15
|
-
console.log(`Error: ${input} not found`);
|
|
16
|
-
return 1;
|
|
17
|
-
}
|
|
18
|
-
const links = Array.from(fs.readFileSync(input, "utf8").matchAll(/^- \*\*[\d-]+\*\* \[(\w+)\]: (.+?) — (https?:\/\/\S+)(?: — (.+))?$/gm), (m) => ({ tag: m[1], title: m[2], url: m[3] }));
|
|
19
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
20
|
-
const byTag = new Map();
|
|
21
|
-
for (const link of links)
|
|
22
|
-
byTag.set(link.tag, [...(byTag.get(link.tag) ?? []), link]);
|
|
23
|
-
const lines = ["<!DOCTYPE NETSCAPE-Bookmark-file-1>", '<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">', "<TITLE>Bookmarks</TITLE>", "<H1>Bookmarks</H1>", "<DL><p>"];
|
|
24
|
-
for (const tag of Array.from(byTag.keys()).sort()) {
|
|
25
|
-
lines.push(` <DT><H3 ADD_DATE="${timestamp}" LAST_MODIFIED="${timestamp}">${esc(tag)}</H3>`, " <DL><p>");
|
|
26
|
-
for (const link of (byTag.get(tag) ?? []).sort((a, b) => a.title.localeCompare(b.title)))
|
|
27
|
-
lines.push(` <DT><A HREF="${esc(link.url)}" ADD_DATE="${timestamp}">${esc(link.title)}</A>`);
|
|
28
|
-
lines.push(" </DL><p>");
|
|
29
|
-
}
|
|
30
|
-
lines.push("</DL><p>");
|
|
31
|
-
fs.writeFileSync(output, lines.join("\n"), "utf8");
|
|
32
|
-
console.log("Bookmarks exported");
|
|
33
|
-
console.log(`Source: ${input} (${links.length} links)`);
|
|
34
|
-
console.log(`Output: ${output}`);
|
|
35
|
-
return 0;
|
|
36
|
-
}
|
|
37
|
-
if (import.meta.url === `file://${process.argv[1]}`)
|
|
38
|
-
process.exit(main());
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
const SKIP = [/mail\.google\.com/i, /outlook\.(office|live)\.com/i, /calendar\.google\.com/i, /linkedin\.com\/in\//i, /slack\.com\/archives/i, /zoom\.us\/j\//i, /teams\.microsoft\.com/i];
|
|
3
|
-
const TAGS = [[/github\.com|gitlab\.com|codecommit|code\.amazon/i, "repo"], [/docs\.|documentation|\/docs\/|readme|userguide|developer-guide/i, "docs"], [/wiki\.|\/wiki\/|confluence|runbook/i, "wiki"], [/dashboard|console\.|portal|admin\.|grafana|cloudwatch/i, "tool"], [/learn\.|training\.|course|certification|workshop|tutorial/i, "training"], [/template|boilerplate|starter|scaffold/i, "template"], [/spec\.|standard|architecture|rfc|design-doc/i, "reference"]];
|
|
4
|
-
function inferTag(url) {
|
|
5
|
-
return TAGS.find(([pattern]) => pattern.test(url))?.[1] ?? "external";
|
|
6
|
-
}
|
|
7
|
-
function existingUrls(file) {
|
|
8
|
-
if (!fs.existsSync(file))
|
|
9
|
-
return new Set();
|
|
10
|
-
return new Set(Array.from(fs.readFileSync(file, "utf8").matchAll(/-->\s*(https?:\/\/\S+)|—\s+(https?:\/\/\S+)/g), (match) => match[1] ?? match[2]));
|
|
11
|
-
}
|
|
12
|
-
export function main(argv = process.argv.slice(2)) {
|
|
13
|
-
if (argv.length < 2) {
|
|
14
|
-
console.log("Usage: import-bookmarks <bookmarks.html> <bookmarks.md>");
|
|
15
|
-
return 1;
|
|
16
|
-
}
|
|
17
|
-
const [htmlPath, mdPath] = argv;
|
|
18
|
-
if (!fs.existsSync(htmlPath)) {
|
|
19
|
-
console.log(`Error: ${htmlPath} not found`);
|
|
20
|
-
return 1;
|
|
21
|
-
}
|
|
22
|
-
const html = fs.readFileSync(htmlPath, "utf8");
|
|
23
|
-
const bookmarks = Array.from(html.matchAll(/<A\b[^>]*HREF=["']([^"']+)["'][^>]*>(.*?)<\/A>/gi), (match) => ({ url: match[1], title: match[2].replace(/<[^>]+>/g, "").trim() }));
|
|
24
|
-
console.log(`Found ${bookmarks.length} bookmarks in HTML`);
|
|
25
|
-
const seen = existingUrls(mdPath);
|
|
26
|
-
console.log(`Found ${seen.size} existing entries in bookmarks.md`);
|
|
27
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
28
|
-
const entries = [];
|
|
29
|
-
let skipped = 0;
|
|
30
|
-
let preserved = 0;
|
|
31
|
-
for (const bookmark of bookmarks) {
|
|
32
|
-
if (!/^https?:\/\//.test(bookmark.url) || SKIP.some((pattern) => pattern.test(bookmark.url))) {
|
|
33
|
-
skipped += 1;
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
if (seen.has(bookmark.url)) {
|
|
37
|
-
preserved += 1;
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
entries.push(`- **${today}** [${inferTag(bookmark.url)}]: ${bookmark.title} — ${bookmark.url} — (imported from browser)`);
|
|
41
|
-
}
|
|
42
|
-
if (entries.length)
|
|
43
|
-
fs.appendFileSync(mdPath, `${fs.existsSync(mdPath) ? "\n" : ""}${entries.join("\n")}\n`, "utf8");
|
|
44
|
-
console.log(`Added ${entries.length} new bookmarks`);
|
|
45
|
-
console.log(`Preserved ${preserved} existing bookmarks`);
|
|
46
|
-
console.log(`Skipped ${skipped} routine/non-http bookmarks`);
|
|
47
|
-
return 0;
|
|
48
|
-
}
|
|
49
|
-
if (import.meta.url === `file://${process.argv[1]}`)
|
|
50
|
-
process.exit(main());
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as os from "node:os";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
const SOUL_PATH = process.env.SOUL_PATH ?? path.join(os.homedir(), ".soul");
|
|
5
|
-
const INSTINCTS_DIR = path.join(SOUL_PATH, "knowledge", "instincts");
|
|
6
|
-
function parse(file) {
|
|
7
|
-
const text = fs.readFileSync(file, "utf8");
|
|
8
|
-
if (!text.startsWith("---"))
|
|
9
|
-
return null;
|
|
10
|
-
const [, fm, body] = text.split("---", 3);
|
|
11
|
-
const meta = {};
|
|
12
|
-
for (const line of fm.trim().split(/\r?\n/)) {
|
|
13
|
-
const idx = line.indexOf(":");
|
|
14
|
-
if (idx === -1)
|
|
15
|
-
continue;
|
|
16
|
-
const key = line.slice(0, idx).trim();
|
|
17
|
-
const raw = line.slice(idx + 1).trim().replace(/^["']|["']$/g, "");
|
|
18
|
-
meta[key] = key === "confidence" ? Number(raw) : key === "observation_count" ? Number.parseInt(raw, 10) : raw;
|
|
19
|
-
}
|
|
20
|
-
meta._body = body.trim();
|
|
21
|
-
meta._path = file;
|
|
22
|
-
return meta;
|
|
23
|
-
}
|
|
24
|
-
function files() {
|
|
25
|
-
const roots = [path.join(INSTINCTS_DIR, "global"), path.join(INSTINCTS_DIR, "projects")];
|
|
26
|
-
const out = [];
|
|
27
|
-
for (const root of roots) {
|
|
28
|
-
if (!fs.existsSync(root))
|
|
29
|
-
continue;
|
|
30
|
-
const stack = [root];
|
|
31
|
-
while (stack.length) {
|
|
32
|
-
const dir = stack.pop();
|
|
33
|
-
for (const name of fs.readdirSync(dir)) {
|
|
34
|
-
const file = path.join(dir, name);
|
|
35
|
-
if (fs.statSync(file).isDirectory())
|
|
36
|
-
stack.push(file);
|
|
37
|
-
else if (file.endsWith(".md"))
|
|
38
|
-
out.push(file);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return out;
|
|
43
|
-
}
|
|
44
|
-
function write(inst, file) {
|
|
45
|
-
const body = String(inst._body ?? "");
|
|
46
|
-
const lines = ["---"];
|
|
47
|
-
for (const [key, value] of Object.entries(inst))
|
|
48
|
-
if (!key.startsWith("_"))
|
|
49
|
-
lines.push(`${key}: ${typeof value === "number" ? value : String(value)}`);
|
|
50
|
-
lines.push("---", "", body);
|
|
51
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
52
|
-
fs.writeFileSync(file, `${lines.join("\n").trimEnd()}\n`, "utf8");
|
|
53
|
-
}
|
|
54
|
-
export function main(argv = process.argv.slice(2)) {
|
|
55
|
-
const [command, ...rest] = argv;
|
|
56
|
-
const instincts = files().map(parse).filter((item) => Boolean(item));
|
|
57
|
-
if (command === "status") {
|
|
58
|
-
const global = instincts.filter((item) => item.scope === "global").length;
|
|
59
|
-
console.log(`Total instincts: ${instincts.length}`);
|
|
60
|
-
console.log(` Global: ${global}, Project: ${instincts.length - global}`);
|
|
61
|
-
return 0;
|
|
62
|
-
}
|
|
63
|
-
if (command === "list") {
|
|
64
|
-
for (const inst of instincts)
|
|
65
|
-
console.log(` [${Number(inst.confidence ?? 0).toFixed(2)}] ${inst.id ?? "?"} - ${inst.trigger ?? "?"} (${inst.scope ?? "?"})`);
|
|
66
|
-
return 0;
|
|
67
|
-
}
|
|
68
|
-
if (command === "show") {
|
|
69
|
-
const found = instincts.find((item) => item.id === rest[0]);
|
|
70
|
-
if (!found) {
|
|
71
|
-
console.error(`Instinct '${rest[0]}' not found.`);
|
|
72
|
-
return 1;
|
|
73
|
-
}
|
|
74
|
-
console.log(fs.readFileSync(String(found._path), "utf8"));
|
|
75
|
-
return 0;
|
|
76
|
-
}
|
|
77
|
-
if (command === "create") {
|
|
78
|
-
const id = rest[rest.indexOf("--id") + 1];
|
|
79
|
-
const trigger = rest[rest.indexOf("--trigger") + 1];
|
|
80
|
-
const domain = rest[rest.indexOf("--domain") + 1];
|
|
81
|
-
const scope = rest.includes("--scope") ? rest[rest.indexOf("--scope") + 1] : "project";
|
|
82
|
-
if (!id || !trigger || !domain)
|
|
83
|
-
return 2;
|
|
84
|
-
const file = scope === "global" ? path.join(INSTINCTS_DIR, "global", `${id}.md`) : path.join(INSTINCTS_DIR, "projects", rest[rest.indexOf("--project-id") + 1] ?? "default", "instincts", `${id}.md`);
|
|
85
|
-
write({ id, trigger, confidence: 0.3, domain, source: "session-observation", scope, created: new Date().toISOString().slice(0, 10), last_observed: new Date().toISOString().slice(0, 10), observation_count: 1, _body: `# ${id}\n\n## Action\nTBD` }, file);
|
|
86
|
-
console.log(`Created ${file}`);
|
|
87
|
-
return 0;
|
|
88
|
-
}
|
|
89
|
-
console.error("usage: instinct-cli <status|list|show|create>");
|
|
90
|
-
return 2;
|
|
91
|
-
}
|
|
92
|
-
if (import.meta.url === `file://${process.argv[1]}`)
|
|
93
|
-
process.exit(main());
|