@kontourai/flow-agents 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/.github/workflows/ci.yml +6 -1
  2. package/.github/workflows/kit-gates-demo.yml +6 -2
  3. package/.github/workflows/runtime-compat.yml +5 -2
  4. package/CHANGELOG.md +51 -0
  5. package/CONTRIBUTING.md +30 -0
  6. package/README.md +26 -5
  7. package/agents/dev.json +1 -1
  8. package/agents/tool-planner.json +1 -1
  9. package/build/src/cli/{flow-kit.js → kit.js} +122 -108
  10. package/build/src/cli/validate-source-tree.js +4 -4
  11. package/build/src/cli/workflow-sidecar.js +70 -5
  12. package/build/src/cli.js +3 -3
  13. package/build/src/flow-kit/validate.js +89 -62
  14. package/build/src/tools/build-universal-bundles.js +78 -17
  15. package/build/src/tools/generate-context-map.js +49 -7
  16. package/build/src/tools/validate-source-tree.js +32 -1
  17. package/console.telemetry.json +1 -1
  18. package/docs/adr/0004-gates-expect-surface-claims.md +7 -7
  19. package/docs/adr/0007-flow-skill-kit-tool-boundary.md +169 -0
  20. package/docs/adr/0007-skill-audit.md +112 -0
  21. package/docs/adr/0008-kit-operation-boundary.md +88 -0
  22. package/docs/context-map.md +18 -22
  23. package/docs/flow-kit-repository-contract.md +5 -5
  24. package/docs/getting-started.md +177 -0
  25. package/docs/index.md +19 -8
  26. package/docs/kit-authoring-guide.md +125 -13
  27. package/docs/knowledge-kit.md +2 -2
  28. package/docs/operating-layers.md +2 -2
  29. package/docs/spec/runtime-hook-surface.md +1 -1
  30. package/docs/veritas-integration.md +4 -4
  31. package/docs/vision.md +1 -1
  32. package/docs/workflow-eval-strategy.md +2 -2
  33. package/docs/workflow-usage-guide.md +2 -2
  34. package/evals/acceptance/test_opencode_harness.sh +18 -10
  35. package/evals/acceptance/test_pi_harness.sh +10 -6
  36. package/evals/ci/run-baseline.sh +1 -1
  37. package/evals/fixtures/builder-kit-workflow-state/happy-path.json +2 -2
  38. package/evals/fixtures/builder-kit-workflow-state/mid-work-resume.json +2 -2
  39. package/evals/fixtures/console-learning-projection/artifacts/console-learning-correction/learning.json +1 -1
  40. package/evals/fixtures/flow-kit-repository/mixed-runtime-kit/flows/runtime.flow.json +4 -4
  41. package/evals/fixtures/flow-kit-repository/valid-local-kit/flows/review.flow.json +4 -4
  42. package/evals/fixtures/kit-conformance-levels/k0-flows-only/flows/review.flow.json +4 -4
  43. package/evals/fixtures/kit-conformance-levels/k1-agent-extension/flows/build.flow.json +4 -4
  44. package/evals/fixtures/kit-conformance-levels/k2-with-evals/flows/synthesize.flow.json +4 -4
  45. package/evals/fixtures/kit-conformance-levels/third-party-extension/flows/review.flow.json +4 -4
  46. package/evals/fixtures/pull-work-provider/github-issues.json +5 -5
  47. package/evals/fixtures/surface-trust/accepted-claim-trust-report.json +2 -2
  48. package/evals/fixtures/surface-trust/artifact-absent.json +2 -2
  49. package/evals/fixtures/surface-trust/integrity-mismatch-trust-report.json +2 -2
  50. package/evals/fixtures/surface-trust/missing-authority-trust-report.json +2 -2
  51. package/evals/fixtures/surface-trust/provider-absent.json +2 -2
  52. package/evals/fixtures/surface-trust/rejected-claim-trust-report.json +2 -2
  53. package/evals/fixtures/surface-trust/stale-claim-trust-snapshot.json +2 -2
  54. package/evals/integration/test_activate_npx_context.sh +2 -2
  55. package/evals/integration/test_bundle_install.sh +17 -12
  56. package/evals/integration/test_console_learning_projection.sh +2 -2
  57. package/evals/integration/test_flow_kit_install_git.sh +7 -7
  58. package/evals/integration/test_flow_kit_repository.sh +4 -4
  59. package/evals/integration/test_goal_fit_hook.sh +144 -0
  60. package/evals/integration/test_kit_conformance_levels.sh +56 -2
  61. package/evals/integration/test_local_flow_kit_install.sh +7 -7
  62. package/evals/integration/test_publish_change_helper.sh +1 -1
  63. package/evals/integration/test_pull_work_provider.sh +1 -1
  64. package/evals/integration/test_runtime_adapter_activation.sh +3 -3
  65. package/evals/integration/test_workflow_sidecar_writer.sh +9 -9
  66. package/evals/lib/node.sh +2 -2
  67. package/evals/static/test_package.sh +3 -3
  68. package/evals/static/test_workflow_skills.sh +19 -19
  69. package/integrations/strands/flow_agents_strands/steering.py +1 -1
  70. package/integrations/strands-ts/src/hooks.ts +1 -1
  71. package/kits/builder/flows/build.flow.json +48 -48
  72. package/kits/builder/flows/shape.flow.json +36 -36
  73. package/kits/builder/kit.json +17 -0
  74. package/{skills → kits/builder/skills}/builder-shape/SKILL.md +4 -4
  75. package/{skills → kits/builder/skills}/idea-to-backlog/SKILL.md +1 -1
  76. package/kits/knowledge/adapters/obsidian-store/index.js +137 -26
  77. package/kits/knowledge/evals/contract-suite/suite.test.js +90 -0
  78. package/kits/knowledge/flows/compile.flow.json +12 -12
  79. package/kits/knowledge/flows/consolidate.flow.json +16 -16
  80. package/kits/knowledge/flows/ingest.flow.json +12 -12
  81. package/kits/knowledge/flows/retire.flow.json +16 -16
  82. package/kits/knowledge/flows/store-contract.flow.json +12 -12
  83. package/kits/knowledge/flows/synthesize.flow.json +16 -16
  84. package/kits/knowledge/kit.json +16 -9
  85. package/kits/release-evidence/flows/release-evidence.flow.json +3 -3
  86. package/package.json +11 -5
  87. package/packaging/packs.json +1 -21
  88. package/schemas/workflow-evidence.schema.json +2 -1
  89. package/scripts/README.md +1 -1
  90. package/scripts/hooks/stop-goal-fit.js +66 -18
  91. package/scripts/kit.js +2 -0
  92. package/skills/README.md +23 -0
  93. package/src/cli/{flow-kit.ts → kit.ts} +124 -109
  94. package/src/cli/validate-source-tree.ts +4 -4
  95. package/src/cli/workflow-sidecar.ts +62 -4
  96. package/src/cli.ts +3 -3
  97. package/src/flow-kit/validate.ts +118 -58
  98. package/src/tools/build-universal-bundles.ts +74 -13
  99. package/src/tools/generate-context-map.ts +36 -6
  100. package/src/tools/validate-source-tree.ts +27 -1
  101. package/scripts/flow-kit.js +0 -2
  102. package/skills/context-budget/SKILL.md +0 -40
  103. package/skills/explore/SKILL.md +0 -137
  104. package/skills/feedback-loop/SKILL.md +0 -87
  105. package/skills/frontend-design/SKILL.md +0 -80
  106. /package/{skills → kits/builder/skills}/deliver/SKILL.md +0 -0
  107. /package/{skills → kits/builder/skills}/design-probe/SKILL.md +0 -0
  108. /package/{skills → kits/builder/skills}/evidence-gate/SKILL.md +0 -0
  109. /package/{skills → kits/builder/skills}/execute-plan/SKILL.md +0 -0
  110. /package/{skills → kits/builder/skills}/fix-bug/SKILL.md +0 -0
  111. /package/{skills → kits/builder/skills}/learning-review/SKILL.md +0 -0
  112. /package/{skills → kits/builder/skills}/pickup-probe/SKILL.md +0 -0
  113. /package/{skills → kits/builder/skills}/plan-work/SKILL.md +0 -0
  114. /package/{skills → kits/builder/skills}/pull-work/SKILL.md +0 -0
  115. /package/{skills → kits/builder/skills}/release-readiness/SKILL.md +0 -0
  116. /package/{skills → kits/builder/skills}/review-work/SKILL.md +0 -0
  117. /package/{skills → kits/builder/skills}/tdd-workflow/SKILL.md +0 -0
  118. /package/{skills → kits/builder/skills}/verify-work/SKILL.md +0 -0
  119. /package/{skills → kits/knowledge/skills}/knowledge-capture/SKILL.md +0 -0
@@ -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; }); }
@@ -2,6 +2,7 @@
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { execFileSync } from "node:child_process";
5
+ import { createRequire } from "node:module";
5
6
 
6
7
  type AnyObj = Record<string, any>;
7
8
 
@@ -24,6 +25,46 @@ function appendJsonl(file: string, payload: AnyObj): void {
24
25
  function die(message: string): never { throw new Error(message); }
25
26
  function slugify(value: string, fallback: string): string { return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || fallback; }
26
27
 
28
+ // Optional Hachure trust-bundle validation. No-ops gracefully when hachure is not installed.
29
+ // Install hachure (^0.4.0) as an optional dependency to enable schema validation.
30
+ function tryLoadHachureValidator(): ((bundle: unknown) => { valid: boolean; errors: string[] }) | null {
31
+ try {
32
+ const _require = createRequire(import.meta.url);
33
+ const hachureDir = path.dirname(_require.resolve("hachure"));
34
+ const schemasDir = path.join(hachureDir, "schemas");
35
+ const Ajv = _require("ajv/dist/2020");
36
+ const schemas: Record<string, any> = {};
37
+ for (const file of fs.readdirSync(schemasDir)) {
38
+ if (!file.endsWith(".schema.json")) continue;
39
+ schemas[file] = JSON.parse(fs.readFileSync(path.join(schemasDir, file), "utf8"));
40
+ }
41
+ const ajv = new Ajv({ strict: false, allErrors: true });
42
+ for (const [filename, schema] of Object.entries(schemas)) {
43
+ if (filename === "trust-bundle.schema.json") continue;
44
+ ajv.addSchema(schema, filename);
45
+ }
46
+ const trustBundleSchema = schemas["trust-bundle.schema.json"];
47
+ if (!trustBundleSchema) return null;
48
+ const validate = ajv.compile(trustBundleSchema);
49
+ return (bundle: unknown) => {
50
+ const valid = validate(bundle);
51
+ if (valid) return { valid: true, errors: [] };
52
+ const errors = ((validate as any).errors ?? []).map((err: any) => {
53
+ const loc = err.instancePath || err.schemaPath || "";
54
+ return `${loc} ${err.message ?? "invalid"}`.trim();
55
+ });
56
+ return { valid: false, errors };
57
+ };
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+ let _hachureValidator: ReturnType<typeof tryLoadHachureValidator> | undefined;
63
+ function getHachureValidator(): ReturnType<typeof tryLoadHachureValidator> {
64
+ if (_hachureValidator === undefined) _hachureValidator = tryLoadHachureValidator();
65
+ return _hachureValidator;
66
+ }
67
+
27
68
  function safeRepoIdentifier(value: string): string {
28
69
  const trimmed = value.trim().replace(/\.git$/, "");
29
70
  if (!trimmed || trimmed.length > 120) return "";
@@ -372,11 +413,27 @@ function normalizeCheck(raw: AnyObj): AnyObj {
372
413
  }
373
414
  function normalizeSurfaceRefs(refs: any): AnyObj[] {
374
415
  if (!Array.isArray(refs)) die("surface_trust_refs must be an array");
416
+ const hachureValidate = getHachureValidator();
375
417
  return refs.map((ref) => {
376
418
  const keys = JSON.stringify(ref).match(/"([^"]+)":/g) ?? [];
377
419
  for (const key of keys.map((k) => k.slice(1, -2))) if (key.toLowerCase().includes("veritas")) die(`unsupported field in Surface trust ref: ${key}`);
378
420
  const out = { ...ref };
379
- if (!["TrustReport", "Trust Snapshot"].includes(out.artifact_kind)) die("artifact_kind must be one of");
421
+ // trust.bundle is the canonical Hachure-aligned artifact kind; TrustReport/Trust Snapshot are legacy aliases
422
+ if (!["trust.bundle", "TrustReport", "Trust Snapshot"].includes(out.artifact_kind)) die("artifact_kind must be one of: trust.bundle, TrustReport, Trust Snapshot");
423
+ // When hachure is installed, validate the referenced trust artifact if it is a local file
424
+ if (hachureValidate && out.artifact_ref && typeof out.artifact_ref === "string" && fs.existsSync(out.artifact_ref)) {
425
+ try {
426
+ const bundle = JSON.parse(fs.readFileSync(out.artifact_ref, "utf8"));
427
+ const result = hachureValidate(bundle);
428
+ if (!result.valid) {
429
+ const errorSummary = result.errors.slice(0, 3).join("; ");
430
+ die(`trust.bundle artifact at ${out.artifact_ref} failed Hachure schema validation: ${errorSummary}`);
431
+ }
432
+ } catch (err) {
433
+ if (err instanceof Error && err.message.includes("failed Hachure schema validation")) throw err;
434
+ // File read or parse errors are not re-thrown: the artifact_ref validation path is advisory
435
+ }
436
+ }
380
437
  const status = deriveSurfaceStatus(out);
381
438
  if (out.status === "pass" && status !== "pass") die("surface_trust_refs contradicts Surface trust facts");
382
439
  return out;
@@ -394,15 +451,16 @@ function surfaceCheckFromArtifact(file: string, index: number): AnyObj {
394
451
  const lower = JSON.stringify(raw).toLowerCase();
395
452
  let ref: AnyObj;
396
453
  if (lower.includes("provider") && lower.includes("absent")) {
397
- ref = { artifact_kind: "TrustReport", artifact_ref: file, gate_id: "provider.unavailable", claim_type: "surface.claim", claim_status: "unknown", subject: "builder-kit", freshness: { status: "unknown", summary: "No trust provider is configured" }, authority: { producer: "unknown", summary: "No trust provider is configured" }, integrity: { status: "unknown", summary: "Unknown" }, status: "not_verified", summary: "No trust provider is configured" };
454
+ ref = { artifact_kind: "trust.bundle", artifact_ref: file, gate_id: "provider.unavailable", claim_type: "builder.trust.bundle", claim_status: "unknown", subject: "builder-kit", freshness: { status: "unknown", summary: "No trust provider is configured" }, authority: { producer: "unknown", summary: "No trust provider is configured" }, integrity: { status: "unknown", summary: "Unknown" }, status: "not_verified", summary: "No trust provider is configured" };
398
455
  } else if (lower.includes("artifact") && lower.includes("absent")) {
399
- ref = { artifact_kind: "TrustReport", artifact_ref: file, gate_id: "artifact.unavailable", claim_type: "surface.claim", claim_status: "unknown", subject: "builder-kit", freshness: { status: "unknown", summary: "Artifact not readable" }, authority: { producer: "unknown", summary: "Artifact not readable" }, integrity: { status: "unknown", summary: "Artifact not readable" }, status: "not_verified", summary: "artifact not readable" };
456
+ ref = { artifact_kind: "trust.bundle", artifact_ref: file, gate_id: "artifact.unavailable", claim_type: "builder.trust.bundle", claim_status: "unknown", subject: "builder-kit", freshness: { status: "unknown", summary: "Artifact not readable" }, authority: { producer: "unknown", summary: "Artifact not readable" }, integrity: { status: "unknown", summary: "Artifact not readable" }, status: "not_verified", summary: "artifact not readable" };
400
457
  } else {
401
458
  const claimStatus = lower.includes("rejected") ? "rejected" : "accepted";
402
459
  const freshness = lower.includes("stale") ? "stale" : "fresh";
403
460
  const producer = lower.includes("missing-authority") ? "unknown" : "surface-local";
404
461
  const integrity = lower.includes("mismatch") ? "mismatch" : "matched";
405
- ref = { artifact_kind: file.includes("snapshot") ? "Trust Snapshot" : "TrustReport", artifact_ref: file, gate_id: "builder.surface.claim", claim_type: "surface.claim", claim_status: claimStatus, subject: "builder-kit", freshness: { status: freshness, summary: freshness === "fresh" ? "fresh" : "not currently verifiable" }, authority: { producer, summary: producer === "unknown" ? "missing authority" : "Local Surface trust producer." }, integrity: { status: integrity, summary: integrity === "matched" ? "matched" : "integrity mismatch" } };
462
+ // Use trust.bundle as the canonical Hachure-aligned artifact_kind for all trust-backed evidence refs
463
+ ref = { artifact_kind: "trust.bundle", artifact_ref: file, gate_id: "builder.trust.bundle", claim_type: "builder.trust.bundle", claim_status: claimStatus, subject: "builder-kit", freshness: { status: freshness, summary: freshness === "fresh" ? "fresh" : "not currently verifiable" }, authority: { producer, summary: producer === "unknown" ? "missing authority" : "Local Surface trust producer." }, integrity: { status: integrity, summary: integrity === "matched" ? "matched" : "integrity mismatch" } };
406
464
  ref.status = deriveSurfaceStatus(ref);
407
465
  ref.summary = ref.status === "pass" ? "accepted" : ref.status === "not_verified" ? "not currently verifiable" : (claimStatus === "rejected" ? "rejected" : producer === "unknown" ? "missing authority" : "integrity mismatch");
408
466
  }
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"],