@kontourai/flow-agents 1.0.1 → 1.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 (111) hide show
  1. package/.github/workflows/ci.yml +110 -0
  2. package/.github/workflows/runtime-compat.yml +5 -2
  3. package/CHANGELOG.md +42 -0
  4. package/README.md +26 -5
  5. package/build/src/cli/console-learning-projection.js +19 -2
  6. package/build/src/cli/effective-backlog-settings.js +18 -2
  7. package/build/src/cli/fixture-retirement-audit.js +19 -2
  8. package/build/src/cli/init.js +19 -2
  9. package/build/src/cli/{flow-kit.js → kit.js} +122 -108
  10. package/build/src/cli/promote-workflow-artifact.js +19 -2
  11. package/build/src/cli/publish-change-helper.js +19 -2
  12. package/build/src/cli/pull-work-provider.js +19 -2
  13. package/build/src/cli/runtime-adapter.js +20 -2
  14. package/build/src/cli/usage-feedback.js +19 -2
  15. package/build/src/cli/utterance-check.js +19 -2
  16. package/build/src/cli/validate-hook-influence.js +19 -2
  17. package/build/src/cli/validate-source-tree.js +4 -4
  18. package/build/src/cli/veritas-governance.js +19 -2
  19. package/build/src/cli/workflow-artifact-cleanup-audit.js +19 -2
  20. package/build/src/cli.js +3 -3
  21. package/build/src/flow-kit/validate.js +58 -62
  22. package/build/src/runtime-adapters.js +55 -24
  23. package/build/src/tools/build-universal-bundles.js +83 -19
  24. package/build/src/tools/generate-context-map.js +68 -9
  25. package/build/src/tools/validate-package.js +19 -2
  26. package/build/src/tools/validate-source-tree.js +51 -3
  27. package/context/scripts/telemetry/console-presets.sh +1 -1
  28. package/docs/adr/0007-flow-skill-kit-tool-boundary.md +169 -0
  29. package/docs/adr/0007-skill-audit.md +112 -0
  30. package/docs/adr/0008-kit-operation-boundary.md +88 -0
  31. package/docs/context-map.md +18 -22
  32. package/docs/flow-kit-repository-contract.md +5 -5
  33. package/docs/getting-started.md +177 -0
  34. package/docs/index.md +19 -8
  35. package/docs/kit-authoring-guide.md +46 -10
  36. package/docs/knowledge-kit.md +2 -2
  37. package/docs/spec/runtime-hook-surface.md +1 -1
  38. package/docs/vision.md +1 -1
  39. package/docs/workflow-usage-guide.md +1 -1
  40. package/evals/ci/run-baseline.sh +55 -8
  41. package/evals/fixtures/builder-kit-workflow-state/happy-path.json +2 -2
  42. package/evals/fixtures/builder-kit-workflow-state/mid-work-resume.json +2 -2
  43. package/evals/fixtures/console-learning-projection/artifacts/console-learning-correction/learning.json +1 -1
  44. package/evals/fixtures/pull-work-provider/github-issues.json +5 -5
  45. package/evals/integration/test_activate_npx_context.sh +2 -2
  46. package/evals/integration/test_bundle_install.sh +17 -12
  47. package/evals/integration/test_console_learning_projection.sh +1 -1
  48. package/evals/integration/test_flow_kit_install_git.sh +7 -7
  49. package/evals/integration/test_flow_kit_repository.sh +4 -4
  50. package/evals/integration/test_kit_conformance_levels.sh +1 -1
  51. package/evals/integration/test_local_flow_kit_install.sh +7 -7
  52. package/evals/integration/test_publish_change_helper.sh +1 -1
  53. package/evals/integration/test_pull_work_provider.sh +1 -1
  54. package/evals/integration/test_runtime_adapter_activation.sh +140 -19
  55. package/evals/lib/node.sh +2 -2
  56. package/evals/run.sh +2 -0
  57. package/evals/static/test_console_presets.sh +49 -0
  58. package/evals/static/test_workflow_skills.sh +15 -15
  59. package/integrations/strands/flow_agents_strands/steering.py +1 -1
  60. package/integrations/strands-ts/src/hooks.ts +1 -1
  61. package/kits/builder/kit.json +17 -0
  62. package/{skills → kits/builder/skills}/builder-shape/SKILL.md +4 -4
  63. package/{skills → kits/builder/skills}/idea-to-backlog/SKILL.md +1 -1
  64. package/kits/knowledge/kit.json +16 -9
  65. package/package.json +8 -5
  66. package/packaging/packs.json +1 -21
  67. package/scripts/README.md +1 -1
  68. package/scripts/kit.js +2 -0
  69. package/scripts/telemetry/console-presets.sh +1 -1
  70. package/skills/README.md +23 -0
  71. package/src/cli/console-learning-projection.ts +7 -1
  72. package/src/cli/effective-backlog-settings.ts +6 -1
  73. package/src/cli/fixture-retirement-audit.ts +7 -1
  74. package/src/cli/init.ts +7 -1
  75. package/src/cli/{flow-kit.ts → kit.ts} +124 -109
  76. package/src/cli/promote-workflow-artifact.ts +7 -1
  77. package/src/cli/publish-change-helper.ts +7 -1
  78. package/src/cli/pull-work-provider.ts +7 -1
  79. package/src/cli/runtime-adapter.ts +8 -1
  80. package/src/cli/usage-feedback.ts +7 -1
  81. package/src/cli/utterance-check.ts +7 -1
  82. package/src/cli/validate-hook-influence.ts +7 -1
  83. package/src/cli/validate-source-tree.ts +4 -4
  84. package/src/cli/veritas-governance.ts +7 -1
  85. package/src/cli/workflow-artifact-cleanup-audit.ts +7 -1
  86. package/src/cli.ts +3 -3
  87. package/src/flow-kit/validate.ts +63 -57
  88. package/src/runtime-adapters.ts +54 -26
  89. package/src/tools/build-universal-bundles.ts +67 -14
  90. package/src/tools/generate-context-map.ts +43 -7
  91. package/src/tools/validate-package.ts +7 -1
  92. package/src/tools/validate-source-tree.ts +34 -2
  93. package/scripts/flow-kit.js +0 -2
  94. package/skills/context-budget/SKILL.md +0 -40
  95. package/skills/explore/SKILL.md +0 -137
  96. package/skills/feedback-loop/SKILL.md +0 -87
  97. package/skills/frontend-design/SKILL.md +0 -80
  98. /package/{skills → kits/builder/skills}/deliver/SKILL.md +0 -0
  99. /package/{skills → kits/builder/skills}/design-probe/SKILL.md +0 -0
  100. /package/{skills → kits/builder/skills}/evidence-gate/SKILL.md +0 -0
  101. /package/{skills → kits/builder/skills}/execute-plan/SKILL.md +0 -0
  102. /package/{skills → kits/builder/skills}/fix-bug/SKILL.md +0 -0
  103. /package/{skills → kits/builder/skills}/learning-review/SKILL.md +0 -0
  104. /package/{skills → kits/builder/skills}/pickup-probe/SKILL.md +0 -0
  105. /package/{skills → kits/builder/skills}/plan-work/SKILL.md +0 -0
  106. /package/{skills → kits/builder/skills}/pull-work/SKILL.md +0 -0
  107. /package/{skills → kits/builder/skills}/release-readiness/SKILL.md +0 -0
  108. /package/{skills → kits/builder/skills}/review-work/SKILL.md +0 -0
  109. /package/{skills → kits/builder/skills}/tdd-workflow/SKILL.md +0 -0
  110. /package/{skills → kits/builder/skills}/verify-work/SKILL.md +0 -0
  111. /package/{skills → kits/knowledge/skills}/knowledge-capture/SKILL.md +0 -0
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import { fileURLToPath } from "node:url";
2
3
  import * as path from "node:path";
3
4
  import { parseArgs, flagString } from "../lib/args.js";
4
5
  import { isoNow, relPath } from "../lib/fs.js";
@@ -59,5 +60,21 @@ export function main(argv = process.argv.slice(2)) {
59
60
  console.log(`archived_artifact=${archive}`);
60
61
  return 0;
61
62
  }
62
- if (import.meta.url === `file://${process.argv[1]}`)
63
- process.exit(main());
63
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
64
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
65
+ // entry-point guard fires correctly when the module is loaded directly as a script.
66
+ const _selfRealPath = (() => { try {
67
+ return fs.realpathSync(fileURLToPath(import.meta.url));
68
+ }
69
+ catch {
70
+ return fileURLToPath(import.meta.url);
71
+ } })();
72
+ const _argv1RealPath = (() => { try {
73
+ return fs.realpathSync(process.argv[1]);
74
+ }
75
+ catch {
76
+ return process.argv[1];
77
+ } })();
78
+ if (_selfRealPath === _argv1RealPath) {
79
+ process.exitCode = main();
80
+ }
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import { fileURLToPath } from "node:url";
2
3
  import * as path from "node:path";
3
4
  import { parseArgs, flagString } from "../lib/args.js";
4
5
  import { readJson } from "../lib/fs.js";
@@ -150,5 +151,21 @@ export function main(argv = process.argv.slice(2)) {
150
151
  return 1;
151
152
  }
152
153
  }
153
- if (import.meta.url === `file://${process.argv[1]}`)
154
- process.exit(main());
154
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
155
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
156
+ // entry-point guard fires correctly when the module is loaded directly as a script.
157
+ const _selfRealPath = (() => { try {
158
+ return fs.realpathSync(fileURLToPath(import.meta.url));
159
+ }
160
+ catch {
161
+ return fileURLToPath(import.meta.url);
162
+ } })();
163
+ const _argv1RealPath = (() => { try {
164
+ return fs.realpathSync(process.argv[1]);
165
+ }
166
+ catch {
167
+ return process.argv[1];
168
+ } })();
169
+ if (_selfRealPath === _argv1RealPath) {
170
+ process.exitCode = main();
171
+ }
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import { fileURLToPath } from "node:url";
2
3
  import { parseArgs, flagList, flagString } from "../lib/args.js";
3
4
  const FLOW_ARTIFACT_PATTERN = /(?<path>\.flow-agents\/[^\s`'")]+)/g;
4
5
  const GITHUB_ISSUE_PATTERN = /(?:(?<owner>[A-Za-z0-9_.-]+)\/(?<repo>[A-Za-z0-9_.-]+))?#(?<number>\d+)/g;
@@ -465,5 +466,21 @@ export function main(argv = process.argv.slice(2)) {
465
466
  console.log(JSON.stringify({ items }, null, 2));
466
467
  return 0;
467
468
  }
468
- if (import.meta.url === `file://${process.argv[1]}`)
469
- process.exit(main());
469
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
470
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
471
+ // entry-point guard fires correctly when the module is loaded directly as a script.
472
+ const _selfRealPath = (() => { try {
473
+ return fs.realpathSync(fileURLToPath(import.meta.url));
474
+ }
475
+ catch {
476
+ return fileURLToPath(import.meta.url);
477
+ } })();
478
+ const _argv1RealPath = (() => { try {
479
+ return fs.realpathSync(process.argv[1]);
480
+ }
481
+ catch {
482
+ return process.argv[1];
483
+ } })();
484
+ if (_selfRealPath === _argv1RealPath) {
485
+ process.exitCode = main();
486
+ }
@@ -1,3 +1,5 @@
1
+ import * as fs from "node:fs";
2
+ import { fileURLToPath } from "node:url";
1
3
  import * as path from "node:path";
2
4
  import { parseArgs, flagString } from "../lib/args.js";
3
5
  import { activateCodexLocal, activateStrandsLocal } from "../runtime-adapters.js";
@@ -23,5 +25,21 @@ export function main(argv = process.argv.slice(2)) {
23
25
  console.log(JSON.stringify(result, null, 2));
24
26
  return Array.isArray(result.errors) && result.errors.length ? 1 : 0;
25
27
  }
26
- if (import.meta.url === `file://${process.argv[1]}`)
27
- process.exit(main());
28
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
29
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
30
+ // entry-point guard fires correctly when the module is loaded directly as a script.
31
+ const _selfRealPath = (() => { try {
32
+ return fs.realpathSync(fileURLToPath(import.meta.url));
33
+ }
34
+ catch {
35
+ return fileURLToPath(import.meta.url);
36
+ } })();
37
+ const _argv1RealPath = (() => { try {
38
+ return fs.realpathSync(process.argv[1]);
39
+ }
40
+ catch {
41
+ return process.argv[1];
42
+ } })();
43
+ if (_selfRealPath === _argv1RealPath) {
44
+ process.exitCode = main();
45
+ }
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import { fileURLToPath } from "node:url";
2
3
  import * as os from "node:os";
3
4
  import * as path from "node:path";
4
5
  import { parseArgs, flagBool, flagList, flagString } from "../lib/args.js";
@@ -439,5 +440,21 @@ export function main(argv = process.argv.slice(2)) {
439
440
  return 1;
440
441
  }
441
442
  }
442
- if (import.meta.url === `file://${process.argv[1]}`)
443
- process.exit(main());
443
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
444
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
445
+ // entry-point guard fires correctly when the module is loaded directly as a script.
446
+ const _selfRealPath = (() => { try {
447
+ return fs.realpathSync(fileURLToPath(import.meta.url));
448
+ }
449
+ catch {
450
+ return fileURLToPath(import.meta.url);
451
+ } })();
452
+ const _argv1RealPath = (() => { try {
453
+ return fs.realpathSync(process.argv[1]);
454
+ }
455
+ catch {
456
+ return process.argv[1];
457
+ } })();
458
+ if (_selfRealPath === _argv1RealPath) {
459
+ process.exitCode = main();
460
+ }
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import { fileURLToPath } from "node:url";
2
3
  import * as path from "node:path";
3
4
  import { flagBool, flagString, parseArgs } from "../lib/args.js";
4
5
  // ---------------------------------------------------------------------------
@@ -232,5 +233,21 @@ export async function main(argv = process.argv.slice(2)) {
232
233
  }
233
234
  return runCheck(rest);
234
235
  }
235
- if (import.meta.url === `file://${process.argv[1]}`)
236
- process.exit(await main());
236
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
237
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
238
+ // entry-point guard fires correctly when the module is loaded directly as a script.
239
+ const _selfRealPath = (() => { try {
240
+ return fs.realpathSync(fileURLToPath(import.meta.url));
241
+ }
242
+ catch {
243
+ return fileURLToPath(import.meta.url);
244
+ } })();
245
+ const _argv1RealPath = (() => { try {
246
+ return fs.realpathSync(process.argv[1]);
247
+ }
248
+ catch {
249
+ return process.argv[1];
250
+ } })();
251
+ if (_selfRealPath === _argv1RealPath) {
252
+ process.exitCode = await main();
253
+ }
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import { fileURLToPath } from "node:url";
2
3
  const validTiers = new Set(["adapter", "design-target", "installed-command", "live-acceptance", "documented-runtime-gap"]);
3
4
  const validRuntimes = new Set(["codex", "claude-code", "kiro-cli"]);
4
5
  const validHooks = new Set(["workflow-steering", "stop-goal-fit"]);
@@ -148,5 +149,21 @@ export function main(argv = process.argv.slice(2)) {
148
149
  return 1;
149
150
  }
150
151
  }
151
- if (import.meta.url === `file://${process.argv[1]}`)
152
- process.exit(main());
152
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
153
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
154
+ // entry-point guard fires correctly when the module is loaded directly as a script.
155
+ const _selfRealPath = (() => { try {
156
+ return fs.realpathSync(fileURLToPath(import.meta.url));
157
+ }
158
+ catch {
159
+ return fileURLToPath(import.meta.url);
160
+ } })();
161
+ const _argv1RealPath = (() => { try {
162
+ return fs.realpathSync(process.argv[1]);
163
+ }
164
+ catch {
165
+ return process.argv[1];
166
+ } })();
167
+ if (_selfRealPath === _argv1RealPath) {
168
+ process.exitCode = main();
169
+ }
@@ -1,11 +1,11 @@
1
1
  import * as path from "node:path";
2
2
  import { parseArgs } from "../lib/args.js";
3
3
  import { validateKitRepository } from "../flow-kit/validate.js";
4
- export function main(argv = process.argv.slice(2)) {
4
+ export async function main(argv = process.argv.slice(2)) {
5
5
  const args = parseArgs(argv);
6
6
  const kit = args.flags.kit;
7
7
  if (typeof kit === "string") {
8
- const errors = validateKitRepository(path.resolve(kit));
8
+ const errors = await validateKitRepository(path.resolve(kit));
9
9
  if (errors.length) {
10
10
  console.log("Flow Kit repository validation failed:");
11
11
  for (const error of errors)
@@ -17,7 +17,7 @@ export function main(argv = process.argv.slice(2)) {
17
17
  }
18
18
  const root = path.resolve(".");
19
19
  const builder = path.join(root, "kits", "builder");
20
- const errors = validateKitRepository(builder);
20
+ const errors = await validateKitRepository(builder);
21
21
  if (errors.length) {
22
22
  console.log("Source tree validation failed:");
23
23
  for (const error of errors)
@@ -44,5 +44,5 @@ catch {
44
44
  return process.argv[1];
45
45
  } })();
46
46
  if (_selfVST === _argv1VST) {
47
- process.exitCode = main();
47
+ main().then((code) => { process.exitCode = code; }).catch((err) => { console.error(err); process.exitCode = 1; });
48
48
  }
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import { fileURLToPath } from "node:url";
2
3
  import * as path from "node:path";
3
4
  import { spawnSync } from "node:child_process";
4
5
  import { flagBool, flagString, parseArgs } from "../lib/args.js";
@@ -258,5 +259,21 @@ export function main(argv = process.argv.slice(2)) {
258
259
  const options = parseEvidenceOptions(argv);
259
260
  return options ? runEvidence(options) : 2;
260
261
  }
261
- if (import.meta.url === `file://${process.argv[1]}`)
262
- process.exit(main());
262
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
263
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
264
+ // entry-point guard fires correctly when the module is loaded directly as a script.
265
+ const _selfRealPath = (() => { try {
266
+ return fs.realpathSync(fileURLToPath(import.meta.url));
267
+ }
268
+ catch {
269
+ return fileURLToPath(import.meta.url);
270
+ } })();
271
+ const _argv1RealPath = (() => { try {
272
+ return fs.realpathSync(process.argv[1]);
273
+ }
274
+ catch {
275
+ return process.argv[1];
276
+ } })();
277
+ if (_selfRealPath === _argv1RealPath) {
278
+ process.exitCode = main();
279
+ }
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import { fileURLToPath } from "node:url";
2
3
  import * as path from "node:path";
3
4
  import { flagBool, flagString, parseArgs } from "../lib/args.js";
4
5
  const ACTIVE_STATUSES = new Set([
@@ -268,5 +269,21 @@ export function main(argv = process.argv.slice(2)) {
268
269
  printText(result);
269
270
  return 0;
270
271
  }
271
- if (import.meta.url === `file://${process.argv[1]}`)
272
- process.exit(main());
272
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
273
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
274
+ // entry-point guard fires correctly when the module is loaded directly as a script.
275
+ const _selfRealPath = (() => { try {
276
+ return fs.realpathSync(fileURLToPath(import.meta.url));
277
+ }
278
+ catch {
279
+ return fileURLToPath(import.meta.url);
280
+ } })();
281
+ const _argv1RealPath = (() => { try {
282
+ return fs.realpathSync(process.argv[1]);
283
+ }
284
+ catch {
285
+ return process.argv[1];
286
+ } })();
287
+ if (_selfRealPath === _argv1RealPath) {
288
+ process.exitCode = main();
289
+ }
package/build/src/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { basename } from "node:path";
3
3
  import { main as effectiveBacklogSettings } from "./cli/effective-backlog-settings.js";
4
4
  import { main as consoleLearningProjection } from "./cli/console-learning-projection.js";
5
- import { main as flowKit } from "./cli/flow-kit.js";
5
+ import { main as kit } from "./cli/kit.js";
6
6
  import { main as fixtureRetirementAudit } from "./cli/fixture-retirement-audit.js";
7
7
  import { main as init } from "./cli/init.js";
8
8
  import { main as promoteWorkflowArtifact } from "./cli/promote-workflow-artifact.js";
@@ -27,7 +27,7 @@ const availableCommands = new Map([
27
27
  ["effective-backlog-settings", effectiveBacklogSettings],
28
28
  ["filter-installed-packs", filterInstalledPacks],
29
29
  ["fixture-retirement-audit", fixtureRetirementAudit],
30
- ["flow-kit", flowKit],
30
+ ["kit", kit],
31
31
  ["init", init],
32
32
  ["promote-workflow-artifact", promoteWorkflowArtifact],
33
33
  ["publish-change", publishChange],
@@ -49,7 +49,7 @@ const aliases = new Map([
49
49
  ["flow-agents-effective-backlog-settings", "effective-backlog-settings"],
50
50
  ["flow-agents-filter-installed-packs", "filter-installed-packs"],
51
51
  ["flow-agents-fixture-retirement-audit", "fixture-retirement-audit"],
52
- ["flow-agents-flow-kit", "flow-kit"],
52
+ ["flow-agents-kit", "kit"],
53
53
  ["flow-agents-promote-workflow-artifact", "promote-workflow-artifact"],
54
54
  ["flow-agents-publish-change", "publish-change"],
55
55
  ["flow-agents-pull-work-provider", "pull-work-provider"],
@@ -1,49 +1,52 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { readJson } from "../lib/fs.js";
4
- const ASSET_CLASSES = ["flows", "skills", "docs", "adapters", "evals", "assets"];
4
+ // Extension-only asset classes: validated by Flow Agents. Flows are validated by @kontourai/flow.
5
+ const EXTENSION_ASSET_CLASSES = ["skills", "docs", "adapters", "evals", "assets"];
5
6
  // Core container fields owned by kontourai/flow (flow-kit-container.schema.json).
6
7
  // agent-extension fields are skills, docs, adapters, evals, assets.
7
8
  const CORE_CONTAINER_FIELDS = new Set(["schema_version", "id", "name", "description", "product_name", "flows"]);
8
9
  const AGENT_EXTENSION_CLASSES = new Set(["skills", "docs", "adapters", "evals", "assets"]);
10
+ let _validateKitContainerCache = null;
11
+ async function loadValidateKitContainer() {
12
+ if (_validateKitContainerCache)
13
+ return _validateKitContainerCache;
14
+ let mod;
15
+ try {
16
+ mod = await import("@kontourai/flow");
17
+ }
18
+ catch (err) {
19
+ throw new Error("container validation requires @kontourai/flow; run from an npm-installed flow-agents workspace " +
20
+ `or use 'flow kit validate' (original error: ${err.message})`);
21
+ }
22
+ if (typeof mod.validateKitContainer !== "function") {
23
+ throw new Error("@kontourai/flow did not export validateKitContainer");
24
+ }
25
+ _validateKitContainerCache = mod.validateKitContainer;
26
+ return _validateKitContainerCache;
27
+ }
9
28
  /**
10
- * Validates that the manifest satisfies the core Flow Kit container contract
11
- * (as specified by kontourai/flow PR #67) with all agent-extension fields stripped.
12
- * Returns a list of violation messages (empty = valid).
29
+ * Delegates core Flow Kit container validation to @kontourai/flow's validateKitContainer.
30
+ * The container contract lives once, in Flow. Returns a list of violation messages (empty = valid).
13
31
  *
14
32
  * The degradation invariant: every Flow Agents Kit MUST remain a valid core
15
33
  * Flow Kit container when agent-extension fields are ignored.
34
+ *
35
+ * Loads @kontourai/flow lazily (on first call) so that runtime ops (list/status/activate)
36
+ * that never invoke validation can run in standalone installed bundles where
37
+ * @kontourai/flow is not present.
38
+ *
39
+ * @param kitDir Real kit directory path for file-existence checks on flows[].path entries.
40
+ * Pass the actual kit directory when available; pass "" for structural-only checks.
16
41
  */
17
- export function validateCoreContainer(manifest, label) {
18
- const errors = [];
19
- if (manifest.schema_version !== "1.0") {
20
- errors.push(`${label}: .schema_version must be "1.0"`);
21
- }
22
- if (typeof manifest.id !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(manifest.id)) {
23
- errors.push(`${label}: .id must be a stable kebab-case string`);
24
- }
25
- if (typeof manifest.name !== "string" || !manifest.name.trim()) {
26
- errors.push(`${label}: .name must be a non-empty string`);
27
- }
28
- if (!Array.isArray(manifest.flows) || manifest.flows.length === 0) {
29
- errors.push(`${label}: .flows must be a non-empty list`);
30
- }
31
- else {
32
- manifest.flows.forEach((entry, index) => {
33
- if (typeof entry !== "object" || entry === null) {
34
- errors.push(`${label}: flows[${index}] must be an object`);
35
- return;
36
- }
37
- const flow = entry;
38
- if (typeof flow.id !== "string" || !flow.id) {
39
- errors.push(`${label}: flows[${index}].id must be a string`);
40
- }
41
- if (typeof flow.path !== "string" || !flow.path) {
42
- errors.push(`${label}: flows[${index}].path must be a string`);
43
- }
44
- });
45
- }
46
- return errors;
42
+ async function delegateCoreContainerValidation(kitDir, manifest) {
43
+ const validateKitContainer = await loadValidateKitContainer();
44
+ const result = validateKitContainer(kitDir, manifest);
45
+ if (result.valid)
46
+ return [];
47
+ return result.diagnostics
48
+ .filter((d) => d.severity === "error")
49
+ .map((d) => `${d.path}: ${d.message}`);
47
50
  }
48
51
  /**
49
52
  * Derives the consumer-target level (K0/K1/K2) and target audience list from
@@ -56,11 +59,16 @@ export function validateCoreContainer(manifest, label) {
56
59
  * - targets.flow: always present when K0 (any Flow consumer can evaluate gates).
57
60
  * - targets.flow-agents: present when K1 (agent extension assets activate in >=1 harness).
58
61
  * - third-party: any top-level keys that are not core fields and not Flow Agents extension classes.
62
+ *
63
+ * @param manifest The kit.json manifest object.
64
+ * @param kitDir Kit directory for flow file-existence checks. Defaults to "" (structural-only).
65
+ * Pass the real kit directory from `inspect` to get authoritative K0 validation.
59
66
  */
60
- export function deriveKitTargets(manifest) {
67
+ export async function deriveKitTargets(manifest, kitDir = "") {
61
68
  const kitId = typeof manifest.id === "string" ? manifest.id : "<unknown>";
62
69
  const kitName = typeof manifest.name === "string" ? manifest.name : "<unknown>";
63
- const coreErrors = validateCoreContainer(manifest, "kit.json");
70
+ // Delegate core container validation to @kontourai/flow.
71
+ const coreErrors = await delegateCoreContainerValidation(kitDir, manifest);
64
72
  const k0 = coreErrors.length === 0;
65
73
  const hasAgentExtension = AGENT_EXTENSION_CLASSES.size > 0 &&
66
74
  [...AGENT_EXTENSION_CLASSES].some((cls) => Array.isArray(manifest[cls]) && manifest[cls].length > 0);
@@ -87,7 +95,7 @@ export function deriveKitTargets(manifest) {
87
95
  third_party_extensions: thirdPartyExtensions,
88
96
  };
89
97
  }
90
- export function validateKitRepository(kitDir) {
98
+ export async function validateKitRepository(kitDir) {
91
99
  const errors = [];
92
100
  const manifestPath = path.join(kitDir, "kit.json");
93
101
  let manifest;
@@ -98,27 +106,16 @@ export function validateKitRepository(kitDir) {
98
106
  errors.push(`${manifestPath}: invalid JSON: ${error.message}`);
99
107
  return errors;
100
108
  }
101
- if (manifest.schema_version !== "1.0")
102
- errors.push(`${manifestPath}: .schema_version must be "1.0"`);
103
- if (typeof manifest.id !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(manifest.id)) {
104
- errors.push(`${manifestPath}: .id must be a stable kebab-case string`);
105
- }
106
- if (typeof manifest.name !== "string" || !manifest.name.trim())
107
- errors.push(`${manifestPath}: .name must be a non-empty string`);
108
- // Degradation invariant: every Flow Agents Kit must remain a valid core Flow Kit container
109
- // when agent-extension fields are stripped. Strip extensions and re-validate core contract.
110
- const coreManifest = {};
111
- for (const key of Object.keys(manifest)) {
112
- if (CORE_CONTAINER_FIELDS.has(key))
113
- coreManifest[key] = manifest[key];
114
- }
115
- const coreErrors = validateCoreContainer(coreManifest, manifestPath);
116
- for (const err of coreErrors) {
117
- // Deduplicate: only add if not already covered by top-level checks above.
118
- if (!errors.some((existing) => existing === err))
119
- errors.push(err);
120
- }
121
- for (const section of ASSET_CLASSES) {
109
+ // Delegate core container validation (schema_version, id, name, flows including file
110
+ // existence) to @kontourai/flow — the container contract lives once, in Flow.
111
+ // This enforces the degradation invariant: a Flow Agents Kit must remain a valid
112
+ // core Flow Kit container when extension fields are stripped.
113
+ const coreErrors = await delegateCoreContainerValidation(kitDir, manifest);
114
+ for (const err of coreErrors)
115
+ errors.push(err);
116
+ // Flow Agents extension validation: skills, docs, adapters, evals, assets.
117
+ // Flows are validated above by @kontourai/flow; only extension classes are checked here.
118
+ for (const section of EXTENSION_ASSET_CLASSES) {
122
119
  const entries = manifest[section];
123
120
  if (entries === undefined)
124
121
  continue;
@@ -155,15 +152,14 @@ export function validateKitRepository(kitDir) {
155
152
  return;
156
153
  }
157
154
  if (!fs.existsSync(resolved)) {
158
- const noun = section === "flows" ? "Flow Definition" : "asset";
159
- errors.push(`${manifestPath}: ${section}[${index}].path points at missing ${noun}: ${rel}`);
155
+ errors.push(`${manifestPath}: ${section}[${index}].path points at missing asset: ${rel}`);
160
156
  }
161
157
  });
162
158
  }
163
159
  return errors;
164
160
  }
165
- export function assertKitRepository(kitDir) {
166
- const errors = validateKitRepository(kitDir);
161
+ export async function assertKitRepository(kitDir) {
162
+ const errors = await validateKitRepository(kitDir);
167
163
  if (errors.length) {
168
164
  const error = new Error("Flow Kit repository validation failed");
169
165
  error.diagnostics = errors;
@@ -118,31 +118,48 @@ export function readKitInventory(sourceRoot, dest) {
118
118
  function safeSegment(value) {
119
119
  return value.replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^[.-]+|[.-]+$/g, "") || "asset";
120
120
  }
121
+ // Asset classes that are directly activated (copied to the runtime directory) by both adapters.
122
+ // flows: gate definitions read by the adapter's flow-routing layer.
123
+ // skills: agent guidance markdown copied to skills/<kit-id>/ for agent discovery.
124
+ // docs: documentation markdown copied to docs/<kit-id>/ for agent reference.
125
+ const ACTIVATED_ASSET_CLASSES = new Set(["flows", "skills", "docs"]);
121
126
  export function activateCodexLocal(sourceRoot, dest) {
122
127
  const inventory = readKitInventory(sourceRoot, dest);
123
128
  const runtimeDir = path.join(dest, ".flow-agents", "runtime", "codex");
124
129
  const generated = [];
125
130
  const skipped = [];
126
131
  for (const asset of inventory.assets) {
127
- if (asset.asset_class !== "flows") {
128
- skipped.push({ asset_class: asset.asset_class, path: asset.relative_path, kit_id: asset.kit_id, asset_id: asset.asset_id, reason: "asset class is diagnostic-only for codex-local" });
129
- continue;
132
+ if (asset.asset_class === "flows") {
133
+ if (!asset.asset_id) {
134
+ skipped.push({ asset_class: asset.asset_class, path: asset.relative_path, kit_id: asset.kit_id, asset_id: null, reason: "flow asset is missing an id" });
135
+ continue;
136
+ }
137
+ const output = path.join(runtimeDir, "flows", safeSegment(asset.kit_id), `${safeSegment(asset.asset_id)}.flow.json`);
138
+ fs.mkdirSync(path.dirname(output), { recursive: true });
139
+ fs.copyFileSync(asset.source_path, output);
140
+ generated.push({ asset_class: asset.asset_class, path: relPath(dest, output), kit_id: asset.kit_id, asset_id: asset.asset_id, source_path: asset.source_path.split(path.sep).join("/") });
130
141
  }
131
- if (!asset.asset_id) {
132
- skipped.push({ asset_class: asset.asset_class, path: asset.relative_path, kit_id: asset.kit_id, asset_id: null, reason: "flow asset is missing an id" });
133
- continue;
142
+ else if (asset.asset_class === "skills" || asset.asset_class === "docs") {
143
+ // Copy skills and docs to runtime/<adapter>/<class>/<kit-id>/<filename> so the
144
+ // agent's guidance index (AGENTS.md) can reference them and they are co-located
145
+ // with flow definitions for the same kit.
146
+ const filename = path.basename(asset.source_path);
147
+ const output = path.join(runtimeDir, asset.asset_class, safeSegment(asset.kit_id), filename);
148
+ fs.mkdirSync(path.dirname(output), { recursive: true });
149
+ fs.copyFileSync(asset.source_path, output);
150
+ generated.push({ asset_class: asset.asset_class, path: relPath(dest, output), kit_id: asset.kit_id, asset_id: asset.asset_id ?? "", source_path: asset.source_path.split(path.sep).join("/") });
151
+ }
152
+ else {
153
+ skipped.push({ asset_class: asset.asset_class, path: asset.relative_path, kit_id: asset.kit_id, asset_id: asset.asset_id, reason: "asset class is not activated by codex-local" });
134
154
  }
135
- const output = path.join(runtimeDir, "flows", safeSegment(asset.kit_id), `${safeSegment(asset.asset_id)}.flow.json`);
136
- fs.mkdirSync(path.dirname(output), { recursive: true });
137
- fs.copyFileSync(asset.source_path, output);
138
- generated.push({ asset_class: asset.asset_class, path: relPath(dest, output), kit_id: asset.kit_id, asset_id: asset.asset_id, source_path: asset.source_path.split(path.sep).join("/") });
139
155
  }
140
156
  fs.mkdirSync(runtimeDir, { recursive: true });
141
- const manifest = { schema_version: "1.0", adapter: "codex-local", supported_asset_classes: ["flows"], generated_runtime_files: generated, skipped_assets: skipped, warnings: inventory.warnings, errors: inventory.errors };
157
+ const supportedClasses = Array.from(ACTIVATED_ASSET_CLASSES);
158
+ const manifest = { schema_version: "1.0", adapter: "codex-local", supported_asset_classes: supportedClasses, generated_runtime_files: generated, skipped_assets: skipped, warnings: inventory.warnings, errors: inventory.errors };
142
159
  const manifestPath = path.join(runtimeDir, "activation.json");
143
160
  writeJson(manifestPath, manifest);
144
161
  generated.push({ asset_class: "activation-manifest", path: relPath(dest, manifestPath), kit_id: "runtime", asset_id: "codex-local.activation", source_path: manifestPath.split(path.sep).join("/") });
145
- return { selected_adapter: "codex-local", supported_asset_classes: ["flows"], generated_runtime_files: generated, skipped_assets: skipped, warnings: inventory.warnings, errors: inventory.errors };
162
+ return { selected_adapter: "codex-local", supported_asset_classes: supportedClasses, generated_runtime_files: generated, skipped_assets: skipped, warnings: inventory.warnings, errors: inventory.errors };
146
163
  }
147
164
  // Decision Q3 (Issue #32): Option (a) — new adapter id "strands-local" rather than
148
165
  // loading kit flows inside FlowAgentsHooks. Rationale: activation is a workspace-prep
@@ -154,27 +171,41 @@ export function activateStrandsLocal(sourceRoot, dest) {
154
171
  const inventory = readKitInventory(sourceRoot, dest);
155
172
  // Runtime flows land at .flow-agents/runtime/strands/flows/<kit-id>/<asset-id>.flow.json
156
173
  // so the Strands steering context can glob for *.flow.json under this path.
174
+ // Runtime skills land at .flow-agents/runtime/strands/skills/<kit-id>/<filename> and
175
+ // docs at .flow-agents/runtime/strands/docs/<kit-id>/<filename> for system-prompt injection.
157
176
  const runtimeDir = path.join(dest, ".flow-agents", "runtime", "strands");
158
177
  const generated = [];
159
178
  const skipped = [];
160
179
  for (const asset of inventory.assets) {
161
- if (asset.asset_class !== "flows") {
162
- skipped.push({ asset_class: asset.asset_class, path: asset.relative_path, kit_id: asset.kit_id, asset_id: asset.asset_id, reason: "asset class is diagnostic-only for strands-local" });
163
- continue;
180
+ if (asset.asset_class === "flows") {
181
+ if (!asset.asset_id) {
182
+ skipped.push({ asset_class: asset.asset_class, path: asset.relative_path, kit_id: asset.kit_id, asset_id: null, reason: "flow asset is missing an id" });
183
+ continue;
184
+ }
185
+ const output = path.join(runtimeDir, "flows", safeSegment(asset.kit_id), `${safeSegment(asset.asset_id)}.flow.json`);
186
+ fs.mkdirSync(path.dirname(output), { recursive: true });
187
+ fs.copyFileSync(asset.source_path, output);
188
+ generated.push({ asset_class: asset.asset_class, path: relPath(dest, output), kit_id: asset.kit_id, asset_id: asset.asset_id, source_path: asset.source_path.split(path.sep).join("/") });
164
189
  }
165
- if (!asset.asset_id) {
166
- skipped.push({ asset_class: asset.asset_class, path: asset.relative_path, kit_id: asset.kit_id, asset_id: null, reason: "flow asset is missing an id" });
167
- continue;
190
+ else if (asset.asset_class === "skills" || asset.asset_class === "docs") {
191
+ // Mirror the codex-local layout: strands/<class>/<kit-id>/<filename>.
192
+ // The Strands system-prompt injection layer can glob for all *.md files under
193
+ // .flow-agents/runtime/strands/skills/ to include agent guidance in the context.
194
+ const filename = path.basename(asset.source_path);
195
+ const output = path.join(runtimeDir, asset.asset_class, safeSegment(asset.kit_id), filename);
196
+ fs.mkdirSync(path.dirname(output), { recursive: true });
197
+ fs.copyFileSync(asset.source_path, output);
198
+ generated.push({ asset_class: asset.asset_class, path: relPath(dest, output), kit_id: asset.kit_id, asset_id: asset.asset_id ?? "", source_path: asset.source_path.split(path.sep).join("/") });
199
+ }
200
+ else {
201
+ skipped.push({ asset_class: asset.asset_class, path: asset.relative_path, kit_id: asset.kit_id, asset_id: asset.asset_id, reason: "asset class is not activated by strands-local" });
168
202
  }
169
- const output = path.join(runtimeDir, "flows", safeSegment(asset.kit_id), `${safeSegment(asset.asset_id)}.flow.json`);
170
- fs.mkdirSync(path.dirname(output), { recursive: true });
171
- fs.copyFileSync(asset.source_path, output);
172
- generated.push({ asset_class: asset.asset_class, path: relPath(dest, output), kit_id: asset.kit_id, asset_id: asset.asset_id, source_path: asset.source_path.split(path.sep).join("/") });
173
203
  }
174
204
  fs.mkdirSync(runtimeDir, { recursive: true });
175
- const manifest = { schema_version: "1.0", adapter: "strands-local", supported_asset_classes: ["flows"], generated_runtime_files: generated, skipped_assets: skipped, warnings: inventory.warnings, errors: inventory.errors };
205
+ const supportedClasses = Array.from(ACTIVATED_ASSET_CLASSES);
206
+ const manifest = { schema_version: "1.0", adapter: "strands-local", supported_asset_classes: supportedClasses, generated_runtime_files: generated, skipped_assets: skipped, warnings: inventory.warnings, errors: inventory.errors };
176
207
  const manifestPath = path.join(runtimeDir, "activation.json");
177
208
  writeJson(manifestPath, manifest);
178
209
  generated.push({ asset_class: "activation-manifest", path: relPath(dest, manifestPath), kit_id: "runtime", asset_id: "strands-local.activation", source_path: manifestPath.split(path.sep).join("/") });
179
- return { selected_adapter: "strands-local", supported_asset_classes: ["flows"], generated_runtime_files: generated, skipped_assets: skipped, warnings: inventory.warnings, errors: inventory.errors };
210
+ return { selected_adapter: "strands-local", supported_asset_classes: supportedClasses, generated_runtime_files: generated, skipped_assets: skipped, warnings: inventory.warnings, errors: inventory.errors };
180
211
  }