@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
@@ -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)));
@@ -6,7 +6,7 @@ flow_agents_local_kontour_console_url() {
6
6
  }
7
7
 
8
8
  flow_agents_kontour_cloud_console_url() {
9
- printf '%s\n' "${FLOW_AGENTS_KONTOUR_CLOUD_CONSOLE_URL:-https://console.kontourai.com}"
9
+ printf '%s\n' "${FLOW_AGENTS_KONTOUR_CLOUD_CONSOLE_URL:-https://console.kontourai.io}"
10
10
  }
11
11
 
12
12
  flow_agents_kontour_hosted_console_url() {
@@ -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.
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import { fileURLToPath } from "node:url";
2
3
  import * as path from "node:path";
3
4
  import { flagBool, flagString, parseArgs } from "../lib/args.js";
4
5
  import { buildWorkflowLearningProjection, readWorkflowLearningSources } from "../lib/workflow-learning-projection.js";
@@ -137,4 +138,9 @@ export function main(argv = process.argv.slice(2)): number {
137
138
  }
138
139
  }
139
140
 
140
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
141
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
142
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
143
+ // entry-point guard fires correctly when the module is loaded directly as a script.
144
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
145
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
146
+ if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }
@@ -96,4 +96,9 @@ export function main(argv = process.argv.slice(2)): number {
96
96
  }
97
97
  }
98
98
 
99
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
99
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
100
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
101
+ // entry-point guard fires correctly when the module is loaded directly as a script.
102
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
103
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
104
+ if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import * as fs from "node:fs";
3
+ import { fileURLToPath } from "node:url";
3
4
  import * as path from "node:path";
4
5
 
5
6
  type FixtureAuditItem = {
@@ -151,4 +152,9 @@ export function main(argv = process.argv.slice(2)): number {
151
152
  }
152
153
  }
153
154
 
154
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
155
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
156
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
157
+ // entry-point guard fires correctly when the module is loaded directly as a script.
158
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
159
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
160
+ if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }
package/src/cli/init.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import * as fs from "node:fs";
3
+ import { fileURLToPath } from "node:url";
3
4
  import * as os from "node:os";
4
5
  import * as path from "node:path";
5
6
  import { createInterface } from "node:readline/promises";
@@ -458,4 +459,9 @@ export async function mainDogfood(argv = process.argv.slice(2)): Promise<number>
458
459
  }
459
460
  }
460
461
 
461
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(await main());
462
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
463
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
464
+ // entry-point guard fires correctly when the module is loaded directly as a script.
465
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
466
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
467
+ if (_selfRealPath === _argv1RealPath) { process.exitCode = await main(); }
@@ -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; }); }
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import { fileURLToPath } from "node:url";
2
3
  import * as path from "node:path";
3
4
  import { parseArgs, flagString } from "../lib/args.js";
4
5
  import { isoNow, relPath } from "../lib/fs.js";
@@ -61,4 +62,9 @@ export function main(argv = process.argv.slice(2)): number {
61
62
  return 0;
62
63
  }
63
64
 
64
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
65
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
66
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
67
+ // entry-point guard fires correctly when the module is loaded directly as a script.
68
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
69
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
70
+ if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import { fileURLToPath } from "node:url";
2
3
  import * as path from "node:path";
3
4
  import { parseArgs, flagString } from "../lib/args.js";
4
5
  import { readJson } from "../lib/fs.js";
@@ -140,4 +141,9 @@ export function main(argv = process.argv.slice(2)): number {
140
141
  }
141
142
  }
142
143
 
143
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
144
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
145
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
146
+ // entry-point guard fires correctly when the module is loaded directly as a script.
147
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
148
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
149
+ if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import { fileURLToPath } from "node:url";
2
3
  import { parseArgs, flagList, flagString } from "../lib/args.js";
3
4
 
4
5
  const FLOW_ARTIFACT_PATTERN = /(?<path>\.flow-agents\/[^\s`'")]+)/g;
@@ -478,4 +479,9 @@ export function main(argv = process.argv.slice(2)): number {
478
479
  return 0;
479
480
  }
480
481
 
481
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
482
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
483
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
484
+ // entry-point guard fires correctly when the module is loaded directly as a script.
485
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
486
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
487
+ if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }
@@ -1,3 +1,5 @@
1
+ import * as fs from "node:fs";
2
+ import { fileURLToPath } from "node:url";
1
3
  import * as path from "node:path";
2
4
  import { parseArgs, flagString } from "../lib/args.js";
3
5
  import { activateCodexLocal, activateStrandsLocal } from "../runtime-adapters.js";
@@ -26,4 +28,9 @@ export function main(argv = process.argv.slice(2)): number {
26
28
  return Array.isArray(result.errors) && result.errors.length ? 1 : 0;
27
29
  }
28
30
 
29
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
31
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
32
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
33
+ // entry-point guard fires correctly when the module is loaded directly as a script.
34
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
35
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
36
+ if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import { fileURLToPath } from "node:url";
2
3
  import * as os from "node:os";
3
4
  import * as path from "node:path";
4
5
  import { parseArgs, flagBool, flagList, flagString } from "../lib/args.js";
@@ -415,4 +416,9 @@ export function main(argv = process.argv.slice(2)): number {
415
416
  }
416
417
  }
417
418
 
418
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
419
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
420
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
421
+ // entry-point guard fires correctly when the module is loaded directly as a script.
422
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
423
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
424
+ if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import { fileURLToPath } from "node:url";
2
3
  import * as path from "node:path";
3
4
  import { flagBool, flagString, parseArgs } from "../lib/args.js";
4
5
 
@@ -321,4 +322,9 @@ export async function main(argv = process.argv.slice(2)): Promise<number> {
321
322
  return runCheck(rest);
322
323
  }
323
324
 
324
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(await main());
325
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
326
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
327
+ // entry-point guard fires correctly when the module is loaded directly as a script.
328
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
329
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
330
+ if (_selfRealPath === _argv1RealPath) { process.exitCode = await main(); }
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import { fileURLToPath } from "node:url";
2
3
 
3
4
  const validTiers = new Set(["adapter", "design-target", "installed-command", "live-acceptance", "documented-runtime-gap"]);
4
5
  const validRuntimes = new Set(["codex", "claude-code", "kiro-cli"]);
@@ -116,4 +117,9 @@ export function main(argv = process.argv.slice(2)): number {
116
117
  }
117
118
  }
118
119
 
119
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
120
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
121
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
122
+ // entry-point guard fires correctly when the module is loaded directly as a script.
123
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
124
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
125
+ if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }
@@ -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; }); }
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import { fileURLToPath } from "node:url";
2
3
  import * as path from "node:path";
3
4
  import { spawnSync } from "node:child_process";
4
5
  import { flagBool, flagString, parseArgs } from "../lib/args.js";
@@ -319,4 +320,9 @@ export function main(argv = process.argv.slice(2)): number {
319
320
  return options ? runEvidence(options) : 2;
320
321
  }
321
322
 
322
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
323
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
324
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
325
+ // entry-point guard fires correctly when the module is loaded directly as a script.
326
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
327
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
328
+ if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import { fileURLToPath } from "node:url";
2
3
  import * as path from "node:path";
3
4
  import { flagBool, flagString, parseArgs } from "../lib/args.js";
4
5
 
@@ -278,4 +279,9 @@ export function main(argv = process.argv.slice(2)): number {
278
279
  return 0;
279
280
  }
280
281
 
281
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
282
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
283
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
284
+ // entry-point guard fires correctly when the module is loaded directly as a script.
285
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
286
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
287
+ if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }