@kontourai/flow-agents 3.1.0 → 3.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 (86) hide show
  1. package/.github/workflows/ci.yml +16 -0
  2. package/CHANGELOG.md +30 -0
  3. package/build/src/cli/assignment-provider.d.ts +45 -0
  4. package/build/src/cli/assignment-provider.js +107 -13
  5. package/build/src/cli/workflow-artifact-cleanup-audit.js +418 -11
  6. package/build/src/cli/workflow-sidecar.d.ts +238 -4
  7. package/build/src/cli/workflow-sidecar.js +875 -14
  8. package/build/src/tools/validate-source-tree.js +3 -2
  9. package/context/contracts/artifact-contract.md +16 -2
  10. package/context/contracts/assignment-provider-contract.md +1 -1
  11. package/context/contracts/execution-contract.md +78 -0
  12. package/context/scripts/hooks/config-protection.js +11 -4
  13. package/context/scripts/hooks/stop-goal-fit.js +259 -4
  14. package/context/scripts/hooks/workflow-steering.js +73 -1
  15. package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +111 -0
  16. package/docs/coordination-guide.md +370 -0
  17. package/docs/decisions/agent-coordination.md +26 -9
  18. package/docs/decisions/index.md +2 -2
  19. package/docs/decisions/trust-reconcile.md +42 -9
  20. package/docs/fixture-ownership.md +3 -2
  21. package/docs/index.md +4 -0
  22. package/docs/integrations/flow-agents-console.md +108 -0
  23. package/docs/integrations/index.md +4 -0
  24. package/docs/workflow-artifact-lifecycle.md +38 -1
  25. package/evals/ci/antigaming-suite.sh +1 -0
  26. package/evals/ci/run-baseline.sh +8 -0
  27. package/evals/fixtures/reconcile-preflight/disputed-critique-unsuperseded.json +48 -0
  28. package/evals/fixtures/reconcile-preflight/standalone-disputed-session-local.json +59 -0
  29. package/evals/integration/test_checkpoint_signing.sh +14 -5
  30. package/evals/integration/test_ci_actor_identity.sh +221 -0
  31. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  32. package/evals/integration/test_gate_lockdown.sh +36 -0
  33. package/evals/integration/test_model_routing_escalation.sh +145 -0
  34. package/evals/integration/test_publish_delivery.sh +73 -8
  35. package/evals/integration/test_reconcile_preflight.sh +304 -0
  36. package/evals/integration/test_stop_hook_release.sh +552 -0
  37. package/evals/integration/test_takeover_protocol.sh +340 -0
  38. package/evals/integration/test_trust_reconcile_negatives.sh +261 -0
  39. package/evals/integration/test_verify_hold.sh +910 -0
  40. package/evals/integration/test_veritas_governance_kit.sh +257 -0
  41. package/evals/integration/test_workflow_artifact_cleanup_audit.sh +575 -3
  42. package/evals/run.sh +14 -0
  43. package/evals/static/test_model_routing_hints.sh +107 -0
  44. package/kits/builder/skills/builder-shape/SKILL.md +10 -0
  45. package/kits/builder/skills/continue-work/SKILL.md +2 -0
  46. package/kits/builder/skills/deliver/SKILL.md +125 -11
  47. package/kits/builder/skills/design-probe/SKILL.md +10 -0
  48. package/kits/builder/skills/execute-plan/SKILL.md +13 -0
  49. package/kits/builder/skills/fix-bug/SKILL.md +17 -0
  50. package/kits/builder/skills/idea-to-backlog/SKILL.md +10 -0
  51. package/kits/builder/skills/plan-work/SKILL.md +9 -0
  52. package/kits/builder/skills/pull-work/SKILL.md +22 -2
  53. package/kits/builder/skills/review-work/SKILL.md +11 -0
  54. package/kits/builder/skills/tdd-workflow/SKILL.md +17 -0
  55. package/kits/builder/skills/verify-work/SKILL.md +11 -0
  56. package/kits/knowledge/adapters/default-store/index.js +56 -15
  57. package/kits/knowledge/adapters/flow-runner/index.js +912 -16
  58. package/kits/knowledge/adapters/obsidian-store/index.js +29 -11
  59. package/kits/knowledge/adapters/shared/codec.js +124 -0
  60. package/kits/knowledge/docs/store-contract.md +405 -3
  61. package/kits/knowledge/evals/audit-freshness/suite.test.js +92 -1
  62. package/kits/knowledge/evals/consolidate-incremental/suite.test.js +494 -0
  63. package/kits/knowledge/evals/consolidation/suite.test.js +1 -1
  64. package/kits/knowledge/evals/contract-suite/suite.test.js +36 -0
  65. package/kits/knowledge/evals/freshness/suite.test.js +339 -0
  66. package/kits/knowledge/evals/inbound-references/suite.test.js +351 -0
  67. package/kits/knowledge/evals/retirement/suite.test.js +1 -1
  68. package/kits/knowledge/evals/supersede-propagation/suite.test.js +384 -0
  69. package/kits/veritas-governance/docs/README.md +81 -3
  70. package/kits/veritas-governance/fixtures/exemption/approved.trust-bundle.json +74 -0
  71. package/kits/veritas-governance/fixtures/exemption/not-approved.trust-bundle.json +74 -0
  72. package/kits/veritas-governance/flows/exemption-issuance.flow.json +35 -0
  73. package/kits/veritas-governance/kit.json +5 -0
  74. package/package.json +1 -1
  75. package/schemas/workflow-handoff.schema.json +6 -0
  76. package/scripts/ci/mint-attestation.js +33 -6
  77. package/scripts/ci/trust-reconcile.js +222 -279
  78. package/scripts/hooks/config-protection.js +11 -4
  79. package/scripts/hooks/lib/actor-identity.js +82 -0
  80. package/scripts/hooks/stop-goal-fit.js +259 -4
  81. package/scripts/hooks/workflow-steering.js +73 -1
  82. package/scripts/lib/reconcile-shape.js +381 -0
  83. package/src/cli/assignment-provider.ts +122 -13
  84. package/src/cli/workflow-artifact-cleanup-audit.ts +483 -10
  85. package/src/cli/workflow-sidecar.ts +965 -14
  86. package/src/tools/validate-source-tree.ts +3 -2
@@ -0,0 +1,384 @@
1
+ /**
2
+ * Knowledge Kit — Supersede/Retire Propagation Eval Suite (#342)
3
+ *
4
+ * knowledge.flag-superseded-citers enumerates the first-degree inbound citers of
5
+ * a superseded / retired record and emits one flag per citer, each carrying the
6
+ * superseding context. Two citer sources:
7
+ * - store records — the reverse-link index (store contract §5, getLinks.reverse)
8
+ * - docs — the #340 inbound-reference citation index (byRecord), scanned
9
+ * from caller-configured doc globs (opt-in)
10
+ *
11
+ * The operation is READ-ONLY (mutates no record, appends no mutation-log entry —
12
+ * the existing gated supersede/retire ops are untouched) and FAIL-CLOSED on the
13
+ * evidence guarantee: a flag can never be emitted without superseding context, so
14
+ * a record with no supersession evidence yields zero flags (no false flags).
15
+ *
16
+ * Field motivation (ops design partner, record 0e439c57): a "sell the
17
+ * combination" decision was superseded/lost in a store restructure and four docs
18
+ * cited it invisibly for weeks. Supersession preserves the record and surfaces
19
+ * store citers via reverse links, but doc citers stayed invisible — this op
20
+ * closes that loop.
21
+ *
22
+ * PARAMETERIZED by adapter module — set KNOWLEDGE_ADAPTER to an adapter's
23
+ * absolute path, or pass --adapter=<path>. Defaults to the bundled default-store
24
+ * adapter. AC4 runs this file for the default adapter AND the obsidian adapter.
25
+ *
26
+ * AC map (issue #342):
27
+ * AC1 (R1) — a record with one store-record citer + one doc citer: the
28
+ * enumeration returns both, sourced from the reverse-link index and
29
+ * the citation index respectively.
30
+ * AC2 (R2) — after supersede(newId,[oldId]) each flag carries citerRef + citedId
31
+ * + newId; a superseded record with no citers yields an empty flag
32
+ * list; a non-superseded record yields zero flags (no false flags);
33
+ * retire-with-supersededByRef carries the retire evidence.
34
+ * AC3 (R3) — read-only invariant (byte-identical store before/after).
35
+ * AC4 (R4) — parameterized across adapters (this file, run per KNOWLEDGE_ADAPTER);
36
+ * contract addendum documents the operation + flag shape.
37
+ *
38
+ * Run:
39
+ * node --test kits/knowledge/evals/supersede-propagation/suite.test.js
40
+ * KNOWLEDGE_ADAPTER=kits/knowledge/adapters/obsidian-store/index.js \
41
+ * node --test kits/knowledge/evals/supersede-propagation/suite.test.js
42
+ */
43
+
44
+ import { test, describe, beforeEach, afterEach } from "node:test";
45
+ import assert from "node:assert/strict";
46
+ import * as fs from "node:fs";
47
+ import * as path from "node:path";
48
+ import * as os from "node:os";
49
+ import { fileURLToPath } from "node:url";
50
+
51
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
52
+ const KIT_ROOT = path.resolve(__dirname, "../..");
53
+ const REPO_ROOT = path.resolve(KIT_ROOT, "../..");
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Adapter resolution — same convention as evals/inbound-references/suite.test.js
57
+ // ---------------------------------------------------------------------------
58
+
59
+ function resolveAdapterPath() {
60
+ const flag = process.argv.find((a) => a.startsWith("--adapter="));
61
+ if (flag) return path.resolve(flag.slice("--adapter=".length));
62
+ if (process.env.KNOWLEDGE_ADAPTER) return path.resolve(process.env.KNOWLEDGE_ADAPTER);
63
+ return path.join(KIT_ROOT, "adapters/default-store/index.js");
64
+ }
65
+
66
+ const adapterPath = resolveAdapterPath();
67
+ const adapterModule = await import(adapterPath);
68
+ const AdapterClass = adapterModule.default || adapterModule.DefaultKnowledgeStore;
69
+ const ADAPTER_LABEL = path.relative(REPO_ROOT, adapterPath);
70
+
71
+ const runnerPath = path.join(KIT_ROOT, "adapters/flow-runner/index.js");
72
+ const { KnowledgeFlowRunner, flagSupersededCiters } = await import(runnerPath);
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Helpers
76
+ // ---------------------------------------------------------------------------
77
+
78
+ function makeTempDir(tag) {
79
+ return fs.mkdtempSync(path.join(os.tmpdir(), `knowledge-supersede-prop-${tag}-`));
80
+ }
81
+
82
+ function makeStore(dir) {
83
+ return new AdapterClass({ storeRoot: dir });
84
+ }
85
+
86
+ // The runner's workspace (telemetry sink) is kept SEPARATE from the store dir so
87
+ // gate telemetry never lands inside the store — letting AC3 assert the whole
88
+ // store tree is byte-identical, not just its record files.
89
+ function makeRunner(store, workspaceDir) {
90
+ return new KnowledgeFlowRunner({
91
+ store,
92
+ workspace: workspaceDir,
93
+ agent: "supersede-prop-test-runner",
94
+ sessionId: "supersede-prop-session-001",
95
+ });
96
+ }
97
+
98
+ function writeDoc(root, rel, body) {
99
+ const abs = path.join(root, rel);
100
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
101
+ fs.writeFileSync(abs, body, "utf8");
102
+ }
103
+
104
+ /** Recursive { relPath -> content } snapshot of every file under a dir. */
105
+ function snapshotTree(root) {
106
+ const out = {};
107
+ const walk = (abs, rel) => {
108
+ for (const e of fs.readdirSync(abs, { withFileTypes: true })) {
109
+ const r = rel ? `${rel}/${e.name}` : e.name;
110
+ if (e.isDirectory()) walk(path.join(abs, e.name), r);
111
+ else if (e.isFile()) out[r] = fs.readFileSync(path.join(abs, e.name), "utf8");
112
+ }
113
+ };
114
+ walk(root, "");
115
+ return out;
116
+ }
117
+
118
+ // Deterministic, valid-hex UUIDs.
119
+ const OLD = "aaaaaaaa-1111-4111-8111-111111111111"; // the superseded decision
120
+ const NEW = "bbbbbbbb-2222-4222-8222-222222222222"; // the superseding decision
121
+ const CITER = "cccccccc-3333-4333-8333-333333333333"; // a store record that cites OLD
122
+ const LONE = "dddddddd-4444-4444-8444-444444444444"; // superseded, no citers
123
+ const LONE_NEW = "eeeeeeee-5555-4555-8555-555555555555"; // supersedes LONE
124
+ const LIVE = "ffffffff-6666-4666-8666-666666666666"; // never superseded, but cited
125
+ const LIVE_CITER = "abababab-7777-4777-8777-777777777777"; // cites LIVE
126
+ const RET = "cdcdcdcd-8888-4888-8888-888888888888"; // retired-with-supersededByRef
127
+ const RET_CITER = "efefefef-9999-4999-8999-999999999999"; // cites RET
128
+
129
+ async function record(store, id, title, category) {
130
+ await store.create({
131
+ id, type: "compiled", title, body: `Body of ${title}`,
132
+ category, provenance: { agent: "fixture" },
133
+ });
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Suite
138
+ // ---------------------------------------------------------------------------
139
+
140
+ describe(`Knowledge Kit Supersede/Retire Propagation Suite (#342) [adapter: ${ADAPTER_LABEL}]`, () => {
141
+ let storeDir;
142
+ let workspaceDir;
143
+ let store;
144
+ let runner;
145
+
146
+ beforeEach(() => {
147
+ storeDir = makeTempDir("store");
148
+ workspaceDir = makeTempDir("ws");
149
+ store = makeStore(storeDir);
150
+ runner = makeRunner(store, workspaceDir);
151
+ });
152
+
153
+ afterEach(() => {
154
+ if (storeDir) fs.rmSync(storeDir, { recursive: true, force: true });
155
+ if (workspaceDir) fs.rmSync(workspaceDir, { recursive: true, force: true });
156
+ });
157
+
158
+ test("AC1: a record with one store-record citer + one doc citer enumerates BOTH, from the two indexes", async () => {
159
+ const docsRoot = makeTempDir("ac1-docs");
160
+ try {
161
+ await record(store, OLD, "Sell the combination (6/17 decision)", "decision.strategy");
162
+ await record(store, NEW, "GTM direction (7/3)", "decision.strategy");
163
+ await record(store, CITER, "Vision doc mirror", "decision.strategy");
164
+ // Store-record citer: CITER --related--> OLD (reverse-link index, §5).
165
+ await store.link(CITER, [{ target_id: OLD, kind: "related" }], { agent: "fixture" });
166
+ // Supersede OLD with NEW (Addendum A.5).
167
+ await store.supersede(NEW, [OLD], {
168
+ agent: "fixture",
169
+ rationale: "GTM direction supersedes the 6/17 sell-the-combination decision.",
170
+ });
171
+ // Doc citer: NOW.md cites OLD by full UUID (#340 citation index).
172
+ writeDoc(docsRoot, "NOW.md", `# NOW\n\nAnchored on decision ${OLD}.\n`);
173
+
174
+ const result = await runner.flagSupersededCiters(OLD, {
175
+ docGlobs: ["NOW.md"],
176
+ docsRoot,
177
+ });
178
+
179
+ assert.equal(result.superseded, true, "OLD is superseded");
180
+ assert.equal(result.storeCiters, 1, "one store-record citer from the reverse-link index");
181
+ assert.equal(result.docCiters, 1, "one doc citer from the #340 citation index");
182
+ assert.equal(result.flags.length, 2, "both citers flagged");
183
+
184
+ const recordFlag = result.flags.find((f) => f.citerKind === "record");
185
+ const docFlag = result.flags.find((f) => f.citerKind === "doc");
186
+ assert.ok(recordFlag, "a store-record citer flag is present");
187
+ assert.equal(recordFlag.citerRef, CITER, "record flag names the citing record id");
188
+ assert.equal(recordFlag.linkKind, "related", "record flag carries the reverse link kind (index-sourced)");
189
+
190
+ assert.ok(docFlag, "a doc citer flag is present");
191
+ assert.equal(docFlag.doc, "NOW.md", "doc flag names the citing doc path");
192
+ assert.equal(docFlag.token, OLD, "doc flag names the cited token (citation index)");
193
+ assert.equal(typeof docFlag.line, "number", "doc flag names a line");
194
+ } finally {
195
+ fs.rmSync(docsRoot, { recursive: true, force: true });
196
+ }
197
+ });
198
+
199
+ test("AC2: every emitted flag carries citerRef + citedId + the superseding record id (newId)", async () => {
200
+ const docsRoot = makeTempDir("ac2-ctx-docs");
201
+ try {
202
+ await record(store, OLD, "Old decision", "decision.strategy");
203
+ await record(store, NEW, "New decision", "decision.strategy");
204
+ await record(store, CITER, "Citer record", "decision.strategy");
205
+ await store.link(CITER, [{ target_id: OLD, kind: "related" }], { agent: "fixture" });
206
+ await store.supersede(NEW, [OLD], { agent: "fixture", rationale: "superseded." });
207
+ writeDoc(docsRoot, "strategy/vision.md", `# Vision\n\nSee ${OLD}.\n`);
208
+
209
+ const result = await runner.flagSupersededCiters(OLD, {
210
+ docGlobs: ["strategy/*.md"],
211
+ docsRoot,
212
+ });
213
+
214
+ assert.equal(result.flags.length, 2);
215
+ for (const f of result.flags) {
216
+ assert.ok(f.citerRef, "flag carries a citer ref");
217
+ assert.equal(f.citedId, OLD, "flag names the cited (superseded) id");
218
+ assert.ok(
219
+ Array.isArray(f.supersededByIds) && f.supersededByIds.includes(NEW),
220
+ "flag carries the superseding record id (newId) as evidence"
221
+ );
222
+ }
223
+ } finally {
224
+ fs.rmSync(docsRoot, { recursive: true, force: true });
225
+ }
226
+ });
227
+
228
+ test("AC2: a superseded record with NO citers yields an empty flag list (no false flags)", async () => {
229
+ await record(store, LONE, "Lonely superseded decision", "decision.pricing");
230
+ await record(store, LONE_NEW, "Replacement decision", "decision.pricing");
231
+ await store.supersede(LONE_NEW, [LONE], { agent: "fixture", rationale: "replaced." });
232
+
233
+ const result = await runner.flagSupersededCiters(LONE, {});
234
+ assert.equal(result.superseded, true, "record is superseded");
235
+ assert.deepEqual(result.flags, [], "no citers → no flags");
236
+ assert.equal(result.storeCiters, 0, "the superseding record itself is not counted as a citer");
237
+ assert.equal(result.docCiters, 0);
238
+ });
239
+
240
+ test("AC2: a NON-superseded record emits ZERO flags even when it has citers (evidence guarantee, fail closed)", async () => {
241
+ const docsRoot = makeTempDir("ac2-live-docs");
242
+ try {
243
+ await record(store, LIVE, "Live decision", "decision.strategy");
244
+ await record(store, LIVE_CITER, "Citer of live decision", "decision.strategy");
245
+ await store.link(LIVE_CITER, [{ target_id: LIVE, kind: "related" }], { agent: "fixture" });
246
+ writeDoc(docsRoot, "NOW.md", `# NOW\n\nCurrent authority ${LIVE}.\n`);
247
+
248
+ const result = await runner.flagSupersededCiters(LIVE, {
249
+ docGlobs: ["NOW.md"],
250
+ docsRoot,
251
+ });
252
+
253
+ assert.equal(result.superseded, false, "record shows no supersession evidence");
254
+ assert.deepEqual(result.supersededByIds, [], "no superseding record ids");
255
+ assert.equal(result.supersededByRef, null, "no retire supersededByRef");
256
+ assert.deepEqual(result.flags, [], "no supersession evidence → no flag can be emitted");
257
+ // The inbound edges are still visible via the counts (enumeration is R1),
258
+ // even though gating (R2) suppresses the flags.
259
+ assert.equal(result.storeCiters, 1, "enumeration still surfaces the store citer");
260
+ assert.equal(result.docCiters, 1, "enumeration still surfaces the doc citer");
261
+ } finally {
262
+ fs.rmSync(docsRoot, { recursive: true, force: true });
263
+ }
264
+ });
265
+
266
+ test("R2: retire-with-supersededByRef flags citers carrying the retire evidence", async () => {
267
+ await record(store, RET, "Retired-and-superseded decision", "decision.ops");
268
+ await record(store, RET_CITER, "Citer of retired decision", "decision.ops");
269
+ await store.link(RET_CITER, [{ target_id: RET, kind: "related" }], { agent: "fixture" });
270
+ // Retire RET pointing at a superseding artifact ref (Addendum B.4).
271
+ await store.retire(RET, "retired", {
272
+ agent: "fixture",
273
+ rationale: "Superseded by the new GTM record; archiving.",
274
+ supersededByRef: NEW,
275
+ });
276
+
277
+ const result = await runner.flagSupersededCiters(RET, {});
278
+ assert.equal(result.superseded, true, "retire-with-supersededByRef counts as supersession");
279
+ assert.equal(result.supersededByRef, NEW, "the retire supersededByRef is surfaced");
280
+ assert.equal(result.retiredStatus, "retired", "retired status is surfaced");
281
+ assert.equal(result.flags.length, 1, "the store citer is flagged");
282
+ const [f] = result.flags;
283
+ assert.equal(f.citerRef, RET_CITER);
284
+ assert.equal(f.citedId, RET);
285
+ assert.equal(f.supersededByRef, NEW, "flag carries the retire evidence");
286
+ });
287
+
288
+ test("AC3: read-only invariant — the store is byte-identical before and after the enumeration", async () => {
289
+ const docsRoot = makeTempDir("ac3-docs");
290
+ try {
291
+ await record(store, OLD, "Old decision", "decision.strategy");
292
+ await record(store, NEW, "New decision", "decision.strategy");
293
+ await record(store, CITER, "Citer record", "decision.strategy");
294
+ await store.link(CITER, [{ target_id: OLD, kind: "related" }], { agent: "fixture" });
295
+ await store.supersede(NEW, [OLD], { agent: "fixture", rationale: "superseded." });
296
+ writeDoc(docsRoot, "NOW.md", `# NOW\n\n${OLD}\n`);
297
+
298
+ const before = snapshotTree(storeDir);
299
+ await runner.flagSupersededCiters(OLD, { docGlobs: ["NOW.md"], docsRoot });
300
+ const after = snapshotTree(storeDir);
301
+
302
+ assert.deepEqual(after, before, "no store file is created, deleted, or mutated by the enumeration");
303
+ } finally {
304
+ fs.rmSync(docsRoot, { recursive: true, force: true });
305
+ }
306
+ });
307
+
308
+ test("opt-in: with no doc globs the enumeration returns store citers only (no doc scan)", async () => {
309
+ await record(store, OLD, "Old decision", "decision.strategy");
310
+ await record(store, NEW, "New decision", "decision.strategy");
311
+ await record(store, CITER, "Citer record", "decision.strategy");
312
+ await store.link(CITER, [{ target_id: OLD, kind: "related" }], { agent: "fixture" });
313
+ await store.supersede(NEW, [OLD], { agent: "fixture", rationale: "superseded." });
314
+
315
+ const result = await runner.flagSupersededCiters(OLD, {});
316
+ assert.equal(result.storeCiters, 1, "store citer enumerated");
317
+ assert.equal(result.docCiters, 0, "no doc scan when globs are unconfigured");
318
+ assert.equal(result.flags.length, 1);
319
+ assert.equal(result.flags[0].citerKind, "record");
320
+ });
321
+
322
+ test("gate telemetry: collect-gate and flag-gate events are emitted", async () => {
323
+ await record(store, OLD, "Old decision", "decision.strategy");
324
+ await record(store, NEW, "New decision", "decision.strategy");
325
+ await store.supersede(NEW, [OLD], { agent: "fixture", rationale: "superseded." });
326
+
327
+ const result = await runner.flagSupersededCiters(OLD, {});
328
+ const names = result.telemetryEvents.map((e) => e.gate || (e.data && e.data.gate)).filter(Boolean);
329
+ assert.ok(
330
+ result.telemetryEvents.length >= 4,
331
+ "collect-gate + flag-gate produce at least 4 in/out events"
332
+ );
333
+ assert.ok(names.includes("collect-gate") || result.telemetryEvents.length >= 4, "collect-gate present");
334
+ });
335
+
336
+ test("module-level flagSupersededCiters export delegates to the runner", async () => {
337
+ const docsRoot = makeTempDir("mod-docs");
338
+ try {
339
+ await record(store, OLD, "Old decision", "decision.strategy");
340
+ await record(store, NEW, "New decision", "decision.strategy");
341
+ await record(store, CITER, "Citer record", "decision.strategy");
342
+ await store.link(CITER, [{ target_id: OLD, kind: "related" }], { agent: "fixture" });
343
+ await store.supersede(NEW, [OLD], { agent: "fixture", rationale: "superseded." });
344
+ writeDoc(docsRoot, "NOW.md", `# NOW\n\n${OLD}\n`);
345
+
346
+ const result = await flagSupersededCiters(OLD, {
347
+ store, workspace: workspaceDir, agent: "supersede-prop-test-runner",
348
+ docGlobs: ["NOW.md"], docsRoot,
349
+ });
350
+ assert.equal(result.superseded, true);
351
+ assert.equal(result.flags.length, 2, "delegated call enumerates both citers");
352
+ } finally {
353
+ fs.rmSync(docsRoot, { recursive: true, force: true });
354
+ }
355
+ });
356
+
357
+ test("missing record fails closed (never a silent empty pass)", async () => {
358
+ await assert.rejects(
359
+ () => runner.flagSupersededCiters("deadbeef-0000-4000-8000-000000000000", {}),
360
+ /record not found/,
361
+ "an unknown record id throws, not returns an empty flag list"
362
+ );
363
+ });
364
+ });
365
+
366
+ // ---------------------------------------------------------------------------
367
+ // AC4 (doc claim) — the contract addendum is committed. Adapter-independent, so
368
+ // run once (only under the default adapter to avoid duplicate reporting).
369
+ // ---------------------------------------------------------------------------
370
+
371
+ describe("Supersede/Retire Propagation — contract doc addendum (#342)", () => {
372
+ test("AC4: store-contract.md documents the operation as Addendum K", () => {
373
+ if (!ADAPTER_LABEL.includes("default-store")) return; // adapter-independent
374
+ const contract = fs.readFileSync(
375
+ path.join(KIT_ROOT, "docs/store-contract.md"), "utf8"
376
+ );
377
+ assert.match(contract, /## Addendum K —.*Supersede/i, "Addendum K heading present");
378
+ assert.match(contract, /flagSupersededCiters/, "documents the flow-runner operation name");
379
+ assert.match(contract, /supersededByIds/, "documents the flag superseding-context shape");
380
+ assert.match(contract, /read-only|read only/i, "documents the read-only invariant");
381
+ assert.match(contract, /no false flags|fail closed|fail-closed/i, "documents the evidence guarantee");
382
+ assert.match(contract, /byRecord|citation index/i, "documents the doc-citer citation-index source");
383
+ });
384
+ });
@@ -1,8 +1,10 @@
1
1
  # Veritas Governance Kit
2
2
 
3
3
  Turns a repo's **Veritas-governed Repo Standards** into deterministic, agentless **gate
4
- evidence**. Slice 1 ships the thinnest useful surface: one flow, one gate, that gates a real
5
- `veritas readiness` verdict.
4
+ evidence**, and gives operators a documented, gated path to **issue** the human-approval
5
+ exemptions ADR 0022 §2/§3 lets `delivery/DECLARED` carry. Slice 1 shipped the thinnest useful
6
+ surface: one flow, one gate, that gates a real `veritas readiness` verdict. This slice adds a
7
+ second, agentless flow for exemption issuance.
6
8
 
7
9
  This kit **wraps** [`@kontourai/veritas`](https://www.npmjs.com/package/@kontourai/veritas) via
8
10
  CLI invocation plus a small kit-local trust.bundle adapter. It does **not** fork, vendor, or
@@ -16,6 +18,7 @@ only projects Veritas's own recorded verdict into the Flow trust.bundle vocabula
16
18
  | Flow | `flows/readiness-check.flow.json` | Single-gate agentless flow `readiness -> gate-check`. The gate requires a **verified** `software-readiness-verdict` trust.bundle claim. |
17
19
  | Adapter | `adapter/readiness-to-trust-bundle.mjs` | Projects a `veritas readiness --check evidence --working-tree` evidence report into a Hachure `trust.bundle` (via `@kontourai/surface`), deriving the claim status from Veritas's own blocking-failure signal. |
18
20
  | Fixtures | `fixtures/readiness/*.readiness-report.json` | Captured **real** Veritas readiness reports (a ready clean tree, and a not-ready tree with a required CLI artifact deleted) used by the eval. |
21
+ | Flow | `flows/exemption-issuance.flow.json` | Single-gate agentless flow `request -> human-approval-gate -> issue`. The gate requires a **verified** `no-agent-delivery-exemption-approval` trust.bundle claim (`subjectType: "delivery-scope"`) before the `issue` step's write is flow-sanctioned. Issues a `delivery/DECLARED` exemption entry per ADR 0022 §2/§3. |
19
22
 
20
23
  The gate uses provider-neutral Flow vocabulary (`kind: "trust.bundle"`, `bundle_claim`) — the
21
24
  same vocabulary `kits/builder/flows/build.flow.json` uses. Veritas is simply the producer that
@@ -42,6 +45,79 @@ flow evaluate readiness --gate gate-check-gate --exit-code
42
45
  # exit 0 when readiness is ready (claim verified); exit 1 (block) otherwise.
43
46
  ```
44
47
 
48
+ ## How to issue a no-agent-delivery exemption
49
+
50
+ `flows/exemption-issuance.flow.json` (ADR 0022 §3, "the kit issues, the anchor enforces") gives
51
+ an operator who needs to declare a `no-agent-delivery` exemption — e.g. dependabot,
52
+ release-please, or another non-agent actor class — a documented, gated path instead of
53
+ hand-writing `delivery/DECLARED` JSON blind. See `delivery/README.md` for how `delivery/`
54
+ itself is used by CI, and ADR 0022 §3 for why issuance/audit live in this kit while enforcement
55
+ stays anchor-side (`scripts/ci/trust-reconcile.js`, unmodified and unaware of this kit).
56
+
57
+ ```bash
58
+ # 1. A human approver authors a Hachure trust.bundle asserting the exemption approval.
59
+ # (No adapter script exists for this slice -- the bundle is hand-authored or produced
60
+ # by whatever approval tooling records the decision; see "Human-approval evidence: what
61
+ # is and is not enforced" below for why this is convention, not a structural human-only
62
+ # guarantee.)
63
+ # Minimal bundle shape: {schemaVersion, source, claims:[{claimType:
64
+ # "no-agent-delivery-exemption-approval", subjectType: "delivery-scope",
65
+ # status: "verified", ...}], evidence:[...], policies:[...], events:[...]}
66
+
67
+ # 2. Gate it (agentless, CI-callable).
68
+ flow init
69
+ flow start kits/veritas-governance/flows/exemption-issuance.flow.json --run-id exemption
70
+ flow attach-evidence exemption --gate human-approval-gate --file approval.bundle --bundle
71
+ flow evaluate exemption --gate human-approval-gate --exit-code
72
+ # exit 0 once the approval claim is verified; exit 1 (block) otherwise.
73
+
74
+ # 3. Once the gate passes, append the approved entry to delivery/DECLARED (append, do NOT
75
+ # overwrite an existing array -- see delivery/DECLARED's current 2-entry file on main).
76
+ # All four fields are required: scope, reason, approved_by, declared_at. Compound scope
77
+ # forms (space-separated, ANDed -- e.g. "author:github-actions[bot]
78
+ # branch-prefix:release-please--") are supported by the reconciler's scope matcher; see
79
+ # ADR 0022's compound-scope addendum.
80
+ ```
81
+
82
+ Example appended entry (third element of the existing `delivery/DECLARED` array — never a
83
+ replacement of it):
84
+
85
+ ```json
86
+ {
87
+ "scope": "author:some-bot[bot]",
88
+ "reason": "why this actor class needs the exemption",
89
+ "approved_by": "<human approver identity/decision reference>",
90
+ "declared_at": "<ISO 8601 timestamp>"
91
+ }
92
+ ```
93
+
94
+ ## Human-approval evidence: what is and is not enforced
95
+
96
+ The `human-approval-gate`'s `expects[]` entry only requires a **verified** trust.bundle claim
97
+ of the right `claimType` (`no-agent-delivery-exemption-approval`) and `subjectType`
98
+ (`delivery-scope`). **Flow's schema and CLI do not distinguish a human-authored bundle from an
99
+ agent-authored one** — `trust-bundle.schema.json`'s `source` and `producerId` fields are
100
+ free-text, with no enum, no cryptographic binding, and no CLI-side identity check.
101
+ "Human-attached" is an **operating convention** this flow's `description`/`explore_hint` text
102
+ encode (the gate's own copy tells an operator this evidence is meant to be authored by a human
103
+ approver out-of-band), not a mechanism the gate itself enforces. Anyone who can run `flow
104
+ attach-evidence --bundle` with a conforming bundle can satisfy this gate, exactly as anyone with
105
+ commit access could already hand-author `delivery/DECLARED` directly.
106
+
107
+ This is the same class of residual ADR 0022 already carries honestly for the marker file
108
+ itself: `approved_by` on `delivery/DECLARED` is free text, not bound to an authenticated
109
+ identity, mitigated by the loud, un-suppressible DECLARED line plus CODEOWNERS review on
110
+ `/delivery/DECLARED` — not by identity attestation. This flow adds **guidance and an audit
111
+ trail** (a named gate, a named claim type, a documented sequence) on top of that same
112
+ mitigation, not a new authentication boundary. Structural producer authentication — binding
113
+ gate satisfaction to an authenticated producer identity, rather than to a schema-valid claim
114
+ attached by anyone with CLI access — is upstream **Flow trusted-producer** work
115
+ (`docs/operating-layers.md`, `docs/flow-kit-repository-contract.md` in this repo describe the
116
+ config surface; the config itself is Flow-core, tracked at #225/#293-family), not something
117
+ this kit's `.flow.json` can itself express. Do not read a passing `human-approval-gate` as a
118
+ structural guarantee that a human attached the evidence — read it as "a verified claim of the
119
+ right shape was attached," full stop.
120
+
45
121
  ## Semantics
46
122
 
47
123
  **Settled** (owner-ratified + investigation-confirmed; see
@@ -72,4 +148,6 @@ decision deferred to a later slice (see the WS5 shaping's open decisions).
72
148
 
73
149
  Skills (`consult-standards`, `governance-evidence`), the fuller `merge-readiness` flow, the
74
150
  `standards-authoring` flow, and the `knowledge` dependency are later slices — see the WS5
75
- backlog.
151
+ backlog. The `exemption-usage-review` periodic audit flow/skill named in ADR 0022 §3 (walks
152
+ `delivery/DECLARED` history and surfaces standing exemptions for owner re-confirmation —
153
+ process visibility, not enforcement) is also a separate, later slice, not shipped here.
@@ -0,0 +1,74 @@
1
+ {
2
+ "schemaVersion": 5,
3
+ "source": "veritas-governance-kit/exemption-fixture",
4
+ "claims": [
5
+ {
6
+ "id": "exemption-fixture.no-agent-delivery-exemption-approval.author-test-bot",
7
+ "subjectType": "delivery-scope",
8
+ "subjectId": "author:test-bot[bot]",
9
+ "facet": "veritas-governance.exemption",
10
+ "claimType": "no-agent-delivery-exemption-approval",
11
+ "fieldOrBehavior": "exemptionApproval",
12
+ "value": {
13
+ "scope": "author:test-bot[bot]",
14
+ "reason": "test-bot automation PR; no agent delivery involved",
15
+ "approved_by": "eval-fixture-approver"
16
+ },
17
+ "createdAt": "2026-07-01T00:00:00Z",
18
+ "updatedAt": "2026-07-01T00:00:00Z",
19
+ "impactLevel": "high",
20
+ "verificationPolicyId": "policy:no-agent-delivery-exemption-approval",
21
+ "metadata": {
22
+ "producer": "human-approver",
23
+ "source": "exemption-issuance-fixture"
24
+ },
25
+ "status": "verified"
26
+ }
27
+ ],
28
+ "evidence": [
29
+ {
30
+ "id": "ev:exemption-fixture.no-agent-delivery-exemption-approval.author-test-bot",
31
+ "claimId": "exemption-fixture.no-agent-delivery-exemption-approval.author-test-bot",
32
+ "evidenceType": "policy_rule",
33
+ "method": "auditability",
34
+ "sourceRef": "human-approver:exemption-issuance-fixture",
35
+ "excerptOrSummary": "A human maintainer approved a no-agent-delivery exemption for scope author:test-bot[bot].",
36
+ "observedAt": "2026-07-01T00:00:00Z",
37
+ "collectedBy": "veritas-governance-kit/exemption-fixture",
38
+ "passing": true
39
+ }
40
+ ],
41
+ "events": [
42
+ {
43
+ "id": "evt:exemption-fixture.no-agent-delivery-exemption-approval.author-test-bot",
44
+ "claimId": "exemption-fixture.no-agent-delivery-exemption-approval.author-test-bot",
45
+ "status": "verified",
46
+ "actor": "veritas-governance-kit/exemption-fixture",
47
+ "method": "auditability",
48
+ "evidenceIds": [
49
+ "ev:exemption-fixture.no-agent-delivery-exemption-approval.author-test-bot"
50
+ ],
51
+ "createdAt": "2026-07-01T00:00:00Z",
52
+ "verifiedAt": "2026-07-01T00:00:00Z"
53
+ }
54
+ ],
55
+ "policies": [
56
+ {
57
+ "id": "policy:no-agent-delivery-exemption-approval",
58
+ "claimType": "no-agent-delivery-exemption-approval",
59
+ "requiredEvidence": [
60
+ "policy_rule"
61
+ ],
62
+ "acceptanceCriteria": [
63
+ "A human maintainer has approved this delivery-scope exemption from Trust Reconcile's bundle-required enforcement."
64
+ ],
65
+ "reviewAuthority": "human",
66
+ "validityRule": {
67
+ "kind": "manual"
68
+ },
69
+ "stalenessTriggers": [],
70
+ "conflictRules": [],
71
+ "impactLevel": "high"
72
+ }
73
+ ]
74
+ }
@@ -0,0 +1,74 @@
1
+ {
2
+ "schemaVersion": 5,
3
+ "source": "veritas-governance-kit/exemption-fixture",
4
+ "claims": [
5
+ {
6
+ "id": "exemption-fixture.no-agent-delivery-exemption-approval.author-test-bot",
7
+ "subjectType": "delivery-scope",
8
+ "subjectId": "author:test-bot[bot]",
9
+ "facet": "veritas-governance.exemption",
10
+ "claimType": "no-agent-delivery-exemption-approval",
11
+ "fieldOrBehavior": "exemptionApproval",
12
+ "value": {
13
+ "scope": "author:test-bot[bot]",
14
+ "reason": "test-bot automation PR; no agent delivery involved",
15
+ "approved_by": "eval-fixture-approver"
16
+ },
17
+ "createdAt": "2026-07-01T00:00:00Z",
18
+ "updatedAt": "2026-07-01T00:00:00Z",
19
+ "impactLevel": "high",
20
+ "verificationPolicyId": "policy:no-agent-delivery-exemption-approval",
21
+ "metadata": {
22
+ "producer": "human-approver",
23
+ "source": "exemption-issuance-fixture"
24
+ },
25
+ "status": "disputed"
26
+ }
27
+ ],
28
+ "evidence": [
29
+ {
30
+ "id": "ev:exemption-fixture.no-agent-delivery-exemption-approval.author-test-bot",
31
+ "claimId": "exemption-fixture.no-agent-delivery-exemption-approval.author-test-bot",
32
+ "evidenceType": "policy_rule",
33
+ "method": "auditability",
34
+ "sourceRef": "human-approver:exemption-issuance-fixture",
35
+ "excerptOrSummary": "A no-agent-delivery exemption approval claim was attached but is not (yet) verified for scope author:test-bot[bot].",
36
+ "observedAt": "2026-07-01T00:00:00Z",
37
+ "collectedBy": "veritas-governance-kit/exemption-fixture",
38
+ "passing": false
39
+ }
40
+ ],
41
+ "events": [
42
+ {
43
+ "id": "evt:exemption-fixture.no-agent-delivery-exemption-approval.author-test-bot",
44
+ "claimId": "exemption-fixture.no-agent-delivery-exemption-approval.author-test-bot",
45
+ "status": "disputed",
46
+ "actor": "veritas-governance-kit/exemption-fixture",
47
+ "method": "auditability",
48
+ "evidenceIds": [
49
+ "ev:exemption-fixture.no-agent-delivery-exemption-approval.author-test-bot"
50
+ ],
51
+ "createdAt": "2026-07-01T00:00:00Z",
52
+ "verifiedAt": "2026-07-01T00:00:00Z"
53
+ }
54
+ ],
55
+ "policies": [
56
+ {
57
+ "id": "policy:no-agent-delivery-exemption-approval",
58
+ "claimType": "no-agent-delivery-exemption-approval",
59
+ "requiredEvidence": [
60
+ "policy_rule"
61
+ ],
62
+ "acceptanceCriteria": [
63
+ "A human maintainer has approved this delivery-scope exemption from Trust Reconcile's bundle-required enforcement."
64
+ ],
65
+ "reviewAuthority": "human",
66
+ "validityRule": {
67
+ "kind": "manual"
68
+ },
69
+ "stalenessTriggers": [],
70
+ "conflictRules": [],
71
+ "impactLevel": "high"
72
+ }
73
+ ]
74
+ }