@kontourai/flow-agents 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.github/workflows/ci.yml +4 -0
  2. package/CHANGELOG.md +17 -0
  3. package/build/src/cli/assignment-provider.d.ts +45 -0
  4. package/build/src/cli/assignment-provider.js +97 -12
  5. package/build/src/cli/workflow-sidecar.d.ts +14 -4
  6. package/build/src/cli/workflow-sidecar.js +100 -10
  7. package/context/contracts/assignment-provider-contract.md +1 -1
  8. package/context/contracts/execution-contract.md +78 -0
  9. package/context/scripts/hooks/config-protection.js +11 -4
  10. package/context/scripts/hooks/stop-goal-fit.js +259 -4
  11. package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +111 -0
  12. package/evals/ci/run-baseline.sh +2 -0
  13. package/evals/integration/test_checkpoint_signing.sh +4 -3
  14. package/evals/integration/test_gate_lockdown.sh +36 -0
  15. package/evals/integration/test_model_routing_escalation.sh +145 -0
  16. package/evals/integration/test_publish_delivery.sh +14 -6
  17. package/evals/integration/test_stop_hook_release.sh +552 -0
  18. package/evals/integration/test_trust_reconcile_negatives.sh +170 -0
  19. package/evals/run.sh +6 -0
  20. package/evals/static/test_model_routing_hints.sh +107 -0
  21. package/kits/builder/skills/builder-shape/SKILL.md +10 -0
  22. package/kits/builder/skills/deliver/SKILL.md +52 -11
  23. package/kits/builder/skills/design-probe/SKILL.md +10 -0
  24. package/kits/builder/skills/execute-plan/SKILL.md +13 -0
  25. package/kits/builder/skills/fix-bug/SKILL.md +17 -0
  26. package/kits/builder/skills/idea-to-backlog/SKILL.md +10 -0
  27. package/kits/builder/skills/plan-work/SKILL.md +9 -0
  28. package/kits/builder/skills/pull-work/SKILL.md +10 -0
  29. package/kits/builder/skills/review-work/SKILL.md +11 -0
  30. package/kits/builder/skills/tdd-workflow/SKILL.md +17 -0
  31. package/kits/builder/skills/verify-work/SKILL.md +11 -0
  32. package/kits/knowledge/adapters/default-store/index.js +56 -15
  33. package/kits/knowledge/adapters/flow-runner/index.js +912 -16
  34. package/kits/knowledge/adapters/obsidian-store/index.js +29 -11
  35. package/kits/knowledge/adapters/shared/codec.js +124 -0
  36. package/kits/knowledge/docs/store-contract.md +405 -3
  37. package/kits/knowledge/evals/audit-freshness/suite.test.js +92 -1
  38. package/kits/knowledge/evals/consolidate-incremental/suite.test.js +494 -0
  39. package/kits/knowledge/evals/consolidation/suite.test.js +1 -1
  40. package/kits/knowledge/evals/contract-suite/suite.test.js +36 -0
  41. package/kits/knowledge/evals/freshness/suite.test.js +339 -0
  42. package/kits/knowledge/evals/inbound-references/suite.test.js +351 -0
  43. package/kits/knowledge/evals/retirement/suite.test.js +1 -1
  44. package/kits/knowledge/evals/supersede-propagation/suite.test.js +384 -0
  45. package/package.json +1 -1
  46. package/schemas/workflow-handoff.schema.json +6 -0
  47. package/scripts/ci/mint-attestation.js +33 -6
  48. package/scripts/ci/trust-reconcile.js +144 -26
  49. package/scripts/hooks/config-protection.js +11 -4
  50. package/scripts/hooks/stop-goal-fit.js +259 -4
  51. package/src/cli/assignment-provider.ts +110 -12
  52. package/src/cli/workflow-sidecar.ts +99 -10
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Knowledge Kit — Inbound-Reference Integrity Eval Suite (#340)
3
+ *
4
+ * knowledge.check-inbound-references scans caller-configured doc globs, extracts
5
+ * record citations (full UUID, ≥8-char short-id prefix, slug alias — the #339
6
+ * identity forms), and resolves each against the store. Any unresolvable
7
+ * DEFINITE citation fails the check (fail closed). The scan is READ-ONLY,
8
+ * OPT-IN (zero globs = no-op pass), and adapter-agnostic.
9
+ *
10
+ * PARAMETERIZED by adapter module — set KNOWLEDGE_ADAPTER to an adapter's
11
+ * absolute path, or pass --adapter=<path>. Defaults to the bundled default-store
12
+ * adapter. AC4 runs this file for the default adapter AND the obsidian adapter.
13
+ *
14
+ * Extraction precision (the commit-SHA problem): a bare non-resolving ≥8-hex
15
+ * token is treated as prose / an abbreviated commit SHA and IGNORED, never
16
+ * failed — so the gate produces zero false positives on commit hashes. The
17
+ * documented cost: a *broken* bare-hex short-id (no marker/wikilink) is not
18
+ * caught. Full-UUID and marker/wikilink citations are protected fail-closed.
19
+ *
20
+ * AC map (issue #340):
21
+ * AC1 — three citation forms extracted + resolved
22
+ * AC2 — nonexistent id fails closed (names doc + token); no-op pass with no globs
23
+ * AC3 — read-only invariant (byte-identical store before/after)
24
+ * AC4 — parameterized across adapters (this file, run per KNOWLEDGE_ADAPTER)
25
+ * AC5 — result exposes the citation index; contract doc addendum present
26
+ *
27
+ * Run:
28
+ * node --test kits/knowledge/evals/inbound-references/suite.test.js
29
+ * KNOWLEDGE_ADAPTER=kits/knowledge/adapters/obsidian-store/index.js \
30
+ * node --test kits/knowledge/evals/inbound-references/suite.test.js
31
+ */
32
+
33
+ import { test, describe, before, after } from "node:test";
34
+ import assert from "node:assert/strict";
35
+ import * as fs from "node:fs";
36
+ import * as path from "node:path";
37
+ import * as os from "node:os";
38
+ import { fileURLToPath } from "node:url";
39
+
40
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
41
+ const KIT_ROOT = path.resolve(__dirname, "../..");
42
+ const REPO_ROOT = path.resolve(KIT_ROOT, "../..");
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Adapter resolution — same convention as evals/contract-suite/suite.test.js
46
+ // ---------------------------------------------------------------------------
47
+
48
+ function resolveAdapterPath() {
49
+ const flag = process.argv.find((a) => a.startsWith("--adapter="));
50
+ if (flag) return path.resolve(flag.slice("--adapter=".length));
51
+ if (process.env.KNOWLEDGE_ADAPTER) return path.resolve(process.env.KNOWLEDGE_ADAPTER);
52
+ return path.join(KIT_ROOT, "adapters/default-store/index.js");
53
+ }
54
+
55
+ const adapterPath = resolveAdapterPath();
56
+ const adapterModule = await import(adapterPath);
57
+ const AdapterClass = adapterModule.default || adapterModule.DefaultKnowledgeStore;
58
+ const ADAPTER_LABEL = path.relative(REPO_ROOT, adapterPath);
59
+
60
+ const runnerPath = path.join(KIT_ROOT, "adapters/flow-runner/index.js");
61
+ const { KnowledgeFlowRunner, checkInboundReferences } = await import(runnerPath);
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Helpers
65
+ // ---------------------------------------------------------------------------
66
+
67
+ function makeTempDir(tag) {
68
+ return fs.mkdtempSync(path.join(os.tmpdir(), `knowledge-inbound-refs-${tag}-`));
69
+ }
70
+
71
+ function makeStore(dir) {
72
+ return new AdapterClass({ storeRoot: dir });
73
+ }
74
+
75
+ // The runner's workspace (telemetry sink) is kept SEPARATE from the store dir so
76
+ // gate telemetry never lands inside the store — letting AC3 assert the whole
77
+ // store tree is byte-identical, not just its record files.
78
+ function makeRunner(store, workspaceDir) {
79
+ return new KnowledgeFlowRunner({
80
+ store,
81
+ workspace: workspaceDir,
82
+ agent: "inbound-refs-test-runner",
83
+ sessionId: "inbound-refs-session-001",
84
+ });
85
+ }
86
+
87
+ function writeDoc(root, rel, body) {
88
+ const abs = path.join(root, rel);
89
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
90
+ fs.writeFileSync(abs, body, "utf8");
91
+ }
92
+
93
+ /** Recursive { relPath -> content } snapshot of every file under a dir. */
94
+ function snapshotTree(root) {
95
+ const out = {};
96
+ const walk = (abs, rel) => {
97
+ for (const e of fs.readdirSync(abs, { withFileTypes: true })) {
98
+ const r = rel ? `${rel}/${e.name}` : e.name;
99
+ if (e.isDirectory()) walk(path.join(abs, e.name), r);
100
+ else if (e.isFile()) out[r] = fs.readFileSync(path.join(abs, e.name), "utf8");
101
+ }
102
+ };
103
+ walk(root, "");
104
+ return out;
105
+ }
106
+
107
+ // Deterministic, valid-hex UUIDs so short-id prefixes are predictable.
108
+ const UUID_A = "aaaaaaaa-1111-4111-8111-111111111111"; // cited by full UUID
109
+ const UUID_B = "bbbbbbbb-2222-4222-8222-222222222222"; // cited by 8-char prefix "bbbbbbbb"
110
+ const SLUG_C = "decision.strategy/gtm-2026"; // cited by slug alias
111
+
112
+ async function seedStore(store) {
113
+ await store.create({
114
+ id: UUID_A, type: "compiled", title: "GTM direction",
115
+ body: "Body A", category: "decision.strategy", provenance: { agent: "fixture" },
116
+ });
117
+ await store.create({
118
+ id: UUID_B, type: "compiled", title: "Pricing model",
119
+ body: "Body B", category: "decision.pricing", provenance: { agent: "fixture" },
120
+ });
121
+ await store.create({
122
+ id: "cccccccc-3333-4333-8333-333333333333", type: "concept", title: "North Star",
123
+ body: "Body C", category: "decision.strategy", aliases: [SLUG_C],
124
+ provenance: { agent: "fixture" },
125
+ });
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Suite
130
+ // ---------------------------------------------------------------------------
131
+
132
+ describe(`Knowledge Kit Inbound-Reference Integrity Suite (#340) [adapter: ${ADAPTER_LABEL}]`, () => {
133
+ let storeDir;
134
+ let workspaceDir;
135
+ let store;
136
+ let runner;
137
+
138
+ before(async () => {
139
+ storeDir = makeTempDir("store");
140
+ workspaceDir = makeTempDir("ws");
141
+ store = makeStore(storeDir);
142
+ runner = makeRunner(store, workspaceDir);
143
+ await seedStore(store);
144
+ });
145
+
146
+ after(() => {
147
+ if (storeDir) fs.rmSync(storeDir, { recursive: true, force: true });
148
+ if (workspaceDir) fs.rmSync(workspaceDir, { recursive: true, force: true });
149
+ });
150
+
151
+ test("AC1: all three citation forms (full UUID, 8-char prefix, slug) are extracted and resolved", async () => {
152
+ const docsRoot = makeTempDir("ac1-docs");
153
+ try {
154
+ // NOW.md cites A by full UUID (bare) and B by 8-char short-id prefix (bare).
155
+ writeDoc(docsRoot, "NOW.md",
156
+ `# NOW\n\nThe GTM decision ${UUID_A} supersedes the old plan.\n` +
157
+ `Pricing follows short-id bbbbbbbb in the pricing note.\n`);
158
+ // strategy/vision.md cites C by slug alias via a citation marker + a wikilink.
159
+ writeDoc(docsRoot, "strategy/vision.md",
160
+ `# Vision\n\nAnchored on rec:${SLUG_C} and cross-linked as [[${SLUG_C}]].\n`);
161
+
162
+ const result = await runner.checkInboundReferences({
163
+ docGlobs: ["NOW.md", "strategy/*.md"],
164
+ docsRoot,
165
+ });
166
+
167
+ assert.equal(result.ok, true, "all citations resolve → ok");
168
+ assert.deepEqual(result.unresolved, [], "no unresolved citations");
169
+ assert.deepEqual(result.scanned.sort(), ["NOW.md", "strategy/vision.md"]);
170
+
171
+ // Every seeded record is reachable via byRecord (citer enumeration).
172
+ assert.ok(result.byRecord[UUID_A], "record A cited (full UUID form)");
173
+ assert.ok(result.byRecord[UUID_B], "record B cited (8-char prefix form)");
174
+ assert.ok(
175
+ result.byRecord["cccccccc-3333-4333-8333-333333333333"],
176
+ "record C cited (slug alias form)"
177
+ );
178
+
179
+ // The three forms are all represented.
180
+ const forms = new Set(result.citations.filter((c) => c.resolved).map((c) => c.form));
181
+ assert.ok(forms.has("uuid"), "uuid form extracted");
182
+ assert.ok(forms.has("bare"), "bare short-id prefix extracted (resolve-gated tier)");
183
+ assert.ok(
184
+ forms.has("marker") || forms.has("wikilink"),
185
+ "slug extracted via marker/wikilink form"
186
+ );
187
+
188
+ // The 8-char prefix resolved to the FULL id of record B (#339 prefix path).
189
+ const bareB = result.citations.find((c) => c.form === "bare" && c.token === "bbbbbbbb");
190
+ assert.ok(bareB, "the bbbbbbbb short-id was extracted");
191
+ assert.equal(bareB.recordId, UUID_B, "prefix resolved to record B's full id");
192
+ } finally {
193
+ fs.rmSync(docsRoot, { recursive: true, force: true });
194
+ }
195
+ });
196
+
197
+ test("AC2: a nonexistent id fails closed, naming the doc path + cited id", async () => {
198
+ const docsRoot = makeTempDir("ac2-docs");
199
+ try {
200
+ const DEAD_UUID = "deadbeef-0000-4000-8000-000000000000";
201
+ writeDoc(docsRoot, "NOW.md",
202
+ `# NOW\n\nThis cites a severed record ${DEAD_UUID} that no longer exists.\n` +
203
+ `And a broken marked short-id rec:99999999 too.\n`);
204
+
205
+ const result = await runner.checkInboundReferences({
206
+ docGlobs: ["NOW.md"],
207
+ docsRoot,
208
+ });
209
+
210
+ assert.equal(result.ok, false, "an unresolvable definite citation fails closed (not empty success)");
211
+ assert.ok(result.unresolved.length >= 2, "both broken citations reported");
212
+
213
+ const deadRow = result.unresolved.find((u) => u.token === DEAD_UUID);
214
+ assert.ok(deadRow, "the severed UUID citation is reported");
215
+ assert.equal(deadRow.doc, "NOW.md", "failure names the doc path");
216
+ assert.equal(deadRow.token, DEAD_UUID, "failure names the unresolved id");
217
+ assert.equal(typeof deadRow.line, "number", "failure names the line");
218
+ assert.equal(deadRow.reason, "not-found");
219
+
220
+ const markedRow = result.unresolved.find((u) => u.token === "99999999");
221
+ assert.ok(markedRow, "the marked broken short-id is reported");
222
+ assert.equal(markedRow.form, "marker");
223
+ } finally {
224
+ fs.rmSync(docsRoot, { recursive: true, force: true });
225
+ }
226
+ });
227
+
228
+ test("AC2: zero configured globs returns a no-op pass (opt-in)", async () => {
229
+ const result = await runner.checkInboundReferences({});
230
+ assert.equal(result.ok, true, "no globs → passing no-op");
231
+ assert.deepEqual(result.scanned, [], "nothing scanned");
232
+ assert.deepEqual(result.citations, [], "no citations");
233
+ assert.deepEqual(result.unresolved, [], "no failures");
234
+ });
235
+
236
+ test("commit-SHA safety: a bare non-resolving hex token is ignored, not failed (documented miss)", async () => {
237
+ const docsRoot = makeTempDir("sha-docs");
238
+ try {
239
+ // These look like abbreviated / full commit SHAs. None resolve to a record.
240
+ writeDoc(docsRoot, "NOW.md",
241
+ `# NOW\n\nFixed in a1b2c3d4 and reverted by a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6.\n`);
242
+
243
+ const result = await runner.checkInboundReferences({
244
+ docGlobs: ["NOW.md"],
245
+ docsRoot,
246
+ });
247
+
248
+ assert.equal(result.ok, true, "bare non-resolving hex → no false-positive failure");
249
+ assert.deepEqual(result.unresolved, [], "commit SHAs never fail the gate");
250
+ assert.equal(
251
+ result.citations.filter((c) => c.form === "bare").length,
252
+ 0,
253
+ "non-resolving bare hex is not indexed as a citation"
254
+ );
255
+ } finally {
256
+ fs.rmSync(docsRoot, { recursive: true, force: true });
257
+ }
258
+ });
259
+
260
+ test("AC3: read-only invariant — the store is byte-identical before and after the check", async () => {
261
+ const docsRoot = makeTempDir("ac3-docs");
262
+ try {
263
+ writeDoc(docsRoot, "NOW.md",
264
+ `# NOW\n\n${UUID_A} and bbbbbbbb and rec:${SLUG_C} and a broken deadbeef-0000-4000-8000-000000000000.\n`);
265
+
266
+ const before = snapshotTree(storeDir);
267
+ await runner.checkInboundReferences({ docGlobs: ["NOW.md"], docsRoot });
268
+ const after = snapshotTree(storeDir);
269
+
270
+ assert.deepEqual(after, before, "no store file is created, deleted, or mutated by the check");
271
+ } finally {
272
+ fs.rmSync(docsRoot, { recursive: true, force: true });
273
+ }
274
+ });
275
+
276
+ test("AC5: the result exposes the citation index (citations + byDoc + byRecord)", async () => {
277
+ const docsRoot = makeTempDir("ac5-docs");
278
+ try {
279
+ writeDoc(docsRoot, "NOW.md", `# NOW\n\nGTM ${UUID_A}; pricing bbbbbbbb.\n`);
280
+ writeDoc(docsRoot, "strategy/vision.md", `# Vision\n\nrec:${SLUG_C}\n`);
281
+
282
+ const result = await runner.checkInboundReferences({
283
+ docGlobs: ["NOW.md", "strategy/*.md"],
284
+ docsRoot,
285
+ });
286
+
287
+ // Shape: citations carry doc + line + column + token + form + resolved + recordId.
288
+ for (const c of result.citations) {
289
+ for (const k of ["doc", "line", "column", "token", "form", "resolved", "recordId"]) {
290
+ assert.ok(k in c, `citation entry exposes "${k}"`);
291
+ }
292
+ }
293
+ // byDoc: doc → cited record ids (every scanned doc present).
294
+ assert.ok("NOW.md" in result.byDoc, "byDoc keyed by scanned doc path");
295
+ assert.ok("strategy/vision.md" in result.byDoc);
296
+ // byRecord: record id → citers (downstream supersede/retire propagation).
297
+ const citersOfA = result.byRecord[UUID_A];
298
+ assert.ok(Array.isArray(citersOfA) && citersOfA.length >= 1, "byRecord enumerates citers of A");
299
+ assert.equal(citersOfA[0].doc, "NOW.md", "citer records the citing doc");
300
+ } finally {
301
+ fs.rmSync(docsRoot, { recursive: true, force: true });
302
+ }
303
+ });
304
+
305
+ test("gate telemetry: scan-gate and resolve-gate events are emitted", async () => {
306
+ const docsRoot = makeTempDir("tel-docs");
307
+ try {
308
+ writeDoc(docsRoot, "NOW.md", `# NOW\n\n${UUID_A}\n`);
309
+ const result = await runner.checkInboundReferences({ docGlobs: ["NOW.md"], docsRoot });
310
+ assert.ok(
311
+ result.telemetryEvents.length >= 4,
312
+ "scan-gate + resolve-gate produce at least 4 in/out events"
313
+ );
314
+ } finally {
315
+ fs.rmSync(docsRoot, { recursive: true, force: true });
316
+ }
317
+ });
318
+
319
+ test("module-level checkInboundReferences export delegates to the runner", async () => {
320
+ const docsRoot = makeTempDir("mod-docs");
321
+ try {
322
+ writeDoc(docsRoot, "NOW.md", `# NOW\n\n${UUID_A}\n`);
323
+ const result = await checkInboundReferences({
324
+ store, workspace: workspaceDir, agent: "inbound-refs-test-runner",
325
+ docGlobs: ["NOW.md"], docsRoot,
326
+ });
327
+ assert.equal(result.ok, true);
328
+ assert.ok(result.byRecord[UUID_A], "delegated call resolves the same citation index");
329
+ } finally {
330
+ fs.rmSync(docsRoot, { recursive: true, force: true });
331
+ }
332
+ });
333
+ });
334
+
335
+ // ---------------------------------------------------------------------------
336
+ // AC5 (doc claim) — the contract addendum is committed. Adapter-independent, so
337
+ // run once (only under the default adapter to avoid duplicate reporting).
338
+ // ---------------------------------------------------------------------------
339
+
340
+ describe("Inbound-Reference Integrity — contract doc addendum (#340)", () => {
341
+ test("AC5: store-contract.md documents the check as an addendum", () => {
342
+ const contract = fs.readFileSync(
343
+ path.join(KIT_ROOT, "docs/store-contract.md"), "utf8"
344
+ );
345
+ assert.match(contract, /## Addendum I —.*Inbound-Reference/i, "Addendum I heading present");
346
+ assert.match(contract, /docGlobs/, "documents the docGlobs option");
347
+ assert.match(contract, /fail closed|fail-closed/i, "documents fail-closed semantics");
348
+ assert.match(contract, /commit SHA|commit hash/i, "documents the commit-SHA false-positive handling");
349
+ assert.match(contract, /byRecord|citation index/i, "documents the citation-index shape");
350
+ });
351
+ });
@@ -65,7 +65,7 @@ function makeRunner(store, dir) {
65
65
  }
66
66
 
67
67
  function readTelemetryEvents(dir) {
68
- const sinkPath = path.join(dir, ".telemetry", "full.jsonl");
68
+ const sinkPath = path.join(dir, ".kontourai", "telemetry", "full.jsonl");
69
69
  if (!fs.existsSync(sinkPath)) return [];
70
70
  return fs.readFileSync(sinkPath, "utf8")
71
71
  .trim()