@kontourai/flow-agents 1.1.0 → 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 (75) hide show
  1. package/.github/workflows/runtime-compat.yml +5 -2
  2. package/CHANGELOG.md +26 -0
  3. package/README.md +26 -5
  4. package/build/src/cli/{flow-kit.js → kit.js} +122 -108
  5. package/build/src/cli/validate-source-tree.js +4 -4
  6. package/build/src/cli.js +3 -3
  7. package/build/src/flow-kit/validate.js +58 -62
  8. package/build/src/tools/build-universal-bundles.js +64 -17
  9. package/build/src/tools/generate-context-map.js +49 -7
  10. package/build/src/tools/validate-source-tree.js +32 -1
  11. package/docs/adr/0007-flow-skill-kit-tool-boundary.md +169 -0
  12. package/docs/adr/0007-skill-audit.md +112 -0
  13. package/docs/adr/0008-kit-operation-boundary.md +88 -0
  14. package/docs/context-map.md +18 -22
  15. package/docs/flow-kit-repository-contract.md +5 -5
  16. package/docs/getting-started.md +177 -0
  17. package/docs/index.md +19 -8
  18. package/docs/kit-authoring-guide.md +26 -7
  19. package/docs/knowledge-kit.md +2 -2
  20. package/docs/spec/runtime-hook-surface.md +1 -1
  21. package/docs/vision.md +1 -1
  22. package/docs/workflow-usage-guide.md +1 -1
  23. package/evals/fixtures/builder-kit-workflow-state/happy-path.json +2 -2
  24. package/evals/fixtures/builder-kit-workflow-state/mid-work-resume.json +2 -2
  25. package/evals/fixtures/console-learning-projection/artifacts/console-learning-correction/learning.json +1 -1
  26. package/evals/fixtures/pull-work-provider/github-issues.json +5 -5
  27. package/evals/integration/test_activate_npx_context.sh +2 -2
  28. package/evals/integration/test_bundle_install.sh +17 -12
  29. package/evals/integration/test_console_learning_projection.sh +1 -1
  30. package/evals/integration/test_flow_kit_install_git.sh +7 -7
  31. package/evals/integration/test_flow_kit_repository.sh +4 -4
  32. package/evals/integration/test_kit_conformance_levels.sh +1 -1
  33. package/evals/integration/test_local_flow_kit_install.sh +7 -7
  34. package/evals/integration/test_publish_change_helper.sh +1 -1
  35. package/evals/integration/test_pull_work_provider.sh +1 -1
  36. package/evals/integration/test_runtime_adapter_activation.sh +3 -3
  37. package/evals/lib/node.sh +2 -2
  38. package/evals/static/test_workflow_skills.sh +15 -15
  39. package/integrations/strands/flow_agents_strands/steering.py +1 -1
  40. package/integrations/strands-ts/src/hooks.ts +1 -1
  41. package/kits/builder/kit.json +17 -0
  42. package/{skills → kits/builder/skills}/builder-shape/SKILL.md +4 -4
  43. package/{skills → kits/builder/skills}/idea-to-backlog/SKILL.md +1 -1
  44. package/kits/knowledge/kit.json +16 -9
  45. package/package.json +8 -5
  46. package/packaging/packs.json +1 -21
  47. package/scripts/README.md +1 -1
  48. package/scripts/kit.js +2 -0
  49. package/skills/README.md +23 -0
  50. package/src/cli/{flow-kit.ts → kit.ts} +124 -109
  51. package/src/cli/validate-source-tree.ts +4 -4
  52. package/src/cli.ts +3 -3
  53. package/src/flow-kit/validate.ts +63 -57
  54. package/src/tools/build-universal-bundles.ts +60 -13
  55. package/src/tools/generate-context-map.ts +36 -6
  56. package/src/tools/validate-source-tree.ts +27 -1
  57. package/scripts/flow-kit.js +0 -2
  58. package/skills/context-budget/SKILL.md +0 -40
  59. package/skills/explore/SKILL.md +0 -137
  60. package/skills/feedback-loop/SKILL.md +0 -87
  61. package/skills/frontend-design/SKILL.md +0 -80
  62. /package/{skills → kits/builder/skills}/deliver/SKILL.md +0 -0
  63. /package/{skills → kits/builder/skills}/design-probe/SKILL.md +0 -0
  64. /package/{skills → kits/builder/skills}/evidence-gate/SKILL.md +0 -0
  65. /package/{skills → kits/builder/skills}/execute-plan/SKILL.md +0 -0
  66. /package/{skills → kits/builder/skills}/fix-bug/SKILL.md +0 -0
  67. /package/{skills → kits/builder/skills}/learning-review/SKILL.md +0 -0
  68. /package/{skills → kits/builder/skills}/pickup-probe/SKILL.md +0 -0
  69. /package/{skills → kits/builder/skills}/plan-work/SKILL.md +0 -0
  70. /package/{skills → kits/builder/skills}/pull-work/SKILL.md +0 -0
  71. /package/{skills → kits/builder/skills}/release-readiness/SKILL.md +0 -0
  72. /package/{skills → kits/builder/skills}/review-work/SKILL.md +0 -0
  73. /package/{skills → kits/builder/skills}/tdd-workflow/SKILL.md +0 -0
  74. /package/{skills → kits/builder/skills}/verify-work/SKILL.md +0 -0
  75. /package/{skills → kits/knowledge/skills}/knowledge-capture/SKILL.md +0 -0
@@ -7,13 +7,6 @@
7
7
  "description": "Small default surface for reliable coding and workflow execution.",
8
8
  "skills": [
9
9
  "search-first",
10
- "plan-work",
11
- "execute-plan",
12
- "review-work",
13
- "verify-work",
14
- "evidence-gate",
15
- "feedback-loop",
16
- "knowledge-capture",
17
10
  "browser-test"
18
11
  ],
19
12
  "agents": [
@@ -32,23 +25,10 @@
32
25
  "default": false,
33
26
  "description": "Development workflow depth for backlog, release, dependency, GitHub, TDD, and frontend work.",
34
27
  "skills": [
35
- "builder-shape",
36
- "idea-to-backlog",
37
- "pull-work",
38
- "design-probe",
39
- "pickup-probe",
40
- "deliver",
41
- "fix-bug",
42
- "tdd-workflow",
43
- "release-readiness",
44
- "learning-review",
45
28
  "dependency-update",
46
29
  "eval-rebuild",
47
- "explore",
48
30
  "github-cli",
49
- "frontend-design",
50
- "agentic-engineering",
51
- "context-budget"
31
+ "agentic-engineering"
52
32
  ],
53
33
  "agents": [
54
34
  "dev",
package/scripts/README.md CHANGED
@@ -11,7 +11,7 @@ These files are stable launchers for TypeScript code compiled under `build/src/`
11
11
  | `build-universal-bundles.js` | `build/src/tools/build-universal-bundles.js` |
12
12
  | `filter-installed-packs.js` | `build/src/tools/filter-installed-packs.js` |
13
13
  | `generate-context-map.js` | `build/src/tools/generate-context-map.js` |
14
- | `flow-kit.js` | `build/src/cli/flow-kit.js` |
14
+ | `kit.js` | `build/src/cli/kit.js` |
15
15
  | `pull-work-provider.js` | `build/src/cli/pull-work-provider.js` |
16
16
  | `effective-backlog-settings.js` | `build/src/cli/effective-backlog-settings.js` |
17
17
  | `publish-change-helper.js` | `build/src/cli/publish-change-helper.js` |
package/scripts/kit.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import("../build/src/cli/kit.js").then(({ main }) => main().then((code) => process.exit(code)));
@@ -0,0 +1,23 @@
1
+ # skills/
2
+
3
+ This directory contains standalone skills that are **not yet claimed by a specific kit**.
4
+
5
+ ## Status: Pending Tool Reclassification
6
+
7
+ The 6 items here are currently classified as TOOLS pending formal reclassification in a follow-up ADR pass (tracking: #62 follow-up, ADR 0007).
8
+
9
+ | Skill | Notes |
10
+ | --- | --- |
11
+ | agentic-engineering | Pending reclassification as a tool or context doc |
12
+ | browser-test | Pending reclassification — closely tied to Playwright power |
13
+ | dependency-update | Pending reclassification — tooling-oriented |
14
+ | eval-rebuild | Pending reclassification — project-specific build hook |
15
+ | github-cli | Pending reclassification — CLI tool wrapper |
16
+ | search-first | Pending reclassification — research heuristic |
17
+
18
+ ## Kit-owned skills
19
+
20
+ Builder Kit skills now live in `kits/builder/skills/`.
21
+ Knowledge Kit skills now live in `kits/knowledge/skills/`.
22
+
23
+ See `docs/adr/0007-skill-audit.md` for the full audit table and disposition rationale.
@@ -34,7 +34,7 @@ function contentHash(root: string): string {
34
34
  return `sha256:${hash.digest("hex")}`;
35
35
  }
36
36
 
37
- /** Content hash that excludes .git and other VCS/cache directories (for install-git clones). */
37
+ /** Content hash that excludes .git and other VCS/cache directories (for install git clones). */
38
38
  function kitContentHash(root: string): string {
39
39
  const EXCLUDE_DIRS = new Set([".git", "__pycache__", ".pytest_cache"]);
40
40
  const hash = crypto.createHash("sha256");
@@ -50,13 +50,40 @@ function kitContentHash(root: string): string {
50
50
  return `sha256:${hash.digest("hex")}`;
51
51
  }
52
52
 
53
- function installLocal(argv: string[]): number {
53
+ /**
54
+ * install <source> [--dest <path>] [--force] [--update] [--ref <branch|tag|sha>]
55
+ *
56
+ * Installs a Flow Kit from a local path or a git URL.
57
+ *
58
+ * - Local path: validates then copies the kit into the destination registry.
59
+ * - Git URL (http://, https://, git+, ssh://, file://): shallow-clones the repository,
60
+ * validates the kit container with @kontourai/flow, then delegates to the install path.
61
+ * Supports an optional #ref fragment in the URL or a separate --ref flag.
62
+ */
63
+ async function install(argv: string[]): Promise<number> {
64
+ const args = parseArgs(argv);
65
+ const source = args.positionals[0] ?? "";
66
+ if (!source) {
67
+ console.error("install: missing <source> argument");
68
+ console.error("usage: flow-agents kit install <path-or-git-url> [--dest <path>] [--ref <ref>] [--force] [--update]");
69
+ return 2;
70
+ }
71
+
72
+ // Detect git URL: starts with http(s)://, git+, ssh://, file://, or ends with .git
73
+ const isGitUrl = /^(https?:\/\/|git\+|ssh:\/\/|file:\/\/)/.test(source) || source.endsWith(".git");
74
+
75
+ if (isGitUrl) {
76
+ return await installGitSource(source, argv);
77
+ }
78
+ return await installLocalSource(path.resolve(source), argv);
79
+ }
80
+
81
+ async function installLocalSource(source: string, argv: string[]): Promise<number> {
54
82
  const args = parseArgs(argv);
55
- const source = path.resolve(args.positionals[0] ?? "");
56
83
  const dest = path.resolve(flagString(args.flags, "dest", ".") ?? ".");
57
84
  let manifest: Record<string, unknown>;
58
85
  try {
59
- manifest = assertKitRepository(source);
86
+ manifest = await assertKitRepository(source);
60
87
  } catch (error) {
61
88
  console.log("Flow Kit repository validation failed:");
62
89
  for (const diagnostic of ((error as Error & { diagnostics?: string[] }).diagnostics ?? [(error as Error).message])) console.log(` - ${diagnostic}`);
@@ -93,6 +120,86 @@ function installLocal(argv: string[]): number {
93
120
  return 0;
94
121
  }
95
122
 
123
+ async function installGitSource(rawUrl: string, argv: string[]): Promise<number> {
124
+ const args = parseArgs(argv);
125
+
126
+ // Parse ref: #fragment in URL takes precedence over --ref flag.
127
+ let repoUrl = rawUrl;
128
+ let ref: string | null = null;
129
+ const hashIdx = rawUrl.indexOf("#");
130
+ if (hashIdx !== -1) {
131
+ repoUrl = rawUrl.slice(0, hashIdx);
132
+ ref = rawUrl.slice(hashIdx + 1) || null;
133
+ }
134
+ if (!ref) ref = flagString(args.flags, "ref") ?? null;
135
+
136
+ const dest = path.resolve(flagString(args.flags, "dest", ".") ?? ".");
137
+ const force = flagBool(args.flags, "force") ?? false;
138
+ const update = flagBool(args.flags, "update") ?? false;
139
+
140
+ // Shallow-clone into a temporary directory.
141
+ const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), "flow-kit-git-"));
142
+ try {
143
+ const cloneArgs = ["clone", "--depth", "1"];
144
+ if (ref) cloneArgs.push("--branch", ref);
145
+ cloneArgs.push("--", repoUrl, tmpBase);
146
+ try {
147
+ child_process.execFileSync("git", cloneArgs, { stdio: ["ignore", "pipe", "pipe"] });
148
+ } catch (err) {
149
+ const msg = err instanceof Error && (err as NodeJS.ErrnoException & { stderr?: Buffer }).stderr
150
+ ? ((err as NodeJS.ErrnoException & { stderr?: Buffer }).stderr as Buffer).toString().trim()
151
+ : String(err);
152
+ console.error(`install: git clone failed: ${msg}`);
153
+ return 1;
154
+ }
155
+
156
+ // Validate the cloned kit using the same logic as install local.
157
+ let manifest: Record<string, unknown>;
158
+ try {
159
+ manifest = await assertKitRepository(tmpBase);
160
+ } catch (error) {
161
+ console.log("Flow Kit repository validation failed:");
162
+ for (const diagnostic of ((error as Error & { diagnostics?: string[] }).diagnostics ?? [(error as Error).message])) {
163
+ console.log(` - ${diagnostic}`);
164
+ }
165
+ return 1;
166
+ }
167
+
168
+ // Delegate to the shared install logic (copy + registry update).
169
+ const kitId = String(manifest.id);
170
+ const hash = kitContentHash(tmpBase);
171
+ const registry = loadRegistry(dest);
172
+ const existing = registry.kits.find((entry) => entry.id === kitId);
173
+ const target = installedPath(dest, kitId);
174
+ assertPathContained(dest, target);
175
+ const sourceText = repoUrl + (ref ? `#${ref}` : "");
176
+ if (existing && existing.source !== sourceText && !update) {
177
+ console.log(`conflict: kit '${kitId}' is already installed from ${existing.source}; rerun with --update to replace it`);
178
+ return 2;
179
+ }
180
+ if (existing && existing.source === sourceText && existing.hash === hash && fs.existsSync(target) && !force) {
181
+ console.log(`kit '${kitId}' is already installed from ${sourceText}`);
182
+ return 0;
183
+ }
184
+ copyDir(tmpBase, target);
185
+ const entry: Record<string, unknown> = {
186
+ id: kitId,
187
+ source: sourceText,
188
+ hash,
189
+ installed_at: existing && existing.source === sourceText && !update ? existing.installed_at : isoNow(),
190
+ installed_path: target,
191
+ state: "installed",
192
+ };
193
+ if (typeof manifest.version === "string" && manifest.version) entry.version = manifest.version;
194
+ registry.kits = existing ? registry.kits.map((item) => item.id === kitId ? entry : item) : [...registry.kits, entry];
195
+ writeJson(registryPath(dest), registry);
196
+ console.log(`${existing ? "updated" : "installed"} git kit '${kitId}' from ${sourceText} at ${target}`);
197
+ return 0;
198
+ } finally {
199
+ fs.rmSync(tmpBase, { recursive: true, force: true });
200
+ }
201
+ }
202
+
96
203
  function list(argv: string[]): number {
97
204
  const args = parseArgs(argv);
98
205
  const dest = path.resolve(flagString(args.flags, "dest", ".") ?? ".");
@@ -153,7 +260,8 @@ function activate(argv: string[]): number {
153
260
  * inspect <kit-dir> [--json]
154
261
  *
155
262
  * Derives conformance level (K0/K1/K2) and consumer targets from a kit's
156
- * observable asset classes. Exits 1 if the kit fails core container validation.
263
+ * observable asset classes. Delegates core container validation to @kontourai/flow.
264
+ * Exits 1 if the kit fails core container validation.
157
265
  * Outputs stable JSON suitable for use by catalog tooling and CI.
158
266
  *
159
267
  * K-levels (issue #52):
@@ -166,7 +274,7 @@ function activate(argv: string[]): number {
166
274
  * flow-agents present at K1+ (Flow Agents extension activated)
167
275
  * <namespace> unknown top-level keys list verbatim as third-party consumer targets
168
276
  */
169
- function inspect(argv: string[]): number {
277
+ async function inspect(argv: string[]): Promise<number> {
170
278
  const args = parseArgs(argv);
171
279
  const kitDir = path.resolve(args.positionals[0] ?? ".");
172
280
  const manifestPath = path.join(kitDir, "kit.json");
@@ -181,116 +289,23 @@ function inspect(argv: string[]): number {
181
289
  console.error(`inspect: invalid JSON in ${manifestPath}: ${(err as Error).message}`);
182
290
  return 1;
183
291
  }
184
- const result = deriveKitTargets(manifest);
292
+ // Pass the real kitDir so @kontourai/flow can validate flow file existence for K0.
293
+ const result = await deriveKitTargets(manifest, kitDir);
185
294
  console.log(JSON.stringify(result, null, 2));
186
295
  return result.conformance.k0 ? 0 : 1;
187
296
  }
188
297
 
189
-
190
- /**
191
- * install-git <repo-url>[#ref] [--ref <branch|tag|sha>] [--dest <path>] [--force] [--update]
192
- *
193
- * Shallow-clones a remote git repository to a temporary directory, validates the kit
194
- * container with the same logic used by install-local, then delegates to the existing
195
- * install path. Supports an optional #ref fragment in the URL or a separate --ref flag.
196
- *
197
- * Implements kontourai/flow-agents#56 (git-ref install surface).
198
- */
199
- function installGit(argv: string[]): number {
200
- const args = parseArgs(argv);
201
- const rawUrl = args.positionals[0] ?? "";
202
- if (!rawUrl) {
203
- console.error("install-git: missing <repo-url> argument");
204
- console.error("usage: flow-kit install-git <repo-url>[#ref] [--ref <branch|tag|sha>] [--dest <path>]");
205
- return 2;
206
- }
207
-
208
- // Parse ref: #fragment in URL takes precedence over --ref flag.
209
- let repoUrl = rawUrl;
210
- let ref: string | null = null;
211
- const hashIdx = rawUrl.indexOf("#");
212
- if (hashIdx !== -1) {
213
- repoUrl = rawUrl.slice(0, hashIdx);
214
- ref = rawUrl.slice(hashIdx + 1) || null;
215
- }
216
- if (!ref) ref = flagString(args.flags, "ref") ?? null;
217
-
218
- const dest = path.resolve(flagString(args.flags, "dest", ".") ?? ".");
219
- const force = flagBool(args.flags, "force") ?? false;
220
- const update = flagBool(args.flags, "update") ?? false;
221
-
222
- // Shallow-clone into a temporary directory.
223
- const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), "flow-kit-git-"));
224
- try {
225
- const cloneArgs = ["clone", "--depth", "1"];
226
- if (ref) cloneArgs.push("--branch", ref);
227
- cloneArgs.push("--", repoUrl, tmpBase);
228
- try {
229
- child_process.execFileSync("git", cloneArgs, { stdio: ["ignore", "pipe", "pipe"] });
230
- } catch (err) {
231
- const msg = err instanceof Error && (err as NodeJS.ErrnoException & { stderr?: Buffer }).stderr
232
- ? ((err as NodeJS.ErrnoException & { stderr?: Buffer }).stderr as Buffer).toString().trim()
233
- : String(err);
234
- console.error(`install-git: git clone failed: ${msg}`);
235
- return 1;
236
- }
237
-
238
- // Validate the cloned kit using the same logic as install-local.
239
- let manifest: Record<string, unknown>;
240
- try {
241
- manifest = assertKitRepository(tmpBase);
242
- } catch (error) {
243
- console.log("Flow Kit repository validation failed:");
244
- for (const diagnostic of ((error as Error & { diagnostics?: string[] }).diagnostics ?? [(error as Error).message])) {
245
- console.log(` - ${diagnostic}`);
246
- }
247
- return 1;
248
- }
249
-
250
- // Delegate to the shared install logic (copy + registry update).
251
- const kitId = String(manifest.id);
252
- const hash = kitContentHash(tmpBase);
253
- const registry = loadRegistry(dest);
254
- const existing = registry.kits.find((entry) => entry.id === kitId);
255
- const target = installedPath(dest, kitId);
256
- assertPathContained(dest, target);
257
- const sourceText = repoUrl + (ref ? `#${ref}` : "");
258
- if (existing && existing.source !== sourceText && !update) {
259
- console.log(`conflict: kit '${kitId}' is already installed from ${existing.source}; rerun with --update to replace it`);
260
- return 2;
261
- }
262
- if (existing && existing.source === sourceText && existing.hash === hash && fs.existsSync(target) && !force) {
263
- console.log(`kit '${kitId}' is already installed from ${sourceText}`);
264
- return 0;
265
- }
266
- copyDir(tmpBase, target);
267
- const entry: Record<string, unknown> = {
268
- id: kitId,
269
- source: sourceText,
270
- hash,
271
- installed_at: existing && existing.source === sourceText && !update ? existing.installed_at : isoNow(),
272
- installed_path: target,
273
- state: "installed",
274
- };
275
- if (typeof manifest.version === "string" && manifest.version) entry.version = manifest.version;
276
- registry.kits = existing ? registry.kits.map((item) => item.id === kitId ? entry : item) : [...registry.kits, entry];
277
- writeJson(registryPath(dest), registry);
278
- console.log(`${existing ? "updated" : "installed"} git kit '${kitId}' from ${sourceText} at ${target}`);
279
- return 0;
280
- } finally {
281
- fs.rmSync(tmpBase, { recursive: true, force: true });
282
- }
283
- }
284
-
285
- export function main(argv = process.argv.slice(2)): number {
298
+ export async function main(argv = process.argv.slice(2)): Promise<number> {
286
299
  const [command, ...rest] = argv;
287
- if (command === "install-local") return installLocal(rest);
288
- if (command === "install-git") return installGit(rest);
300
+ if (command === "install") return await install(rest);
301
+ // Legacy sub-subcommands forwarded for backward compatibility within the kit subcommand.
302
+ if (command === "install-local") return await installLocalSource(path.resolve(rest[0] ?? ""), rest);
303
+ if (command === "install-git") return await installGitSource(rest[0] ?? "", rest);
289
304
  if (command === "list") return list(rest);
290
305
  if (command === "status") return status(rest);
291
306
  if (command === "activate") return activate(rest);
292
- if (command === "inspect") return inspect(rest);
293
- console.error("usage: flow-kit <install-local|install-git|list|status|activate|inspect> ...");
307
+ if (command === "inspect") return await inspect(rest);
308
+ console.error("usage: flow-agents kit <install|activate|inspect|list|status> ...");
294
309
  return 2;
295
310
  }
296
311
 
@@ -299,4 +314,4 @@ export function main(argv = process.argv.slice(2)): number {
299
314
  // entry-point guard fires correctly when the module is loaded directly as a script.
300
315
  const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
301
316
  const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
302
- if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }
317
+ if (_selfRealPath === _argv1RealPath) { main().then((code) => { process.exitCode = code; }).catch((err) => { console.error(err); process.exitCode = 1; }); }
@@ -2,11 +2,11 @@ import * as path from "node:path";
2
2
  import { parseArgs } from "../lib/args.js";
3
3
  import { validateKitRepository } from "../flow-kit/validate.js";
4
4
 
5
- export function main(argv = process.argv.slice(2)): number {
5
+ export async function main(argv = process.argv.slice(2)): Promise<number> {
6
6
  const args = parseArgs(argv);
7
7
  const kit = args.flags.kit;
8
8
  if (typeof kit === "string") {
9
- const errors = validateKitRepository(path.resolve(kit));
9
+ const errors = await validateKitRepository(path.resolve(kit));
10
10
  if (errors.length) {
11
11
  console.log("Flow Kit repository validation failed:");
12
12
  for (const error of errors) console.log(` - ${error}`);
@@ -17,7 +17,7 @@ export function main(argv = process.argv.slice(2)): number {
17
17
  }
18
18
  const root = path.resolve(".");
19
19
  const builder = path.join(root, "kits", "builder");
20
- const errors = validateKitRepository(builder);
20
+ const errors = await validateKitRepository(builder);
21
21
  if (errors.length) {
22
22
  console.log("Source tree validation failed:");
23
23
  for (const error of errors) console.log(` - ${error}`);
@@ -33,4 +33,4 @@ import * as _fsVST from "node:fs";
33
33
  import { fileURLToPath as _ftpVST } from "node:url";
34
34
  const _selfVST = (() => { try { return _fsVST.realpathSync(_ftpVST(import.meta.url)); } catch { return _ftpVST(import.meta.url); } })();
35
35
  const _argv1VST = (() => { try { return _fsVST.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
36
- if (_selfVST === _argv1VST) { process.exitCode = main(); }
36
+ if (_selfVST === _argv1VST) { main().then((code) => { process.exitCode = code; }).catch((err) => { console.error(err); process.exitCode = 1; }); }
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;