@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.
Files changed (97) hide show
  1. package/.github/dependabot.yml +23 -0
  2. package/.github/workflows/publish-npm.yml +1 -1
  3. package/.github/workflows/release-please.yml +31 -0
  4. package/.github/workflows/runtime-compat.yml +118 -0
  5. package/CHANGELOG.md +38 -0
  6. package/CONTRIBUTING.md +4 -0
  7. package/README.md +58 -19
  8. package/build/src/cli/init.js +215 -5
  9. package/build/src/cli/utterance-check.js +236 -0
  10. package/build/src/cli.js +3 -0
  11. package/build/src/tools/build-universal-bundles.js +268 -0
  12. package/build/src/tools/filter-installed-packs.js +3 -0
  13. package/build/src/tools/validate-source-tree.js +6 -1
  14. package/context/scripts/telemetry/lib/config.sh +5 -1
  15. package/context/settings/flow-agents-settings.json +7 -0
  16. package/docs/agent-system-guidebook.md +4 -5
  17. package/docs/context-map.md +1 -0
  18. package/docs/index.md +46 -6
  19. package/docs/integrations/conformance.md +246 -0
  20. package/docs/integrations/framework-adapter.md +275 -0
  21. package/docs/integrations/harness-install.md +213 -0
  22. package/docs/integrations/index.md +54 -0
  23. package/docs/north-star.md +3 -3
  24. package/docs/repository-structure.md +1 -1
  25. package/docs/skills-map.md +10 -4
  26. package/docs/spec/runtime-hook-surface.md +472 -0
  27. package/docs/survey-utterance-check.md +308 -0
  28. package/docs/vision.md +45 -0
  29. package/docs/workflow-usage-guide.md +1 -1
  30. package/evals/acceptance/run.sh +4 -2
  31. package/evals/acceptance/test_opencode_harness.sh +121 -0
  32. package/evals/acceptance/test_pi_harness.sh +98 -0
  33. package/evals/integration/test_bundle_install.sh +226 -1
  34. package/evals/integration/test_bundle_lifecycle.sh +641 -0
  35. package/evals/integration/test_utterance_check.sh +518 -0
  36. package/evals/run.sh +2 -0
  37. package/evals/static/test_universal_bundles.sh +137 -2
  38. package/integrations/strands/README.md +256 -0
  39. package/integrations/strands/example.py +74 -0
  40. package/integrations/strands/flow_agents_strands/__init__.py +27 -0
  41. package/integrations/strands/flow_agents_strands/hooks.py +194 -0
  42. package/integrations/strands/flow_agents_strands/policy.py +348 -0
  43. package/integrations/strands/flow_agents_strands/steering.py +172 -0
  44. package/integrations/strands/flow_agents_strands/telemetry.py +238 -0
  45. package/integrations/strands/pyproject.toml +38 -0
  46. package/integrations/strands/tests/__init__.py +0 -0
  47. package/integrations/strands/tests/test_hooks.py +304 -0
  48. package/integrations/strands/tests/test_policy.py +315 -0
  49. package/integrations/strands/tests/test_telemetry.py +184 -0
  50. package/integrations/strands-ts/README.md +224 -0
  51. package/integrations/strands-ts/bin/conformance-shim.mjs +257 -0
  52. package/integrations/strands-ts/package.json +53 -0
  53. package/integrations/strands-ts/src/hooks.ts +208 -0
  54. package/integrations/strands-ts/src/index.ts +22 -0
  55. package/integrations/strands-ts/src/policy.ts +345 -0
  56. package/integrations/strands-ts/src/telemetry.ts +251 -0
  57. package/integrations/strands-ts/test/test-policy.ts +322 -0
  58. package/integrations/strands-ts/test/test-telemetry.ts +226 -0
  59. package/integrations/strands-ts/tsconfig.json +20 -0
  60. package/package.json +7 -2
  61. package/packaging/conformance/README.md +142 -0
  62. package/packaging/conformance/fixtures/config-protection--allow-no-path.json +18 -0
  63. package/packaging/conformance/fixtures/config-protection--allow-safe-file.json +20 -0
  64. package/packaging/conformance/fixtures/config-protection--block-biome.json +20 -0
  65. package/packaging/conformance/fixtures/config-protection--block-eslintrc.json +20 -0
  66. package/packaging/conformance/fixtures/quality-gate--allow-no-path.json +17 -0
  67. package/packaging/conformance/fixtures/quality-gate--allow-nonexistent-file.json +19 -0
  68. package/packaging/conformance/fixtures/stop-goal-fit--allow-clean-cwd.json +17 -0
  69. package/packaging/conformance/fixtures/stop-goal-fit--block-strict-mode.json +23 -0
  70. package/packaging/conformance/fixtures/stop-goal-fit--warn-active-delivery.json +21 -0
  71. package/packaging/conformance/fixtures/workflow-steering--allow-no-state.json +16 -0
  72. package/packaging/conformance/fixtures/workflow-steering--inject-active-state.json +29 -0
  73. package/packaging/conformance/fixtures/workflow-steering--inject-subagent-steering.json +25 -0
  74. package/packaging/conformance/package.json +4 -0
  75. package/packaging/conformance/run-conformance.js +322 -0
  76. package/packaging/manifest.json +59 -0
  77. package/schemas/flow-agents-settings.schema.json +48 -0
  78. package/scripts/README.md +5 -0
  79. package/scripts/dogfood.js +16 -0
  80. package/scripts/hooks/opencode-hook-adapter.js +123 -0
  81. package/scripts/hooks/opencode-telemetry-hook.js +101 -0
  82. package/scripts/hooks/pi-hook-adapter.js +123 -0
  83. package/scripts/hooks/pi-telemetry-hook.js +105 -0
  84. package/scripts/hooks/run-hook.js +8 -0
  85. package/scripts/hooks/utterance-check.js +327 -0
  86. package/scripts/telemetry/lib/config.sh +5 -1
  87. package/skills/idea-to-backlog/SKILL.md +1 -1
  88. package/src/cli/init.ts +219 -6
  89. package/src/cli/utterance-check.ts +324 -0
  90. package/src/cli.ts +3 -0
  91. package/src/tools/build-universal-bundles.ts +266 -0
  92. package/src/tools/filter-installed-packs.ts +3 -0
  93. package/src/tools/validate-source-tree.ts +6 -1
  94. package/build/src/cli/docs-preview.js +0 -39
  95. package/build/src/cli/export-bookmarks.js +0 -38
  96. package/build/src/cli/import-bookmarks.js +0 -50
  97. package/build/src/cli/instinct-cli.js +0 -93
@@ -0,0 +1,236 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { flagBool, flagString, parseArgs } from "../lib/args.js";
4
+ // ---------------------------------------------------------------------------
5
+ // Helpers
6
+ // ---------------------------------------------------------------------------
7
+ function usage() {
8
+ console.error([
9
+ "usage: flow-agents utterance-check check [options]",
10
+ "",
11
+ "Check an agent utterance for evidence coverage using @kontourai/survey.",
12
+ "Requires @kontourai/survey to be installed in the target workspace.",
13
+ "",
14
+ "Options:",
15
+ " --utterance TEXT Utterance text to check (required unless --not-configured).",
16
+ " --bundle-path FILE Trust bundle JSON file. Omit for an empty bundle (all unsupported).",
17
+ " --agent-id ID Agent identifier for provenance (default: flow-agents-utterance-check).",
18
+ " --extractor NAME Extractor to use: 'reference' (default, pattern-based) or 'anthropic'",
19
+ " (model-backed, requires ANTHROPIC_API_KEY and @kontourai/survey/anthropic).",
20
+ " --model MODEL Model for the anthropic extractor (e.g. claude-haiku-4-5).",
21
+ " --not-configured Skip survey call; output not_configured without error.",
22
+ " --strict Exit non-zero when any badge is disputed, rejected, or unsupported.",
23
+ " --help Show this help.",
24
+ ].join("\n"));
25
+ }
26
+ function excerptText(text, maxLen = 200) {
27
+ const trimmed = text.trim().replace(/\s+/g, " ");
28
+ return trimmed.length > maxLen ? `${trimmed.slice(0, maxLen - 3)}...` : trimmed;
29
+ }
30
+ function badgeSummary(statements) {
31
+ if (statements.length === 0)
32
+ return "no factual statements extracted";
33
+ const counts = {};
34
+ for (const s of statements) {
35
+ counts[s.badge] = (counts[s.badge] ?? 0) + 1;
36
+ }
37
+ return Object.entries(counts)
38
+ .sort((a, b) => b[1] - a[1])
39
+ .map(([badge, n]) => `${badge}:${n}`)
40
+ .join(", ");
41
+ }
42
+ function hasConcerningBadge(badge) {
43
+ return badge === "disputed" || badge === "rejected" || badge === "unsupported";
44
+ }
45
+ async function loadSurvey() {
46
+ try {
47
+ const pkg = "@kontourai/survey";
48
+ // Dynamic import avoids a static dependency on @kontourai/survey —
49
+ // the same pattern survey/src/anthropic.ts uses for @anthropic-ai/sdk.
50
+ const mod = await Function("m", "return import(m)")(pkg);
51
+ return mod;
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ }
57
+ /**
58
+ * Dynamically import @kontourai/survey/anthropic and create the Anthropic extractor.
59
+ * Fails open with a clear not_configured message when the key or peer dep is missing.
60
+ */
61
+ async function loadAnthropicExtractor(model) {
62
+ const apiKey = process.env.ANTHROPIC_API_KEY;
63
+ if (!apiKey) {
64
+ return {
65
+ notConfigured: true,
66
+ reason: "anthropic extractor requires ANTHROPIC_API_KEY to be set. " +
67
+ "Set the environment variable or switch extractor to 'reference'.",
68
+ };
69
+ }
70
+ try {
71
+ const pkg = "@kontourai/survey/anthropic";
72
+ const mod = await Function("m", "return import(m)")(pkg);
73
+ if (typeof mod.createAnthropicUtteranceExtractor !== "function") {
74
+ return {
75
+ notConfigured: true,
76
+ reason: "@kontourai/survey/anthropic does not export createAnthropicUtteranceExtractor. " +
77
+ "Update @kontourai/survey to a version that supports the anthropic extractor.",
78
+ };
79
+ }
80
+ const opts = { apiKey };
81
+ if (model)
82
+ opts.model = model;
83
+ return mod.createAnthropicUtteranceExtractor(opts);
84
+ }
85
+ catch (err) {
86
+ const msg = err instanceof Error ? err.message : String(err);
87
+ return {
88
+ notConfigured: true,
89
+ reason: `@kontourai/survey/anthropic is not available: ${msg}. ` +
90
+ "Install @kontourai/survey with the anthropic subpath export, or switch extractor to 'reference'.",
91
+ };
92
+ }
93
+ }
94
+ // ---------------------------------------------------------------------------
95
+ // Core check logic
96
+ // ---------------------------------------------------------------------------
97
+ async function runCheck(argv) {
98
+ const { flags } = parseArgs(argv);
99
+ if (flagBool(flags, "help")) {
100
+ usage();
101
+ return 0;
102
+ }
103
+ const agentId = flagString(flags, "agent-id") ?? "flow-agents-utterance-check";
104
+ const notConfigured = flagBool(flags, "not-configured");
105
+ const strict = flagBool(flags, "strict");
106
+ const extractorName = flagString(flags, "extractor") ?? "reference";
107
+ const model = flagString(flags, "model");
108
+ if (notConfigured) {
109
+ const report = {
110
+ status: "not_configured",
111
+ agent_id: agentId,
112
+ utterance_excerpt: "",
113
+ statements: [],
114
+ summary: "@kontourai/survey is not configured for this workspace.",
115
+ };
116
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
117
+ return 0;
118
+ }
119
+ const utterance = flagString(flags, "utterance");
120
+ if (!utterance) {
121
+ usage();
122
+ return 3;
123
+ }
124
+ const bundlePath = flagString(flags, "bundle-path");
125
+ let bundle = { claims: [] };
126
+ if (bundlePath) {
127
+ const resolved = path.resolve(bundlePath);
128
+ try {
129
+ const raw = fs.readFileSync(resolved, "utf8");
130
+ bundle = JSON.parse(raw);
131
+ }
132
+ catch (err) {
133
+ const msg = err instanceof Error ? err.message : String(err);
134
+ process.stderr.write(`[UtteranceCheck] could not read bundle from ${resolved}: ${msg}\n`);
135
+ }
136
+ }
137
+ const survey = await loadSurvey();
138
+ if (!survey) {
139
+ const report = {
140
+ status: "not_configured",
141
+ agent_id: agentId,
142
+ utterance_excerpt: excerptText(utterance),
143
+ statements: [],
144
+ summary: "@kontourai/survey is not installed. Install it or run with --not-configured.",
145
+ };
146
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
147
+ process.stderr.write("[UtteranceCheck] not_configured: @kontourai/survey is not installed in this workspace.\n");
148
+ return 1;
149
+ }
150
+ const { surveyAgentUtterance, referenceUtteranceExtractor } = survey;
151
+ // Resolve which extractor to use.
152
+ let extractor;
153
+ if (extractorName === "anthropic") {
154
+ const anthropicResult = await loadAnthropicExtractor(model);
155
+ if ("notConfigured" in anthropicResult) {
156
+ // Fail open: emit not_configured with a clear reason rather than erroring.
157
+ const report = {
158
+ status: "not_configured",
159
+ agent_id: agentId,
160
+ utterance_excerpt: excerptText(utterance),
161
+ statements: [],
162
+ summary: anthropicResult.reason,
163
+ };
164
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
165
+ process.stderr.write(`[UtteranceCheck] not_configured: ${anthropicResult.reason}\n`);
166
+ return 0;
167
+ }
168
+ extractor = anthropicResult;
169
+ }
170
+ else {
171
+ extractor = referenceUtteranceExtractor;
172
+ }
173
+ let trustReport;
174
+ try {
175
+ trustReport = await surveyAgentUtterance(utterance, extractor, {
176
+ bundle,
177
+ agentId,
178
+ });
179
+ }
180
+ catch (err) {
181
+ const msg = err instanceof Error ? err.message : String(err);
182
+ const report = {
183
+ status: "error",
184
+ agent_id: agentId,
185
+ utterance_excerpt: excerptText(utterance),
186
+ statements: [],
187
+ summary: `Survey call failed: ${msg}`,
188
+ };
189
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
190
+ process.stderr.write(`[UtteranceCheck] survey call failed: ${msg}\n`);
191
+ return 1;
192
+ }
193
+ const statements = trustReport.statements.map((s) => ({
194
+ excerpt: s.excerpt,
195
+ badge: s.badge,
196
+ target: s.target,
197
+ span: s.span,
198
+ }));
199
+ const summary = badgeSummary(statements);
200
+ const report = {
201
+ status: "ok",
202
+ agent_id: agentId,
203
+ utterance_excerpt: excerptText(utterance),
204
+ statements,
205
+ summary,
206
+ };
207
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
208
+ const concerning = statements.filter((s) => hasConcerningBadge(s.badge));
209
+ if (concerning.length > 0) {
210
+ process.stderr.write(`[UtteranceCheck] ${concerning.length} statement(s) lack evidence coverage: ${summary}\n`);
211
+ for (const s of concerning.slice(0, 4)) {
212
+ process.stderr.write(` - [${s.badge}] "${excerptText(s.excerpt, 100)}"\n`);
213
+ }
214
+ }
215
+ if (strict && concerning.length > 0)
216
+ return 2;
217
+ return 0;
218
+ }
219
+ // ---------------------------------------------------------------------------
220
+ // Entry point
221
+ // ---------------------------------------------------------------------------
222
+ export async function main(argv = process.argv.slice(2)) {
223
+ const [subcommand, ...rest] = argv;
224
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
225
+ usage();
226
+ return 0;
227
+ }
228
+ if (subcommand !== "check") {
229
+ console.error(`Unknown utterance-check subcommand: ${subcommand}`);
230
+ usage();
231
+ return 3;
232
+ }
233
+ return runCheck(rest);
234
+ }
235
+ if (import.meta.url === `file://${process.argv[1]}`)
236
+ process.exit(await main());
package/build/src/cli.js CHANGED
@@ -19,6 +19,7 @@ import { main as validateSource } from "./tools/validate-source-tree.js";
19
19
  import { main as validatePackage } from "./tools/validate-package.js";
20
20
  import { main as validateHookInfluence } from "./cli/validate-hook-influence.js";
21
21
  import { main as runtimeAdapter } from "./cli/runtime-adapter.js";
22
+ import { main as utteranceCheck } from "./cli/utterance-check.js";
22
23
  const availableCommands = new Map([
23
24
  ["build-bundles", () => buildBundles()],
24
25
  ["console-learning-projection", consoleLearningProjection],
@@ -32,6 +33,7 @@ const availableCommands = new Map([
32
33
  ["publish-change", publishChange],
33
34
  ["pull-work-provider", pullWorkProvider],
34
35
  ["runtime-adapter", runtimeAdapter],
36
+ ["utterance-check", utteranceCheck],
35
37
  ["telemetry-doctor", telemetryDoctor],
36
38
  ["usage-feedback", usageFeedback],
37
39
  ["veritas-governance", veritasGovernance],
@@ -56,6 +58,7 @@ const aliases = new Map([
56
58
  ["flow-agents-usage-feedback", "usage-feedback"],
57
59
  ["flow-agents-veritas-governance", "veritas-governance"],
58
60
  ["flow-agents-validate-hook-influence", "validate-hook-influence"],
61
+ ["flow-agents-utterance-check", "utterance-check"],
59
62
  ["flow-agents-validate-source", "validate-source"],
60
63
  ["flow-agents-workflow-artifact-cleanup-audit", "workflow-artifact-cleanup-audit"],
61
64
  ]);
@@ -361,6 +361,270 @@ function buildCodex(agents) {
361
361
  writeText(path.join(bundle, "install.sh"), installScript("Codex", "/path/to/workspace"));
362
362
  fs.chmodSync(path.join(bundle, "install.sh"), 0o755);
363
363
  }
364
+ function exportOpencodeAgent(spec) {
365
+ // Determine agent mode: orchestrator-like agents -> primary, others -> subagent
366
+ const primaryAgents = new Set(["dev"]);
367
+ const mode = primaryAgents.has(spec.name) ? "primary" : "subagent";
368
+ 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.");
369
+ const lines = ["---"];
370
+ lines.push(`description: ${String(spec.description ?? "").trim()}`);
371
+ lines.push(`mode: ${mode}`);
372
+ lines.push(`model: ${mapped("claude_model_map", spec.model)}`);
373
+ lines.push("---");
374
+ lines.push("");
375
+ lines.push(prompt);
376
+ return lines.join("\n");
377
+ }
378
+ function exportOpencodePlugin() {
379
+ // Generate the Flow Agents opencode plugin.
380
+ // opencode plugins are auto-loaded from .opencode/plugins/*.js at startup.
381
+ //
382
+ // NOTE: opencode has no direct user-prompt-submit hook. For prompt-submit
383
+ // workflow steering, we wire the steering command behind session.created
384
+ // (for session-start steering context) and tool.execute.before (for
385
+ // policy). This is the closest reasonable approximation — documented here
386
+ // as an honest gap matching the codex live-hook-influence caveat pattern.
387
+ return `/**
388
+ * Flow Agents opencode plugin.
389
+ *
390
+ * Auto-loaded from .opencode/plugins/flow-agents.js at opencode startup.
391
+ * Delegates policy and telemetry decisions to shared scripts in scripts/hooks/
392
+ * using the same payload contract as the claude/codex adapters.
393
+ *
394
+ * EVENT MAPPING NOTE: opencode has no direct user-prompt-submit hook.
395
+ * Workflow steering (workflow-steering.js) is wired to session.created
396
+ * (session-start context) and tool.execute.before (per-tool policy).
397
+ * This approximates the UserPromptSubmit behavior in other runtimes but
398
+ * cannot intercept mid-session user messages before they are processed.
399
+ * This is an accepted gap documented here analogously to the codex
400
+ * live-hook-influence caveat.
401
+ */
402
+
403
+ import { spawnSync } from 'node:child_process';
404
+ import { join, basename } from 'node:path';
405
+
406
+ // opencode runs plugins inside its own compiled (Bun-based) binary, so
407
+ // process.execPath points at opencode itself — spawning it with a script
408
+ // path silently does nothing (caught by live acceptance smoke 2026-06-11).
409
+ // Resolve a real node binary instead; fall back to PATH lookup.
410
+ const NODE_BIN = basename(process.execPath).startsWith('node') ? process.execPath : 'node';
411
+
412
+ export const FlowAgentsPlugin = async ({ project, client, $, directory, worktree }) => {
413
+ const root = directory || process.cwd();
414
+
415
+ // The hook scripts read the event payload from stdin; an empty stdin makes
416
+ // the telemetry pipeline silently skip the emit (fail-open), so every spawn
417
+ // must pass a payload (caught by live acceptance smoke 2026-06-11).
418
+ function hookPayload(eventName, detail) {
419
+ return JSON.stringify({ hook_event_name: eventName, cwd: root, ...(detail || {}) });
420
+ }
421
+
422
+ function runAdapter(adapterScript, eventName, detail, ...args) {
423
+ const adapterPath = join(root, 'scripts', 'hooks', adapterScript);
424
+ const result = spawnSync(NODE_BIN, [adapterPath, eventName, ...args], {
425
+ input: hookPayload(eventName, detail),
426
+ encoding: 'utf8',
427
+ cwd: root,
428
+ env: { ...process.env, FLOW_AGENTS_HOOK_RUNTIME: 'opencode' },
429
+ timeout: 30000,
430
+ });
431
+ try {
432
+ return JSON.parse(result.stdout || '{}');
433
+ } catch {
434
+ return { allow: true };
435
+ }
436
+ }
437
+
438
+ function runTelemetry(eventName, detail) {
439
+ const telemetryPath = join(root, 'scripts', 'hooks', 'opencode-telemetry-hook.js');
440
+ spawnSync(NODE_BIN, [telemetryPath, eventName, 'dev'], {
441
+ input: hookPayload(eventName, detail),
442
+ encoding: 'utf8',
443
+ cwd: root,
444
+ env: { ...process.env, FLOW_AGENTS_TELEMETRY_RUNTIME: 'opencode' },
445
+ timeout: 10000,
446
+ });
447
+ }
448
+
449
+ return {
450
+ 'session.created': async (_input, _output) => {
451
+ runTelemetry('session.created');
452
+ // Wire workflow steering on session start for context injection
453
+ runAdapter('opencode-hook-adapter.js', 'session.created', null, 'workflow-steering', 'workflow-steering.js', 'default');
454
+ },
455
+ 'tool.execute.before': async (input, output) => {
456
+ const detail = { tool: input && input.tool, args: output && output.args };
457
+ runTelemetry('tool.execute.before', detail);
458
+ const policyResult = runAdapter('opencode-hook-adapter.js', 'tool.execute.before', detail, 'config-protection', 'config-protection.js', 'default');
459
+ if (policyResult && policyResult.allow === false) {
460
+ throw new Error(policyResult.reason || 'Blocked by Flow Agents hook policy.');
461
+ }
462
+ },
463
+ 'tool.execute.after': async (input, output) => {
464
+ const detail = { tool: input && input.tool };
465
+ runTelemetry('tool.execute.after', detail);
466
+ runAdapter('opencode-hook-adapter.js', 'tool.execute.after', detail, 'quality-gate', 'quality-gate.js', 'default');
467
+ },
468
+ 'session.idle': async (_input, _output) => {
469
+ runTelemetry('session.idle');
470
+ runAdapter('opencode-hook-adapter.js', 'session.idle', null, 'stop-goal-fit', 'stop-goal-fit.js', 'default');
471
+ },
472
+ 'session.error': async (_input, _output) => {
473
+ runTelemetry('session.error');
474
+ },
475
+ 'session.compacted': async (_input, _output) => {
476
+ runTelemetry('session.compacted');
477
+ },
478
+ 'permission.asked': async (_input, _output) => {
479
+ runTelemetry('permission.asked');
480
+ },
481
+ 'file.edited': async (_input, _output) => {
482
+ runTelemetry('file.edited');
483
+ },
484
+ };
485
+ };
486
+ `;
487
+ }
488
+ function exportOpencodeConfig() {
489
+ // opencode's config schema requires `instructions` to be an ARRAY of
490
+ // instruction file paths/globs (a bare string fails validation and aborts
491
+ // startup). AGENTS.md is loaded natively by opencode, so the config stays
492
+ // minimal rather than double-including it.
493
+ return `${JSON.stringify({
494
+ $schema: "https://opencode.ai/config.json",
495
+ }, null, 2)}\n`;
496
+ }
497
+ function buildOpencode(agents) {
498
+ const bundle = path.join(dist, "opencode");
499
+ resetDir(bundle);
500
+ copySharedContent(bundle, "opencode", "<bundle-root>");
501
+ writeText(path.join(bundle, manifest.opencode.task_dir, ".gitkeep"), "");
502
+ for (const spec of agents) {
503
+ writeText(path.join(bundle, ".opencode/agents", `${spec.name}.md`), exportOpencodeAgent(spec));
504
+ }
505
+ for (const skill of fs.readdirSync(path.join(root, "skills"))) {
506
+ const skillPath = path.join(root, "skills", skill, "SKILL.md");
507
+ if (fs.existsSync(skillPath))
508
+ writeText(path.join(bundle, ".opencode/skills", skill, "SKILL.md"), sanitizeText(readText(skillPath), "opencode", "<bundle-root>"));
509
+ }
510
+ writeText(path.join(bundle, ".opencode/plugins/flow-agents.js"), exportOpencodePlugin());
511
+ writeText(path.join(bundle, "opencode.json"), exportOpencodeConfig());
512
+ writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("opencode", agents, manifest.opencode.task_dir));
513
+ writeText(path.join(bundle, "README.md"), exportTargetReadme("opencode", "bash install.sh /path/to/workspace"));
514
+ writeText(path.join(bundle, "install.sh"), installScript("opencode", "/path/to/workspace"));
515
+ fs.chmodSync(path.join(bundle, "install.sh"), 0o755);
516
+ }
517
+ function exportPiExtension() {
518
+ // Generate the Flow Agents pi extension.
519
+ // pi extensions are auto-discovered from .pi/extensions/*.ts (needs project trust).
520
+ // pi has no named-subagent registry; agents are not exported. The extension
521
+ // provides workflow steering (via before_agent_start context injection),
522
+ // tool-call policy (via tool_call event), and telemetry delegation to shared scripts.
523
+ return `/**
524
+ * Flow Agents pi extension.
525
+ *
526
+ * Auto-discovered from .pi/extensions/flow-agents.ts at startup (needs project trust).
527
+ * Delegates policy and telemetry to shared scripts/hooks/ using spawnSync,
528
+ * mirroring the payload contract used by the claude/codex adapters.
529
+ *
530
+ * NOTE: pi has no named-subagent registry. Agents are not exported for pi.
531
+ * Rely on AGENTS.md + skills + this extension for workflow guidance.
532
+ */
533
+
534
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
535
+ import { spawnSync } from "node:child_process";
536
+ import { join, basename } from "node:path";
537
+
538
+ // pi may run extensions under a non-node runtime (Bun), where process.execPath
539
+ // is not a node binary and spawning it with a script path silently fails.
540
+ // Same failure class the opencode live smoke caught on 2026-06-11.
541
+ const NODE_BIN = basename(process.execPath).startsWith("node") ? process.execPath : "node";
542
+
543
+ export default function (pi: ExtensionAPI) {
544
+ const root = process.cwd();
545
+
546
+ function runAdapter(adapterScript: string, eventName: string, hookId: string, relScript: string): { allow: boolean; context?: string; reason?: string } {
547
+ const adapterPath = join(root, "scripts", "hooks", adapterScript);
548
+ const payload = JSON.stringify({ hook_event_name: eventName, cwd: root });
549
+ const result = spawnSync(NODE_BIN, [adapterPath, eventName, hookId, relScript, "default"], {
550
+ input: payload,
551
+ encoding: "utf8",
552
+ cwd: root,
553
+ env: { ...process.env, FLOW_AGENTS_HOOK_RUNTIME: "pi" },
554
+ timeout: 30000,
555
+ });
556
+ try {
557
+ return JSON.parse(result.stdout || "{}") as { allow: boolean; context?: string; reason?: string };
558
+ } catch {
559
+ return { allow: true };
560
+ }
561
+ }
562
+
563
+ function runTelemetry(eventName: string): void {
564
+ const telemetryPath = join(root, "scripts", "hooks", "pi-telemetry-hook.js");
565
+ const payload = JSON.stringify({ hook_event_name: eventName, cwd: root });
566
+ spawnSync(NODE_BIN, [telemetryPath, eventName, "dev"], {
567
+ input: payload,
568
+ encoding: "utf8",
569
+ cwd: root,
570
+ env: { ...process.env, FLOW_AGENTS_TELEMETRY_RUNTIME: "pi" },
571
+ timeout: 10000,
572
+ });
573
+ }
574
+
575
+ pi.on("session_start", async (_event, _ctx) => {
576
+ runTelemetry("session_start");
577
+ });
578
+
579
+ pi.on("before_agent_start", async (event, _ctx) => {
580
+ runTelemetry("before_agent_start");
581
+ // Inject workflow steering context at agent start
582
+ const result = runAdapter("pi-hook-adapter.js", "before_agent_start", "workflow-steering", "workflow-steering.js");
583
+ if (result.context) {
584
+ return {
585
+ systemPrompt: event.systemPrompt + "\\n\\n" + result.context,
586
+ };
587
+ }
588
+ });
589
+
590
+ pi.on("tool_call", async (event, _ctx) => {
591
+ runTelemetry("tool_call");
592
+ const result = runAdapter("pi-hook-adapter.js", "tool_call", "config-protection", "config-protection.js");
593
+ if (result && result.allow === false) {
594
+ return { block: true, reason: result.reason || "Blocked by Flow Agents hook policy." };
595
+ }
596
+ });
597
+
598
+ pi.on("tool_result", async (_event, _ctx) => {
599
+ runTelemetry("tool_result");
600
+ runAdapter("pi-hook-adapter.js", "tool_result", "quality-gate", "quality-gate.js");
601
+ });
602
+
603
+ pi.on("session_shutdown", async (_event, _ctx) => {
604
+ runTelemetry("session_shutdown");
605
+ runAdapter("pi-hook-adapter.js", "session_shutdown", "stop-goal-fit", "stop-goal-fit.js");
606
+ });
607
+ }
608
+ `;
609
+ }
610
+ function buildPi(agents) {
611
+ const bundle = path.join(dist, "pi");
612
+ resetDir(bundle);
613
+ copySharedContent(bundle, "pi", "<bundle-root>");
614
+ writeText(path.join(bundle, manifest.pi.task_dir, ".gitkeep"), "");
615
+ // pi has no named-subagent registry; agents are left canonical/unexported.
616
+ // Skills are exported to .pi/skills/ (direct .md files supported in that dir).
617
+ for (const skill of fs.readdirSync(path.join(root, "skills"))) {
618
+ const skillPath = path.join(root, "skills", skill, "SKILL.md");
619
+ if (fs.existsSync(skillPath))
620
+ writeText(path.join(bundle, ".pi/skills", skill, "SKILL.md"), sanitizeText(readText(skillPath), "pi", "<bundle-root>"));
621
+ }
622
+ writeText(path.join(bundle, ".pi/extensions/flow-agents.ts"), exportPiExtension());
623
+ writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("pi", agents, manifest.pi.task_dir));
624
+ writeText(path.join(bundle, "README.md"), exportTargetReadme("pi", "bash install.sh /path/to/workspace"));
625
+ writeText(path.join(bundle, "install.sh"), installScript("pi", "/path/to/workspace"));
626
+ fs.chmodSync(path.join(bundle, "install.sh"), 0o755);
627
+ }
364
628
  function buildCatalog(agents) {
365
629
  const kitsCatalog = path.join(root, "kits/catalog.json");
366
630
  return {
@@ -379,6 +643,8 @@ export function main() {
379
643
  buildKiro(agents);
380
644
  buildClaudeCode(agents);
381
645
  buildCodex(agents);
646
+ buildOpencode(agents);
647
+ buildPi(agents);
382
648
  writeText(path.join(dist, "catalog.json"), `${JSON.stringify(buildCatalog(agents), null, 2)}\n`);
383
649
  writeText(path.join(dist, "README.md"), "# Universal Bundles\n\nRun `npm run build:bundles` from the repo root to regenerate these bundles.\n");
384
650
  console.log("Built bundles:");
@@ -386,6 +652,8 @@ export function main() {
386
652
  console.log(" - dist/kiro");
387
653
  console.log(" - dist/claude-code");
388
654
  console.log(" - dist/codex");
655
+ console.log(" - dist/opencode");
656
+ console.log(" - dist/pi");
389
657
  if (printDiagnostics && dropDiagnostics.length) {
390
658
  console.error("Export sanitization diagnostics:");
391
659
  for (const item of dropDiagnostics)
@@ -108,10 +108,13 @@ export function main(argv = process.argv.slice(2)) {
108
108
  removed += pruneNamedDirs(rootDir, "skills", allPackMembers(packs, "skills"), selected.skills, dryRun);
109
109
  removed += pruneNamedDirs(rootDir, ".claude/skills", allPackMembers(packs, "skills"), selected.skills, dryRun);
110
110
  removed += pruneNamedDirs(rootDir, ".codex/skills", allPackMembers(packs, "skills"), selected.skills, dryRun);
111
+ removed += pruneNamedDirs(rootDir, ".opencode/skills", allPackMembers(packs, "skills"), selected.skills, dryRun);
112
+ removed += pruneNamedDirs(rootDir, ".pi/skills", allPackMembers(packs, "skills"), selected.skills, dryRun);
111
113
  removed += pruneNamedDirs(rootDir, "powers", allPackMembers(packs, "powers"), selected.powers, dryRun);
112
114
  removed += pruneAgentFiles(rootDir, "agents", ".json", allPackMembers(packs, "agents"), selected.agents, dryRun);
113
115
  removed += pruneAgentFiles(rootDir, ".claude/agents", ".md", allPackMembers(packs, "agents"), selected.agents, dryRun);
114
116
  removed += pruneAgentFiles(rootDir, ".codex/agents", ".toml", allPackMembers(packs, "agents"), selected.agents, dryRun);
117
+ removed += pruneAgentFiles(rootDir, ".opencode/agents", ".md", allPackMembers(packs, "agents"), selected.agents, dryRun);
115
118
  const summary = {
116
119
  selected_packs: [...selectedNames].sort(),
117
120
  removed_entries: removed,
@@ -62,12 +62,17 @@ const hookFilePolicies = new Map([
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$/,
@@ -6,7 +6,11 @@ TELEMETRY_CONFIG_FILE="${TELEMETRY_CONFIG_FILE:-${TELEMETRY_DIR}/telemetry.conf}
6
6
 
7
7
  # Defaults
8
8
  TELEMETRY_ENABLED="${TELEMETRY_ENABLED:-true}"
9
- TELEMETRY_DATA_DIR="${TELEMETRY_DATA_DIR:-$(cd "${TELEMETRY_DIR}/../../.." && pwd)/.telemetry}"
9
+ # TELEMETRY_DIR is <workspace>/scripts/telemetry, so the workspace root is
10
+ # two levels up. Three levels escaped into the workspace's PARENT directory
11
+ # (caught by live acceptance smoke 2026-06-11: events landed in /tmp/.telemetry
12
+ # instead of the installed workspace).
13
+ TELEMETRY_DATA_DIR="${TELEMETRY_DATA_DIR:-$(cd "${TELEMETRY_DIR}/../.." && pwd)/.telemetry}"
10
14
  TELEMETRY_SESSION_DIR="${TELEMETRY_SESSION_DIR:-${TELEMETRY_DATA_DIR}/sessions}"
11
15
  TELEMETRY_ENRICH_SYSTEM="${TELEMETRY_ENRICH_SYSTEM:-true}"
12
16
  TELEMETRY_ENRICH_WORKSPACE="${TELEMETRY_ENRICH_WORKSPACE:-true}"
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "../../schemas/flow-agents-settings.schema.json",
3
+ "schema_version": "1.0",
4
+ "utteranceCheck": {
5
+ "enabled": false
6
+ }
7
+ }
@@ -86,7 +86,7 @@ Flow Agents works like an agent workbench with seven cooperating layers:
86
86
  | Powers | Tool bundles and activation guidance for integrations. | `powers/` |
87
87
  | Agents | Specialist roles with scoped responsibilities. | `agents/`, `agent-cards/` |
88
88
  | Workflows | State, gates, handoffs, and task memory. | Kontour Flow concepts, `.flow-agents/`, `npm run workflow:sidecar --` |
89
- | Hooks | Just-in-time reminders or blockers from current workflow state. | `hooks/`, exported runtime configs |
89
+ | Hooks | Just-in-time reminders or blockers from current workflow state. | `scripts/hooks/`, exported runtime configs |
90
90
  | Evidence | Tests, evals, telemetry, findings, and outcome records. | `evals/`, `.telemetry/`, sidecars |
91
91
 
92
92
  Each layer should stay small enough to explain independently. When the system feels complicated, the fix is usually to move behavior to the right layer, not to add more global prompt text.
@@ -252,13 +252,12 @@ The intended pattern is that every important workflow rule gets a test at the lo
252
252
 
253
253
  Packs keep the global surface understandable.
254
254
 
255
- `packaging/packs.json` groups capabilities into sets such as:
255
+ `packaging/packs.json` groups capabilities into sets. Currently defined:
256
256
 
257
257
  - `core`
258
258
  - `development`
259
- - `knowledge`
260
- - `aws`
261
- - `experimental`
259
+
260
+ Future packs (knowledge, AWS, experimental) are deferred until another producer proof shows repeated friction.
262
261
 
263
262
  All-pack installs remain the default today. `FLOW_AGENTS_PACKS` lets users opt into a smaller installed surface, and domain depth belongs in packs so a global setup can be narrowed without changing the source bundle.
264
263
 
@@ -50,6 +50,7 @@ Machine-readable workflow state lives beside Markdown artifacts in `.flow-agents
50
50
  | Schema | Title | ID |
51
51
  | --- | --- | --- |
52
52
  | backlog-provider-settings.schema.json | Flow Agents Backlog Provider Settings | https://flow-agents.dev/schemas/backlog-provider-settings.schema.json |
53
+ | flow-agents-settings.schema.json | Flow Agents Settings | https://flow-agents.dev/schemas/flow-agents-settings.schema.json |
53
54
  | workflow-acceptance.schema.json | Flow Agents Workflow Acceptance | https://flow-agents.dev/schemas/workflow-acceptance.schema.json |
54
55
  | workflow-critique.schema.json | Flow Agents Workflow Critique | https://flow-agents.dev/schemas/workflow-critique.schema.json |
55
56
  | workflow-evidence.schema.json | Flow Agents Workflow Evidence | https://flow-agents.dev/schemas/workflow-evidence.schema.json |