@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,7 +2,8 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { readJson } from "../lib/fs.js";
4
4
 
5
- const ASSET_CLASSES = ["flows", "skills", "docs", "adapters", "evals", "assets"] as const;
5
+ // Extension-only asset classes: validated by Flow Agents. Flows are validated by @kontourai/flow.
6
+ const EXTENSION_ASSET_CLASSES = ["skills", "docs", "adapters", "evals", "assets"] as const;
6
7
 
7
8
  // Core container fields owned by kontourai/flow (flow-kit-container.schema.json).
8
9
  // agent-extension fields are skills, docs, adapters, evals, assets.
@@ -23,6 +24,47 @@ export interface KitConformanceLevel {
23
24
  k2: boolean;
24
25
  }
25
26
 
27
+ /**
28
+ * Kit trust level — WHO vouches for a kit, orthogonal to the K-level capability axis.
29
+ *
30
+ * - "first-party": the kit is authored and published by Kontour (kontourai); its id is in the
31
+ * FIRST_PARTY_KIT_IDS allowlist maintained in this repository. These kits are built, tested,
32
+ * and distributed with the flow-agents package.
33
+ * - "verified": reserved for a future third-party verification process (e.g. self-certification
34
+ * via the conformance kit + cryptographic attestation / Veritas claims). Not yet implemented.
35
+ * - "unverified": default for all kits not in the first-party allowlist. This says nothing about
36
+ * the kit's quality — it only means Kontour has not vouched for it.
37
+ *
38
+ * The v2 path for "verified": cryptographic signing / attestation against the conformance kit
39
+ * and Veritas claims substrate is the natural next step and is intentionally deferred.
40
+ */
41
+ export type KitTrustLevel = "first-party" | "verified" | "unverified";
42
+
43
+ /**
44
+ * Allowlist of kit IDs that Kontour authors, tests, and ships with the flow-agents package.
45
+ *
46
+ * Criteria for inclusion:
47
+ * 1. The kit directory lives under kits/ in the kontourai/flow-agents repository.
48
+ * 2. The kit is published by @kontourai (npm package @kontourai/flow-agents).
49
+ * 3. Kontour owns and maintains the kit's content and release lifecycle.
50
+ *
51
+ * To add a new first-party kit: add its id here AND ensure it lives under kits/ in this repo.
52
+ * Third-party forks or community kits published elsewhere are NOT first-party, even if they
53
+ * share a similar id — first-party is tied to provenance in this specific repository.
54
+ */
55
+ export const FIRST_PARTY_KIT_IDS: ReadonlySet<string> = new Set(["builder", "knowledge"]);
56
+
57
+ /**
58
+ * Derive the trust level for a kit id.
59
+ *
60
+ * v1 determination: allowlist check against FIRST_PARTY_KIT_IDS.
61
+ * "verified" is reserved for future third-party verification (not yet granted to any kit).
62
+ */
63
+ export function deriveKitTrust(kitId: string): KitTrustLevel {
64
+ if (FIRST_PARTY_KIT_IDS.has(kitId)) return "first-party";
65
+ return "unverified";
66
+ }
67
+
26
68
  export interface KitTargetsResult {
27
69
  kit_id: string;
28
70
  kit_name: string;
@@ -31,49 +73,63 @@ export interface KitTargetsResult {
31
73
  targets: KitTargetConsumer[];
32
74
  /** Extension field namespaces that are not Flow or Flow Agents-owned. */
33
75
  third_party_extensions: string[];
76
+ /**
77
+ * Trust level: who vouches for this kit. Orthogonal to the K-level capability axis.
78
+ * "first-party" = Kontour-published; "verified" = reserved (future); "unverified" = default.
79
+ */
80
+ trust: KitTrustLevel;
81
+ }
82
+
83
+ // Lazy-loaded cache for validateKitContainer from @kontourai/flow.
84
+ // list/status/activate are runtime ops that never call validation and must NOT load
85
+ // @kontourai/flow (it is unresolvable in a standalone installed bundle).
86
+ // Only validate/inspect (authoring ops) trigger this load.
87
+ type ValidateKitContainerFn = (kitDir: string, manifest: Record<string, unknown>) => { valid: boolean; diagnostics: { severity: string; path: string; message: string }[] };
88
+ let _validateKitContainerCache: ValidateKitContainerFn | null = null;
89
+
90
+ async function loadValidateKitContainer(): Promise<ValidateKitContainerFn> {
91
+ if (_validateKitContainerCache) return _validateKitContainerCache;
92
+ let mod: { validateKitContainer?: unknown };
93
+ try {
94
+ mod = await import("@kontourai/flow") as { validateKitContainer?: unknown };
95
+ } catch (err) {
96
+ throw new Error(
97
+ "container validation requires @kontourai/flow; run from an npm-installed flow-agents workspace " +
98
+ `or use 'flow kit validate' (original error: ${(err as Error).message})`
99
+ );
100
+ }
101
+ if (typeof mod.validateKitContainer !== "function") {
102
+ throw new Error("@kontourai/flow did not export validateKitContainer");
103
+ }
104
+ _validateKitContainerCache = mod.validateKitContainer as ValidateKitContainerFn;
105
+ return _validateKitContainerCache;
34
106
  }
35
107
 
36
108
  /**
37
- * Validates that the manifest satisfies the core Flow Kit container contract
38
- * (as specified by kontourai/flow PR #67) with all agent-extension fields stripped.
39
- * Returns a list of violation messages (empty = valid).
109
+ * Delegates core Flow Kit container validation to @kontourai/flow's validateKitContainer.
110
+ * The container contract lives once, in Flow. Returns a list of violation messages (empty = valid).
40
111
  *
41
112
  * The degradation invariant: every Flow Agents Kit MUST remain a valid core
42
113
  * Flow Kit container when agent-extension fields are ignored.
114
+ *
115
+ * Loads @kontourai/flow lazily (on first call) so that runtime ops (list/status/activate)
116
+ * that never invoke validation can run in standalone installed bundles where
117
+ * @kontourai/flow is not present.
118
+ *
119
+ * @param kitDir Real kit directory path for file-existence checks on flows[].path entries.
120
+ * Pass the actual kit directory when available; pass "" for structural-only checks.
43
121
  */
44
- export function validateCoreContainer(manifest: Record<string, unknown>, label: string): string[] {
45
- const errors: string[] = [];
46
- if (manifest.schema_version !== "1.0") {
47
- errors.push(`${label}: .schema_version must be "1.0"`);
48
- }
49
- if (typeof manifest.id !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(manifest.id)) {
50
- errors.push(`${label}: .id must be a stable kebab-case string`);
51
- }
52
- if (typeof manifest.name !== "string" || !manifest.name.trim()) {
53
- errors.push(`${label}: .name must be a non-empty string`);
54
- }
55
- if (!Array.isArray(manifest.flows) || manifest.flows.length === 0) {
56
- errors.push(`${label}: .flows must be a non-empty list`);
57
- } else {
58
- manifest.flows.forEach((entry: unknown, index: number) => {
59
- if (typeof entry !== "object" || entry === null) {
60
- errors.push(`${label}: flows[${index}] must be an object`);
61
- return;
62
- }
63
- const flow = entry as Record<string, unknown>;
64
- if (typeof flow.id !== "string" || !flow.id) {
65
- errors.push(`${label}: flows[${index}].id must be a string`);
66
- }
67
- if (typeof flow.path !== "string" || !flow.path) {
68
- errors.push(`${label}: flows[${index}].path must be a string`);
69
- }
70
- });
71
- }
72
- return errors;
122
+ async function delegateCoreContainerValidation(kitDir: string, manifest: Record<string, unknown>): Promise<string[]> {
123
+ const validateKitContainer = await loadValidateKitContainer();
124
+ const result = validateKitContainer(kitDir, manifest);
125
+ if (result.valid) return [];
126
+ return result.diagnostics
127
+ .filter((d) => d.severity === "error")
128
+ .map((d) => `${d.path}: ${d.message}`);
73
129
  }
74
130
 
75
131
  /**
76
- * Derives the consumer-target level (K0/K1/K2) and target audience list from
132
+ * Derives the consumer-target level (K0/K1/K2), target audience list, and trust level from
77
133
  * observable asset classes in the kit manifest. Does not require file I/O.
78
134
  *
79
135
  * Derivation rules (from kontourai/flow-agents#52 and Brian's layering review):
@@ -83,12 +139,21 @@ export function validateCoreContainer(manifest: Record<string, unknown>, label:
83
139
  * - targets.flow: always present when K0 (any Flow consumer can evaluate gates).
84
140
  * - targets.flow-agents: present when K1 (agent extension assets activate in >=1 harness).
85
141
  * - third-party: any top-level keys that are not core fields and not Flow Agents extension classes.
142
+ *
143
+ * Trust derivation (from kontourai/flow-agents#79):
144
+ * - "first-party": kit id is in FIRST_PARTY_KIT_IDS (Kontour-authored kits in this repo).
145
+ * - "unverified": all other kits (default; "verified" is reserved for a future process).
146
+ *
147
+ * @param manifest The kit.json manifest object.
148
+ * @param kitDir Kit directory for flow file-existence checks. Defaults to "" (structural-only).
149
+ * Pass the real kit directory from `inspect` to get authoritative K0 validation.
86
150
  */
87
- export function deriveKitTargets(manifest: Record<string, unknown>): KitTargetsResult {
151
+ export async function deriveKitTargets(manifest: Record<string, unknown>, kitDir = ""): Promise<KitTargetsResult> {
88
152
  const kitId = typeof manifest.id === "string" ? manifest.id : "<unknown>";
89
153
  const kitName = typeof manifest.name === "string" ? manifest.name : "<unknown>";
90
154
 
91
- const coreErrors = validateCoreContainer(manifest, "kit.json");
155
+ // Delegate core container validation to @kontourai/flow.
156
+ const coreErrors = await delegateCoreContainerValidation(kitDir, manifest);
92
157
  const k0 = coreErrors.length === 0;
93
158
 
94
159
  const hasAgentExtension = AGENT_EXTENSION_CLASSES.size > 0 &&
@@ -110,16 +175,20 @@ export function deriveKitTargets(manifest: Record<string, unknown>): KitTargetsR
110
175
  if (k1) targets.push("flow-agents");
111
176
  for (const ns of thirdPartyExtensions) targets.push(ns);
112
177
 
178
+ // Derive trust level orthogonally to the K-level capability axis.
179
+ const trust = deriveKitTrust(kitId);
180
+
113
181
  return {
114
182
  kit_id: kitId,
115
183
  kit_name: kitName,
116
184
  conformance: { k0, k1, k2 },
117
185
  targets,
118
186
  third_party_extensions: thirdPartyExtensions,
187
+ trust,
119
188
  };
120
189
  }
121
190
 
122
- export function validateKitRepository(kitDir: string): string[] {
191
+ export async function validateKitRepository(kitDir: string): Promise<string[]> {
123
192
  const errors: string[] = [];
124
193
  const manifestPath = path.join(kitDir, "kit.json");
125
194
  let manifest: Record<string, unknown>;
@@ -129,25 +198,17 @@ export function validateKitRepository(kitDir: string): string[] {
129
198
  errors.push(`${manifestPath}: invalid JSON: ${(error as Error).message}`);
130
199
  return errors;
131
200
  }
132
- if (manifest.schema_version !== "1.0") errors.push(`${manifestPath}: .schema_version must be "1.0"`);
133
- if (typeof manifest.id !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(manifest.id)) {
134
- errors.push(`${manifestPath}: .id must be a stable kebab-case string`);
135
- }
136
- if (typeof manifest.name !== "string" || !manifest.name.trim()) errors.push(`${manifestPath}: .name must be a non-empty string`);
137
201
 
138
- // Degradation invariant: every Flow Agents Kit must remain a valid core Flow Kit container
139
- // when agent-extension fields are stripped. Strip extensions and re-validate core contract.
140
- const coreManifest: Record<string, unknown> = {};
141
- for (const key of Object.keys(manifest)) {
142
- if (CORE_CONTAINER_FIELDS.has(key)) coreManifest[key] = manifest[key];
143
- }
144
- const coreErrors = validateCoreContainer(coreManifest, manifestPath);
145
- for (const err of coreErrors) {
146
- // Deduplicate: only add if not already covered by top-level checks above.
147
- if (!errors.some((existing) => existing === err)) errors.push(err);
148
- }
202
+ // Delegate core container validation (schema_version, id, name, flows including file
203
+ // existence) to @kontourai/flow the container contract lives once, in Flow.
204
+ // This enforces the degradation invariant: a Flow Agents Kit must remain a valid
205
+ // core Flow Kit container when extension fields are stripped.
206
+ const coreErrors = await delegateCoreContainerValidation(kitDir, manifest);
207
+ for (const err of coreErrors) errors.push(err);
149
208
 
150
- for (const section of ASSET_CLASSES) {
209
+ // Flow Agents extension validation: skills, docs, adapters, evals, assets.
210
+ // Flows are validated above by @kontourai/flow; only extension classes are checked here.
211
+ for (const section of EXTENSION_ASSET_CLASSES) {
151
212
  const entries = manifest[section];
152
213
  if (entries === undefined) continue;
153
214
  if (!Array.isArray(entries)) {
@@ -182,16 +243,15 @@ export function validateKitRepository(kitDir: string): string[] {
182
243
  return;
183
244
  }
184
245
  if (!fs.existsSync(resolved)) {
185
- const noun = section === "flows" ? "Flow Definition" : "asset";
186
- errors.push(`${manifestPath}: ${section}[${index}].path points at missing ${noun}: ${rel}`);
246
+ errors.push(`${manifestPath}: ${section}[${index}].path points at missing asset: ${rel}`);
187
247
  }
188
248
  });
189
249
  }
190
250
  return errors;
191
251
  }
192
252
 
193
- export function assertKitRepository(kitDir: string): Record<string, unknown> {
194
- const errors = validateKitRepository(kitDir);
253
+ export async function assertKitRepository(kitDir: string): Promise<Record<string, unknown>> {
254
+ const errors = await validateKitRepository(kitDir);
195
255
  if (errors.length) {
196
256
  const error = new Error("Flow Kit repository validation failed") as Error & { diagnostics?: string[] };
197
257
  error.diagnostics = errors;
@@ -12,6 +12,57 @@ const textExtensions = new Set([".css", ".html", ".js", ".json", ".md", ".sh", "
12
12
  const dropDiagnostics: string[] = [];
13
13
  const printDiagnostics = !["0", "false", "no"].includes(String(process.env.FLOW_AGENTS_EXPORT_DIAGNOSTICS ?? "1").toLowerCase());
14
14
 
15
+ /**
16
+ * Collect all skill source paths across skills/ and kit-owned skills.
17
+ * Returns an array of {name, src} pairs where name is the install name
18
+ * (same as the directory name) and src is the absolute SKILL.md path.
19
+ * Kit-owned skills are discovered by reading kit.json `skills` arrays;
20
+ * each entry's `path` is resolved relative to the kit directory.
21
+ */
22
+ function collectAllSkills(): Array<{ name: string; src: string }> {
23
+ const results: Array<{ name: string; src: string }> = [];
24
+ const seen = new Set<string>();
25
+
26
+ // 1. Top-level skills/ directory (tools pending reclassification).
27
+ const skillsDir = path.join(root, "skills");
28
+ if (fs.existsSync(skillsDir)) {
29
+ for (const skill of fs.readdirSync(skillsDir).sort()) {
30
+ const skillPath = path.join(skillsDir, skill, "SKILL.md");
31
+ if (fs.existsSync(skillPath) && !seen.has(skill)) {
32
+ seen.add(skill);
33
+ results.push({ name: skill, src: skillPath });
34
+ }
35
+ }
36
+ }
37
+
38
+ // 2. Kit-owned skills declared in kits/<kit>/kit.json `skills` arrays.
39
+ const kitsDir = path.join(root, "kits");
40
+ if (fs.existsSync(kitsDir)) {
41
+ for (const kitName of fs.readdirSync(kitsDir).sort()) {
42
+ const kitJson = path.join(kitsDir, kitName, "kit.json");
43
+ if (!fs.existsSync(kitJson)) continue;
44
+ let kitManifest: Record<string, unknown>;
45
+ try { kitManifest = loadJson<Record<string, unknown>>(kitJson); } catch { continue; }
46
+ const skills = Array.isArray(kitManifest["skills"]) ? kitManifest["skills"] as unknown[] : [];
47
+ for (const entry of skills) {
48
+ if (typeof entry !== "object" || entry === null) continue;
49
+ const skillEntry = entry as Record<string, unknown>;
50
+ const relPath = typeof skillEntry["path"] === "string" ? skillEntry["path"] : null;
51
+ if (!relPath) continue;
52
+ // Derive install name from the directory containing SKILL.md (one level up).
53
+ const absPath = path.resolve(path.join(kitsDir, kitName), relPath);
54
+ const skillName = path.basename(path.dirname(absPath));
55
+ if (fs.existsSync(absPath) && !seen.has(skillName)) {
56
+ seen.add(skillName);
57
+ results.push({ name: skillName, src: absPath });
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ return results.sort((a, b) => a.name.localeCompare(b.name));
64
+ }
65
+
15
66
  function resetDir(dir: string): void {
16
67
  fs.rmSync(dir, { recursive: true, force: true });
17
68
  fs.mkdirSync(dir, { recursive: true });
@@ -302,9 +353,8 @@ function buildClaudeCode(agents: Agent[]): void {
302
353
  copySharedContent(bundle, "claude-code", "<bundle-root>");
303
354
  writeText(path.join(bundle, manifest.claude_code.task_dir, ".gitkeep"), "");
304
355
  for (const spec of agents) writeText(path.join(bundle, ".claude/agents", `${spec.name}.md`), exportClaudeAgent(spec));
305
- for (const skill of fs.readdirSync(path.join(root, "skills"))) {
306
- const skillPath = path.join(root, "skills", skill, "SKILL.md");
307
- if (fs.existsSync(skillPath)) writeText(path.join(bundle, ".claude/skills", skill, "SKILL.md"), sanitizeText(readText(skillPath), "claude-code", "<bundle-root>"));
356
+ for (const { name, src } of collectAllSkills()) {
357
+ writeText(path.join(bundle, ".claude/skills", name, "SKILL.md"), sanitizeText(readText(src), "claude-code", "<bundle-root>"));
308
358
  }
309
359
  writeText(path.join(bundle, ".claude/settings.json"), exportClaudeSettings());
310
360
  writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("Claude Code", agents, manifest.claude_code.task_dir));
@@ -324,9 +374,8 @@ function buildCodex(agents: Agent[]): void {
324
374
  for (const [profileName, profile] of Object.entries(manifest.codex.profiles ?? {})) writeText(path.join(bundle, ".codex", `${profileName}.config.toml`), exportCodexProfileConfig(profile as Record<string, unknown>, settings));
325
375
  writeText(path.join(bundle, ".codex/hooks.json"), exportCodexHooks());
326
376
  for (const spec of targetAgents) writeText(path.join(bundle, ".codex/agents", `${spec.name}.toml`), exportCodexAgent(spec));
327
- for (const skill of fs.readdirSync(path.join(root, "skills"))) {
328
- const skillPath = path.join(root, "skills", skill, "SKILL.md");
329
- if (fs.existsSync(skillPath)) writeText(path.join(bundle, ".codex/skills", skill, "SKILL.md"), sanitizeText(readText(skillPath), "codex", "<bundle-root>"));
377
+ for (const { name, src } of collectAllSkills()) {
378
+ writeText(path.join(bundle, ".codex/skills", name, "SKILL.md"), sanitizeText(readText(src), "codex", "<bundle-root>"));
330
379
  }
331
380
  writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("Codex", targetAgents, manifest.codex.task_dir));
332
381
  writeText(path.join(bundle, "README.md"), exportTargetReadme("Codex", "bash install.sh /path/to/workspace"));
@@ -390,6 +439,7 @@ function exportOpencodePlugin(): string {
390
439
 
391
440
  import { spawnSync } from 'node:child_process';
392
441
  import { join, basename } from 'node:path';
442
+ import { mkdirSync, writeFileSync } from 'node:fs';
393
443
 
394
444
  // opencode runs plugins inside its own compiled (Bun-based) binary, so
395
445
  // process.execPath points at opencode itself — spawning it with a script
@@ -400,6 +450,19 @@ const NODE_BIN = basename(process.execPath).startsWith('node') ? process.execPat
400
450
  export const FlowAgentsPlugin = async ({ project, client, $, directory, worktree }) => {
401
451
  const root = directory || process.cwd();
402
452
 
453
+ // Deterministic load marker. opencode invokes this factory at startup but
454
+ // does not reliably surface plugin console output to its log file, and its
455
+ // internal "loading plugin" message was dropped in opencode 1.17.x. Write a
456
+ // marker into the workspace telemetry dir so acceptance tests can confirm the
457
+ // plugin loaded without depending on opencode internals. Best-effort only.
458
+ try {
459
+ const telemetryDir = join(root, '.telemetry');
460
+ mkdirSync(telemetryDir, { recursive: true });
461
+ writeFileSync(join(telemetryDir, 'opencode-plugin.loaded'), 'flow-agents');
462
+ } catch (_err) {
463
+ // Marker is diagnostic only; never block plugin load on a write failure.
464
+ }
465
+
403
466
  // The hook scripts read the event payload from stdin; an empty stdin makes
404
467
  // the telemetry pipeline silently skip the emit (fail-open), so every spawn
405
468
  // must pass a payload (caught by live acceptance smoke 2026-06-11).
@@ -490,9 +553,8 @@ function buildOpencode(agents: Agent[]): void {
490
553
  for (const spec of agents) {
491
554
  writeText(path.join(bundle, ".opencode/agents", `${spec.name}.md`), exportOpencodeAgent(spec));
492
555
  }
493
- for (const skill of fs.readdirSync(path.join(root, "skills"))) {
494
- const skillPath = path.join(root, "skills", skill, "SKILL.md");
495
- if (fs.existsSync(skillPath)) writeText(path.join(bundle, ".opencode/skills", skill, "SKILL.md"), sanitizeText(readText(skillPath), "opencode", "<bundle-root>"));
556
+ for (const { name, src } of collectAllSkills()) {
557
+ writeText(path.join(bundle, ".opencode/skills", name, "SKILL.md"), sanitizeText(readText(src), "opencode", "<bundle-root>"));
496
558
  }
497
559
  writeText(path.join(bundle, ".opencode/plugins/flow-agents.js"), exportOpencodePlugin());
498
560
  writeText(path.join(bundle, "opencode.json"), exportOpencodeConfig());
@@ -602,9 +664,8 @@ function buildPi(agents: Agent[]): void {
602
664
  writeText(path.join(bundle, manifest.pi.task_dir, ".gitkeep"), "");
603
665
  // pi has no named-subagent registry; agents are left canonical/unexported.
604
666
  // Skills are exported to .pi/skills/ (direct .md files supported in that dir).
605
- for (const skill of fs.readdirSync(path.join(root, "skills"))) {
606
- const skillPath = path.join(root, "skills", skill, "SKILL.md");
607
- if (fs.existsSync(skillPath)) writeText(path.join(bundle, ".pi/skills", skill, "SKILL.md"), sanitizeText(readText(skillPath), "pi", "<bundle-root>"));
667
+ for (const { name, src } of collectAllSkills()) {
668
+ writeText(path.join(bundle, ".pi/skills", name, "SKILL.md"), sanitizeText(readText(src), "pi", "<bundle-root>"));
608
669
  }
609
670
  writeText(path.join(bundle, ".pi/extensions/flow-agents.ts"), exportPiExtension());
610
671
  writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("pi", agents, manifest.pi.task_dir));
@@ -617,7 +678,7 @@ function buildCatalog(agents: Agent[]): Record<string, unknown> {
617
678
  return {
618
679
  source_root: ".",
619
680
  agents: agents.slice().sort((a, b) => a.name.localeCompare(b.name)).map((spec) => spec.name),
620
- skills: fs.readdirSync(path.join(root, "skills")).filter((name) => fs.existsSync(path.join(root, "skills", name, "SKILL.md"))).sort(),
681
+ skills: collectAllSkills().map(({ name }) => name),
621
682
  powers: fs.readdirSync(path.join(root, "powers")).filter((name) => fs.existsSync(path.join(root, "powers", name, "mcp.json"))).sort(),
622
683
  packs: packs.packs ?? [],
623
684
  kits: fs.existsSync(kitsCatalog) ? loadJson<Record<string, unknown>>(kitsCatalog).kits ?? [] : [],
@@ -74,15 +74,45 @@ function repoShape(manifest: Record<string, unknown>): string[][] {
74
74
  return rows;
75
75
  }
76
76
 
77
+ /** Collect all skill {name, absPath} pairs from skills/ and kit-owned skills. */
78
+ function allSkillPaths(): Array<{ name: string; absPath: string }> {
79
+ const results: Array<{ name: string; absPath: string }> = [];
80
+ const seen = new Set<string>();
81
+ const skillsDir = path.join(root, "skills");
82
+ if (exists(skillsDir)) {
83
+ for (const name of fs.readdirSync(skillsDir).sort()) {
84
+ const absPath = path.join(skillsDir, name, "SKILL.md");
85
+ if (exists(absPath) && !seen.has(name)) { seen.add(name); results.push({ name, absPath }); }
86
+ }
87
+ }
88
+ const kitsDir = path.join(root, "kits");
89
+ if (exists(kitsDir)) {
90
+ for (const kitName of fs.readdirSync(kitsDir).sort()) {
91
+ const kitJson = path.join(kitsDir, kitName, "kit.json");
92
+ if (!exists(kitJson)) continue;
93
+ let kitManifest: Record<string, unknown>;
94
+ try { kitManifest = loadJson<Record<string, unknown>>(kitJson); } catch { continue; }
95
+ const skills = Array.isArray(kitManifest["skills"]) ? kitManifest["skills"] as unknown[] : [];
96
+ for (const entry of skills) {
97
+ if (typeof entry !== "object" || entry === null) continue;
98
+ const skillEntry = entry as Record<string, unknown>;
99
+ const relPath = typeof skillEntry["path"] === "string" ? skillEntry["path"] : null;
100
+ if (!relPath) continue;
101
+ const absPath = path.resolve(path.join(kitsDir, kitName), relPath);
102
+ const skillName = path.basename(path.dirname(absPath));
103
+ if (exists(absPath) && !seen.has(skillName)) { seen.add(skillName); results.push({ name: skillName, absPath }); }
104
+ }
105
+ }
106
+ }
107
+ return results.sort((a, b) => a.name.localeCompare(b.name));
108
+ }
109
+
77
110
  function listSkillRows(): [string[][], string[][]] {
78
111
  const workflowRows: string[][] = [];
79
112
  const supportRows: string[][] = [];
80
- const skillsDir = path.join(root, "skills");
81
- for (const name of fs.readdirSync(skillsDir).sort()) {
82
- const skillPath = path.join(skillsDir, name, "SKILL.md");
83
- if (!exists(skillPath)) continue;
84
- const meta = frontmatter(readText(skillPath));
85
- const row = [meta.name ?? name, rel(skillPath), oneLine(meta.description ?? "")];
113
+ for (const { name, absPath } of allSkillPaths()) {
114
+ const meta = frontmatter(readText(absPath));
115
+ const row = [meta.name ?? name, rel(absPath), oneLine(meta.description ?? "")];
86
116
  if (workflowSkills.has(row[0])) workflowRows.push(row);
87
117
  else supportRows.push(row);
88
118
  }
@@ -37,7 +37,7 @@ const publicScriptWrappers = new Map<string, { target: string; significantLines:
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()));'] }],
@@ -301,6 +301,28 @@ function validateAgentPaths(reporter: Reporter, manifest: any): void {
301
301
  }
302
302
  }
303
303
  function validateLegacyRefs(reporter: Reporter): void {
304
+ // Collect all kit-owned asset relative paths so legacy-ref scanning can skip matches
305
+ // that are subpaths of kit-owned assets. E.g. legacyRefRe matches "skills/plan-work/SKILL.md"
306
+ // within "kits/builder/skills/plan-work/SKILL.md"; the kit declares and validates these.
307
+ const kitOwnedSubPaths = new Set<string>();
308
+ const kitsDir = path.join(root, "kits");
309
+ if (fs.existsSync(kitsDir)) {
310
+ for (const kitName of fs.readdirSync(kitsDir)) {
311
+ const kitJson = path.join(kitsDir, kitName, "kit.json");
312
+ if (!fs.existsSync(kitJson)) continue;
313
+ try {
314
+ const kitManifest = loadJson<Record<string, unknown>>(kitJson);
315
+ for (const section of ["skills", "docs", "adapters", "evals", "assets"]) {
316
+ const entries = Array.isArray(kitManifest[section]) ? kitManifest[section] as unknown[] : [];
317
+ for (const entry of entries) {
318
+ if (typeof entry !== "object" || entry === null) continue;
319
+ const relPath = (entry as Record<string, unknown>)["path"];
320
+ if (typeof relPath === "string" && relPath) kitOwnedSubPaths.add(relPath);
321
+ }
322
+ }
323
+ } catch { /* skip invalid kit.json */ }
324
+ }
325
+ }
304
326
  for (const file of walkFiles(path.join(root, "evals")).sort()) {
305
327
  if (!textRefExtensions.has(path.extname(file))) continue;
306
328
  const parts = path.relative(path.join(root, "evals"), file).split(path.sep);
@@ -310,6 +332,10 @@ function validateLegacyRefs(reporter: Reporter): void {
310
332
  const ref = match[0].replace(/[.,)'"\]]+$/, "");
311
333
  if (/[{}$]/.test(ref)) continue;
312
334
  if (ref.split(/[\\/]/).includes("node_modules")) continue;
335
+ // Skip refs that are declared kit-owned asset paths or their parent directories
336
+ // (e.g. "skills/plan-work/SKILL.md" or "skills/plan-work" matched inside
337
+ // "kits/builder/skills/plan-work/SKILL.md" in eval files).
338
+ if (kitOwnedSubPaths.has(ref) || [...kitOwnedSubPaths].some((p) => p.startsWith(ref + "/"))) continue;
313
339
  const candidates = [path.join(root, ref), ...(ref.startsWith("evals/") ? [] : [path.join(root, "evals", ref)])];
314
340
  if (!candidates.some(fs.existsSync)) reporter.fail(`${rel(file)}: references missing source path: ${ref}`);
315
341
  }
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- import("../build/src/cli/flow-kit.js").then(({ main }) => process.exit(main()));
@@ -1,40 +0,0 @@
1
- ---
2
- name: context-budget
3
- description: >-
4
- Audit token overhead across Flow Agents bundles — agent specs, skills, context files,
5
- MCP servers. Produces budget report with per-component breakdown and optimization suggestions.
6
- ---
7
-
8
- # Context Budget Audit
9
-
10
- Scan installed Flow Agents bundles and estimate token overhead per component. Produces a structured budget report with optimization suggestions.
11
-
12
- ## Workflow
13
-
14
- ### Phase 1: Inventory
15
-
16
- Run `bash context/scripts/context-budget/budget-scan.sh` to discover all loaded components. The script walks `~/.flow-agents/` and outputs JSON with per-bundle breakdowns.
17
-
18
- ### Phase 2: Classify
19
-
20
- Bucket each component from the scan output:
21
- - **Always loaded**: context files matching package dependency patterns, skill frontmatter descriptions
22
- - **On-demand**: full SKILL.md body (loaded on skill activation), deferred context (`context/deferred/`)
23
- - **Per-agent**: agent-spec systemPrompt, agent-specific MCP servers
24
-
25
- ### Phase 3: Detect Issues
26
-
27
- Flag problems from the scan data:
28
- - Heavy agent specs: systemPrompt > 200 lines
29
- - Bloated skill descriptions: frontmatter description > 30 words
30
- - MCP over-subscription: agent with > 10 MCP servers or > 50 total tools
31
- - Context bloat: any single context file > 100 lines
32
- - Deferred candidates: context files > 2% of model context that aren't safety/routing
33
-
34
- ### Phase 4: Report
35
-
36
- Structured output:
37
- - Per-bundle breakdown (tokens by category)
38
- - Per-agent breakdown (what each agent loads at spawn)
39
- - Top-N optimization suggestions ranked by token savings
40
- - Use `--verbose` flag on budget-scan.sh for per-file token counts