@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
package/src/cli.ts 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";
@@ -28,7 +28,7 @@ const availableCommands = new Map<string, (argv: string[]) => number | Promise<n
28
28
  ["effective-backlog-settings", effectiveBacklogSettings],
29
29
  ["filter-installed-packs", filterInstalledPacks],
30
30
  ["fixture-retirement-audit", fixtureRetirementAudit],
31
- ["flow-kit", flowKit],
31
+ ["kit", kit],
32
32
  ["init", init],
33
33
  ["promote-workflow-artifact", promoteWorkflowArtifact],
34
34
  ["publish-change", publishChange],
@@ -51,7 +51,7 @@ const aliases = new Map<string, string>([
51
51
  ["flow-agents-effective-backlog-settings", "effective-backlog-settings"],
52
52
  ["flow-agents-filter-installed-packs", "filter-installed-packs"],
53
53
  ["flow-agents-fixture-retirement-audit", "fixture-retirement-audit"],
54
- ["flow-agents-flow-kit", "flow-kit"],
54
+ ["flow-agents-kit", "kit"],
55
55
  ["flow-agents-promote-workflow-artifact", "promote-workflow-artifact"],
56
56
  ["flow-agents-publish-change", "publish-change"],
57
57
  ["flow-agents-pull-work-provider", "pull-work-provider"],
@@ -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.
@@ -33,43 +34,52 @@ export interface KitTargetsResult {
33
34
  third_party_extensions: string[];
34
35
  }
35
36
 
37
+ // Lazy-loaded cache for validateKitContainer from @kontourai/flow.
38
+ // list/status/activate are runtime ops that never call validation and must NOT load
39
+ // @kontourai/flow (it is unresolvable in a standalone installed bundle).
40
+ // Only validate/inspect (authoring ops) trigger this load.
41
+ type ValidateKitContainerFn = (kitDir: string, manifest: Record<string, unknown>) => { valid: boolean; diagnostics: { severity: string; path: string; message: string }[] };
42
+ let _validateKitContainerCache: ValidateKitContainerFn | null = null;
43
+
44
+ async function loadValidateKitContainer(): Promise<ValidateKitContainerFn> {
45
+ if (_validateKitContainerCache) return _validateKitContainerCache;
46
+ let mod: { validateKitContainer?: unknown };
47
+ try {
48
+ mod = await import("@kontourai/flow") as { validateKitContainer?: unknown };
49
+ } catch (err) {
50
+ throw new Error(
51
+ "container validation requires @kontourai/flow; run from an npm-installed flow-agents workspace " +
52
+ `or use 'flow kit validate' (original error: ${(err as Error).message})`
53
+ );
54
+ }
55
+ if (typeof mod.validateKitContainer !== "function") {
56
+ throw new Error("@kontourai/flow did not export validateKitContainer");
57
+ }
58
+ _validateKitContainerCache = mod.validateKitContainer as ValidateKitContainerFn;
59
+ return _validateKitContainerCache;
60
+ }
61
+
36
62
  /**
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).
63
+ * Delegates core Flow Kit container validation to @kontourai/flow's validateKitContainer.
64
+ * The container contract lives once, in Flow. Returns a list of violation messages (empty = valid).
40
65
  *
41
66
  * The degradation invariant: every Flow Agents Kit MUST remain a valid core
42
67
  * Flow Kit container when agent-extension fields are ignored.
68
+ *
69
+ * Loads @kontourai/flow lazily (on first call) so that runtime ops (list/status/activate)
70
+ * that never invoke validation can run in standalone installed bundles where
71
+ * @kontourai/flow is not present.
72
+ *
73
+ * @param kitDir Real kit directory path for file-existence checks on flows[].path entries.
74
+ * Pass the actual kit directory when available; pass "" for structural-only checks.
43
75
  */
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;
76
+ async function delegateCoreContainerValidation(kitDir: string, manifest: Record<string, unknown>): Promise<string[]> {
77
+ const validateKitContainer = await loadValidateKitContainer();
78
+ const result = validateKitContainer(kitDir, manifest);
79
+ if (result.valid) return [];
80
+ return result.diagnostics
81
+ .filter((d) => d.severity === "error")
82
+ .map((d) => `${d.path}: ${d.message}`);
73
83
  }
74
84
 
75
85
  /**
@@ -83,12 +93,17 @@ export function validateCoreContainer(manifest: Record<string, unknown>, label:
83
93
  * - targets.flow: always present when K0 (any Flow consumer can evaluate gates).
84
94
  * - targets.flow-agents: present when K1 (agent extension assets activate in >=1 harness).
85
95
  * - third-party: any top-level keys that are not core fields and not Flow Agents extension classes.
96
+ *
97
+ * @param manifest The kit.json manifest object.
98
+ * @param kitDir Kit directory for flow file-existence checks. Defaults to "" (structural-only).
99
+ * Pass the real kit directory from `inspect` to get authoritative K0 validation.
86
100
  */
87
- export function deriveKitTargets(manifest: Record<string, unknown>): KitTargetsResult {
101
+ export async function deriveKitTargets(manifest: Record<string, unknown>, kitDir = ""): Promise<KitTargetsResult> {
88
102
  const kitId = typeof manifest.id === "string" ? manifest.id : "<unknown>";
89
103
  const kitName = typeof manifest.name === "string" ? manifest.name : "<unknown>";
90
104
 
91
- const coreErrors = validateCoreContainer(manifest, "kit.json");
105
+ // Delegate core container validation to @kontourai/flow.
106
+ const coreErrors = await delegateCoreContainerValidation(kitDir, manifest);
92
107
  const k0 = coreErrors.length === 0;
93
108
 
94
109
  const hasAgentExtension = AGENT_EXTENSION_CLASSES.size > 0 &&
@@ -119,7 +134,7 @@ export function deriveKitTargets(manifest: Record<string, unknown>): KitTargetsR
119
134
  };
120
135
  }
121
136
 
122
- export function validateKitRepository(kitDir: string): string[] {
137
+ export async function validateKitRepository(kitDir: string): Promise<string[]> {
123
138
  const errors: string[] = [];
124
139
  const manifestPath = path.join(kitDir, "kit.json");
125
140
  let manifest: Record<string, unknown>;
@@ -129,25 +144,17 @@ export function validateKitRepository(kitDir: string): string[] {
129
144
  errors.push(`${manifestPath}: invalid JSON: ${(error as Error).message}`);
130
145
  return errors;
131
146
  }
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
147
 
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
- }
148
+ // Delegate core container validation (schema_version, id, name, flows including file
149
+ // existence) to @kontourai/flow the container contract lives once, in Flow.
150
+ // This enforces the degradation invariant: a Flow Agents Kit must remain a valid
151
+ // core Flow Kit container when extension fields are stripped.
152
+ const coreErrors = await delegateCoreContainerValidation(kitDir, manifest);
153
+ for (const err of coreErrors) errors.push(err);
149
154
 
150
- for (const section of ASSET_CLASSES) {
155
+ // Flow Agents extension validation: skills, docs, adapters, evals, assets.
156
+ // Flows are validated above by @kontourai/flow; only extension classes are checked here.
157
+ for (const section of EXTENSION_ASSET_CLASSES) {
151
158
  const entries = manifest[section];
152
159
  if (entries === undefined) continue;
153
160
  if (!Array.isArray(entries)) {
@@ -182,16 +189,15 @@ export function validateKitRepository(kitDir: string): string[] {
182
189
  return;
183
190
  }
184
191
  if (!fs.existsSync(resolved)) {
185
- const noun = section === "flows" ? "Flow Definition" : "asset";
186
- errors.push(`${manifestPath}: ${section}[${index}].path points at missing ${noun}: ${rel}`);
192
+ errors.push(`${manifestPath}: ${section}[${index}].path points at missing asset: ${rel}`);
187
193
  }
188
194
  });
189
195
  }
190
196
  return errors;
191
197
  }
192
198
 
193
- export function assertKitRepository(kitDir: string): Record<string, unknown> {
194
- const errors = validateKitRepository(kitDir);
199
+ export async function assertKitRepository(kitDir: string): Promise<Record<string, unknown>> {
200
+ const errors = await validateKitRepository(kitDir);
195
201
  if (errors.length) {
196
202
  const error = new Error("Flow Kit repository validation failed") as Error & { diagnostics?: string[] };
197
203
  error.diagnostics = errors;
@@ -126,31 +126,47 @@ function safeSegment(value: string): string {
126
126
  return value.replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^[.-]+|[.-]+$/g, "") || "asset";
127
127
  }
128
128
 
129
+ // Asset classes that are directly activated (copied to the runtime directory) by both adapters.
130
+ // flows: gate definitions read by the adapter's flow-routing layer.
131
+ // skills: agent guidance markdown copied to skills/<kit-id>/ for agent discovery.
132
+ // docs: documentation markdown copied to docs/<kit-id>/ for agent reference.
133
+ const ACTIVATED_ASSET_CLASSES = new Set(["flows", "skills", "docs"]);
134
+
129
135
  export function activateCodexLocal(sourceRoot: string, dest: string): Record<string, unknown> {
130
136
  const inventory = readKitInventory(sourceRoot, dest);
131
137
  const runtimeDir = path.join(dest, ".flow-agents", "runtime", "codex");
132
138
  const generated: Record<string, string>[] = [];
133
139
  const skipped: Record<string, string | null>[] = [];
134
140
  for (const asset of inventory.assets) {
135
- if (asset.asset_class !== "flows") {
136
- 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" });
137
- continue;
138
- }
139
- if (!asset.asset_id) {
140
- 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" });
141
- continue;
141
+ if (asset.asset_class === "flows") {
142
+ if (!asset.asset_id) {
143
+ 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" });
144
+ continue;
145
+ }
146
+ const output = path.join(runtimeDir, "flows", safeSegment(asset.kit_id), `${safeSegment(asset.asset_id)}.flow.json`);
147
+ fs.mkdirSync(path.dirname(output), { recursive: true });
148
+ fs.copyFileSync(asset.source_path, output);
149
+ 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("/") });
150
+ } else if (asset.asset_class === "skills" || asset.asset_class === "docs") {
151
+ // Copy skills and docs to runtime/<adapter>/<class>/<kit-id>/<filename> so the
152
+ // agent's guidance index (AGENTS.md) can reference them and they are co-located
153
+ // with flow definitions for the same kit.
154
+ const filename = path.basename(asset.source_path);
155
+ const output = path.join(runtimeDir, asset.asset_class, safeSegment(asset.kit_id), filename);
156
+ fs.mkdirSync(path.dirname(output), { recursive: true });
157
+ fs.copyFileSync(asset.source_path, output);
158
+ 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("/") });
159
+ } else {
160
+ 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" });
142
161
  }
143
- const output = path.join(runtimeDir, "flows", safeSegment(asset.kit_id), `${safeSegment(asset.asset_id)}.flow.json`);
144
- fs.mkdirSync(path.dirname(output), { recursive: true });
145
- fs.copyFileSync(asset.source_path, output);
146
- 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("/") });
147
162
  }
148
163
  fs.mkdirSync(runtimeDir, { recursive: true });
149
- 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 };
164
+ const supportedClasses = Array.from(ACTIVATED_ASSET_CLASSES);
165
+ 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 };
150
166
  const manifestPath = path.join(runtimeDir, "activation.json");
151
167
  writeJson(manifestPath, manifest);
152
168
  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("/") });
153
- return { selected_adapter: "codex-local", supported_asset_classes: ["flows"], generated_runtime_files: generated, skipped_assets: skipped, warnings: inventory.warnings, errors: inventory.errors };
169
+ return { selected_adapter: "codex-local", supported_asset_classes: supportedClasses, generated_runtime_files: generated, skipped_assets: skipped, warnings: inventory.warnings, errors: inventory.errors };
154
170
  }
155
171
 
156
172
  // Decision Q3 (Issue #32): Option (a) — new adapter id "strands-local" rather than
@@ -163,27 +179,39 @@ export function activateStrandsLocal(sourceRoot: string, dest: string): Record<s
163
179
  const inventory = readKitInventory(sourceRoot, dest);
164
180
  // Runtime flows land at .flow-agents/runtime/strands/flows/<kit-id>/<asset-id>.flow.json
165
181
  // so the Strands steering context can glob for *.flow.json under this path.
182
+ // Runtime skills land at .flow-agents/runtime/strands/skills/<kit-id>/<filename> and
183
+ // docs at .flow-agents/runtime/strands/docs/<kit-id>/<filename> for system-prompt injection.
166
184
  const runtimeDir = path.join(dest, ".flow-agents", "runtime", "strands");
167
185
  const generated: Record<string, string>[] = [];
168
186
  const skipped: Record<string, string | null>[] = [];
169
187
  for (const asset of inventory.assets) {
170
- if (asset.asset_class !== "flows") {
171
- 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" });
172
- continue;
173
- }
174
- if (!asset.asset_id) {
175
- 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" });
176
- continue;
188
+ if (asset.asset_class === "flows") {
189
+ if (!asset.asset_id) {
190
+ 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" });
191
+ continue;
192
+ }
193
+ const output = path.join(runtimeDir, "flows", safeSegment(asset.kit_id), `${safeSegment(asset.asset_id)}.flow.json`);
194
+ fs.mkdirSync(path.dirname(output), { recursive: true });
195
+ fs.copyFileSync(asset.source_path, output);
196
+ 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("/") });
197
+ } else if (asset.asset_class === "skills" || asset.asset_class === "docs") {
198
+ // Mirror the codex-local layout: strands/<class>/<kit-id>/<filename>.
199
+ // The Strands system-prompt injection layer can glob for all *.md files under
200
+ // .flow-agents/runtime/strands/skills/ to include agent guidance in the context.
201
+ const filename = path.basename(asset.source_path);
202
+ const output = path.join(runtimeDir, asset.asset_class, safeSegment(asset.kit_id), filename);
203
+ fs.mkdirSync(path.dirname(output), { recursive: true });
204
+ fs.copyFileSync(asset.source_path, output);
205
+ 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("/") });
206
+ } else {
207
+ 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" });
177
208
  }
178
- const output = path.join(runtimeDir, "flows", safeSegment(asset.kit_id), `${safeSegment(asset.asset_id)}.flow.json`);
179
- fs.mkdirSync(path.dirname(output), { recursive: true });
180
- fs.copyFileSync(asset.source_path, output);
181
- 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("/") });
182
209
  }
183
210
  fs.mkdirSync(runtimeDir, { recursive: true });
184
- 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 };
211
+ const supportedClasses = Array.from(ACTIVATED_ASSET_CLASSES);
212
+ 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 };
185
213
  const manifestPath = path.join(runtimeDir, "activation.json");
186
214
  writeJson(manifestPath, manifest);
187
215
  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("/") });
188
- return { selected_adapter: "strands-local", supported_asset_classes: ["flows"], generated_runtime_files: generated, skipped_assets: skipped, warnings: inventory.warnings, errors: inventory.errors };
216
+ return { selected_adapter: "strands-local", supported_asset_classes: supportedClasses, generated_runtime_files: generated, skipped_assets: skipped, warnings: inventory.warnings, errors: inventory.errors };
189
217
  }
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs";
3
+ import { fileURLToPath } from "node:url";
3
4
  import path from "node:path";
4
5
  import { loadJson, readText, root, walkFiles, writeText } from "./common.js";
5
6
 
@@ -11,6 +12,57 @@ const textExtensions = new Set([".css", ".html", ".js", ".json", ".md", ".sh", "
11
12
  const dropDiagnostics: string[] = [];
12
13
  const printDiagnostics = !["0", "false", "no"].includes(String(process.env.FLOW_AGENTS_EXPORT_DIAGNOSTICS ?? "1").toLowerCase());
13
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
+
14
66
  function resetDir(dir: string): void {
15
67
  fs.rmSync(dir, { recursive: true, force: true });
16
68
  fs.mkdirSync(dir, { recursive: true });
@@ -301,9 +353,8 @@ function buildClaudeCode(agents: Agent[]): void {
301
353
  copySharedContent(bundle, "claude-code", "<bundle-root>");
302
354
  writeText(path.join(bundle, manifest.claude_code.task_dir, ".gitkeep"), "");
303
355
  for (const spec of agents) writeText(path.join(bundle, ".claude/agents", `${spec.name}.md`), exportClaudeAgent(spec));
304
- for (const skill of fs.readdirSync(path.join(root, "skills"))) {
305
- const skillPath = path.join(root, "skills", skill, "SKILL.md");
306
- 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>"));
307
358
  }
308
359
  writeText(path.join(bundle, ".claude/settings.json"), exportClaudeSettings());
309
360
  writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("Claude Code", agents, manifest.claude_code.task_dir));
@@ -323,9 +374,8 @@ function buildCodex(agents: Agent[]): void {
323
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));
324
375
  writeText(path.join(bundle, ".codex/hooks.json"), exportCodexHooks());
325
376
  for (const spec of targetAgents) writeText(path.join(bundle, ".codex/agents", `${spec.name}.toml`), exportCodexAgent(spec));
326
- for (const skill of fs.readdirSync(path.join(root, "skills"))) {
327
- const skillPath = path.join(root, "skills", skill, "SKILL.md");
328
- 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>"));
329
379
  }
330
380
  writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("Codex", targetAgents, manifest.codex.task_dir));
331
381
  writeText(path.join(bundle, "README.md"), exportTargetReadme("Codex", "bash install.sh /path/to/workspace"));
@@ -489,9 +539,8 @@ function buildOpencode(agents: Agent[]): void {
489
539
  for (const spec of agents) {
490
540
  writeText(path.join(bundle, ".opencode/agents", `${spec.name}.md`), exportOpencodeAgent(spec));
491
541
  }
492
- for (const skill of fs.readdirSync(path.join(root, "skills"))) {
493
- const skillPath = path.join(root, "skills", skill, "SKILL.md");
494
- if (fs.existsSync(skillPath)) writeText(path.join(bundle, ".opencode/skills", skill, "SKILL.md"), sanitizeText(readText(skillPath), "opencode", "<bundle-root>"));
542
+ for (const { name, src } of collectAllSkills()) {
543
+ writeText(path.join(bundle, ".opencode/skills", name, "SKILL.md"), sanitizeText(readText(src), "opencode", "<bundle-root>"));
495
544
  }
496
545
  writeText(path.join(bundle, ".opencode/plugins/flow-agents.js"), exportOpencodePlugin());
497
546
  writeText(path.join(bundle, "opencode.json"), exportOpencodeConfig());
@@ -601,9 +650,8 @@ function buildPi(agents: Agent[]): void {
601
650
  writeText(path.join(bundle, manifest.pi.task_dir, ".gitkeep"), "");
602
651
  // pi has no named-subagent registry; agents are left canonical/unexported.
603
652
  // Skills are exported to .pi/skills/ (direct .md files supported in that dir).
604
- for (const skill of fs.readdirSync(path.join(root, "skills"))) {
605
- const skillPath = path.join(root, "skills", skill, "SKILL.md");
606
- if (fs.existsSync(skillPath)) writeText(path.join(bundle, ".pi/skills", skill, "SKILL.md"), sanitizeText(readText(skillPath), "pi", "<bundle-root>"));
653
+ for (const { name, src } of collectAllSkills()) {
654
+ writeText(path.join(bundle, ".pi/skills", name, "SKILL.md"), sanitizeText(readText(src), "pi", "<bundle-root>"));
607
655
  }
608
656
  writeText(path.join(bundle, ".pi/extensions/flow-agents.ts"), exportPiExtension());
609
657
  writeText(path.join(bundle, "AGENTS.md"), exportRootAgentsMd("pi", agents, manifest.pi.task_dir));
@@ -616,7 +664,7 @@ function buildCatalog(agents: Agent[]): Record<string, unknown> {
616
664
  return {
617
665
  source_root: ".",
618
666
  agents: agents.slice().sort((a, b) => a.name.localeCompare(b.name)).map((spec) => spec.name),
619
- skills: fs.readdirSync(path.join(root, "skills")).filter((name) => fs.existsSync(path.join(root, "skills", name, "SKILL.md"))).sort(),
667
+ skills: collectAllSkills().map(({ name }) => name),
620
668
  powers: fs.readdirSync(path.join(root, "powers")).filter((name) => fs.existsSync(path.join(root, "powers", name, "mcp.json"))).sort(),
621
669
  packs: packs.packs ?? [],
622
670
  kits: fs.existsSync(kitsCatalog) ? loadJson<Record<string, unknown>>(kitsCatalog).kits ?? [] : [],
@@ -646,4 +694,9 @@ export function main(): number {
646
694
  }
647
695
  return 0;
648
696
  }
649
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
697
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
698
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
699
+ // entry-point guard fires correctly when the module is loaded directly as a script.
700
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
701
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
702
+ if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs";
3
+ import { fileURLToPath } from "node:url";
3
4
  import path from "node:path";
4
5
  import { exists, loadJson, markdownTable, oneLine, readText, rel, root, writeText } from "./common.js";
5
6
 
@@ -73,15 +74,45 @@ function repoShape(manifest: Record<string, unknown>): string[][] {
73
74
  return rows;
74
75
  }
75
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
+
76
110
  function listSkillRows(): [string[][], string[][]] {
77
111
  const workflowRows: string[][] = [];
78
112
  const supportRows: string[][] = [];
79
- const skillsDir = path.join(root, "skills");
80
- for (const name of fs.readdirSync(skillsDir).sort()) {
81
- const skillPath = path.join(skillsDir, name, "SKILL.md");
82
- if (!exists(skillPath)) continue;
83
- const meta = frontmatter(readText(skillPath));
84
- 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 ?? "")];
85
116
  if (workflowSkills.has(row[0])) workflowRows.push(row);
86
117
  else supportRows.push(row);
87
118
  }
@@ -196,4 +227,9 @@ export function main(argv = process.argv.slice(2)): number {
196
227
  return 0;
197
228
  }
198
229
 
199
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
230
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
231
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
232
+ // entry-point guard fires correctly when the module is loaded directly as a script.
233
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
234
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
235
+ if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs";
3
+ import { fileURLToPath } from "node:url";
3
4
  import path from "node:path";
4
5
 
5
6
  export function main(argv = process.argv.slice(2)): number {
@@ -54,4 +55,9 @@ export function main(argv = process.argv.slice(2)): number {
54
55
  console.log(errors === 0 ? "Result: PASS" : `Result: FAIL (${errors} error(s))`);
55
56
  return errors === 0 ? 0 : 1;
56
57
  }
57
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
58
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
59
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
60
+ // entry-point guard fires correctly when the module is loaded directly as a script.
61
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
62
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
63
+ if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs";
3
+ import { fileURLToPath } from "node:url";
3
4
  import path from "node:path";
4
5
  import { spawnSync } from "node:child_process";
5
6
  import { loadJson, readText, rel, root, walkFiles } from "./common.js";
@@ -36,7 +37,7 @@ const publicScriptWrappers = new Map<string, { target: string; significantLines:
36
37
  ] }],
37
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))));'] }],
38
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))));'] }],
39
- ["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)));'] }],
40
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()));'] }],
41
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()));'] }],
42
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()));'] }],
@@ -300,6 +301,28 @@ function validateAgentPaths(reporter: Reporter, manifest: any): void {
300
301
  }
301
302
  }
302
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
+ }
303
326
  for (const file of walkFiles(path.join(root, "evals")).sort()) {
304
327
  if (!textRefExtensions.has(path.extname(file))) continue;
305
328
  const parts = path.relative(path.join(root, "evals"), file).split(path.sep);
@@ -309,6 +332,10 @@ function validateLegacyRefs(reporter: Reporter): void {
309
332
  const ref = match[0].replace(/[.,)'"\]]+$/, "");
310
333
  if (/[{}$]/.test(ref)) continue;
311
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;
312
339
  const candidates = [path.join(root, ref), ...(ref.startsWith("evals/") ? [] : [path.join(root, "evals", ref)])];
313
340
  if (!candidates.some(fs.existsSync)) reporter.fail(`${rel(file)}: references missing source path: ${ref}`);
314
341
  }
@@ -491,4 +518,9 @@ export function main(argv = process.argv.slice(2)): number {
491
518
  console.log("Source tree validation passed.");
492
519
  return 0;
493
520
  }
494
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
521
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
522
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
523
+ // entry-point guard fires correctly when the module is loaded directly as a script.
524
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
525
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
526
+ if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- import("../build/src/cli/flow-kit.js").then(({ main }) => process.exit(main()));