@kontourai/flow-agents 1.0.0 → 1.1.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 (53) hide show
  1. package/.github/workflows/ci.yml +110 -0
  2. package/CHANGELOG.md +24 -0
  3. package/build/src/cli/console-learning-projection.js +19 -2
  4. package/build/src/cli/effective-backlog-settings.js +18 -2
  5. package/build/src/cli/fixture-retirement-audit.js +19 -2
  6. package/build/src/cli/flow-kit.js +135 -5
  7. package/build/src/cli/init.js +19 -2
  8. package/build/src/cli/promote-workflow-artifact.js +19 -2
  9. package/build/src/cli/publish-change-helper.js +19 -2
  10. package/build/src/cli/pull-work-provider.js +19 -2
  11. package/build/src/cli/runtime-adapter.js +20 -2
  12. package/build/src/cli/usage-feedback.js +19 -2
  13. package/build/src/cli/utterance-check.js +19 -2
  14. package/build/src/cli/validate-hook-influence.js +19 -2
  15. package/build/src/cli/validate-source-tree.js +19 -2
  16. package/build/src/cli/veritas-governance.js +19 -2
  17. package/build/src/cli/workflow-artifact-cleanup-audit.js +19 -2
  18. package/build/src/runtime-adapters.js +56 -25
  19. package/build/src/tools/build-universal-bundles.js +19 -2
  20. package/build/src/tools/generate-context-map.js +19 -2
  21. package/build/src/tools/validate-package.js +19 -2
  22. package/build/src/tools/validate-source-tree.js +20 -3
  23. package/context/scripts/telemetry/console-presets.sh +1 -1
  24. package/docs/fixture-ownership.md +1 -1
  25. package/docs/kit-authoring-guide.md +20 -3
  26. package/evals/ci/run-baseline.sh +55 -8
  27. package/evals/integration/test_activate_npx_context.sh +134 -0
  28. package/evals/integration/test_flow_kit_install_git.sh +163 -0
  29. package/evals/integration/test_runtime_adapter_activation.sh +138 -17
  30. package/evals/run.sh +2 -0
  31. package/evals/static/test_console_presets.sh +49 -0
  32. package/package.json +1 -1
  33. package/scripts/telemetry/console-presets.sh +1 -1
  34. package/src/cli/console-learning-projection.ts +7 -1
  35. package/src/cli/effective-backlog-settings.ts +6 -1
  36. package/src/cli/fixture-retirement-audit.ts +7 -1
  37. package/src/cli/flow-kit.ts +123 -4
  38. package/src/cli/init.ts +7 -1
  39. package/src/cli/promote-workflow-artifact.ts +7 -1
  40. package/src/cli/publish-change-helper.ts +7 -1
  41. package/src/cli/pull-work-provider.ts +7 -1
  42. package/src/cli/runtime-adapter.ts +8 -1
  43. package/src/cli/usage-feedback.ts +7 -1
  44. package/src/cli/utterance-check.ts +7 -1
  45. package/src/cli/validate-hook-influence.ts +7 -1
  46. package/src/cli/validate-source-tree.ts +7 -1
  47. package/src/cli/veritas-governance.ts +7 -1
  48. package/src/cli/workflow-artifact-cleanup-audit.ts +7 -1
  49. package/src/runtime-adapters.ts +55 -27
  50. package/src/tools/build-universal-bundles.ts +7 -1
  51. package/src/tools/generate-context-map.ts +7 -1
  52. package/src/tools/validate-package.ts +7 -1
  53. package/src/tools/validate-source-tree.ts +8 -2
@@ -1,6 +1,9 @@
1
+ import * as child_process from "node:child_process";
1
2
  import * as crypto from "node:crypto";
2
3
  import * as fs from "node:fs";
4
+ import * as os from "node:os";
3
5
  import * as path from "node:path";
6
+ import { fileURLToPath } from "node:url";
4
7
  import { parseArgs, flagBool, flagString } from "../lib/args.js";
5
8
  import { assertPathContained, copyDir, isoNow, readJson, walkFiles, writeJson } from "../lib/fs.js";
6
9
  import { assertKitRepository, deriveKitTargets } from "../flow-kit/validate.js";
@@ -31,6 +34,22 @@ function contentHash(root: string): string {
31
34
  return `sha256:${hash.digest("hex")}`;
32
35
  }
33
36
 
37
+ /** Content hash that excludes .git and other VCS/cache directories (for install-git clones). */
38
+ function kitContentHash(root: string): string {
39
+ const EXCLUDE_DIRS = new Set([".git", "__pycache__", ".pytest_cache"]);
40
+ const hash = crypto.createHash("sha256");
41
+ for (const file of walkFiles(root)) {
42
+ const parts = path.relative(root, file).split(path.sep);
43
+ if (parts.some((p) => EXCLUDE_DIRS.has(p))) continue;
44
+ const rel = parts.join("/");
45
+ hash.update(rel);
46
+ hash.update("\0");
47
+ hash.update(fs.readFileSync(file));
48
+ hash.update("\0");
49
+ }
50
+ return `sha256:${hash.digest("hex")}`;
51
+ }
52
+
34
53
  function installLocal(argv: string[]): number {
35
54
  const args = parseArgs(argv);
36
55
  const source = path.resolve(args.positionals[0] ?? "");
@@ -39,12 +58,10 @@ function installLocal(argv: string[]): number {
39
58
  try {
40
59
  manifest = assertKitRepository(source);
41
60
  } catch (error) {
42
- console.log("warning: Flow validation surface unavailable; local kit check uses the minimal Flow Definition fallback");
43
61
  console.log("Flow Kit repository validation failed:");
44
62
  for (const diagnostic of ((error as Error & { diagnostics?: string[] }).diagnostics ?? [(error as Error).message])) console.log(` - ${diagnostic}`);
45
63
  return 1;
46
64
  }
47
- console.log("warning: Flow validation surface unavailable; local kit check uses the minimal Flow Definition fallback");
48
65
  const kitId = String(manifest.id);
49
66
  const hash = contentHash(source);
50
67
  const registry = loadRegistry(dest);
@@ -169,15 +186,117 @@ function inspect(argv: string[]): number {
169
186
  return result.conformance.k0 ? 0 : 1;
170
187
  }
171
188
 
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
+
172
285
  export function main(argv = process.argv.slice(2)): number {
173
286
  const [command, ...rest] = argv;
174
287
  if (command === "install-local") return installLocal(rest);
288
+ if (command === "install-git") return installGit(rest);
175
289
  if (command === "list") return list(rest);
176
290
  if (command === "status") return status(rest);
177
291
  if (command === "activate") return activate(rest);
178
292
  if (command === "inspect") return inspect(rest);
179
- console.error("usage: flow-kit <install-local|list|status|activate|inspect> ...");
293
+ console.error("usage: flow-kit <install-local|install-git|list|status|activate|inspect> ...");
180
294
  return 2;
181
295
  }
182
296
 
183
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
297
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
298
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
299
+ // entry-point guard fires correctly when the module is loaded directly as a script.
300
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
301
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
302
+ 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(); }
@@ -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(); }
@@ -27,4 +27,10 @@ export function main(argv = process.argv.slice(2)): number {
27
27
  return 0;
28
28
  }
29
29
 
30
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
30
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
31
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS).
32
+ import * as _fsVST from "node:fs";
33
+ import { fileURLToPath as _ftpVST } from "node:url";
34
+ const _selfVST = (() => { try { return _fsVST.realpathSync(_ftpVST(import.meta.url)); } catch { return _ftpVST(import.meta.url); } })();
35
+ const _argv1VST = (() => { try { return _fsVST.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
36
+ if (_selfVST === _argv1VST) { 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 { 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(); }
@@ -86,7 +86,7 @@ export function readKitInventory(sourceRoot: string, dest: string): KitInventory
86
86
  const errors: string[] = [];
87
87
  const assets: KitAsset[] = [];
88
88
  const catalogPath = path.join(sourceRoot, "kits", "catalog.json");
89
- if (!fs.existsSync(catalogPath)) errors.push(`${catalogPath}: missing Kit Catalog`);
89
+ if (!fs.existsSync(catalogPath)) warnings.push(`${catalogPath}: built-in Kit Catalog not found; skipping built-in kits (this is normal when running outside a flow-agents checkout)`);
90
90
  else {
91
91
  const catalog = readJson(catalogPath) as Record<string, unknown>;
92
92
  const kits = catalog.kits;
@@ -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
 
@@ -646,4 +647,9 @@ export function main(): number {
646
647
  }
647
648
  return 0;
648
649
  }
649
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
650
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
651
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
652
+ // entry-point guard fires correctly when the module is loaded directly as a script.
653
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
654
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
655
+ 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
 
@@ -196,4 +197,9 @@ export function main(argv = process.argv.slice(2)): number {
196
197
  return 0;
197
198
  }
198
199
 
199
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
200
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
201
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
202
+ // entry-point guard fires correctly when the module is loaded directly as a script.
203
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
204
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
205
+ 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";
@@ -84,7 +85,7 @@ const fixtureOwnerPolicies = new Map<string, { owners: string[]; classification:
84
85
  ["evals/fixtures/backlog-provider-settings", { owners: ["evals/integration/test_effective_backlog_settings.sh"], classification: "settings precedence fixtures" }],
85
86
  ["evals/fixtures/builder-kit-workflow-state", { owners: ["evals/static/test_workflow_skills.sh"], classification: "Builder Kit workflow-state fixtures" }],
86
87
  ["evals/fixtures/console-learning-projection", { owners: ["evals/integration/test_console_learning_projection.sh"], classification: "console learning projection fixtures" }],
87
- ["evals/fixtures/flow-kit-repository", { owners: ["evals/integration/test_flow_kit_repository.sh", "evals/integration/test_local_flow_kit_install.sh", "evals/integration/test_runtime_adapter_activation.sh", "evals/static/test_workflow_skills.sh"], classification: "Flow Kit repository contract fixtures" }],
88
+ ["evals/fixtures/flow-kit-repository", { owners: ["evals/integration/test_flow_kit_repository.sh", "evals/integration/test_local_flow_kit_install.sh", "evals/integration/test_runtime_adapter_activation.sh", "evals/integration/test_activate_npx_context.sh", "evals/integration/test_flow_kit_install_git.sh", "evals/static/test_workflow_skills.sh"], classification: "Flow Kit repository contract fixtures" }],
88
89
  ["evals/fixtures/kit-conformance-levels", { owners: ["evals/integration/test_kit_conformance_levels.sh"], classification: "K-level conformance and consumer-target derivation fixtures" }],
89
90
  ["evals/fixtures/hook-influence", { owners: ["evals/integration/test_hook_influence_cases.sh", "evals/static/test_workflow_skills.sh", "scripts/validate-hook-influence-cases.js"], classification: "hook influence behavioral cases" }],
90
91
  ["evals/fixtures/pull-work-provider", { owners: ["evals/integration/test_pull_work_provider.sh"], classification: "work item provider normalization fixtures" }],
@@ -491,4 +492,9 @@ export function main(argv = process.argv.slice(2)): number {
491
492
  console.log("Source tree validation passed.");
492
493
  return 0;
493
494
  }
494
- if (import.meta.url === `file://${process.argv[1]}`) process.exit(main());
495
+ // Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
496
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
497
+ // entry-point guard fires correctly when the module is loaded directly as a script.
498
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
499
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
500
+ if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }