@kontourai/flow-agents 1.2.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 (55) hide show
  1. package/.github/workflows/ci.yml +6 -1
  2. package/.github/workflows/kit-gates-demo.yml +6 -2
  3. package/CHANGELOG.md +25 -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/workflow-sidecar.js +70 -5
  8. package/build/src/flow-kit/validate.js +32 -1
  9. package/build/src/tools/build-universal-bundles.js +14 -0
  10. package/console.telemetry.json +1 -1
  11. package/docs/adr/0004-gates-expect-surface-claims.md +7 -7
  12. package/docs/kit-authoring-guide.md +99 -6
  13. package/docs/operating-layers.md +2 -2
  14. package/docs/veritas-integration.md +4 -4
  15. package/docs/workflow-eval-strategy.md +2 -2
  16. package/docs/workflow-usage-guide.md +1 -1
  17. package/evals/acceptance/test_opencode_harness.sh +18 -10
  18. package/evals/acceptance/test_pi_harness.sh +10 -6
  19. package/evals/ci/run-baseline.sh +1 -1
  20. package/evals/fixtures/flow-kit-repository/mixed-runtime-kit/flows/runtime.flow.json +4 -4
  21. package/evals/fixtures/flow-kit-repository/valid-local-kit/flows/review.flow.json +4 -4
  22. package/evals/fixtures/kit-conformance-levels/k0-flows-only/flows/review.flow.json +4 -4
  23. package/evals/fixtures/kit-conformance-levels/k1-agent-extension/flows/build.flow.json +4 -4
  24. package/evals/fixtures/kit-conformance-levels/k2-with-evals/flows/synthesize.flow.json +4 -4
  25. package/evals/fixtures/kit-conformance-levels/third-party-extension/flows/review.flow.json +4 -4
  26. package/evals/fixtures/surface-trust/accepted-claim-trust-report.json +2 -2
  27. package/evals/fixtures/surface-trust/artifact-absent.json +2 -2
  28. package/evals/fixtures/surface-trust/integrity-mismatch-trust-report.json +2 -2
  29. package/evals/fixtures/surface-trust/missing-authority-trust-report.json +2 -2
  30. package/evals/fixtures/surface-trust/provider-absent.json +2 -2
  31. package/evals/fixtures/surface-trust/rejected-claim-trust-report.json +2 -2
  32. package/evals/fixtures/surface-trust/stale-claim-trust-snapshot.json +2 -2
  33. package/evals/integration/test_console_learning_projection.sh +1 -1
  34. package/evals/integration/test_goal_fit_hook.sh +144 -0
  35. package/evals/integration/test_kit_conformance_levels.sh +55 -1
  36. package/evals/integration/test_workflow_sidecar_writer.sh +9 -9
  37. package/evals/static/test_package.sh +3 -3
  38. package/evals/static/test_workflow_skills.sh +4 -4
  39. package/kits/builder/flows/build.flow.json +48 -48
  40. package/kits/builder/flows/shape.flow.json +36 -36
  41. package/kits/knowledge/adapters/obsidian-store/index.js +137 -26
  42. package/kits/knowledge/evals/contract-suite/suite.test.js +90 -0
  43. package/kits/knowledge/flows/compile.flow.json +12 -12
  44. package/kits/knowledge/flows/consolidate.flow.json +16 -16
  45. package/kits/knowledge/flows/ingest.flow.json +12 -12
  46. package/kits/knowledge/flows/retire.flow.json +16 -16
  47. package/kits/knowledge/flows/store-contract.flow.json +12 -12
  48. package/kits/knowledge/flows/synthesize.flow.json +16 -16
  49. package/kits/release-evidence/flows/release-evidence.flow.json +3 -3
  50. package/package.json +5 -2
  51. package/schemas/workflow-evidence.schema.json +2 -1
  52. package/scripts/hooks/stop-goal-fit.js +66 -18
  53. package/src/cli/workflow-sidecar.ts +62 -4
  54. package/src/flow-kit/validate.ts +55 -1
  55. package/src/tools/build-universal-bundles.ts +14 -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.3.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",
@@ -133,6 +133,9 @@
133
133
  "typescript": "^6.0.3"
134
134
  },
135
135
  "dependencies": {
136
- "@kontourai/flow": "^1.2.0"
136
+ "@kontourai/flow": "~1.3.0"
137
+ },
138
+ "optionalDependencies": {
139
+ "hachure": "^0.4.0"
137
140
  }
138
141
  }
@@ -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,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
  }
@@ -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
 
@@ -439,6 +439,7 @@ function exportOpencodePlugin(): string {
439
439
 
440
440
  import { spawnSync } from 'node:child_process';
441
441
  import { join, basename } from 'node:path';
442
+ import { mkdirSync, writeFileSync } from 'node:fs';
442
443
 
443
444
  // opencode runs plugins inside its own compiled (Bun-based) binary, so
444
445
  // process.execPath points at opencode itself — spawning it with a script
@@ -449,6 +450,19 @@ const NODE_BIN = basename(process.execPath).startsWith('node') ? process.execPat
449
450
  export const FlowAgentsPlugin = async ({ project, client, $, directory, worktree }) => {
450
451
  const root = directory || process.cwd();
451
452
 
453
+ // Deterministic load marker. opencode invokes this factory at startup but
454
+ // does not reliably surface plugin console output to its log file, and its
455
+ // internal "loading plugin" message was dropped in opencode 1.17.x. Write a
456
+ // marker into the workspace telemetry dir so acceptance tests can confirm the
457
+ // plugin loaded without depending on opencode internals. Best-effort only.
458
+ try {
459
+ const telemetryDir = join(root, '.telemetry');
460
+ mkdirSync(telemetryDir, { recursive: true });
461
+ writeFileSync(join(telemetryDir, 'opencode-plugin.loaded'), 'flow-agents');
462
+ } catch (_err) {
463
+ // Marker is diagnostic only; never block plugin load on a write failure.
464
+ }
465
+
452
466
  // The hook scripts read the event payload from stdin; an empty stdin makes
453
467
  // the telemetry pipeline silently skip the emit (fail-open), so every spawn
454
468
  // must pass a payload (caught by live acceptance smoke 2026-06-11).