@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
|
@@ -15,6 +15,9 @@ function usage() {
|
|
|
15
15
|
" --utterance TEXT Utterance text to check (required unless --not-configured).",
|
|
16
16
|
" --bundle-path FILE Trust bundle JSON file. Omit for an empty bundle (all unsupported).",
|
|
17
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).",
|
|
18
21
|
" --not-configured Skip survey call; output not_configured without error.",
|
|
19
22
|
" --strict Exit non-zero when any badge is disputed, rejected, or unsupported.",
|
|
20
23
|
" --help Show this help.",
|
|
@@ -51,6 +54,43 @@ async function loadSurvey() {
|
|
|
51
54
|
return undefined;
|
|
52
55
|
}
|
|
53
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
|
+
}
|
|
54
94
|
// ---------------------------------------------------------------------------
|
|
55
95
|
// Core check logic
|
|
56
96
|
// ---------------------------------------------------------------------------
|
|
@@ -63,6 +103,8 @@ async function runCheck(argv) {
|
|
|
63
103
|
const agentId = flagString(flags, "agent-id") ?? "flow-agents-utterance-check";
|
|
64
104
|
const notConfigured = flagBool(flags, "not-configured");
|
|
65
105
|
const strict = flagBool(flags, "strict");
|
|
106
|
+
const extractorName = flagString(flags, "extractor") ?? "reference";
|
|
107
|
+
const model = flagString(flags, "model");
|
|
66
108
|
if (notConfigured) {
|
|
67
109
|
const report = {
|
|
68
110
|
status: "not_configured",
|
|
@@ -106,9 +148,31 @@ async function runCheck(argv) {
|
|
|
106
148
|
return 1;
|
|
107
149
|
}
|
|
108
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
|
+
}
|
|
109
173
|
let trustReport;
|
|
110
174
|
try {
|
|
111
|
-
trustReport = await surveyAgentUtterance(utterance,
|
|
175
|
+
trustReport = await surveyAgentUtterance(utterance, extractor, {
|
|
112
176
|
bundle,
|
|
113
177
|
agentId,
|
|
114
178
|
});
|
|
@@ -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,6 +62,10 @@ 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"] }],
|
|
@@ -95,7 +99,7 @@ const requiredUsageFeedbackFiles = [
|
|
|
95
99
|
const fixtureOwnershipSelfAuditRefs = new Set([
|
|
96
100
|
"evals/integration/test_fixture_retirement_audit.sh",
|
|
97
101
|
]);
|
|
98
|
-
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"]);
|
|
99
103
|
const pythonCommandScanRoots = ["README.md", "docs", "context", "skills", "prompts", "agents", "evals", "scripts", "packaging", "package.json"];
|
|
100
104
|
const allowedPythonCommandFiles = [
|
|
101
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
|
-
|
|
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}"
|
package/docs/context-map.md
CHANGED
|
@@ -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 |
|
package/docs/index.md
CHANGED
|
@@ -4,12 +4,12 @@ title: Kontour Flow Agents
|
|
|
4
4
|
|
|
5
5
|
# Flow Agents
|
|
6
6
|
|
|
7
|
-
<p class="home-lede">
|
|
7
|
+
<p class="home-lede">A portable process-discipline layer for agentic work: canonical policies, evidence, and telemetry that compile to whatever hook surface a host exposes — coding-agent harnesses today, agent frameworks next. Flow Agents keeps work inspectable from idea to release readiness so you ask for outcomes and the system supplies the path, the state, the checks, and the proof.</p>
|
|
8
8
|
|
|
9
9
|
<div class="value-grid">
|
|
10
10
|
<section>
|
|
11
|
-
<strong>
|
|
12
|
-
<span>
|
|
11
|
+
<strong>Four canonical policies</strong>
|
|
12
|
+
<span>Workflow steering, quality gate, stop-goal-fit, and config protection — each a canonical script under <code>scripts/hooks/</code> that compiles to the host's native hook format. Claude Code and Codex are the L2 reference implementations.</span>
|
|
13
13
|
</section>
|
|
14
14
|
<section>
|
|
15
15
|
<strong>Survive context loss</strong>
|
|
@@ -41,7 +41,29 @@ flowchart LR
|
|
|
41
41
|
Evidence -->|not verified| Plan
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
Flow Agents adds the operating layer around the model: skills choose the right workflow, sidecars preserve state, hooks
|
|
44
|
+
Flow Agents adds the operating layer around the model: skills choose the right workflow, sidecars preserve state, hooks enforce the four canonical policies, and evals keep the bundle honest as it changes. The gate semantics underneath — definitions, runs, evidence, route-back — belong to <a href="https://kontourai.github.io/flow/">Kontour Flow</a>; Flow Agents compiles those policies to whatever hook surface a host exposes.
|
|
45
|
+
|
|
46
|
+
## Process-discipline layer
|
|
47
|
+
|
|
48
|
+
The four canonical policy classes are defined in the <a href="spec/runtime-hook-surface.html">Runtime Hook Surface spec</a> using a runtime-neutral vocabulary. Adapters translate them to the host's native hook format at three conformance levels: <strong>L0</strong> (telemetry only), <strong>L1</strong> (steering + stop-goal-fit warning), and <strong>L2</strong> (all four policies with blocking capability).
|
|
49
|
+
|
|
50
|
+
### Runtime and support matrix
|
|
51
|
+
|
|
52
|
+
| Tier | Runtime | Ships | Conformance |
|
|
53
|
+
| --- | --- | --- | --- |
|
|
54
|
+
| Core harness | Claude Code | install + hooks + bundle | L2 — reference implementation |
|
|
55
|
+
| Core harness | Codex | install + hooks + bundle | L2 — reference implementation |
|
|
56
|
+
| Core harness | Kiro | install + hooks + bundle | L2 |
|
|
57
|
+
| Core harness | opencode | agents, skills, plugin, opencode.json | L1 — no prompt-submit hook |
|
|
58
|
+
| Core harness | pi | extension, skills, AGENTS.md | L1 — no stop hook |
|
|
59
|
+
| Official framework adapter | AWS Strands (Python) | `integrations/strands/` spike/preview | L0 + config protection via cancellation |
|
|
60
|
+
| Conformance-certified | Community / third-party | Self-certify | Conformance kit in development |
|
|
61
|
+
|
|
62
|
+
Documented gaps: opencode has no native `prompt.submit`-equivalent event; pi has no stop hook; Codex live hook influence on model context is limited. The <a href="spec/runtime-hook-surface.html">Runtime Hook Surface spec</a> names every gap explicitly using the canonical event taxonomy.
|
|
63
|
+
|
|
64
|
+
## Framework adapters
|
|
65
|
+
|
|
66
|
+
The same canonical policies wire into agent frameworks as in-process language-native packages. `integrations/strands/` contains `flow-agents-strands`, a Python `HookProvider` that emits the canonical telemetry taxonomy and enforces config protection via `BeforeToolCallEvent` cancellation — 50 unit tests, no Strands SDK required. This is a spike/preview. See <a href="spec/runtime-hook-surface.html">the spec</a> for the full framework adapter mapping and minimum viable adapter pseudocode.
|
|
45
67
|
|
|
46
68
|
## Quick Start
|
|
47
69
|
|
|
@@ -49,6 +71,13 @@ Flow Agents adds the operating layer around the model: skills choose the right w
|
|
|
49
71
|
npx @kontourai/flow-agents init --dest /path/to/workspace
|
|
50
72
|
```
|
|
51
73
|
|
|
74
|
+
Runtime-specific installs:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx @kontourai/flow-agents init --runtime claude-code --dest /path/to/workspace --yes
|
|
78
|
+
npx @kontourai/flow-agents init --runtime opencode --dest /path/to/workspace --yes
|
|
79
|
+
npx @kontourai/flow-agents init --runtime pi --dest /path/to/workspace --yes
|
|
80
|
+
```
|
|
52
81
|
|
|
53
82
|
Then ask for the workflow you want, in plain language:
|
|
54
83
|
|
|
@@ -77,6 +106,14 @@ Use fix-bug. Reproduce the problem, diagnose root cause, implement the fix, and
|
|
|
77
106
|
<strong>Workflow Map</strong>
|
|
78
107
|
<span>See the core skills, gates, artifacts, and route-back behavior.</span>
|
|
79
108
|
</a>
|
|
109
|
+
<a class="doc-card" href="spec/runtime-hook-surface.html">
|
|
110
|
+
<strong>Runtime Hook Surface</strong>
|
|
111
|
+
<span>Canonical event taxonomy, four policy classes, conformance levels L0/L1/L2, and host mapping tables for adapter authors.</span>
|
|
112
|
+
</a>
|
|
113
|
+
<a class="doc-card" href="vision.html">
|
|
114
|
+
<strong>Vision and Direction</strong>
|
|
115
|
+
<span>Where Flow Agents is going: kits beyond coding, TypeScript framework adapters, and Kontour Console as the unifying telemetry surface.</span>
|
|
116
|
+
</a>
|
|
80
117
|
<a class="doc-card" href="north-star.html">
|
|
81
118
|
<strong>North Star</strong>
|
|
82
119
|
<span>The product promise, design principles, operating layers, and roadmap.</span>
|
|
@@ -117,6 +154,10 @@ Use fix-bug. Reproduce the problem, diagnose root cause, implement the fix, and
|
|
|
117
154
|
<strong>Developer Reference</strong>
|
|
118
155
|
<span>The generated repo map: commands, agents, skills, scripts, and contracts.</span>
|
|
119
156
|
</a>
|
|
157
|
+
<a class="doc-card" href="integrations/index.html">
|
|
158
|
+
<strong>Integration Examples</strong>
|
|
159
|
+
<span>Worked examples for harness runtimes (Claude Code, opencode, pi), framework adapters (AWS Strands), and third-party self-certification with the conformance kit.</span>
|
|
160
|
+
</a>
|
|
120
161
|
</div>
|
|
121
162
|
|
|
122
163
|
## The Kontour family
|