@kontourai/flow-agents 1.2.0 → 1.4.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 (95) hide show
  1. package/.github/workflows/ci.yml +6 -1
  2. package/.github/workflows/kit-gates-demo.yml +6 -2
  3. package/CHANGELOG.md +33 -0
  4. package/CONTRIBUTING.md +30 -0
  5. package/agents/dev.json +1 -1
  6. package/agents/tool-planner.json +1 -1
  7. package/build/src/cli/console-learning-projection.d.ts +1 -0
  8. package/build/src/cli/effective-backlog-settings.d.ts +1 -0
  9. package/build/src/cli/fixture-retirement-audit.d.ts +2 -0
  10. package/build/src/cli/init.d.ts +17 -0
  11. package/build/src/cli/kit.d.ts +1 -0
  12. package/build/src/cli/promote-workflow-artifact.d.ts +1 -0
  13. package/build/src/cli/publish-change-helper.d.ts +1 -0
  14. package/build/src/cli/pull-work-provider.d.ts +1 -0
  15. package/build/src/cli/runtime-adapter.d.ts +1 -0
  16. package/build/src/cli/telemetry-doctor.d.ts +1 -0
  17. package/build/src/cli/usage-feedback.d.ts +1 -0
  18. package/build/src/cli/utterance-check.d.ts +1 -0
  19. package/build/src/cli/validate-hook-influence.d.ts +1 -0
  20. package/build/src/cli/validate-source-tree.d.ts +1 -0
  21. package/build/src/cli/validate-workflow-artifacts.d.ts +2 -0
  22. package/build/src/cli/veritas-governance.d.ts +1 -0
  23. package/build/src/cli/workflow-artifact-cleanup-audit.d.ts +1 -0
  24. package/build/src/cli/workflow-sidecar.d.ts +32 -0
  25. package/build/src/cli/workflow-sidecar.js +119 -22
  26. package/build/src/cli.d.ts +2 -0
  27. package/build/src/flow-kit/validate.d.ts +81 -0
  28. package/build/src/flow-kit/validate.js +32 -1
  29. package/build/src/index.d.ts +5 -0
  30. package/build/src/index.js +36 -0
  31. package/build/src/lib/args.d.ts +8 -0
  32. package/build/src/lib/fs.d.ts +7 -0
  33. package/build/src/lib/workflow-learning-projection.d.ts +132 -0
  34. package/build/src/runtime-adapters.d.ts +18 -0
  35. package/build/src/tools/build-universal-bundles.d.ts +2 -0
  36. package/build/src/tools/build-universal-bundles.js +14 -0
  37. package/build/src/tools/common.d.ts +9 -0
  38. package/build/src/tools/filter-installed-packs.d.ts +2 -0
  39. package/build/src/tools/generate-context-map.d.ts +2 -0
  40. package/build/src/tools/validate-package.d.ts +2 -0
  41. package/build/src/tools/validate-source-tree.d.ts +2 -0
  42. package/console.telemetry.json +1 -1
  43. package/docs/adr/0004-gates-expect-surface-claims.md +7 -7
  44. package/docs/developer-architecture.md +14 -0
  45. package/docs/kit-authoring-guide.md +99 -6
  46. package/docs/operating-layers.md +2 -2
  47. package/docs/spec/runtime-hook-surface.md +16 -1
  48. package/docs/veritas-integration.md +4 -4
  49. package/docs/workflow-eval-strategy.md +2 -2
  50. package/docs/workflow-usage-guide.md +1 -1
  51. package/evals/acceptance/test_opencode_harness.sh +18 -10
  52. package/evals/acceptance/test_pi_harness.sh +10 -6
  53. package/evals/ci/run-baseline.sh +1 -1
  54. package/evals/fixtures/flow-kit-repository/mixed-runtime-kit/flows/runtime.flow.json +4 -4
  55. package/evals/fixtures/flow-kit-repository/valid-local-kit/flows/review.flow.json +4 -4
  56. package/evals/fixtures/kit-conformance-levels/k0-flows-only/flows/review.flow.json +4 -4
  57. package/evals/fixtures/kit-conformance-levels/k1-agent-extension/flows/build.flow.json +4 -4
  58. package/evals/fixtures/kit-conformance-levels/k2-with-evals/flows/synthesize.flow.json +4 -4
  59. package/evals/fixtures/kit-conformance-levels/third-party-extension/flows/review.flow.json +4 -4
  60. package/evals/fixtures/surface-trust/accepted-claim-trust-report.json +2 -2
  61. package/evals/fixtures/surface-trust/artifact-absent.json +2 -2
  62. package/evals/fixtures/surface-trust/integrity-mismatch-trust-report.json +2 -2
  63. package/evals/fixtures/surface-trust/missing-authority-trust-report.json +2 -2
  64. package/evals/fixtures/surface-trust/provider-absent.json +2 -2
  65. package/evals/fixtures/surface-trust/rejected-claim-trust-report.json +2 -2
  66. package/evals/fixtures/surface-trust/stale-claim-trust-snapshot.json +2 -2
  67. package/evals/integration/test_console_learning_projection.sh +1 -1
  68. package/evals/integration/test_goal_fit_hook.sh +144 -0
  69. package/evals/integration/test_hook_category_behaviors.sh +14 -0
  70. package/evals/integration/test_kit_conformance_levels.sh +55 -1
  71. package/evals/integration/test_workflow_sidecar_writer.sh +9 -9
  72. package/evals/run.sh +2 -0
  73. package/evals/static/test_library_exports.sh +85 -0
  74. package/evals/static/test_package.sh +3 -3
  75. package/evals/static/test_universal_bundles.sh +15 -0
  76. package/evals/static/test_workflow_skills.sh +4 -4
  77. package/kits/builder/flows/build.flow.json +48 -48
  78. package/kits/builder/flows/shape.flow.json +36 -36
  79. package/kits/knowledge/adapters/obsidian-store/index.js +137 -26
  80. package/kits/knowledge/evals/contract-suite/suite.test.js +90 -0
  81. package/kits/knowledge/flows/compile.flow.json +12 -12
  82. package/kits/knowledge/flows/consolidate.flow.json +16 -16
  83. package/kits/knowledge/flows/ingest.flow.json +12 -12
  84. package/kits/knowledge/flows/retire.flow.json +16 -16
  85. package/kits/knowledge/flows/store-contract.flow.json +12 -12
  86. package/kits/knowledge/flows/synthesize.flow.json +16 -16
  87. package/kits/release-evidence/flows/release-evidence.flow.json +3 -3
  88. package/package.json +14 -2
  89. package/schemas/workflow-evidence.schema.json +2 -1
  90. package/scripts/hooks/stop-goal-fit.js +66 -18
  91. package/src/cli/workflow-sidecar.ts +101 -21
  92. package/src/flow-kit/validate.ts +55 -1
  93. package/src/index.ts +53 -0
  94. package/src/tools/build-universal-bundles.ts +14 -0
  95. package/tsconfig.json +1 -0
@@ -14,12 +14,12 @@
14
14
  "expects": [
15
15
  {
16
16
  "id": "similar-sources-found",
17
- "kind": "surface.claim",
17
+ "kind": "trust.bundle",
18
18
  "required": true,
19
19
  "description": "At least one compiled record similar to the target concept has been identified via the similarity detector. Similarity v1 uses category match + link-overlap heuristic. The result is a cluster of source record IDs to synthesize from.",
20
- "claim": {
21
- "type": "knowledge.synthesize.cluster",
22
- "subject": "artifact",
20
+ "bundle_claim": {
21
+ "claimType": "knowledge.synthesize.cluster",
22
+ "subjectType": "artifact",
23
23
  "accepted_statuses": ["trusted", "accepted"]
24
24
  }
25
25
  }
@@ -30,12 +30,12 @@
30
30
  "expects": [
31
31
  {
32
32
  "id": "proposal-recorded",
33
- "kind": "surface.claim",
33
+ "kind": "trust.bundle",
34
34
  "required": true,
35
35
  "description": "A proposal carrying source refs (proposer_id + source_ids in evidence) has been recorded via the store propose op. The concept body is NOT modified at this step — only a proposes link and mutation log entry are created.",
36
- "claim": {
37
- "type": "knowledge.synthesize.proposal",
38
- "subject": "artifact",
36
+ "bundle_claim": {
37
+ "claimType": "knowledge.synthesize.proposal",
38
+ "subjectType": "artifact",
39
39
  "accepted_statuses": ["trusted", "accepted"]
40
40
  }
41
41
  }
@@ -46,12 +46,12 @@
46
46
  "expects": [
47
47
  {
48
48
  "id": "proposal-carries-source-refs",
49
- "kind": "surface.claim",
49
+ "kind": "trust.bundle",
50
50
  "required": true,
51
51
  "description": "The proposal evidence includes source_ids referencing every compiled record that contributed to the proposed summary. Gate rejects if source_ids is empty or any referenced record does not exist.",
52
- "claim": {
53
- "type": "knowledge.synthesize.evidence",
54
- "subject": "artifact",
52
+ "bundle_claim": {
53
+ "claimType": "knowledge.synthesize.evidence",
54
+ "subjectType": "artifact",
55
55
  "accepted_statuses": ["trusted", "accepted"]
56
56
  }
57
57
  }
@@ -62,12 +62,12 @@
62
62
  "expects": [
63
63
  {
64
64
  "id": "mutation-gate-decision",
65
- "kind": "surface.claim",
65
+ "kind": "trust.bundle",
66
66
  "required": true,
67
67
  "description": "A gate decision (apply or reject) has been recorded. If applied: concept body is updated via the store apply op with rationale and all contributing source_ids in provenance. If rejected: the store reject op is called, the concept body is byte-identical to its pre-proposal state, and only a rejection log entry is appended.",
68
- "claim": {
69
- "type": "knowledge.synthesize.gate-decision",
70
- "subject": "artifact",
68
+ "bundle_claim": {
69
+ "claimType": "knowledge.synthesize.gate-decision",
70
+ "subjectType": "artifact",
71
71
  "accepted_statuses": ["trusted", "accepted"]
72
72
  }
73
73
  }
@@ -13,12 +13,12 @@
13
13
  "expects": [
14
14
  {
15
15
  "id": "release-claim-present",
16
- "kind": "surface.claim",
16
+ "kind": "trust.bundle",
17
17
  "required": true,
18
18
  "description": "A trusted release.evidence claim must be attached before this gate can pass.",
19
19
  "explore_hint": "Attach a trust-report JSON file with claim type release.evidence and status trusted.",
20
- "claim": {
21
- "type": "release.evidence",
20
+ "bundle_claim": {
21
+ "claimType": "release.evidence",
22
22
  "accepted_statuses": [
23
23
  "trusted"
24
24
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/flow-agents",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Flow Agents — a Kontour product that applies Flow and Veritas discipline as a portable process layer inside the agent tools you already use: Claude Code, Codex, Kiro, opencode, pi, and GitHub Actions — with framework adapters (AWS Strands preview) on the same policy-engine contract.",
5
5
  "keywords": [
6
6
  "agents",
@@ -22,6 +22,15 @@
22
22
  "access": "public"
23
23
  },
24
24
  "type": "module",
25
+ "main": "build/src/index.js",
26
+ "types": "build/src/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./build/src/index.d.ts",
30
+ "import": "./build/src/index.js"
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
25
34
  "repository": {
26
35
  "type": "git",
27
36
  "url": "git+https://github.com/kontourai/flow-agents.git"
@@ -133,6 +142,9 @@
133
142
  "typescript": "^6.0.3"
134
143
  },
135
144
  "dependencies": {
136
- "@kontourai/flow": "^1.2.0"
145
+ "@kontourai/flow": "~1.3.0"
146
+ },
147
+ "optionalDependencies": {
148
+ "hachure": "^0.4.0"
137
149
  }
138
150
  }
@@ -239,7 +239,8 @@
239
239
  "properties": {
240
240
  "artifact_kind": {
241
241
  "type": "string",
242
- "enum": ["TrustReport", "Trust Snapshot"]
242
+ "description": "Hachure-aligned artifact kind. trust.bundle is the canonical value; TrustReport and Trust Snapshot are legacy aliases.",
243
+ "enum": ["trust.bundle", "TrustReport", "Trust Snapshot"]
243
244
  },
244
245
  "artifact_ref": {
245
246
  "type": "string",
@@ -80,6 +80,15 @@ function hasSidecars(dir) {
80
80
  }
81
81
  }
82
82
 
83
+ /**
84
+ * Returns true if a line of validator output looks like a validator-environment
85
+ * error (shell/npm error, tsc missing, spawn failure) rather than a real
86
+ * artifact validation message. Environment errors must never block goal-fit.
87
+ */
88
+ function isEnvironmentError(line) {
89
+ return /tsc[:\s]|command not found|npm ERR!|npm error|ENOENT|EACCES|Cannot find module|node_modules\/.bin|TypeScript version|version conflict|error TS[0-9]/i.test(line);
90
+ }
91
+
83
92
  function sidecarValidation(root, artifactDir) {
84
93
  const requireSidecars = String(process.env.FLOW_AGENTS_REQUIRE_SIDECARS || '').toLowerCase() === 'true';
85
94
  const requireCritique = String(process.env.FLOW_AGENTS_REQUIRE_CRITIQUE || '').toLowerCase() === 'true';
@@ -88,8 +97,6 @@ function sidecarValidation(root, artifactDir) {
88
97
  const packageRoot = fs.existsSync(path.join(root, 'package.json'))
89
98
  ? root
90
99
  : path.resolve(__dirname, '..', '..');
91
- const packageJson = path.join(packageRoot, 'package.json');
92
- if (!fs.existsSync(packageJson)) return [`${relative(root, artifactDir)} sidecar validation: package.json is missing; cannot run TypeScript workflow validator.`];
93
100
 
94
101
  let sidecarFiles = [];
95
102
  try {
@@ -112,26 +119,67 @@ function sidecarValidation(root, artifactDir) {
112
119
 
113
120
  if (sidecarFiles.length === 0) return [];
114
121
 
115
- const args = ['run', 'workflow:validate-artifacts', '--silent', '--'];
116
- args.push('--skip-markdown-validation');
117
- if (requireSidecars) args.push('--require-sidecars');
118
- if (requireCritique) args.push('--require-critique');
119
- args.push(artifactDir);
122
+ // Part 1 fix: invoke the already-built validator directly via `node`, bypassing
123
+ // `npm run build` (tsc). npm-installed packages ship build/ in the package files,
124
+ // so the compiled JS is always available. Only fall back to npm run if build/ is
125
+ // absent (a raw dev checkout that hasn't been built yet).
126
+ const builtValidator = path.join(packageRoot, 'build', 'src', 'cli', 'validate-workflow-artifacts.js');
127
+ const hasBuild = fs.existsSync(builtValidator);
128
+
129
+ const validatorArgs = ['--skip-markdown-validation'];
130
+ if (requireSidecars) validatorArgs.push('--require-sidecars');
131
+ if (requireCritique) validatorArgs.push('--require-critique');
132
+ validatorArgs.push(artifactDir);
133
+
134
+ let result;
135
+ if (hasBuild) {
136
+ // Direct node invocation: no tsc, no npm build step, works from any npm install.
137
+ result = spawnSync(process.execPath, [builtValidator, ...validatorArgs], {
138
+ cwd: packageRoot,
139
+ encoding: 'utf8',
140
+ timeout: 30000,
141
+ });
142
+ } else {
143
+ // Dev checkout without build/: fall back to npm run (may trigger tsc).
144
+ // If this also fails due to environment issues, Part 2 handles it below.
145
+ const npmArgs = ['run', 'workflow:validate-artifacts', '--silent', '--', ...validatorArgs];
146
+ result = spawnSync('npm', npmArgs, {
147
+ cwd: packageRoot,
148
+ encoding: 'utf8',
149
+ timeout: 30000,
150
+ });
151
+ }
120
152
 
121
- const result = spawnSync('npm', args, {
122
- cwd: packageRoot,
123
- encoding: 'utf8',
124
- timeout: 30000,
125
- });
153
+ // Part 2 fix: treat validator-environment failures as SKIP, never as blocking.
154
+ // A spawn error (ENOENT, timeout) means the validator couldn't run at all.
155
+ if (result.error) {
156
+ // Validator couldn't be launched — environment issue, not a goal-fit failure.
157
+ return [`${relative(root, artifactDir)} sidecar validation skipped: validator could not run (${result.error.code || result.error.message})`];
158
+ }
126
159
 
127
160
  if (result.status === 0) return [];
128
- const output = `${result.stdout || ''}\n${result.stderr || ''}`
161
+
162
+ // Validator ran and exited non-zero. Separate real validation errors from
163
+ // environment errors (tsc missing, npm ERR!, shell errors) so that a broken
164
+ // validator environment never blocks goal-fit.
165
+ const allLines = `${result.stdout || ''}\n${result.stderr || ''}`
129
166
  .split('\n')
130
167
  .map(line => line.trim())
131
- .filter(Boolean)
132
- .slice(0, 12);
133
- if (output.length === 0) output.push(`validator exited with status ${result.status ?? 'unknown'}`);
134
- return output.map(line => `${relative(root, artifactDir)} sidecar validation: ${line}`);
168
+ .filter(Boolean);
169
+
170
+ const envLines = allLines.filter(isEnvironmentError);
171
+ const validationLines = allLines.filter(line => !isEnvironmentError(line));
172
+
173
+ if (envLines.length > 0 && validationLines.length === 0) {
174
+ // Pure environment failure — skip, do not block.
175
+ return [`${relative(root, artifactDir)} sidecar validation skipped: validator environment error (${envLines[0].slice(0, 120)})`];
176
+ }
177
+
178
+ // Real validation errors (possibly mixed with a few env noise lines).
179
+ const output = validationLines.length > 0 ? validationLines : allLines;
180
+ const trimmed = output.slice(0, 12);
181
+ if (trimmed.length === 0) trimmed.push(`validator exited with status ${result.status ?? 'unknown'}`);
182
+ return trimmed.map(line => `${relative(root, artifactDir)} sidecar validation: ${line}`);
135
183
  }
136
184
 
137
185
  function isWorkflowArtifact(artifact) {
@@ -295,7 +343,7 @@ function analyze(root, now = Date.now()) {
295
343
  }
296
344
  warnings.push(...sidecarGuidance(root, path.dirname(latest.file)));
297
345
 
298
- const blocking = warnings.some(w => /status:|Definition Of Done|Goal Fit|sidecar validation|contradicts evidence\.json|workflow state|evidence verdict|evidence check|NOT_VERIFIED gap|critique status|critique open|next action/.test(w));
346
+ const blocking = warnings.some(w => /status:|Definition Of Done|Goal Fit|sidecar validation:|contradicts evidence\.json|workflow state|evidence verdict|evidence check|NOT_VERIFIED gap|critique status|critique open|next action/.test(w));
299
347
  return { warnings, blocking };
300
348
  }
301
349
 
@@ -2,21 +2,23 @@
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";
6
+ import { fileURLToPath } from "node:url";
5
7
 
6
8
  type AnyObj = Record<string, any>;
7
9
 
8
- const statuses = new Set(["new", "planning", "planned", "in_progress", "blocked", "verifying", "verified", "needs_decision", "not_verified", "failed", "delivered", "accepted", "archived"]);
9
- const phases = ["idea", "backlog", "pickup", "planning", "execution", "verification", "goal_fit", "evidence", "release", "learning", "done"];
10
- const checkKinds = new Set(["build", "types", "lint", "test", "security", "diff", "browser", "runtime", "policy", "external"]);
11
- const checkStatuses = new Set(["pass", "fail", "not_verified", "skip"]);
12
- const verdicts = new Set(["pass", "partial", "fail", "not_verified"]);
10
+ export const statuses = new Set(["new", "planning", "planned", "in_progress", "blocked", "verifying", "verified", "needs_decision", "not_verified", "failed", "delivered", "accepted", "archived"]);
11
+ export const phases = ["idea", "backlog", "pickup", "planning", "execution", "verification", "goal_fit", "evidence", "release", "learning", "done"];
12
+ export const checkKinds = new Set(["build", "types", "lint", "test", "security", "diff", "browser", "runtime", "policy", "external"]);
13
+ export const checkStatuses = new Set(["pass", "fail", "not_verified", "skip"]);
14
+ export const verdicts = new Set(["pass", "partial", "fail", "not_verified"]);
13
15
 
14
16
  function now(): string { return new Date().toISOString().replace(/\.\d{3}Z$/, "Z"); }
15
17
  function read(file: string): string { return fs.readFileSync(file, "utf8"); }
16
- function writeJson(file: string, payload: AnyObj): void { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`); }
18
+ export function writeJson(file: string, payload: AnyObj): void { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`); }
17
19
  function printJson(payload: AnyObj): void { console.log(JSON.stringify(payload).replace(/":/g, '": ').replace(/,"/g, ', "')); }
18
- function loadJson(file: string, fallback: AnyObj = {}): AnyObj { return fs.existsSync(file) ? JSON.parse(read(file)) : { ...fallback }; }
19
- function appendJsonl(file: string, payload: AnyObj): void {
20
+ export function loadJson(file: string, fallback: AnyObj = {}): AnyObj { return fs.existsSync(file) ? JSON.parse(read(file)) : { ...fallback }; }
21
+ export function appendJsonl(file: string, payload: AnyObj): void {
20
22
  fs.mkdirSync(path.dirname(file), { recursive: true });
21
23
  const line = JSON.stringify(payload, Object.keys(payload).sort()).replace(/":/g, '": ').replace(/,"/g, ', "');
22
24
  fs.appendFileSync(file, `${line}\n`);
@@ -24,6 +26,60 @@ function appendJsonl(file: string, payload: AnyObj): void {
24
26
  function die(message: string): never { throw new Error(message); }
25
27
  function slugify(value: string, fallback: string): string { return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || fallback; }
26
28
 
29
+ // Optional Hachure trust-bundle validation. No-ops gracefully when hachure is not installed.
30
+ // Install hachure (^0.4.0) as an optional dependency to enable schema validation.
31
+ function tryLoadHachureValidator(): ((bundle: unknown) => { valid: boolean; errors: string[] }) | null {
32
+ try {
33
+ const _require = createRequire(import.meta.url);
34
+ const hachureDir = path.dirname(_require.resolve("hachure"));
35
+ const schemasDir = path.join(hachureDir, "schemas");
36
+ const Ajv = _require("ajv/dist/2020");
37
+ const schemas: Record<string, any> = {};
38
+ for (const file of fs.readdirSync(schemasDir)) {
39
+ if (!file.endsWith(".schema.json")) continue;
40
+ schemas[file] = JSON.parse(fs.readFileSync(path.join(schemasDir, file), "utf8"));
41
+ }
42
+ const ajv = new Ajv({ strict: false, allErrors: true });
43
+ for (const [filename, schema] of Object.entries(schemas)) {
44
+ if (filename === "trust-bundle.schema.json") continue;
45
+ ajv.addSchema(schema, filename);
46
+ }
47
+ const trustBundleSchema = schemas["trust-bundle.schema.json"];
48
+ if (!trustBundleSchema) return null;
49
+ const validate = ajv.compile(trustBundleSchema);
50
+ return (bundle: unknown) => {
51
+ const valid = validate(bundle);
52
+ if (valid) return { valid: true, errors: [] };
53
+ const errors = ((validate as any).errors ?? []).map((err: any) => {
54
+ const loc = err.instancePath || err.schemaPath || "";
55
+ return `${loc} ${err.message ?? "invalid"}`.trim();
56
+ });
57
+ return { valid: false, errors };
58
+ };
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+ let _hachureValidator: ReturnType<typeof tryLoadHachureValidator> | undefined;
64
+ function getHachureValidator(): ReturnType<typeof tryLoadHachureValidator> {
65
+ if (_hachureValidator === undefined) _hachureValidator = tryLoadHachureValidator();
66
+ return _hachureValidator;
67
+ }
68
+
69
+ /**
70
+ * Validate a Hachure trust.bundle against the canonical trust-bundle schema.
71
+ * Returns `{ valid, errors, available }`. When the optional `hachure` dependency
72
+ * is not installed, validation is unavailable and this returns
73
+ * `{ valid: true, errors: [], available: false }` (fail-open) so callers can
74
+ * choose to treat unvalidated bundles as acceptable or gate on `available`.
75
+ * This is the same validator the sidecar writer uses for trust-backed evidence.
76
+ */
77
+ export function validateTrustBundle(bundle: unknown): { valid: boolean; errors: string[]; available: boolean } {
78
+ const validate = getHachureValidator();
79
+ if (!validate) return { valid: true, errors: [], available: false };
80
+ return { ...validate(bundle), available: true };
81
+ }
82
+
27
83
  function safeRepoIdentifier(value: string): string {
28
84
  const trimmed = value.trim().replace(/\.git$/, "");
29
85
  if (!trimmed || trimmed.length > 120) return "";
@@ -68,7 +124,7 @@ function repoIdentifier(): string {
68
124
  return safeRepoIdentifier(path.basename(process.cwd())) || "workspace";
69
125
  }
70
126
 
71
- function sidecarBase(slug: string): AnyObj {
127
+ export function sidecarBase(slug: string): AnyObj {
72
128
  return { schema_version: "1.0", task_slug: slug, repo: repoIdentifier() };
73
129
  }
74
130
 
@@ -336,7 +392,7 @@ function hasNonEmptyString(value: unknown): boolean {
336
392
  function hasPositiveInteger(value: unknown): boolean {
337
393
  return Number.isInteger(value) && Number(value) >= 1;
338
394
  }
339
- function validateEvidenceRef(ref: AnyObj, label: string): AnyObj {
395
+ export function validateEvidenceRef(ref: AnyObj, label: string): AnyObj {
340
396
  if (!["source", "command", "artifact", "provider", "external"].includes(ref.kind)) die(`${label} entry kind must be one of: source, command, artifact, provider, external`);
341
397
  for (const key of Object.keys(ref)) if (!["kind", "url", "file", "line_start", "line_end", "excerpt", "summary"].includes(key)) die(`${label} entries contain unsupported field: ${key}`);
342
398
  if (ref.url !== undefined && !hasNonEmptyString(ref.url)) die(`${label} entry url must be a non-empty string`);
@@ -352,7 +408,7 @@ function validateEvidenceRef(ref: AnyObj, label: string): AnyObj {
352
408
  if ((ref.kind === "provider" || ref.kind === "external") && !hasNonEmptyString(ref.url)) die(`${label} ${ref.kind} refs require url`);
353
409
  return ref;
354
410
  }
355
- function normalizeEvidenceRefs(raw: unknown, label: string): AnyObj[] {
411
+ export function normalizeEvidenceRefs(raw: unknown, label: string): AnyObj[] {
356
412
  if (!Array.isArray(raw)) die(`${label} must be an array`);
357
413
  return raw.map((ref) => {
358
414
  if (typeof ref === "string") die(`${label} entries must be structured evidence reference objects; legacy string refs are not supported`);
@@ -360,7 +416,7 @@ function normalizeEvidenceRefs(raw: unknown, label: string): AnyObj[] {
360
416
  return validateEvidenceRef({ ...ref as AnyObj }, label);
361
417
  });
362
418
  }
363
- function normalizeCheck(raw: AnyObj): AnyObj {
419
+ export function normalizeCheck(raw: AnyObj): AnyObj {
364
420
  const check = { ...raw };
365
421
  if (!check.id || !check.kind || !check.status || !check.summary) die("check requires id, kind, status, and summary");
366
422
  if (!checkKinds.has(check.kind)) die("kind must be one of: build, types, lint, test, security, diff, browser, runtime, policy, external");
@@ -372,11 +428,27 @@ function normalizeCheck(raw: AnyObj): AnyObj {
372
428
  }
373
429
  function normalizeSurfaceRefs(refs: any): AnyObj[] {
374
430
  if (!Array.isArray(refs)) die("surface_trust_refs must be an array");
431
+ const hachureValidate = getHachureValidator();
375
432
  return refs.map((ref) => {
376
433
  const keys = JSON.stringify(ref).match(/"([^"]+)":/g) ?? [];
377
434
  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
435
  const out = { ...ref };
379
- if (!["TrustReport", "Trust Snapshot"].includes(out.artifact_kind)) die("artifact_kind must be one of");
436
+ // trust.bundle is the canonical Hachure-aligned artifact kind; TrustReport/Trust Snapshot are legacy aliases
437
+ if (!["trust.bundle", "TrustReport", "Trust Snapshot"].includes(out.artifact_kind)) die("artifact_kind must be one of: trust.bundle, TrustReport, Trust Snapshot");
438
+ // When hachure is installed, validate the referenced trust artifact if it is a local file
439
+ if (hachureValidate && out.artifact_ref && typeof out.artifact_ref === "string" && fs.existsSync(out.artifact_ref)) {
440
+ try {
441
+ const bundle = JSON.parse(fs.readFileSync(out.artifact_ref, "utf8"));
442
+ const result = hachureValidate(bundle);
443
+ if (!result.valid) {
444
+ const errorSummary = result.errors.slice(0, 3).join("; ");
445
+ die(`trust.bundle artifact at ${out.artifact_ref} failed Hachure schema validation: ${errorSummary}`);
446
+ }
447
+ } catch (err) {
448
+ if (err instanceof Error && err.message.includes("failed Hachure schema validation")) throw err;
449
+ // File read or parse errors are not re-thrown: the artifact_ref validation path is advisory
450
+ }
451
+ }
380
452
  const status = deriveSurfaceStatus(out);
381
453
  if (out.status === "pass" && status !== "pass") die("surface_trust_refs contradicts Surface trust facts");
382
454
  return out;
@@ -394,15 +466,16 @@ function surfaceCheckFromArtifact(file: string, index: number): AnyObj {
394
466
  const lower = JSON.stringify(raw).toLowerCase();
395
467
  let ref: AnyObj;
396
468
  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" };
469
+ 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
470
  } 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" };
471
+ 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
472
  } else {
401
473
  const claimStatus = lower.includes("rejected") ? "rejected" : "accepted";
402
474
  const freshness = lower.includes("stale") ? "stale" : "fresh";
403
475
  const producer = lower.includes("missing-authority") ? "unknown" : "surface-local";
404
476
  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" } };
477
+ // Use trust.bundle as the canonical Hachure-aligned artifact_kind for all trust-backed evidence refs
478
+ 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
479
  ref.status = deriveSurfaceStatus(ref);
407
480
  ref.summary = ref.status === "pass" ? "accepted" : ref.status === "not_verified" ? "not currently verifiable" : (claimStatus === "rejected" ? "rejected" : producer === "unknown" ? "missing authority" : "integrity mismatch");
408
481
  }
@@ -426,7 +499,7 @@ function validateAcceptanceEvidenceRefs(dir: string): void {
426
499
  if (criterion.evidence_refs !== undefined) normalizeEvidenceRefs(criterion.evidence_refs, `acceptance.criteria[${index}].evidence_refs`);
427
500
  });
428
501
  }
429
- function writeState(dir: string, slug: string, status: string, phase: string, timestamp: string, summary: string, next = "continue"): void {
502
+ export function writeState(dir: string, slug: string, status: string, phase: string, timestamp: string, summary: string, next = "continue"): void {
430
503
  writeJson(path.join(dir, "state.json"), { ...loadJson(path.join(dir, "state.json")), ...sidecarBase(slug), status, phase, updated_at: timestamp, artifact_paths: relArtifacts(dir), next_action: { status: next, summary } });
431
504
  }
432
505
  function recordEvidence(p: ReturnType<typeof parseArgs>): number {
@@ -479,7 +552,7 @@ function advanceState(p: ReturnType<typeof parseArgs>): number {
479
552
  return 0;
480
553
  }
481
554
 
482
- function normalizeFinding(raw: AnyObj): AnyObj {
555
+ export function normalizeFinding(raw: AnyObj): AnyObj {
483
556
  if (raw.file_refs !== undefined && !Array.isArray(raw.file_refs)) die("file_refs must be an array");
484
557
  return raw;
485
558
  }
@@ -536,7 +609,7 @@ function recordRelease(p: ReturnType<typeof parseArgs>): number {
536
609
  writeState(dir, slug, "delivered", "release", payload.updated_at, stateSummary);
537
610
  return 0;
538
611
  }
539
- function validateLearningCorrection(record: AnyObj): void {
612
+ export function validateLearningCorrection(record: AnyObj): void {
540
613
  const correction = record.correction;
541
614
  if (correction === undefined) return;
542
615
  if (!correction || typeof correction !== "object" || Array.isArray(correction)) die("correction must be an object");
@@ -566,7 +639,7 @@ function validateLearningPrevention(prevention: unknown): void {
566
639
  if (typeof value.status !== "string" || value.status.length === 0) die("correction.prevention.status is required");
567
640
  if (!["open", "completed", "accepted", "deferred", "rejected"].includes(value.status)) die("correction.prevention.status must be one of: open, completed, accepted, deferred, rejected");
568
641
  }
569
- function normalizeLearning(raw: AnyObj, timestamp: string): AnyObj {
642
+ export function normalizeLearning(raw: AnyObj, timestamp: string): AnyObj {
570
643
  if (!Array.isArray(raw.source_refs)) die("source_refs must be an array");
571
644
  if (!Array.isArray(raw.facts)) die("facts must be an array");
572
645
  if (!Array.isArray(raw.routing)) die("routing must be an array");
@@ -673,4 +746,11 @@ async function main(): Promise<number> {
673
746
  });
674
747
  }
675
748
 
676
- main().then((code) => process.exit(code)).catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); });
749
+ // Run the CLI only when executed directly, not when imported as a library.
750
+ // Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
751
+ // entry-point guard fires correctly when the module is loaded directly as a script.
752
+ const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
753
+ const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
754
+ if (_selfRealPath === _argv1RealPath) {
755
+ main().then((code) => process.exit(code)).catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); });
756
+ }
@@ -24,6 +24,47 @@ export interface KitConformanceLevel {
24
24
  k2: boolean;
25
25
  }
26
26
 
27
+ /**
28
+ * Kit trust level — WHO vouches for a kit, orthogonal to the K-level capability axis.
29
+ *
30
+ * - "first-party": the kit is authored and published by Kontour (kontourai); its id is in the
31
+ * FIRST_PARTY_KIT_IDS allowlist maintained in this repository. These kits are built, tested,
32
+ * and distributed with the flow-agents package.
33
+ * - "verified": reserved for a future third-party verification process (e.g. self-certification
34
+ * via the conformance kit + cryptographic attestation / Veritas claims). Not yet implemented.
35
+ * - "unverified": default for all kits not in the first-party allowlist. This says nothing about
36
+ * the kit's quality — it only means Kontour has not vouched for it.
37
+ *
38
+ * The v2 path for "verified": cryptographic signing / attestation against the conformance kit
39
+ * and Veritas claims substrate is the natural next step and is intentionally deferred.
40
+ */
41
+ export type KitTrustLevel = "first-party" | "verified" | "unverified";
42
+
43
+ /**
44
+ * Allowlist of kit IDs that Kontour authors, tests, and ships with the flow-agents package.
45
+ *
46
+ * Criteria for inclusion:
47
+ * 1. The kit directory lives under kits/ in the kontourai/flow-agents repository.
48
+ * 2. The kit is published by @kontourai (npm package @kontourai/flow-agents).
49
+ * 3. Kontour owns and maintains the kit's content and release lifecycle.
50
+ *
51
+ * To add a new first-party kit: add its id here AND ensure it lives under kits/ in this repo.
52
+ * Third-party forks or community kits published elsewhere are NOT first-party, even if they
53
+ * share a similar id — first-party is tied to provenance in this specific repository.
54
+ */
55
+ export const FIRST_PARTY_KIT_IDS: ReadonlySet<string> = new Set(["builder", "knowledge"]);
56
+
57
+ /**
58
+ * Derive the trust level for a kit id.
59
+ *
60
+ * v1 determination: allowlist check against FIRST_PARTY_KIT_IDS.
61
+ * "verified" is reserved for future third-party verification (not yet granted to any kit).
62
+ */
63
+ export function deriveKitTrust(kitId: string): KitTrustLevel {
64
+ if (FIRST_PARTY_KIT_IDS.has(kitId)) return "first-party";
65
+ return "unverified";
66
+ }
67
+
27
68
  export interface KitTargetsResult {
28
69
  kit_id: string;
29
70
  kit_name: string;
@@ -32,6 +73,11 @@ export interface KitTargetsResult {
32
73
  targets: KitTargetConsumer[];
33
74
  /** Extension field namespaces that are not Flow or Flow Agents-owned. */
34
75
  third_party_extensions: string[];
76
+ /**
77
+ * Trust level: who vouches for this kit. Orthogonal to the K-level capability axis.
78
+ * "first-party" = Kontour-published; "verified" = reserved (future); "unverified" = default.
79
+ */
80
+ trust: KitTrustLevel;
35
81
  }
36
82
 
37
83
  // Lazy-loaded cache for validateKitContainer from @kontourai/flow.
@@ -83,7 +129,7 @@ async function delegateCoreContainerValidation(kitDir: string, manifest: Record<
83
129
  }
84
130
 
85
131
  /**
86
- * Derives the consumer-target level (K0/K1/K2) and target audience list from
132
+ * Derives the consumer-target level (K0/K1/K2), target audience list, and trust level from
87
133
  * observable asset classes in the kit manifest. Does not require file I/O.
88
134
  *
89
135
  * Derivation rules (from kontourai/flow-agents#52 and Brian's layering review):
@@ -94,6 +140,10 @@ async function delegateCoreContainerValidation(kitDir: string, manifest: Record<
94
140
  * - targets.flow-agents: present when K1 (agent extension assets activate in >=1 harness).
95
141
  * - third-party: any top-level keys that are not core fields and not Flow Agents extension classes.
96
142
  *
143
+ * Trust derivation (from kontourai/flow-agents#79):
144
+ * - "first-party": kit id is in FIRST_PARTY_KIT_IDS (Kontour-authored kits in this repo).
145
+ * - "unverified": all other kits (default; "verified" is reserved for a future process).
146
+ *
97
147
  * @param manifest The kit.json manifest object.
98
148
  * @param kitDir Kit directory for flow file-existence checks. Defaults to "" (structural-only).
99
149
  * Pass the real kit directory from `inspect` to get authoritative K0 validation.
@@ -125,12 +175,16 @@ export async function deriveKitTargets(manifest: Record<string, unknown>, kitDir
125
175
  if (k1) targets.push("flow-agents");
126
176
  for (const ns of thirdPartyExtensions) targets.push(ns);
127
177
 
178
+ // Derive trust level orthogonally to the K-level capability axis.
179
+ const trust = deriveKitTrust(kitId);
180
+
128
181
  return {
129
182
  kit_id: kitId,
130
183
  kit_name: kitName,
131
184
  conformance: { k0, k1, k2 },
132
185
  targets,
133
186
  third_party_extensions: thirdPartyExtensions,
187
+ trust,
134
188
  };
135
189
  }
136
190
 
package/src/index.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Public library surface for `@kontourai/flow-agents`.
3
+ *
4
+ * Native orchestration hosts can import the canonical workflow-sidecar
5
+ * writer/validator here instead of shelling out to the
6
+ * `flow-agents-workflow-sidecar` CLI or reimplementing validated
7
+ * read / merge / write of workflow evidence. This is the same code the CLI
8
+ * runs — importing it does not execute the CLI.
9
+ *
10
+ * The sidecar JSON Schemas ship under `schemas/` and can be validated against
11
+ * directly; the helpers below are the canonical writer/validator that produce
12
+ * and check conforming artifacts.
13
+ *
14
+ * @module
15
+ */
16
+ import * as path from "node:path";
17
+ import { loadJson as _loadJson, writeJson as _writeJson } from "./cli/workflow-sidecar.js";
18
+
19
+ export {
20
+ // Trust-bundle (Hachure) validation — the same validator the writer uses.
21
+ validateTrustBundle,
22
+ // Evidence / check / learning validation + normalization. These throw on
23
+ // invalid input (with the same messages the CLI surfaces) and return the
24
+ // normalized object on success.
25
+ normalizeCheck,
26
+ normalizeFinding,
27
+ normalizeLearning,
28
+ normalizeEvidenceRefs,
29
+ validateEvidenceRef,
30
+ validateLearningCorrection,
31
+ // Sidecar read / merge / write primitives.
32
+ loadJson,
33
+ writeJson,
34
+ appendJsonl,
35
+ sidecarBase,
36
+ writeState,
37
+ // Schema vocabularies (the allowed status/phase/kind values).
38
+ statuses,
39
+ phases,
40
+ checkKinds,
41
+ checkStatuses,
42
+ verdicts,
43
+ } from "./cli/workflow-sidecar.js";
44
+
45
+ /** Read a sidecar JSON file from a workflow artifact directory; returns `{}` if absent. */
46
+ export function readSidecar(dir: string, name: string): Record<string, any> {
47
+ return _loadJson(path.join(dir, name));
48
+ }
49
+
50
+ /** Write a sidecar JSON file into a workflow artifact directory (pretty-printed, trailing newline). */
51
+ export function writeSidecar(dir: string, name: string, payload: Record<string, any>): void {
52
+ _writeJson(path.join(dir, name), payload);
53
+ }