@kontourai/flow-agents 1.1.0 → 1.3.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 (119) hide show
  1. package/.github/workflows/ci.yml +6 -1
  2. package/.github/workflows/kit-gates-demo.yml +6 -2
  3. package/.github/workflows/runtime-compat.yml +5 -2
  4. package/CHANGELOG.md +51 -0
  5. package/CONTRIBUTING.md +30 -0
  6. package/README.md +26 -5
  7. package/agents/dev.json +1 -1
  8. package/agents/tool-planner.json +1 -1
  9. package/build/src/cli/{flow-kit.js → kit.js} +122 -108
  10. package/build/src/cli/validate-source-tree.js +4 -4
  11. package/build/src/cli/workflow-sidecar.js +70 -5
  12. package/build/src/cli.js +3 -3
  13. package/build/src/flow-kit/validate.js +89 -62
  14. package/build/src/tools/build-universal-bundles.js +78 -17
  15. package/build/src/tools/generate-context-map.js +49 -7
  16. package/build/src/tools/validate-source-tree.js +32 -1
  17. package/console.telemetry.json +1 -1
  18. package/docs/adr/0004-gates-expect-surface-claims.md +7 -7
  19. package/docs/adr/0007-flow-skill-kit-tool-boundary.md +169 -0
  20. package/docs/adr/0007-skill-audit.md +112 -0
  21. package/docs/adr/0008-kit-operation-boundary.md +88 -0
  22. package/docs/context-map.md +18 -22
  23. package/docs/flow-kit-repository-contract.md +5 -5
  24. package/docs/getting-started.md +177 -0
  25. package/docs/index.md +19 -8
  26. package/docs/kit-authoring-guide.md +125 -13
  27. package/docs/knowledge-kit.md +2 -2
  28. package/docs/operating-layers.md +2 -2
  29. package/docs/spec/runtime-hook-surface.md +1 -1
  30. package/docs/veritas-integration.md +4 -4
  31. package/docs/vision.md +1 -1
  32. package/docs/workflow-eval-strategy.md +2 -2
  33. package/docs/workflow-usage-guide.md +2 -2
  34. package/evals/acceptance/test_opencode_harness.sh +18 -10
  35. package/evals/acceptance/test_pi_harness.sh +10 -6
  36. package/evals/ci/run-baseline.sh +1 -1
  37. package/evals/fixtures/builder-kit-workflow-state/happy-path.json +2 -2
  38. package/evals/fixtures/builder-kit-workflow-state/mid-work-resume.json +2 -2
  39. package/evals/fixtures/console-learning-projection/artifacts/console-learning-correction/learning.json +1 -1
  40. package/evals/fixtures/flow-kit-repository/mixed-runtime-kit/flows/runtime.flow.json +4 -4
  41. package/evals/fixtures/flow-kit-repository/valid-local-kit/flows/review.flow.json +4 -4
  42. package/evals/fixtures/kit-conformance-levels/k0-flows-only/flows/review.flow.json +4 -4
  43. package/evals/fixtures/kit-conformance-levels/k1-agent-extension/flows/build.flow.json +4 -4
  44. package/evals/fixtures/kit-conformance-levels/k2-with-evals/flows/synthesize.flow.json +4 -4
  45. package/evals/fixtures/kit-conformance-levels/third-party-extension/flows/review.flow.json +4 -4
  46. package/evals/fixtures/pull-work-provider/github-issues.json +5 -5
  47. package/evals/fixtures/surface-trust/accepted-claim-trust-report.json +2 -2
  48. package/evals/fixtures/surface-trust/artifact-absent.json +2 -2
  49. package/evals/fixtures/surface-trust/integrity-mismatch-trust-report.json +2 -2
  50. package/evals/fixtures/surface-trust/missing-authority-trust-report.json +2 -2
  51. package/evals/fixtures/surface-trust/provider-absent.json +2 -2
  52. package/evals/fixtures/surface-trust/rejected-claim-trust-report.json +2 -2
  53. package/evals/fixtures/surface-trust/stale-claim-trust-snapshot.json +2 -2
  54. package/evals/integration/test_activate_npx_context.sh +2 -2
  55. package/evals/integration/test_bundle_install.sh +17 -12
  56. package/evals/integration/test_console_learning_projection.sh +2 -2
  57. package/evals/integration/test_flow_kit_install_git.sh +7 -7
  58. package/evals/integration/test_flow_kit_repository.sh +4 -4
  59. package/evals/integration/test_goal_fit_hook.sh +144 -0
  60. package/evals/integration/test_kit_conformance_levels.sh +56 -2
  61. package/evals/integration/test_local_flow_kit_install.sh +7 -7
  62. package/evals/integration/test_publish_change_helper.sh +1 -1
  63. package/evals/integration/test_pull_work_provider.sh +1 -1
  64. package/evals/integration/test_runtime_adapter_activation.sh +3 -3
  65. package/evals/integration/test_workflow_sidecar_writer.sh +9 -9
  66. package/evals/lib/node.sh +2 -2
  67. package/evals/static/test_package.sh +3 -3
  68. package/evals/static/test_workflow_skills.sh +19 -19
  69. package/integrations/strands/flow_agents_strands/steering.py +1 -1
  70. package/integrations/strands-ts/src/hooks.ts +1 -1
  71. package/kits/builder/flows/build.flow.json +48 -48
  72. package/kits/builder/flows/shape.flow.json +36 -36
  73. package/kits/builder/kit.json +17 -0
  74. package/{skills → kits/builder/skills}/builder-shape/SKILL.md +4 -4
  75. package/{skills → kits/builder/skills}/idea-to-backlog/SKILL.md +1 -1
  76. package/kits/knowledge/adapters/obsidian-store/index.js +137 -26
  77. package/kits/knowledge/evals/contract-suite/suite.test.js +90 -0
  78. package/kits/knowledge/flows/compile.flow.json +12 -12
  79. package/kits/knowledge/flows/consolidate.flow.json +16 -16
  80. package/kits/knowledge/flows/ingest.flow.json +12 -12
  81. package/kits/knowledge/flows/retire.flow.json +16 -16
  82. package/kits/knowledge/flows/store-contract.flow.json +12 -12
  83. package/kits/knowledge/flows/synthesize.flow.json +16 -16
  84. package/kits/knowledge/kit.json +16 -9
  85. package/kits/release-evidence/flows/release-evidence.flow.json +3 -3
  86. package/package.json +11 -5
  87. package/packaging/packs.json +1 -21
  88. package/schemas/workflow-evidence.schema.json +2 -1
  89. package/scripts/README.md +1 -1
  90. package/scripts/hooks/stop-goal-fit.js +66 -18
  91. package/scripts/kit.js +2 -0
  92. package/skills/README.md +23 -0
  93. package/src/cli/{flow-kit.ts → kit.ts} +124 -109
  94. package/src/cli/validate-source-tree.ts +4 -4
  95. package/src/cli/workflow-sidecar.ts +62 -4
  96. package/src/cli.ts +3 -3
  97. package/src/flow-kit/validate.ts +118 -58
  98. package/src/tools/build-universal-bundles.ts +74 -13
  99. package/src/tools/generate-context-map.ts +36 -6
  100. package/src/tools/validate-source-tree.ts +27 -1
  101. package/scripts/flow-kit.js +0 -2
  102. package/skills/context-budget/SKILL.md +0 -40
  103. package/skills/explore/SKILL.md +0 -137
  104. package/skills/feedback-loop/SKILL.md +0 -87
  105. package/skills/frontend-design/SKILL.md +0 -80
  106. /package/{skills → kits/builder/skills}/deliver/SKILL.md +0 -0
  107. /package/{skills → kits/builder/skills}/design-probe/SKILL.md +0 -0
  108. /package/{skills → kits/builder/skills}/evidence-gate/SKILL.md +0 -0
  109. /package/{skills → kits/builder/skills}/execute-plan/SKILL.md +0 -0
  110. /package/{skills → kits/builder/skills}/fix-bug/SKILL.md +0 -0
  111. /package/{skills → kits/builder/skills}/learning-review/SKILL.md +0 -0
  112. /package/{skills → kits/builder/skills}/pickup-probe/SKILL.md +0 -0
  113. /package/{skills → kits/builder/skills}/plan-work/SKILL.md +0 -0
  114. /package/{skills → kits/builder/skills}/pull-work/SKILL.md +0 -0
  115. /package/{skills → kits/builder/skills}/release-readiness/SKILL.md +0 -0
  116. /package/{skills → kits/builder/skills}/review-work/SKILL.md +0 -0
  117. /package/{skills → kits/builder/skills}/tdd-workflow/SKILL.md +0 -0
  118. /package/{skills → kits/builder/skills}/verify-work/SKILL.md +0 -0
  119. /package/{skills → kits/knowledge/skills}/knowledge-capture/SKILL.md +0 -0
@@ -2,6 +2,7 @@
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { execFileSync } from "node:child_process";
5
+ import { createRequire } from "node:module";
5
6
  const statuses = new Set(["new", "planning", "planned", "in_progress", "blocked", "verifying", "verified", "needs_decision", "not_verified", "failed", "delivered", "accepted", "archived"]);
6
7
  const phases = ["idea", "backlog", "pickup", "planning", "execution", "verification", "goal_fit", "evidence", "release", "learning", "done"];
7
8
  const checkKinds = new Set(["build", "types", "lint", "test", "security", "diff", "browser", "runtime", "policy", "external"]);
@@ -19,6 +20,51 @@ function appendJsonl(file, payload) {
19
20
  }
20
21
  function die(message) { throw new Error(message); }
21
22
  function slugify(value, fallback) { return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || fallback; }
23
+ // Optional Hachure trust-bundle validation. No-ops gracefully when hachure is not installed.
24
+ // Install hachure (^0.4.0) as an optional dependency to enable schema validation.
25
+ function tryLoadHachureValidator() {
26
+ try {
27
+ const _require = createRequire(import.meta.url);
28
+ const hachureDir = path.dirname(_require.resolve("hachure"));
29
+ const schemasDir = path.join(hachureDir, "schemas");
30
+ const Ajv = _require("ajv/dist/2020");
31
+ const schemas = {};
32
+ for (const file of fs.readdirSync(schemasDir)) {
33
+ if (!file.endsWith(".schema.json"))
34
+ continue;
35
+ schemas[file] = JSON.parse(fs.readFileSync(path.join(schemasDir, file), "utf8"));
36
+ }
37
+ const ajv = new Ajv({ strict: false, allErrors: true });
38
+ for (const [filename, schema] of Object.entries(schemas)) {
39
+ if (filename === "trust-bundle.schema.json")
40
+ continue;
41
+ ajv.addSchema(schema, filename);
42
+ }
43
+ const trustBundleSchema = schemas["trust-bundle.schema.json"];
44
+ if (!trustBundleSchema)
45
+ return null;
46
+ const validate = ajv.compile(trustBundleSchema);
47
+ return (bundle) => {
48
+ const valid = validate(bundle);
49
+ if (valid)
50
+ return { valid: true, errors: [] };
51
+ const errors = (validate.errors ?? []).map((err) => {
52
+ const loc = err.instancePath || err.schemaPath || "";
53
+ return `${loc} ${err.message ?? "invalid"}`.trim();
54
+ });
55
+ return { valid: false, errors };
56
+ };
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ let _hachureValidator;
63
+ function getHachureValidator() {
64
+ if (_hachureValidator === undefined)
65
+ _hachureValidator = tryLoadHachureValidator();
66
+ return _hachureValidator;
67
+ }
22
68
  function safeRepoIdentifier(value) {
23
69
  const trimmed = value.trim().replace(/\.git$/, "");
24
70
  if (!trimmed || trimmed.length > 120)
@@ -444,14 +490,32 @@ function normalizeCheck(raw) {
444
490
  function normalizeSurfaceRefs(refs) {
445
491
  if (!Array.isArray(refs))
446
492
  die("surface_trust_refs must be an array");
493
+ const hachureValidate = getHachureValidator();
447
494
  return refs.map((ref) => {
448
495
  const keys = JSON.stringify(ref).match(/"([^"]+)":/g) ?? [];
449
496
  for (const key of keys.map((k) => k.slice(1, -2)))
450
497
  if (key.toLowerCase().includes("veritas"))
451
498
  die(`unsupported field in Surface trust ref: ${key}`);
452
499
  const out = { ...ref };
453
- if (!["TrustReport", "Trust Snapshot"].includes(out.artifact_kind))
454
- die("artifact_kind must be one of");
500
+ // trust.bundle is the canonical Hachure-aligned artifact kind; TrustReport/Trust Snapshot are legacy aliases
501
+ if (!["trust.bundle", "TrustReport", "Trust Snapshot"].includes(out.artifact_kind))
502
+ die("artifact_kind must be one of: trust.bundle, TrustReport, Trust Snapshot");
503
+ // When hachure is installed, validate the referenced trust artifact if it is a local file
504
+ if (hachureValidate && out.artifact_ref && typeof out.artifact_ref === "string" && fs.existsSync(out.artifact_ref)) {
505
+ try {
506
+ const bundle = JSON.parse(fs.readFileSync(out.artifact_ref, "utf8"));
507
+ const result = hachureValidate(bundle);
508
+ if (!result.valid) {
509
+ const errorSummary = result.errors.slice(0, 3).join("; ");
510
+ die(`trust.bundle artifact at ${out.artifact_ref} failed Hachure schema validation: ${errorSummary}`);
511
+ }
512
+ }
513
+ catch (err) {
514
+ if (err instanceof Error && err.message.includes("failed Hachure schema validation"))
515
+ throw err;
516
+ // File read or parse errors are not re-thrown: the artifact_ref validation path is advisory
517
+ }
518
+ }
455
519
  const status = deriveSurfaceStatus(out);
456
520
  if (out.status === "pass" && status !== "pass")
457
521
  die("surface_trust_refs contradicts Surface trust facts");
@@ -474,17 +538,18 @@ function surfaceCheckFromArtifact(file, index) {
474
538
  const lower = JSON.stringify(raw).toLowerCase();
475
539
  let ref;
476
540
  if (lower.includes("provider") && lower.includes("absent")) {
477
- ref = { artifact_kind: "TrustReport", artifact_ref: file, gate_id: "provider.unavailable", claim_type: "surface.claim", claim_status: "unknown", subject: "builder-kit", freshness: { status: "unknown", summary: "No trust provider is configured" }, authority: { producer: "unknown", summary: "No trust provider is configured" }, integrity: { status: "unknown", summary: "Unknown" }, status: "not_verified", summary: "No trust provider is configured" };
541
+ ref = { artifact_kind: "trust.bundle", artifact_ref: file, gate_id: "provider.unavailable", claim_type: "builder.trust.bundle", claim_status: "unknown", subject: "builder-kit", freshness: { status: "unknown", summary: "No trust provider is configured" }, authority: { producer: "unknown", summary: "No trust provider is configured" }, integrity: { status: "unknown", summary: "Unknown" }, status: "not_verified", summary: "No trust provider is configured" };
478
542
  }
479
543
  else if (lower.includes("artifact") && lower.includes("absent")) {
480
- ref = { artifact_kind: "TrustReport", artifact_ref: file, gate_id: "artifact.unavailable", claim_type: "surface.claim", claim_status: "unknown", subject: "builder-kit", freshness: { status: "unknown", summary: "Artifact not readable" }, authority: { producer: "unknown", summary: "Artifact not readable" }, integrity: { status: "unknown", summary: "Artifact not readable" }, status: "not_verified", summary: "artifact not readable" };
544
+ ref = { artifact_kind: "trust.bundle", artifact_ref: file, gate_id: "artifact.unavailable", claim_type: "builder.trust.bundle", claim_status: "unknown", subject: "builder-kit", freshness: { status: "unknown", summary: "Artifact not readable" }, authority: { producer: "unknown", summary: "Artifact not readable" }, integrity: { status: "unknown", summary: "Artifact not readable" }, status: "not_verified", summary: "artifact not readable" };
481
545
  }
482
546
  else {
483
547
  const claimStatus = lower.includes("rejected") ? "rejected" : "accepted";
484
548
  const freshness = lower.includes("stale") ? "stale" : "fresh";
485
549
  const producer = lower.includes("missing-authority") ? "unknown" : "surface-local";
486
550
  const integrity = lower.includes("mismatch") ? "mismatch" : "matched";
487
- ref = { artifact_kind: file.includes("snapshot") ? "Trust Snapshot" : "TrustReport", artifact_ref: file, gate_id: "builder.surface.claim", claim_type: "surface.claim", claim_status: claimStatus, subject: "builder-kit", freshness: { status: freshness, summary: freshness === "fresh" ? "fresh" : "not currently verifiable" }, authority: { producer, summary: producer === "unknown" ? "missing authority" : "Local Surface trust producer." }, integrity: { status: integrity, summary: integrity === "matched" ? "matched" : "integrity mismatch" } };
551
+ // Use trust.bundle as the canonical Hachure-aligned artifact_kind for all trust-backed evidence refs
552
+ ref = { artifact_kind: "trust.bundle", artifact_ref: file, gate_id: "builder.trust.bundle", claim_type: "builder.trust.bundle", claim_status: claimStatus, subject: "builder-kit", freshness: { status: freshness, summary: freshness === "fresh" ? "fresh" : "not currently verifiable" }, authority: { producer, summary: producer === "unknown" ? "missing authority" : "Local Surface trust producer." }, integrity: { status: integrity, summary: integrity === "matched" ? "matched" : "integrity mismatch" } };
488
553
  ref.status = deriveSurfaceStatus(ref);
489
554
  ref.summary = ref.status === "pass" ? "accepted" : ref.status === "not_verified" ? "not currently verifiable" : (claimStatus === "rejected" ? "rejected" : producer === "unknown" ? "missing authority" : "integrity mismatch");
490
555
  }
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,52 +1,79 @@
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"]);
9
10
  /**
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).
11
+ * Allowlist of kit IDs that Kontour authors, tests, and ships with the flow-agents package.
13
12
  *
14
- * The degradation invariant: every Flow Agents Kit MUST remain a valid core
15
- * Flow Kit container when agent-extension fields are ignored.
13
+ * Criteria for inclusion:
14
+ * 1. The kit directory lives under kits/ in the kontourai/flow-agents repository.
15
+ * 2. The kit is published by @kontourai (npm package @kontourai/flow-agents).
16
+ * 3. Kontour owns and maintains the kit's content and release lifecycle.
17
+ *
18
+ * To add a new first-party kit: add its id here AND ensure it lives under kits/ in this repo.
19
+ * Third-party forks or community kits published elsewhere are NOT first-party, even if they
20
+ * share a similar id — first-party is tied to provenance in this specific repository.
16
21
  */
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`);
22
+ export const FIRST_PARTY_KIT_IDS = new Set(["builder", "knowledge"]);
23
+ /**
24
+ * Derive the trust level for a kit id.
25
+ *
26
+ * v1 determination: allowlist check against FIRST_PARTY_KIT_IDS.
27
+ * "verified" is reserved for future third-party verification (not yet granted to any kit).
28
+ */
29
+ export function deriveKitTrust(kitId) {
30
+ if (FIRST_PARTY_KIT_IDS.has(kitId))
31
+ return "first-party";
32
+ return "unverified";
33
+ }
34
+ let _validateKitContainerCache = null;
35
+ async function loadValidateKitContainer() {
36
+ if (_validateKitContainerCache)
37
+ return _validateKitContainerCache;
38
+ let mod;
39
+ try {
40
+ mod = await import("@kontourai/flow");
24
41
  }
25
- if (typeof manifest.name !== "string" || !manifest.name.trim()) {
26
- errors.push(`${label}: .name must be a non-empty string`);
42
+ catch (err) {
43
+ throw new Error("container validation requires @kontourai/flow; run from an npm-installed flow-agents workspace " +
44
+ `or use 'flow kit validate' (original error: ${err.message})`);
27
45
  }
28
- if (!Array.isArray(manifest.flows) || manifest.flows.length === 0) {
29
- errors.push(`${label}: .flows must be a non-empty list`);
46
+ if (typeof mod.validateKitContainer !== "function") {
47
+ throw new Error("@kontourai/flow did not export validateKitContainer");
30
48
  }
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;
49
+ _validateKitContainerCache = mod.validateKitContainer;
50
+ return _validateKitContainerCache;
47
51
  }
48
52
  /**
49
- * Derives the consumer-target level (K0/K1/K2) and target audience list from
53
+ * Delegates core Flow Kit container validation to @kontourai/flow's validateKitContainer.
54
+ * The container contract lives once, in Flow. Returns a list of violation messages (empty = valid).
55
+ *
56
+ * The degradation invariant: every Flow Agents Kit MUST remain a valid core
57
+ * Flow Kit container when agent-extension fields are ignored.
58
+ *
59
+ * Loads @kontourai/flow lazily (on first call) so that runtime ops (list/status/activate)
60
+ * that never invoke validation can run in standalone installed bundles where
61
+ * @kontourai/flow is not present.
62
+ *
63
+ * @param kitDir Real kit directory path for file-existence checks on flows[].path entries.
64
+ * Pass the actual kit directory when available; pass "" for structural-only checks.
65
+ */
66
+ async function delegateCoreContainerValidation(kitDir, manifest) {
67
+ const validateKitContainer = await loadValidateKitContainer();
68
+ const result = validateKitContainer(kitDir, manifest);
69
+ if (result.valid)
70
+ return [];
71
+ return result.diagnostics
72
+ .filter((d) => d.severity === "error")
73
+ .map((d) => `${d.path}: ${d.message}`);
74
+ }
75
+ /**
76
+ * Derives the consumer-target level (K0/K1/K2), target audience list, and trust level from
50
77
  * observable asset classes in the kit manifest. Does not require file I/O.
51
78
  *
52
79
  * Derivation rules (from kontourai/flow-agents#52 and Brian's layering review):
@@ -56,11 +83,20 @@ export function validateCoreContainer(manifest, label) {
56
83
  * - targets.flow: always present when K0 (any Flow consumer can evaluate gates).
57
84
  * - targets.flow-agents: present when K1 (agent extension assets activate in >=1 harness).
58
85
  * - third-party: any top-level keys that are not core fields and not Flow Agents extension classes.
86
+ *
87
+ * Trust derivation (from kontourai/flow-agents#79):
88
+ * - "first-party": kit id is in FIRST_PARTY_KIT_IDS (Kontour-authored kits in this repo).
89
+ * - "unverified": all other kits (default; "verified" is reserved for a future process).
90
+ *
91
+ * @param manifest The kit.json manifest object.
92
+ * @param kitDir Kit directory for flow file-existence checks. Defaults to "" (structural-only).
93
+ * Pass the real kit directory from `inspect` to get authoritative K0 validation.
59
94
  */
60
- export function deriveKitTargets(manifest) {
95
+ export async function deriveKitTargets(manifest, kitDir = "") {
61
96
  const kitId = typeof manifest.id === "string" ? manifest.id : "<unknown>";
62
97
  const kitName = typeof manifest.name === "string" ? manifest.name : "<unknown>";
63
- const coreErrors = validateCoreContainer(manifest, "kit.json");
98
+ // Delegate core container validation to @kontourai/flow.
99
+ const coreErrors = await delegateCoreContainerValidation(kitDir, manifest);
64
100
  const k0 = coreErrors.length === 0;
65
101
  const hasAgentExtension = AGENT_EXTENSION_CLASSES.size > 0 &&
66
102
  [...AGENT_EXTENSION_CLASSES].some((cls) => Array.isArray(manifest[cls]) && manifest[cls].length > 0);
@@ -79,15 +115,18 @@ export function deriveKitTargets(manifest) {
79
115
  targets.push("flow-agents");
80
116
  for (const ns of thirdPartyExtensions)
81
117
  targets.push(ns);
118
+ // Derive trust level orthogonally to the K-level capability axis.
119
+ const trust = deriveKitTrust(kitId);
82
120
  return {
83
121
  kit_id: kitId,
84
122
  kit_name: kitName,
85
123
  conformance: { k0, k1, k2 },
86
124
  targets,
87
125
  third_party_extensions: thirdPartyExtensions,
126
+ trust,
88
127
  };
89
128
  }
90
- export function validateKitRepository(kitDir) {
129
+ export async function validateKitRepository(kitDir) {
91
130
  const errors = [];
92
131
  const manifestPath = path.join(kitDir, "kit.json");
93
132
  let manifest;
@@ -98,27 +137,16 @@ export function validateKitRepository(kitDir) {
98
137
  errors.push(`${manifestPath}: invalid JSON: ${error.message}`);
99
138
  return errors;
100
139
  }
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) {
140
+ // Delegate core container validation (schema_version, id, name, flows including file
141
+ // existence) to @kontourai/flow — the container contract lives once, in Flow.
142
+ // This enforces the degradation invariant: a Flow Agents Kit must remain a valid
143
+ // core Flow Kit container when extension fields are stripped.
144
+ const coreErrors = await delegateCoreContainerValidation(kitDir, manifest);
145
+ for (const err of coreErrors)
146
+ errors.push(err);
147
+ // Flow Agents extension validation: skills, docs, adapters, evals, assets.
148
+ // Flows are validated above by @kontourai/flow; only extension classes are checked here.
149
+ for (const section of EXTENSION_ASSET_CLASSES) {
122
150
  const entries = manifest[section];
123
151
  if (entries === undefined)
124
152
  continue;
@@ -155,15 +183,14 @@ export function validateKitRepository(kitDir) {
155
183
  return;
156
184
  }
157
185
  if (!fs.existsSync(resolved)) {
158
- const noun = section === "flows" ? "Flow Definition" : "asset";
159
- errors.push(`${manifestPath}: ${section}[${index}].path points at missing ${noun}: ${rel}`);
186
+ errors.push(`${manifestPath}: ${section}[${index}].path points at missing asset: ${rel}`);
160
187
  }
161
188
  });
162
189
  }
163
190
  return errors;
164
191
  }
165
- export function assertKitRepository(kitDir) {
166
- const errors = validateKitRepository(kitDir);
192
+ export async function assertKitRepository(kitDir) {
193
+ const errors = await validateKitRepository(kitDir);
167
194
  if (errors.length) {
168
195
  const error = new Error("Flow Kit repository validation failed");
169
196
  error.diagnostics = errors;
@@ -9,6 +9,61 @@ const packs = loadJson(path.join(root, "packaging/packs.json"));
9
9
  const textExtensions = new Set([".css", ".html", ".js", ".json", ".md", ".sh", ".toml", ".txt", ".yaml", ".yml", ".ts"]);
10
10
  const dropDiagnostics = [];
11
11
  const printDiagnostics = !["0", "false", "no"].includes(String(process.env.FLOW_AGENTS_EXPORT_DIAGNOSTICS ?? "1").toLowerCase());
12
+ /**
13
+ * Collect all skill source paths across skills/ and kit-owned skills.
14
+ * Returns an array of {name, src} pairs where name is the install name
15
+ * (same as the directory name) and src is the absolute SKILL.md path.
16
+ * Kit-owned skills are discovered by reading kit.json `skills` arrays;
17
+ * each entry's `path` is resolved relative to the kit directory.
18
+ */
19
+ function collectAllSkills() {
20
+ const results = [];
21
+ const seen = new Set();
22
+ // 1. Top-level skills/ directory (tools pending reclassification).
23
+ const skillsDir = path.join(root, "skills");
24
+ if (fs.existsSync(skillsDir)) {
25
+ for (const skill of fs.readdirSync(skillsDir).sort()) {
26
+ const skillPath = path.join(skillsDir, skill, "SKILL.md");
27
+ if (fs.existsSync(skillPath) && !seen.has(skill)) {
28
+ seen.add(skill);
29
+ results.push({ name: skill, src: skillPath });
30
+ }
31
+ }
32
+ }
33
+ // 2. Kit-owned skills declared in kits/<kit>/kit.json `skills` arrays.
34
+ const kitsDir = path.join(root, "kits");
35
+ if (fs.existsSync(kitsDir)) {
36
+ for (const kitName of fs.readdirSync(kitsDir).sort()) {
37
+ const kitJson = path.join(kitsDir, kitName, "kit.json");
38
+ if (!fs.existsSync(kitJson))
39
+ continue;
40
+ let kitManifest;
41
+ try {
42
+ kitManifest = loadJson(kitJson);
43
+ }
44
+ catch {
45
+ continue;
46
+ }
47
+ const skills = Array.isArray(kitManifest["skills"]) ? kitManifest["skills"] : [];
48
+ for (const entry of skills) {
49
+ if (typeof entry !== "object" || entry === null)
50
+ continue;
51
+ const skillEntry = entry;
52
+ const relPath = typeof skillEntry["path"] === "string" ? skillEntry["path"] : null;
53
+ if (!relPath)
54
+ continue;
55
+ // Derive install name from the directory containing SKILL.md (one level up).
56
+ const absPath = path.resolve(path.join(kitsDir, kitName), relPath);
57
+ const skillName = path.basename(path.dirname(absPath));
58
+ if (fs.existsSync(absPath) && !seen.has(skillName)) {
59
+ seen.add(skillName);
60
+ results.push({ name: skillName, src: absPath });
61
+ }
62
+ }
63
+ }
64
+ }
65
+ return results.sort((a, b) => a.name.localeCompare(b.name));
66
+ }
12
67
  function resetDir(dir) {
13
68
  fs.rmSync(dir, { recursive: true, force: true });
14
69
  fs.mkdirSync(dir, { recursive: true });
@@ -327,10 +382,8 @@ function buildClaudeCode(agents) {
327
382
  writeText(path.join(bundle, manifest.claude_code.task_dir, ".gitkeep"), "");
328
383
  for (const spec of agents)
329
384
  writeText(path.join(bundle, ".claude/agents", `${spec.name}.md`), exportClaudeAgent(spec));
330
- for (const skill of fs.readdirSync(path.join(root, "skills"))) {
331
- const skillPath = path.join(root, "skills", skill, "SKILL.md");
332
- if (fs.existsSync(skillPath))
333
- writeText(path.join(bundle, ".claude/skills", skill, "SKILL.md"), sanitizeText(readText(skillPath), "claude-code", "<bundle-root>"));
385
+ for (const { name, src } of collectAllSkills()) {
386
+ writeText(path.join(bundle, ".claude/skills", name, "SKILL.md"), sanitizeText(readText(src), "claude-code", "<bundle-root>"));
334
387
  }
335
388
  writeText(path.join(bundle, ".claude/settings.json"), exportClaudeSettings());
336
389
  writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("Claude Code", agents, manifest.claude_code.task_dir));
@@ -352,10 +405,8 @@ function buildCodex(agents) {
352
405
  writeText(path.join(bundle, ".codex/hooks.json"), exportCodexHooks());
353
406
  for (const spec of targetAgents)
354
407
  writeText(path.join(bundle, ".codex/agents", `${spec.name}.toml`), exportCodexAgent(spec));
355
- for (const skill of fs.readdirSync(path.join(root, "skills"))) {
356
- const skillPath = path.join(root, "skills", skill, "SKILL.md");
357
- if (fs.existsSync(skillPath))
358
- writeText(path.join(bundle, ".codex/skills", skill, "SKILL.md"), sanitizeText(readText(skillPath), "codex", "<bundle-root>"));
408
+ for (const { name, src } of collectAllSkills()) {
409
+ writeText(path.join(bundle, ".codex/skills", name, "SKILL.md"), sanitizeText(readText(src), "codex", "<bundle-root>"));
359
410
  }
360
411
  writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("Codex", targetAgents, manifest.codex.task_dir));
361
412
  writeText(path.join(bundle, "README.md"), exportTargetReadme("Codex", "bash install.sh /path/to/workspace"));
@@ -419,6 +470,7 @@ function exportOpencodePlugin() {
419
470
 
420
471
  import { spawnSync } from 'node:child_process';
421
472
  import { join, basename } from 'node:path';
473
+ import { mkdirSync, writeFileSync } from 'node:fs';
422
474
 
423
475
  // opencode runs plugins inside its own compiled (Bun-based) binary, so
424
476
  // process.execPath points at opencode itself — spawning it with a script
@@ -429,6 +481,19 @@ const NODE_BIN = basename(process.execPath).startsWith('node') ? process.execPat
429
481
  export const FlowAgentsPlugin = async ({ project, client, $, directory, worktree }) => {
430
482
  const root = directory || process.cwd();
431
483
 
484
+ // Deterministic load marker. opencode invokes this factory at startup but
485
+ // does not reliably surface plugin console output to its log file, and its
486
+ // internal "loading plugin" message was dropped in opencode 1.17.x. Write a
487
+ // marker into the workspace telemetry dir so acceptance tests can confirm the
488
+ // plugin loaded without depending on opencode internals. Best-effort only.
489
+ try {
490
+ const telemetryDir = join(root, '.telemetry');
491
+ mkdirSync(telemetryDir, { recursive: true });
492
+ writeFileSync(join(telemetryDir, 'opencode-plugin.loaded'), 'flow-agents');
493
+ } catch (_err) {
494
+ // Marker is diagnostic only; never block plugin load on a write failure.
495
+ }
496
+
432
497
  // The hook scripts read the event payload from stdin; an empty stdin makes
433
498
  // the telemetry pipeline silently skip the emit (fail-open), so every spawn
434
499
  // must pass a payload (caught by live acceptance smoke 2026-06-11).
@@ -519,10 +584,8 @@ function buildOpencode(agents) {
519
584
  for (const spec of agents) {
520
585
  writeText(path.join(bundle, ".opencode/agents", `${spec.name}.md`), exportOpencodeAgent(spec));
521
586
  }
522
- for (const skill of fs.readdirSync(path.join(root, "skills"))) {
523
- const skillPath = path.join(root, "skills", skill, "SKILL.md");
524
- if (fs.existsSync(skillPath))
525
- writeText(path.join(bundle, ".opencode/skills", skill, "SKILL.md"), sanitizeText(readText(skillPath), "opencode", "<bundle-root>"));
587
+ for (const { name, src } of collectAllSkills()) {
588
+ writeText(path.join(bundle, ".opencode/skills", name, "SKILL.md"), sanitizeText(readText(src), "opencode", "<bundle-root>"));
526
589
  }
527
590
  writeText(path.join(bundle, ".opencode/plugins/flow-agents.js"), exportOpencodePlugin());
528
591
  writeText(path.join(bundle, "opencode.json"), exportOpencodeConfig());
@@ -632,10 +695,8 @@ function buildPi(agents) {
632
695
  writeText(path.join(bundle, manifest.pi.task_dir, ".gitkeep"), "");
633
696
  // pi has no named-subagent registry; agents are left canonical/unexported.
634
697
  // Skills are exported to .pi/skills/ (direct .md files supported in that dir).
635
- for (const skill of fs.readdirSync(path.join(root, "skills"))) {
636
- const skillPath = path.join(root, "skills", skill, "SKILL.md");
637
- if (fs.existsSync(skillPath))
638
- writeText(path.join(bundle, ".pi/skills", skill, "SKILL.md"), sanitizeText(readText(skillPath), "pi", "<bundle-root>"));
698
+ for (const { name, src } of collectAllSkills()) {
699
+ writeText(path.join(bundle, ".pi/skills", name, "SKILL.md"), sanitizeText(readText(src), "pi", "<bundle-root>"));
639
700
  }
640
701
  writeText(path.join(bundle, ".pi/extensions/flow-agents.ts"), exportPiExtension());
641
702
  writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("pi", agents, manifest.pi.task_dir));
@@ -648,7 +709,7 @@ function buildCatalog(agents) {
648
709
  return {
649
710
  source_root: ".",
650
711
  agents: agents.slice().sort((a, b) => a.name.localeCompare(b.name)).map((spec) => spec.name),
651
- skills: fs.readdirSync(path.join(root, "skills")).filter((name) => fs.existsSync(path.join(root, "skills", name, "SKILL.md"))).sort(),
712
+ skills: collectAllSkills().map(({ name }) => name),
652
713
  powers: fs.readdirSync(path.join(root, "powers")).filter((name) => fs.existsSync(path.join(root, "powers", name, "mcp.json"))).sort(),
653
714
  packs: packs.packs ?? [],
654
715
  kits: fs.existsSync(kitsCatalog) ? loadJson(kitsCatalog).kits ?? [] : [],
@@ -75,16 +75,58 @@ function repoShape(manifest) {
75
75
  rows.push([".flow-agents", "runtime", "Cross-session workflow artifacts and sidecars. Not committed by default."]);
76
76
  return rows;
77
77
  }
78
+ /** Collect all skill {name, absPath} pairs from skills/ and kit-owned skills. */
79
+ function allSkillPaths() {
80
+ const results = [];
81
+ const seen = new Set();
82
+ const skillsDir = path.join(root, "skills");
83
+ if (exists(skillsDir)) {
84
+ for (const name of fs.readdirSync(skillsDir).sort()) {
85
+ const absPath = path.join(skillsDir, name, "SKILL.md");
86
+ if (exists(absPath) && !seen.has(name)) {
87
+ seen.add(name);
88
+ results.push({ name, absPath });
89
+ }
90
+ }
91
+ }
92
+ const kitsDir = path.join(root, "kits");
93
+ if (exists(kitsDir)) {
94
+ for (const kitName of fs.readdirSync(kitsDir).sort()) {
95
+ const kitJson = path.join(kitsDir, kitName, "kit.json");
96
+ if (!exists(kitJson))
97
+ continue;
98
+ let kitManifest;
99
+ try {
100
+ kitManifest = loadJson(kitJson);
101
+ }
102
+ catch {
103
+ continue;
104
+ }
105
+ const skills = Array.isArray(kitManifest["skills"]) ? kitManifest["skills"] : [];
106
+ for (const entry of skills) {
107
+ if (typeof entry !== "object" || entry === null)
108
+ continue;
109
+ const skillEntry = entry;
110
+ const relPath = typeof skillEntry["path"] === "string" ? skillEntry["path"] : null;
111
+ if (!relPath)
112
+ continue;
113
+ const absPath = path.resolve(path.join(kitsDir, kitName), relPath);
114
+ const skillName = path.basename(path.dirname(absPath));
115
+ if (exists(absPath) && !seen.has(skillName)) {
116
+ seen.add(skillName);
117
+ results.push({ name: skillName, absPath });
118
+ }
119
+ }
120
+ }
121
+ }
122
+ return results.sort((a, b) => a.name.localeCompare(b.name));
123
+ }
78
124
  function listSkillRows() {
79
125
  const workflowRows = [];
80
126
  const supportRows = [];
81
- const skillsDir = path.join(root, "skills");
82
- for (const name of fs.readdirSync(skillsDir).sort()) {
83
- const skillPath = path.join(skillsDir, name, "SKILL.md");
84
- if (!exists(skillPath))
85
- continue;
86
- const meta = frontmatter(readText(skillPath));
87
- const row = [meta.name ?? name, rel(skillPath), oneLine(meta.description ?? "")];
127
+ for (const { name, absPath } of allSkillPaths()) {
128
+ const meta = frontmatter(readText(absPath));
129
+ const row = [meta.name ?? name, rel(absPath), oneLine(meta.description ?? "")];
88
130
  if (workflowSkills.has(row[0]))
89
131
  workflowRows.push(row);
90
132
  else
@@ -37,7 +37,7 @@ const publicScriptWrappers = new Map([
37
37
  ] }],
38
38
  ["scripts/filter-installed-packs.js", { target: "../build/src/tools/filter-installed-packs.js", significantLines: ['import("../build/src/tools/filter-installed-packs.js").then(({ main }) => process.exit(main(process.argv.slice(2))));'] }],
39
39
  ["scripts/generate-context-map.js", { target: "../build/src/tools/generate-context-map.js", significantLines: ['import("../build/src/tools/generate-context-map.js").then(({ main }) => process.exit(main(process.argv.slice(2))));'] }],
40
- ["scripts/flow-kit.js", { target: "../build/src/cli/flow-kit.js", significantLines: ['import("../build/src/cli/flow-kit.js").then(({ main }) => process.exit(main()));'] }],
40
+ ["scripts/kit.js", { target: "../build/src/cli/kit.js", significantLines: ['import("../build/src/cli/kit.js").then(({ main }) => main().then((code) => process.exit(code)));'] }],
41
41
  ["scripts/pull-work-provider.js", { target: "../build/src/cli/pull-work-provider.js", significantLines: ['import("../build/src/cli/pull-work-provider.js").then(({ main }) => process.exit(main()));'] }],
42
42
  ["scripts/effective-backlog-settings.js", { target: "../build/src/cli/effective-backlog-settings.js", significantLines: ['import("../build/src/cli/effective-backlog-settings.js").then(({ main }) => process.exit(main()));'] }],
43
43
  ["scripts/publish-change-helper.js", { target: "../build/src/cli/publish-change-helper.js", significantLines: ['import("../build/src/cli/publish-change-helper.js").then(({ main }) => process.exit(main()));'] }],
@@ -391,6 +391,32 @@ function validateAgentPaths(reporter, manifest) {
391
391
  }
392
392
  }
393
393
  function validateLegacyRefs(reporter) {
394
+ // Collect all kit-owned asset relative paths so legacy-ref scanning can skip matches
395
+ // that are subpaths of kit-owned assets. E.g. legacyRefRe matches "skills/plan-work/SKILL.md"
396
+ // within "kits/builder/skills/plan-work/SKILL.md"; the kit declares and validates these.
397
+ const kitOwnedSubPaths = new Set();
398
+ const kitsDir = path.join(root, "kits");
399
+ if (fs.existsSync(kitsDir)) {
400
+ for (const kitName of fs.readdirSync(kitsDir)) {
401
+ const kitJson = path.join(kitsDir, kitName, "kit.json");
402
+ if (!fs.existsSync(kitJson))
403
+ continue;
404
+ try {
405
+ const kitManifest = loadJson(kitJson);
406
+ for (const section of ["skills", "docs", "adapters", "evals", "assets"]) {
407
+ const entries = Array.isArray(kitManifest[section]) ? kitManifest[section] : [];
408
+ for (const entry of entries) {
409
+ if (typeof entry !== "object" || entry === null)
410
+ continue;
411
+ const relPath = entry["path"];
412
+ if (typeof relPath === "string" && relPath)
413
+ kitOwnedSubPaths.add(relPath);
414
+ }
415
+ }
416
+ }
417
+ catch { /* skip invalid kit.json */ }
418
+ }
419
+ }
394
420
  for (const file of walkFiles(path.join(root, "evals")).sort()) {
395
421
  if (!textRefExtensions.has(path.extname(file)))
396
422
  continue;
@@ -404,6 +430,11 @@ function validateLegacyRefs(reporter) {
404
430
  continue;
405
431
  if (ref.split(/[\\/]/).includes("node_modules"))
406
432
  continue;
433
+ // Skip refs that are declared kit-owned asset paths or their parent directories
434
+ // (e.g. "skills/plan-work/SKILL.md" or "skills/plan-work" matched inside
435
+ // "kits/builder/skills/plan-work/SKILL.md" in eval files).
436
+ if (kitOwnedSubPaths.has(ref) || [...kitOwnedSubPaths].some((p) => p.startsWith(ref + "/")))
437
+ continue;
407
438
  const candidates = [path.join(root, ref), ...(ref.startsWith("evals/") ? [] : [path.join(root, "evals", ref)])];
408
439
  if (!candidates.some(fs.existsSync))
409
440
  reporter.fail(`${rel(file)}: references missing source path: ${ref}`);