@kontourai/flow-agents 2.4.0 → 3.1.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 (195) hide show
  1. package/.github/CODEOWNERS +8 -0
  2. package/.github/workflows/ci.yml +20 -0
  3. package/.github/workflows/trust-reconcile.yml +62 -4
  4. package/CHANGELOG.md +38 -0
  5. package/CONTEXT.md +88 -1
  6. package/README.md +4 -0
  7. package/build/src/cli/assignment-provider.d.ts +144 -0
  8. package/build/src/cli/assignment-provider.js +805 -0
  9. package/build/src/cli/effective-assignment-provider-settings.d.ts +1 -0
  10. package/build/src/cli/effective-assignment-provider-settings.js +125 -0
  11. package/build/src/cli/validate-workflow-artifacts.js +5 -1
  12. package/build/src/cli/workflow-sidecar.d.ts +2 -2
  13. package/build/src/cli/workflow-sidecar.js +549 -145
  14. package/build/src/cli.js +6 -0
  15. package/build/src/lib/flow-resolver.d.ts +12 -6
  16. package/build/src/lib/flow-resolver.js +30 -14
  17. package/build/src/tools/validate-source-tree.js +2 -0
  18. package/context/contracts/artifact-contract.md +2 -0
  19. package/context/contracts/assignment-provider-contract.md +248 -0
  20. package/context/contracts/builder-kit-workflow-state-contract.md +2 -0
  21. package/context/contracts/decision-registry-contract.md +2 -0
  22. package/context/contracts/delivery-contract.md +2 -0
  23. package/context/contracts/execution-contract.md +25 -0
  24. package/context/contracts/governance-adapter-contract.md +2 -0
  25. package/context/contracts/knowledge-store-contract.md +197 -0
  26. package/context/contracts/planning-contract.md +2 -0
  27. package/context/contracts/probe-docs-write-contract.md +187 -0
  28. package/context/contracts/review-contract.md +2 -0
  29. package/context/contracts/sandbox-policy.md +2 -0
  30. package/context/contracts/standing-directives.md +13 -0
  31. package/context/contracts/verification-contract.md +2 -0
  32. package/context/contracts/work-item-contract.md +2 -0
  33. package/context/scripts/hooks/config-protection.js +14 -1
  34. package/context/scripts/hooks/lib/config-protection-remedies.js +9 -0
  35. package/context/scripts/hooks/stop-goal-fit.js +4 -2
  36. package/context/scripts/hooks/workflow-steering.js +42 -0
  37. package/context/settings/assignment-provider-settings.json +33 -0
  38. package/docs/adr/0001-flow-agents-consumes-flow.md +2 -0
  39. package/docs/adr/0002-flow-kits-as-extension-unit.md +2 -0
  40. package/docs/adr/0003-flow-agents-coordinates-kits-and-adapters.md +2 -0
  41. package/docs/adr/0004-gates-expect-surface-claims.md +2 -0
  42. package/docs/adr/0005-kubernetes-inspired-resource-contracts.md +2 -0
  43. package/docs/adr/0006-typescript-first-source-policy.md +2 -0
  44. package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
  45. package/docs/adr/0007-skill-audit.md +2 -0
  46. package/docs/adr/0008-kit-operation-boundary.md +2 -0
  47. package/docs/adr/0009-canonical-hook-core-kit-boundary.md +2 -0
  48. package/docs/adr/0010-workflow-trust-state-as-hachure-bundle.md +2 -0
  49. package/docs/adr/0011-mcp-posture.md +2 -0
  50. package/docs/adr/0012-agent-coordination-as-liveness-claims.md +2 -0
  51. package/docs/adr/0013-context-lifecycle.md +2 -0
  52. package/docs/adr/0014-core-vs-domain-kit-boundary.md +2 -0
  53. package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +2 -0
  54. package/docs/adr/0016-three-hard-boundary-model.md +2 -0
  55. package/docs/adr/0017-anti-gaming-trust-security-model.md +2 -0
  56. package/docs/adr/0018-freeze-local-shell-heuristics.md +2 -0
  57. package/docs/adr/0019-kit-dependency-ownership.md +2 -0
  58. package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +2 -0
  59. package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +3 -1
  60. package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +182 -0
  61. package/docs/adr/README.md +49 -0
  62. package/docs/adr/index.md +34 -0
  63. package/docs/context-map.md +1 -0
  64. package/docs/decisions/agent-coordination.md +20 -0
  65. package/docs/decisions/anti-gaming-trust-security.md +20 -0
  66. package/docs/decisions/context-lifecycle.md +18 -0
  67. package/docs/decisions/core-domain-kit-boundary.md +18 -0
  68. package/docs/decisions/flow-flow-agents-boundary.md +18 -0
  69. package/docs/decisions/flow-kit.md +20 -0
  70. package/docs/decisions/flow-skill-kit-tool-boundary.md +20 -0
  71. package/docs/decisions/graph-knowledge-provider.md +63 -0
  72. package/docs/decisions/hook-core-kit-boundary.md +18 -0
  73. package/docs/decisions/index.md +22 -0
  74. package/docs/decisions/kit-dependency-ownership.md +18 -0
  75. package/docs/decisions/kit-operation-boundary.md +18 -0
  76. package/docs/decisions/knowledge-promote-sub-flow.md +59 -0
  77. package/docs/decisions/knowledge-store-provider.md +51 -0
  78. package/docs/decisions/kontour-resource-contract.md +18 -0
  79. package/docs/decisions/mcp-posture.md +18 -0
  80. package/docs/decisions/model-routing.md +63 -0
  81. package/docs/decisions/standing-directives.md +66 -0
  82. package/docs/decisions/three-hard-boundary-model.md +18 -0
  83. package/docs/decisions/trust-reconcile.md +20 -0
  84. package/docs/decisions/typescript-source-policy.md +48 -0
  85. package/docs/decisions/workflow-enforcement.md +18 -0
  86. package/docs/decisions/workflow-trust-state.md +20 -0
  87. package/docs/fixture-ownership.md +1 -0
  88. package/docs/workflow-shared-contracts.md +2 -1
  89. package/docs/workflow-usage-guide.md +7 -1
  90. package/evals/ci/run-baseline.sh +10 -0
  91. package/evals/fixtures/assignment-provider/actor-a.json +6 -0
  92. package/evals/fixtures/assignment-provider/actor-b.json +6 -0
  93. package/evals/fixtures/assignment-provider/github-issue-claimed.json +27 -0
  94. package/evals/fixtures/assignment-provider/github-issue-unassigned.json +7 -0
  95. package/evals/fixtures/assignment-provider/hostile-effective-state.json +11 -0
  96. package/evals/fixtures/assignment-provider/liveness-fresh.json +9 -0
  97. package/evals/fixtures/assignment-provider/liveness-stale.json +9 -0
  98. package/evals/integration/test_assignment_provider_github.sh +318 -0
  99. package/evals/integration/test_assignment_provider_local_file.sh +222 -0
  100. package/evals/integration/test_critique_supersession_roundtrip.sh +182 -0
  101. package/evals/integration/test_current_json_per_actor.sh +516 -0
  102. package/evals/integration/test_ensure_session_ownership_guard.sh +435 -0
  103. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  104. package/evals/integration/test_gate_lockdown.sh +10 -0
  105. package/evals/integration/test_publish_delivery.sh +21 -4
  106. package/evals/integration/test_pull_work_assignment_join.sh +132 -0
  107. package/evals/integration/test_pull_work_liveness_preflight.sh +20 -7
  108. package/evals/integration/test_reconcile_soundness.sh +33 -9
  109. package/evals/integration/test_trust_reconcile.sh +9 -8
  110. package/evals/integration/test_trust_reconcile_negatives.sh +608 -0
  111. package/evals/integration/test_workflow_sidecar_writer.sh +91 -3
  112. package/evals/run.sh +14 -0
  113. package/evals/static/test_flowdef_codeowners_coverage.sh +6 -0
  114. package/evals/static/test_knowledge_providers.sh +32 -0
  115. package/evals/static/test_workflow_skills.sh +15 -2
  116. package/kits/builder/skills/deliver/SKILL.md +36 -0
  117. package/kits/builder/skills/design-probe/SKILL.md +37 -0
  118. package/kits/builder/skills/pickup-probe/SKILL.md +4 -4
  119. package/kits/builder/skills/pull-work/SKILL.md +78 -10
  120. package/kits/knowledge/adapters/default-store/index.js +92 -4
  121. package/kits/knowledge/adapters/obsidian-store/index.js +69 -4
  122. package/kits/knowledge/adapters/shared/codec.js +141 -0
  123. package/kits/knowledge/docs/README.md +121 -2
  124. package/kits/knowledge/docs/store-contract.md +112 -4
  125. package/kits/knowledge/evals/contract-suite/suite.test.js +176 -0
  126. package/kits/knowledge/flows/promote.flow.json +84 -0
  127. package/kits/knowledge/kit.json +50 -0
  128. package/kits/knowledge/promote/distill.js +96 -0
  129. package/kits/knowledge/promote/fixtures/contradiction-registry/CONTEXT.md +7 -0
  130. package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction-policy.md +12 -0
  131. package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction.md +12 -0
  132. package/kits/knowledge/promote/fixtures/repo/CONTEXT.md +11 -0
  133. package/kits/knowledge/promote/fixtures/repo/docs/decisions/index.md +9 -0
  134. package/kits/knowledge/promote/fixtures/repo/docs/decisions/liveness-claim.md +12 -0
  135. package/kits/knowledge/promote/fixtures/session/acceptance.json +9 -0
  136. package/kits/knowledge/promote/fixtures/session/agents/tool-planner/events.jsonl +1 -0
  137. package/kits/knowledge/promote/fixtures/session/agents/tool-verifier/events.jsonl +1 -0
  138. package/kits/knowledge/promote/fixtures/session/agents/tool-worker-1-1/events.jsonl +1 -0
  139. package/kits/knowledge/promote/fixtures/session/kontourai-flow-agents-287--deliver.md +47 -0
  140. package/kits/knowledge/promote/fixtures/session/learning.json +35 -0
  141. package/kits/knowledge/promote/fixtures/session/state.json +19 -0
  142. package/kits/knowledge/promote/health.js +137 -0
  143. package/kits/knowledge/promote/index.js +176 -0
  144. package/kits/knowledge/promote/ingest.js +103 -0
  145. package/kits/knowledge/promote/lib.js +132 -0
  146. package/kits/knowledge/promote/link.js +84 -0
  147. package/kits/knowledge/promote/promote.test.js +174 -0
  148. package/kits/knowledge/providers/conformance/fixtures/git-repo/CONTEXT.md +12 -0
  149. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/old-sprocket-shape.md +13 -0
  150. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/sprocket-shape.md +14 -0
  151. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/widget-format.md +14 -0
  152. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/learnings/fixture-learning.md +7 -0
  153. package/kits/knowledge/providers/conformance/fixtures/work-item/issues.json +30 -0
  154. package/kits/knowledge/providers/conformance/suite.test.js +143 -0
  155. package/kits/knowledge/providers/git-repo/index.js +236 -0
  156. package/kits/knowledge/providers/health/health-pass.test.js +99 -0
  157. package/kits/knowledge/providers/health/index.js +153 -0
  158. package/kits/knowledge/providers/index.js +25 -0
  159. package/kits/knowledge/providers/lib/model.js +91 -0
  160. package/kits/knowledge/providers/lib/schema-validate.js +119 -0
  161. package/kits/knowledge/providers/markdown-vault/index.js +169 -0
  162. package/kits/knowledge/providers/neo4j/connection.js +121 -0
  163. package/kits/knowledge/providers/neo4j/cypher.js +190 -0
  164. package/kits/knowledge/providers/neo4j/fake-driver.js +123 -0
  165. package/kits/knowledge/providers/neo4j/fixtures/spike-ground-truth.json +29 -0
  166. package/kits/knowledge/providers/neo4j/graph-queries.js +210 -0
  167. package/kits/knowledge/providers/neo4j/index.js +280 -0
  168. package/kits/knowledge/providers/neo4j/integration.test.js +122 -0
  169. package/kits/knowledge/providers/neo4j/neo4j.test.js +219 -0
  170. package/kits/knowledge/providers/neo4j/sync.js +235 -0
  171. package/kits/knowledge/providers/work-item/index.js +204 -0
  172. package/package.json +7 -3
  173. package/schemas/assignment-provider-settings.schema.json +125 -0
  174. package/schemas/knowledge/edge.schema.json +54 -0
  175. package/schemas/knowledge/health-report.schema.json +45 -0
  176. package/schemas/knowledge/node.schema.json +49 -0
  177. package/schemas/knowledge/proposal.schema.json +53 -0
  178. package/scripts/README.md +1 -0
  179. package/scripts/ci/trust-reconcile.js +521 -24
  180. package/scripts/freeze-adrs.mjs +364 -0
  181. package/scripts/hooks/config-protection.js +14 -1
  182. package/scripts/hooks/evidence-capture.js +4 -1
  183. package/scripts/hooks/lib/config-protection-remedies.js +9 -0
  184. package/scripts/hooks/lib/current-pointer.js +123 -0
  185. package/scripts/hooks/lib/liveness-heartbeat.js +75 -35
  186. package/scripts/hooks/stop-goal-fit.js +4 -2
  187. package/scripts/hooks/workflow-steering.js +42 -0
  188. package/scripts/statusline/flow-agents-statusline.js +3 -1
  189. package/src/cli/assignment-provider.ts +927 -0
  190. package/src/cli/effective-assignment-provider-settings.ts +112 -0
  191. package/src/cli/validate-workflow-artifacts.ts +5 -1
  192. package/src/cli/workflow-sidecar.ts +577 -139
  193. package/src/cli.ts +6 -0
  194. package/src/lib/flow-resolver.ts +35 -14
  195. package/src/tools/validate-source-tree.ts +2 -0
@@ -26,6 +26,7 @@ The store holds three record types. Every record, regardless of type, shares a c
26
26
  | `body` | string | yes | Primary content. Format is type-specific (see below). |
27
27
  | `category` | string | yes | Dot-separated hierarchical category string, e.g. `"engineering.api"`. Must be non-empty. |
28
28
  | `tags` | string[] | no | Flat list of tag strings for secondary classification. |
29
+ | `aliases` | string[] | no | Human-readable, category-scoped slug aliases resolvable via `get`/`getLinks` (Addendum H). Append-only; never changes `id`. |
29
30
  | `links` | Link[] | no | Outbound links from this record (see §2). |
30
31
  | `provenance` | Provenance | yes | Immutable creation provenance (see §4). |
31
32
  | `created_at` | ISO-8601 string | yes | Creation timestamp in UTC. |
@@ -342,11 +343,14 @@ The adapter MUST implement:
342
343
 
343
344
  | Method | Signature | Description |
344
345
  |---|---|---|
345
- | `get` | `(id: string) => Record \| null` | Retrieve record by id. Returns null if not found. |
346
- | `getLinks` | `(id: string) => { forward: Link[], reverse: Link[] }` | Return all links for a record from the graph index. |
346
+ | `get` | `(idOrHandle: string) => Record \| null` | Retrieve a record. `idOrHandle` may be a full id, a slug alias, or an unambiguous id prefix (Addendum H). Returns null if it resolves to nothing; throws `AMBIGUOUS_ID` on an ambiguous prefix. |
347
+ | `getLinks` | `(idOrHandle: string) => { forward: Link[], reverse: Link[] }` | Return all links for a record from the graph index. Accepts the same handle forms as `get`; an unresolved handle yields empty arrays, an ambiguous prefix throws `AMBIGUOUS_ID`. |
347
348
  | `listByCategory` | `(category: string, options?: { prefix?: boolean }) => Record[]` | List records by exact category match. If `prefix` is true, match all records whose category starts with the given string. |
348
349
  | `listByType` | `(type: RecordType) => Record[]` | List all records of a given type. |
349
350
 
351
+ > **Identity resolution** (`get`/`getLinks`) is specified in full in **Addendum H**. The `prefix`
352
+ > option on `listByCategory` is unrelated — it filters by *category* prefix, not by id.
353
+
350
354
  ---
351
355
 
352
356
  ## 8. Adapter Contract Summary
@@ -374,6 +378,15 @@ missing. The thrown error MUST have a `code` property set to `"MISSING_EVIDENCE"
374
378
  human-readable `message`. This enables the contract suite to distinguish enforcement failures
375
379
  from unexpected errors.
376
380
 
381
+ ### 8.1 Error Codes
382
+
383
+ | `code` | Raised by | Meaning |
384
+ |---|---|---|
385
+ | `MISSING_EVIDENCE` | any mutation op; `create`/`update` when an `aliases` entry is malformed | A required evidence/provenance field is missing, or supplied input is invalid. |
386
+ | `NOT_FOUND` | `link` (unknown `target_id`) and other ops that require an existing target | A referenced record id does not exist. |
387
+ | `AMBIGUOUS_ID` | `get`, `getLinks` | An id prefix matched more than one record (Addendum H). The error carries a `matches` array of the colliding ids. MUST NOT be swallowed into a `null` return. |
388
+ | `SLUG_CONFLICT` | `create`, `update` | A supplied slug alias is already assigned to a *different* record. |
389
+
377
390
  ---
378
391
 
379
392
  ## 9. YAML Frontmatter Convention (Default Adapter)
@@ -387,6 +400,7 @@ type: <raw|compiled|concept>
387
400
  title: <title>
388
401
  category: <category>
389
402
  tags: [<tag>, ...]
403
+ aliases: [<slug>, ...] # optional — human-readable, category-scoped (Addendum H)
390
404
  created_at: <ISO-8601>
391
405
  updated_at: <ISO-8601>
392
406
  provenance:
@@ -409,8 +423,9 @@ mutation_log:
409
423
  <body text, may include [[wikilinks]]>
410
424
  ```
411
425
 
412
- Files are stored as `<store_root>/records/<id>.md`. The graph index lives at
413
- `<store_root>/graph-index.json`.
426
+ Files are stored as `<store_root>/records/<id>.md` keyed by the full `id`, unchanged by this
427
+ addendum. The graph index lives at `<store_root>/graph-index.json`; the slug alias map (Addendum H)
428
+ lives at `<store_root>/alias-index.json`.
414
429
 
415
430
  ---
416
431
 
@@ -1034,3 +1049,96 @@ interface ContradictionFlag {
1034
1049
  counts the in-scope compiled records, `compared` counts the similar pairs the contradiction fn
1035
1050
  judged, and `flags` lists the conflicting pairs. Gate telemetry is emitted at `collect-gate` and
1036
1051
  `flag-gate` (`knowledge.detect-contradictions`).
1052
+
1053
+ ## Addendum H — Record Identity Resolution (short-id prefix + slug aliases, #339)
1054
+
1055
+ ### H.1 Motivation
1056
+
1057
+ On-disk identity is a full `id` (§1.1) plus a file path (§9). Prose that cites a record — a
1058
+ curated doc, a commit message, an issue comment — cannot practically carry a full 36-char UUID, so
1059
+ the field partner cites **8-char short ids** (`12cc5573`, `f5df7b4c`) and would prefer
1060
+ **human-readable slugs**. Neither resolves against exact-id `get`, and a store restructure that
1061
+ re-files a record silently severs every inbound short-id/slug reference.
1062
+
1063
+ This addendum adds a **resolution layer** over the unchanged on-disk identity. It changes nothing
1064
+ about how records are stored (H.6) — it only widens what `get`/`getLinks` accept as input.
1065
+
1066
+ ### H.2 Resolvable Handle Forms
1067
+
1068
+ `get(handle)` and `getLinks(handle)` MUST resolve, in this precedence order:
1069
+
1070
+ 1. **Exact full id** — `handle` equals a stored record `id`. (Unchanged §7 behavior; the O(1)
1071
+ hot path.)
1072
+ 2. **Slug alias** — `handle` is a registered alias in the alias map (H.4) whose target still exists.
1073
+ 3. **Unambiguous id prefix** — `handle` is at least `MIN_ID_PREFIX` (**8**) characters and is a
1074
+ prefix of **exactly one** record `id`.
1075
+
1076
+ The first form that matches wins. A `handle` that matches none of the three resolves to *nothing*.
1077
+
1078
+ ### H.3 Resolution Outcomes
1079
+
1080
+ | Situation | `get` | `getLinks` |
1081
+ |---|---|---|
1082
+ | Resolves to exactly one record | returns that record | returns that record's `{ forward, reverse }` |
1083
+ | Resolves to nothing (no exact id, no alias, no ≥8-char single-match prefix) | returns `null` | returns `{ forward: [], reverse: [] }` |
1084
+ | Prefix (≥8 chars) matches **more than one** record | **throws `AMBIGUOUS_ID`** | **throws `AMBIGUOUS_ID`** |
1085
+
1086
+ An `AMBIGUOUS_ID` error MUST have `code === "AMBIGUOUS_ID"`, a human-readable `message`, and a
1087
+ `matches` array of the colliding full ids. An ambiguous prefix MUST NOT be silently resolved to one
1088
+ arbitrary record, nor collapsed to `null` — ambiguity is a distinct, surfaced condition.
1089
+
1090
+ A `handle` **shorter than `MIN_ID_PREFIX`** is never treated as a prefix (it can still match an
1091
+ exact id or a slug). This keeps a stray short token from matching a large, surprising set.
1092
+
1093
+ ### H.4 Slug Aliases
1094
+
1095
+ - A slug is a lowercase, category-scoped, human-readable handle, e.g.
1096
+ `decision.strategy/2026-07-03-gtm-direction`. It MUST match
1097
+ `^[a-z0-9]([a-z0-9._/-]*[a-z0-9])?$` (≤ 200 chars). Malformed slugs are rejected at
1098
+ `create`/`update` with `MISSING_EVIDENCE`.
1099
+ - Slugs are **caller-supplied** via the optional `aliases: string[]` field on `create` and
1100
+ `update`. The store does **not** auto-derive slugs (auto-slugging is a future concern) — the
1101
+ date-stamped examples cannot be derived from title alone.
1102
+ - Aliases are **append-only**: `update` with an `aliases` field UNIONs the supplied slugs with the
1103
+ record's existing aliases; it never drops a previously issued slug. This is what guarantees H.5.
1104
+ - A slug already assigned to a *different* record is a `SLUG_CONFLICT` (a slug identifies at most
1105
+ one record). Re-supplying a record's own slug is an idempotent no-op.
1106
+
1107
+ ### H.5 Restructure Survival (the alias map)
1108
+
1109
+ The alias map is a store-level structure `{ slug → id }`. It is keyed by **full id** — never by
1110
+ category or file path. Therefore:
1111
+
1112
+ - Recategorizing a record via `update` (which, in the Obsidian adapter, **relocates the note's
1113
+ file**) leaves both the `id` and the `slug → id` mapping untouched.
1114
+ - The id still resolves (it is the record's stable key); the short-id prefix still resolves (it is
1115
+ a prefix of that unchanged id); the slug still resolves (the map still points it at that id).
1116
+
1117
+ So a previously issued short-id prefix or slug MUST still resolve to the same record after a
1118
+ restructure. Note the slug is a **stable alias**, not a value recomputed from the current category:
1119
+ a slug minted under `decision.strategy/…` keeps resolving even after the record moves to
1120
+ `decision.gtm`.
1121
+
1122
+ Like the graph index (§5.2), the alias map is a **derived cache** — each record's own `aliases`
1123
+ array is the source of truth, so an adapter that persists the map SHOULD rebuild it on `reindex()`.
1124
+ The default adapter persists it at `<store_root>/alias-index.json`
1125
+ (`{ schema_version, by_slug: { <slug>: <id> } }`).
1126
+
1127
+ ### H.6 On-Disk Identity Is Unchanged (non-goals)
1128
+
1129
+ - The `id` in the §1.1 envelope and the `records/<id>.md` file naming (§9) are **unchanged**.
1130
+ Aliases are a resolution layer, not a rename.
1131
+ - Exact-id `get`/`getLinks` behavior is byte-for-byte unchanged.
1132
+ - This addendum does **not** migrate existing stores or repair already-broken citations — an
1133
+ adapter/repo may seed its own alias map for legacy short ids, but that is out of scope here.
1134
+ - Full-text/semantic search is out of scope.
1135
+
1136
+ ### H.7 Resolution Constants
1137
+
1138
+ | Constant | Value | Meaning |
1139
+ |---|---|---|
1140
+ | `MIN_ID_PREFIX` | `8` | Minimum handle length eligible for prefix resolution. |
1141
+ | slug pattern | `^[a-z0-9]([a-z0-9._/-]*[a-z0-9])?$` | Valid slug alias shape (≤ 200 chars). |
1142
+
1143
+ An independent adapter conforms to this addendum when the `identity: *` sections of the contract
1144
+ suite (`evals/contract-suite/suite.test.js`, §15–§17) pass against it.
@@ -873,4 +873,180 @@ describe("Knowledge Kit Store Contract Suite", () => {
873
873
  assert.ok(typeof record.body === "string", "body is a string");
874
874
  });
875
875
  });
876
+
877
+ // -----------------------------------------------------------------------
878
+ // §15 identity resolution — short-id prefix (AC1, R1)
879
+ //
880
+ // `get`/`getLinks` accept an unambiguous id prefix (>= 8 chars). An ambiguous
881
+ // prefix throws AMBIGUOUS_ID (a distinct code), never guesses or returns null.
882
+ // On-disk identity is unchanged (R4): records are still keyed by full id.
883
+ // -----------------------------------------------------------------------
884
+ describe("identity: short-id prefix resolution (AC1)", () => {
885
+ let dir, store;
886
+ before(() => { dir = makeTempDir(); store = makeStore(dir); });
887
+ after(() => fs.rmSync(dir, { recursive: true, force: true }));
888
+
889
+ test("get resolves a unique 8-char id prefix to the record", async () => {
890
+ const id = await store.create({
891
+ type: "concept", title: "Prefix Target", body: "b",
892
+ category: "decision.strategy", provenance: { agent: "tester" },
893
+ });
894
+ const prefix = id.slice(0, 8);
895
+ assert.equal(prefix.length, 8, "sanity: prefix is 8 chars");
896
+ const rec = await store.get(prefix);
897
+ assert.ok(rec, "8-char prefix resolves to a record");
898
+ assert.equal(rec.id, id, "prefix resolves to the correct full id");
899
+ });
900
+
901
+ test("getLinks resolves a unique 8-char id prefix", async () => {
902
+ const targetId = await store.create({
903
+ type: "concept", title: "PrefixLinkTarget", body: "t",
904
+ category: "test", provenance: { agent: "tester" },
905
+ });
906
+ const sourceId = await store.create({
907
+ type: "raw", title: "PrefixLinkSource", body: "s", category: "test",
908
+ links: [{ target_id: targetId, kind: "related" }],
909
+ provenance: { agent: "tester" },
910
+ });
911
+ const { forward } = await store.getLinks(sourceId.slice(0, 8));
912
+ assert.ok(forward.some((l) => l.target_id === targetId && l.kind === "related"),
913
+ "getLinks via an 8-char prefix returns the record's forward links");
914
+ });
915
+
916
+ test("an ambiguous prefix throws AMBIGUOUS_ID (never returns null)", async () => {
917
+ // Two records whose full ids deliberately share an 8-char prefix. create
918
+ // accepts an explicit id (§1.1: adapter MAY generate), so the collision is
919
+ // deterministic without depending on random UUIDs colliding.
920
+ await store.create({
921
+ id: "abcdef12-0000-0000-0000-000000000001",
922
+ type: "raw", title: "Ambiguous One", body: "1", category: "test",
923
+ provenance: { agent: "tester" },
924
+ });
925
+ await store.create({
926
+ id: "abcdef12-0000-0000-0000-000000000002",
927
+ type: "raw", title: "Ambiguous Two", body: "2", category: "test",
928
+ provenance: { agent: "tester" },
929
+ });
930
+ await assert.rejects(
931
+ () => store.get("abcdef12"),
932
+ (err) => {
933
+ assert.equal(err.code, "AMBIGUOUS_ID",
934
+ `expected AMBIGUOUS_ID, got code=${err.code}: ${err.message}`);
935
+ return true;
936
+ }
937
+ );
938
+ });
939
+
940
+ test("a prefix shorter than the 8-char minimum does not resolve (null)", async () => {
941
+ await store.create({
942
+ id: "deadbeef-1111-1111-1111-111111111111",
943
+ type: "raw", title: "Short Prefix Guard", body: "x", category: "test",
944
+ provenance: { agent: "tester" },
945
+ });
946
+ assert.equal(await store.get("dead"), null, "a <8-char token is never a prefix match");
947
+ });
948
+
949
+ test("exact full-id get is unchanged (R4)", async () => {
950
+ const id = await store.create({
951
+ type: "raw", title: "Exact Id", body: "e", category: "test",
952
+ provenance: { agent: "tester" },
953
+ });
954
+ const rec = await store.get(id);
955
+ assert.equal(rec.id, id, "exact-id get still returns the record");
956
+ });
957
+ });
958
+
959
+ // -----------------------------------------------------------------------
960
+ // §16 identity resolution — slug aliases (AC2, R2)
961
+ //
962
+ // Records carry human-readable, category-scoped slug aliases; `get`/`getLinks`
963
+ // resolve them through the same surface, whether set at create or via update.
964
+ // -----------------------------------------------------------------------
965
+ describe("identity: slug alias resolution (AC2)", () => {
966
+ let dir, store;
967
+ before(() => { dir = makeTempDir(); store = makeStore(dir); });
968
+ after(() => fs.rmSync(dir, { recursive: true, force: true }));
969
+
970
+ test("a record created with a slug alias resolves via get(slug) and getLinks(slug)", async () => {
971
+ const slug = "decision.strategy/2026-07-03-gtm-direction";
972
+ const targetId = await store.create({
973
+ type: "concept", title: "GTM Target", body: "t",
974
+ category: "decision.strategy", provenance: { agent: "tester" },
975
+ });
976
+ const id = await store.create({
977
+ type: "compiled", title: "GTM Direction", body: "the decision",
978
+ category: "decision.strategy", aliases: [slug],
979
+ links: [{ target_id: targetId, kind: "related" }],
980
+ provenance: { agent: "tester" },
981
+ });
982
+
983
+ const rec = await store.get(slug);
984
+ assert.ok(rec, "slug resolves to a record");
985
+ assert.equal(rec.id, id, "get(slug) returns the aliased record");
986
+
987
+ const { forward } = await store.getLinks(slug);
988
+ assert.ok(forward.some((l) => l.target_id === targetId),
989
+ "getLinks(slug) returns the aliased record's links");
990
+ });
991
+
992
+ test("a slug alias added via update resolves through the same surface", async () => {
993
+ const id = await store.create({
994
+ type: "concept", title: "Later Slug", body: "b",
995
+ category: "decision.strategy", provenance: { agent: "tester" },
996
+ });
997
+ await store.update(id, { aliases: ["decision.strategy/added-later"] }, { agent: "tester" });
998
+ const rec = await store.get("decision.strategy/added-later");
999
+ assert.ok(rec, "update-added slug resolves");
1000
+ assert.equal(rec.id, id, "get(added slug) returns the record");
1001
+ });
1002
+
1003
+ test("a malformed slug alias is rejected at create (MISSING_EVIDENCE)", () =>
1004
+ assertMissingEvidence(
1005
+ () => store.create({
1006
+ type: "raw", title: "Bad Slug", body: "b", category: "test",
1007
+ aliases: ["Not A Valid Slug!"], provenance: { agent: "tester" },
1008
+ }),
1009
+ "malformed slug alias"
1010
+ ));
1011
+ });
1012
+
1013
+ // -----------------------------------------------------------------------
1014
+ // §17 identity resolution — survives restructure (AC3, R3)
1015
+ //
1016
+ // After `update` recategorizes a record (obsidian: the file relocates folders),
1017
+ // the previously issued slug AND short-id prefix still resolve to the same
1018
+ // record — the alias map is keyed by full id, never by category or path.
1019
+ // -----------------------------------------------------------------------
1020
+ describe("identity: alias resolution survives restructure (AC3)", () => {
1021
+ let dir, store;
1022
+ before(() => { dir = makeTempDir(); store = makeStore(dir); });
1023
+ after(() => fs.rmSync(dir, { recursive: true, force: true }));
1024
+
1025
+ test("old slug and short-id prefix still resolve after a recategorize", async () => {
1026
+ const slug = "decision.strategy/2026-06-18-market-fork";
1027
+ const id = await store.create({
1028
+ type: "concept", title: "Market Fork Decision", body: "original",
1029
+ category: "decision.strategy", aliases: [slug],
1030
+ provenance: { agent: "tester" },
1031
+ });
1032
+ const prefix = id.slice(0, 8);
1033
+
1034
+ // Pre-move sanity: both handles resolve.
1035
+ assert.equal((await store.get(slug)).id, id, "slug resolves before move");
1036
+ assert.equal((await store.get(prefix)).id, id, "prefix resolves before move");
1037
+
1038
+ // Restructure: recategorize. In the obsidian adapter this relocates the
1039
+ // note from decision/strategy/ to decision/gtm/.
1040
+ await store.update(id, { category: "decision.gtm" }, { agent: "tester" });
1041
+ const moved = await store.get(id);
1042
+ assert.equal(moved.category, "decision.gtm", "category changed on disk");
1043
+
1044
+ // The whole point of the alias map: old handles survive the restructure.
1045
+ assert.equal((await store.get(slug)).id, id, "old slug survives the restructure");
1046
+ assert.equal((await store.get(prefix)).id, id, "short-id prefix survives the restructure");
1047
+
1048
+ const { reverse } = await store.getLinks(id);
1049
+ assert.ok(Array.isArray(reverse), "getLinks still answers for the moved record");
1050
+ });
1051
+ });
876
1052
  });
@@ -0,0 +1,84 @@
1
+ {
2
+ "id": "knowledge.promote",
3
+ "version": "1.0",
4
+ "description": "Knowledge Kit promote sub-flow (the codebase-facing 'flow within a flow'): ingest a delivered session's artifacts -> distill durable draft deltas (decision topics per the registry contract, CONTEXT.md vocabulary, learnings) -> link provenance (PR, merge SHA, session archive, touched topics) -> health-check the registry for contradictions (overlapping subject nouns with divergent status/content) and propose merge-repair. Invokable standalone AND composable from the Builder promote step via uses_flow. PROPOSALS-ONLY: every output lands as a draft under <session>/proposals/ for the promote step's human/agent to apply; the sub-flow never writes docs directly.",
5
+ "steps": [
6
+ { "id": "ingest", "next": "distill" },
7
+ { "id": "distill", "next": "link" },
8
+ { "id": "link", "next": "health" },
9
+ { "id": "health", "next": "done" },
10
+ { "id": "done", "next": null }
11
+ ],
12
+ "exports": [
13
+ "knowledge.promote.ingest",
14
+ "knowledge.promote.distill",
15
+ "knowledge.promote.link",
16
+ "knowledge.promote.health"
17
+ ],
18
+ "gates": {
19
+ "ingest-gate": {
20
+ "step": "ingest",
21
+ "expects": [
22
+ {
23
+ "id": "session-residue-ingested",
24
+ "kind": "trust.bundle",
25
+ "required": true,
26
+ "description": "A completed session directory has been read (read-only) and its durable residue collected: the plan / Definition Of Done, verification evidence, review critique, learning records, and delegate transcripts. Ingest never mutates the session and never reads outside the given session directory. The residue is the raw material the remaining steps distill — nothing is proposed yet.",
27
+ "bundle_claim": {
28
+ "claimType": "knowledge.promote.ingest",
29
+ "subjectType": "artifact",
30
+ "accepted_statuses": ["trusted", "accepted"]
31
+ }
32
+ }
33
+ ]
34
+ },
35
+ "distill-gate": {
36
+ "step": "distill",
37
+ "expects": [
38
+ {
39
+ "id": "draft-deltas-schema-valid",
40
+ "kind": "trust.bundle",
41
+ "required": true,
42
+ "description": "The distilled residue is rendered as DRAFT deltas that validate BEFORE they are proposed: decision-registry topic files whose frontmatter satisfies schemas/decision-record.schema.json (one file per decision SUBJECT, the subject a CONTEXT.md vocabulary noun), CONTEXT.md vocabulary additions for any new subject noun, and docs/learnings entries. At least one schema-valid draft decision delta is produced for a session that ratified a durable decision. Every draft is written ONLY under <session>/proposals/ — the sub-flow proposes, it never writes into docs/.",
43
+ "bundle_claim": {
44
+ "claimType": "knowledge.promote.distill",
45
+ "subjectType": "artifact",
46
+ "accepted_statuses": ["trusted", "accepted"]
47
+ }
48
+ }
49
+ ]
50
+ },
51
+ "link-gate": {
52
+ "step": "link",
53
+ "expects": [
54
+ {
55
+ "id": "provenance-links-attached",
56
+ "kind": "trust.bundle",
57
+ "required": true,
58
+ "description": "Every draft delta carries its provenance: the pull request, the merge commit SHA, the archived session artifact path, and the touched decision topics. Links become evidence[] refs on the draft decision topics (kinds pr / commit / session-archive / doc) so a reader can trace each proposed delta back to the delivery that produced it. Secret-shaped refs are refused.",
59
+ "bundle_claim": {
60
+ "claimType": "knowledge.promote.link",
61
+ "subjectType": "artifact",
62
+ "accepted_statuses": ["trusted", "accepted"]
63
+ }
64
+ }
65
+ ]
66
+ },
67
+ "health-gate": {
68
+ "step": "health",
69
+ "expects": [
70
+ {
71
+ "id": "contradiction-report-and-merge-proposal",
72
+ "kind": "trust.bundle",
73
+ "required": true,
74
+ "description": "The registry is health-checked over the #317 git-repo provider interface (the SAME reader every provider shares — no forked reader): the proposed draft decisions are folded into the existing registry graph and the duplicate-detection health verb flags topics with overlapping subject nouns. Where two CURRENT decisions collide over one subject with divergent content, the step emits a merge-repair PROPOSAL naming BOTH topics and a merge target. Merge-repair is a proposal, never an auto-edit; it lands under <session>/proposals/health/ for the operator to apply through the registry's supersede/merge edit.",
75
+ "bundle_claim": {
76
+ "claimType": "knowledge.promote.health",
77
+ "subjectType": "artifact",
78
+ "accepted_statuses": ["trusted", "accepted"]
79
+ }
80
+ }
81
+ ]
82
+ }
83
+ }
84
+ }
@@ -59,6 +59,11 @@
59
59
  "id": "knowledge.hygiene-review",
60
60
  "path": "flows/hygiene-review.flow.json",
61
61
  "description": "Hygiene orchestrator (#106 #5, closes the issue): a THIN, opt-in orchestrator over the four hygiene flows (audit-freshness, detect-contradictions, glossary-sync, canonicalize-category). orchestrate → review-gate. Runs each opted-in audit via its EXISTING flow-runner method + gates (reimplements no detection), folds their findings into one operator-facing review of proposed actions normalized to adopt / retire / merge, each citing its sourceFlow + the evidence its origin flow's gate already vouched. Read-only by default; forks NO propose→approve gate — the operator enacts each decision through the source flow's existing gated op (retire via knowledge.retire; adopt via capture/compile or glossary-sync apply; merge via update/recategorize). The only mutation it can trigger is glossary apply, delegated verbatim to knowledge.glossary-sync's propose→apply lineage (consume-never-fork)."
62
+ },
63
+ {
64
+ "id": "knowledge.promote",
65
+ "path": "flows/promote.flow.json",
66
+ "description": "Knowledge promote sub-flow (issue #313, the codebase-facing 'flow within a flow'): ingest a delivered session's artifacts -> distill schema-valid DRAFT decision/vocabulary/learning deltas per the decision-registry contract -> link provenance (PR, merge SHA, session archive, touched topics) -> health-check the registry for contradictions (overlapping subject nouns + divergent content) and propose merge-repair. Invokable standalone AND composable from the Builder promote step via uses_flow. PROPOSALS-ONLY: outputs land under <session>/proposals/ for the promote step to apply; the sub-flow never writes docs directly. Executable logic: promote/index.js."
62
67
  }
63
68
  ],
64
69
  "docs": [
@@ -91,6 +96,36 @@
91
96
  "id": "knowledge.obsidian-store",
92
97
  "path": "adapters/obsidian-store/index.js",
93
98
  "description": "Obsidian store adapter: each record is one human-canonical Obsidian markdown note. Frontmatter carries all contract fields; body rendered with callouts (raw), readable prose + Sources/Related wikilink sections (compiled/concept/snapshot). Category dots map to folder hierarchy; title-slugified filenames with collision suffix; superseded records move to archive/ (supersede-not-delete invariant). Backed by shared codec with default-store."
99
+ },
100
+ {
101
+ "id": "knowledge.markdown-vault-provider",
102
+ "path": "providers/markdown-vault/index.js",
103
+ "description": "Knowledge store PROVIDER (issue #317): adapts the markdown-vault store behind the typed graph model (nodes/edges/provenance). Thin read + proposals-only wrapper over an existing store adapter; existing skills unaffected."
104
+ },
105
+ {
106
+ "id": "knowledge.git-repo-provider",
107
+ "path": "providers/git-repo/index.js",
108
+ "description": "Knowledge store PROVIDER (issue #317): reads git-tracked docs (docs/decisions/* tombstones+evidence, CONTEXT.md glossary, docs/learnings/*) as decision/note nodes with supersedes/merged-into/evidence-of edges. Write side = decision-topic proposals shaped for the promote sub-flow."
109
+ },
110
+ {
111
+ "id": "knowledge.work-item-provider",
112
+ "path": "providers/work-item/index.js",
113
+ "description": "Knowledge store PROVIDER/source-sink adapter (issue #317): maps GitHub issues to issue nodes and flow-agents:work-item-metadata blockers + prose refs to blocks/relates edges via an injectable gh runner. Write side = draft comment/label proposals. Enables backlog hygiene as a knowledge-health pass."
114
+ },
115
+ {
116
+ "id": "knowledge.health-verbs",
117
+ "path": "providers/health/index.js",
118
+ "description": "Provider-agnostic knowledge-health verbs (issue #317): detectDuplicates + checkDependencyLinkIntegrity over any provider's graph, returning schema-valid health reports (schemas/knowledge/health-report.schema.json)."
119
+ },
120
+ {
121
+ "id": "knowledge.store-providers",
122
+ "path": "providers/index.js",
123
+ "description": "Public entry point for the knowledge store providers, health verbs, graph model helpers, and schema validator. See context/contracts/knowledge-store-contract.md."
124
+ },
125
+ {
126
+ "id": "knowledge.promote-runner",
127
+ "path": "promote/index.js",
128
+ "description": "Executable logic for the knowledge.promote sub-flow (issue #313): runPromote({sessionDir, repoRoot, provenance}) runs ingest -> distill -> link -> health and writes DRAFT proposals ONLY under <session>/proposals/. Builds on the #317 knowledge-store provider interface (git-repo provider as the shared registry reader; provider-agnostic duplicate-detection health verb) and validates every draft decision delta against schemas/decision-record.schema.json before proposing. Proposals-only by construction; never mutates docs/, CONTEXT.md, or any path outside the session dir."
94
129
  }
95
130
  ],
96
131
  "evals": [
@@ -153,6 +188,21 @@
153
188
  "id": "knowledge.hygiene-review-suite",
154
189
  "path": "evals/hygiene-review/suite.test.js",
155
190
  "description": "Eval cases for hygiene-review (#106 hygiene #5, closes the issue): opt-in orchestration (omitted audit block is skipped; empty review does nothing); each opted-in audit runs via its existing flow-runner method and its findings are collected verbatim (no detection reimplemented); proposals are normalized to adopt/retire/merge and each cites its sourceFlow + evidence; thin-orchestrator/consume-never-fork invariants — read-only by default mutates no record, no new gate is forked, the only mutation (glossary apply:true) is delegated to glossarySync's own gated propose→apply (verified via proposes-link + mutation-log on the resulting concept); sub-flow telemetry is folded in plus hygiene-review's own orchestrate-gate + review-gate; module-level export delegates to the runner."
191
+ },
192
+ {
193
+ "id": "knowledge.provider-conformance-suite",
194
+ "path": "providers/conformance/suite.test.js",
195
+ "description": "Conformance suite (issue #317, AC3): parameterized suite every knowledge store provider (markdown-vault, git-repo, work-item) must pass — read interface returns schema-valid nodes/edges with provenance, closed edge vocabulary, queryByType subset consistency, and proposeWrite returns a schema-valid 'proposed' proposal that mutates nothing (proposals-only)."
196
+ },
197
+ {
198
+ "id": "knowledge.provider-health-pass-suite",
199
+ "path": "providers/health/health-pass.test.js",
200
+ "description": "Health-pass suite (issue #317, AC1+AC2): the same health command over vault + git-repo providers yields schema-valid reports (AC1); a backlog pass over the work-item adapter driven by a seeded fixture flags a duplicate and a broken blocker link (AC2) — never the real board."
201
+ },
202
+ {
203
+ "id": "knowledge.promote-suite",
204
+ "path": "promote/promote.test.js",
205
+ "description": "Eval cases for the knowledge.promote sub-flow (issue #313): AC1 (a real-modeled completed session dir yields >=1 schema-valid draft decision delta with PR/merge-SHA/session-archive provenance, validated against the real #310 registry validator); AC2 (a seeded-contradiction registry fixture yields a duplicate-detection report naming BOTH topics + a schema-valid merge-repair proposal naming a merge target; a resolved supersede/merge pair is not re-flagged); AC3 (filesystem diff proves zero writes outside the session/proposal dir); R1 (the FlowDefinition exports every gate claim so a parent uses_flow step resolves it)."
156
206
  }
157
207
  ],
158
208
  "skills": [
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Step 2 — DISTILL.
3
+ *
4
+ * Turn the ingested residue into DRAFT deltas that validate BEFORE they are
5
+ * proposed (R2): decision-registry topic files (one per decision SUBJECT, the
6
+ * subject a CONTEXT.md vocabulary noun), CONTEXT.md vocabulary additions for any
7
+ * new subject noun, and docs/learnings entries. Nothing is written to disk here
8
+ * — distill returns draft objects; writing (only under <session>/proposals/)
9
+ * happens in the orchestrator.
10
+ *
11
+ * @module promote/distill
12
+ */
13
+
14
+ import { slugify, readGlossaryTerms, validateDecisionDelta } from "./lib.js";
15
+
16
+ function deriveSubject(decision) {
17
+ if (decision.subject) return decision.subject;
18
+ // Derive a rough noun-phrase subject from the outcome body (draft — the
19
+ // operator refines it at apply time). Take the leading clause, title-case.
20
+ const clause = String(decision.body || "").split(/[.;:]/)[0].trim().split(/\s+/).slice(0, 6).join(" ");
21
+ if (!clause) return "Delivered decision";
22
+ return clause.charAt(0).toUpperCase() + clause.slice(1);
23
+ }
24
+
25
+ /**
26
+ * @param {object} residue from ingestSession.
27
+ * @param {object} [options]
28
+ * @param {string} [options.repoRoot] repo whose CONTEXT.md is checked for vocabulary gaps.
29
+ * @param {string} [options.decided] ISO date to stamp on drafts (default: today).
30
+ * @returns {{ decisions: object[], vocabulary: object[], learnings: object[], warnings: string[] }}
31
+ */
32
+ export function distill(residue, options = {}) {
33
+ const decided = options.decided || new Date().toISOString().slice(0, 10);
34
+ const glossary = options.repoRoot ? readGlossaryTerms(options.repoRoot) : new Set();
35
+ const warnings = [];
36
+
37
+ const decisions = [];
38
+ const vocabulary = [];
39
+ const seenSlugs = new Set();
40
+ const seenVocab = new Set();
41
+
42
+ for (const raw of residue.decisions || []) {
43
+ const subject = deriveSubject(raw);
44
+ const slug = slugify(subject);
45
+ if (!slug || seenSlugs.has(slug)) continue;
46
+ seenSlugs.add(slug);
47
+
48
+ // Base provenance so the draft is already schema-valid at distill time
49
+ // (R2: validate BEFORE proposing). link() enriches with PR + merge SHA.
50
+ const evidence = [
51
+ { kind: "session-archive", ref: `${residue.sessionDir}/${residue.sessionMarkdown || "state.json"}` },
52
+ ];
53
+
54
+ const newVocabulary = !glossary.has(subject.toLowerCase());
55
+ const delta = {
56
+ subject,
57
+ slug,
58
+ status: "current",
59
+ decided,
60
+ body: raw.body || subject,
61
+ evidence,
62
+ derived: !!raw.derived,
63
+ newVocabulary,
64
+ };
65
+ const errors = validateDecisionDelta(delta);
66
+ if (errors.length) {
67
+ warnings.push(`decision draft '${slug}' failed pre-proposal validation: ${errors.join("; ")}`);
68
+ continue;
69
+ }
70
+ decisions.push(delta);
71
+
72
+ if (newVocabulary && !seenVocab.has(slug)) {
73
+ seenVocab.add(slug);
74
+ vocabulary.push({
75
+ term: subject,
76
+ slug,
77
+ definition: (raw.body || subject).split(/[.;]/)[0].trim(),
78
+ });
79
+ }
80
+ }
81
+
82
+ const learnings = (residue.learnings || []).map((rec, i) => ({
83
+ id: rec.id || `learn-${residue.slug}-${i + 1}`,
84
+ title: rec.summary ? rec.summary.split(/[.;]/)[0].trim() : `Learning ${i + 1}`,
85
+ body: rec.summary || "",
86
+ outcome: rec.outcome || null,
87
+ source_refs: Array.isArray(rec.source_refs) ? rec.source_refs : [],
88
+ correction: rec.correction && rec.correction.needed ? {
89
+ type: rec.correction.type || null,
90
+ recurrence_key: rec.correction.recurrence_key || null,
91
+ prevention: rec.correction.prevention || null,
92
+ } : null,
93
+ }));
94
+
95
+ return { decisions, vocabulary, learnings, warnings };
96
+ }
@@ -0,0 +1,7 @@
1
+ # Contradiction Fixture Context
2
+
3
+ ## Glossary
4
+
5
+ ### Cache Eviction Policy
6
+
7
+ How cached entries are chosen for removal under memory pressure.
@@ -0,0 +1,12 @@
1
+ ---
2
+ status: current
3
+ subject: Cache eviction policy
4
+ decided: 2026-06-10
5
+ evidence:
6
+ - kind: issue
7
+ ref: https://github.com/kontourai/flow-agents/issues/401
8
+ ---
9
+
10
+ # Cache eviction policy
11
+
12
+ Entries are evicted using a least-recently-used (LRU) policy.
@@ -0,0 +1,12 @@
1
+ ---
2
+ status: current
3
+ subject: Cache eviction policy
4
+ decided: 2026-06-25
5
+ evidence:
6
+ - kind: issue
7
+ ref: https://github.com/kontourai/flow-agents/issues/415
8
+ ---
9
+
10
+ # Cache eviction policy
11
+
12
+ Entries are evicted using a least-frequently-used (LFU) policy.
@@ -0,0 +1,11 @@
1
+ # Fixture Repo Context
2
+
3
+ ## Glossary
4
+
5
+ ### Liveness Claim
6
+
7
+ An advisory, TTL-bounded coordination claim marking who is actively holding a work item.
8
+
9
+ ### Workflow Sidecar
10
+
11
+ The writer that maintains structured workflow state artifacts.
@@ -0,0 +1,9 @@
1
+ ---
2
+ title: Decision Index
3
+ ---
4
+
5
+ # Decision Index
6
+
7
+ | Topic | Status | Decision |
8
+ | --- | --- | --- |
9
+ | [liveness-claim](./liveness-claim.md) | current | Liveness Claim |