@kontourai/flow-agents 0.3.0 → 1.0.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 (62) hide show
  1. package/.github/workflows/kit-gates-demo.yml +171 -0
  2. package/.github/workflows/release-please.yml +13 -1
  3. package/AGENTS.md +8 -1
  4. package/CHANGELOG.md +53 -0
  5. package/CONTEXT.md +1 -1
  6. package/README.md +13 -2
  7. package/build/src/cli/flow-kit.js +41 -2
  8. package/build/src/flow-kit/validate.js +98 -0
  9. package/build/src/tools/validate-source-tree.js +2 -1
  10. package/context/scripts/hooks/config-protection.js +217 -15
  11. package/docs/fixture-ownership.md +1 -0
  12. package/docs/index.md +9 -1
  13. package/docs/kit-authoring-guide.md +126 -0
  14. package/docs/knowledge-kit.md +69 -0
  15. package/docs/vision.md +22 -0
  16. package/evals/fixtures/kit-conformance-levels/k0-flows-only/flows/review.flow.json +26 -0
  17. package/evals/fixtures/kit-conformance-levels/k0-flows-only/kit.json +13 -0
  18. package/evals/fixtures/kit-conformance-levels/k1-agent-extension/docs/README.md +3 -0
  19. package/evals/fixtures/kit-conformance-levels/k1-agent-extension/flows/build.flow.json +26 -0
  20. package/evals/fixtures/kit-conformance-levels/k1-agent-extension/kit.json +20 -0
  21. package/evals/fixtures/kit-conformance-levels/k2-with-evals/docs/README.md +3 -0
  22. package/evals/fixtures/kit-conformance-levels/k2-with-evals/eval-suites/contract-suite/suite.test.js +1 -0
  23. package/evals/fixtures/kit-conformance-levels/k2-with-evals/flows/synthesize.flow.json +26 -0
  24. package/evals/fixtures/kit-conformance-levels/k2-with-evals/kit.json +27 -0
  25. package/evals/fixtures/kit-conformance-levels/third-party-extension/flows/review.flow.json +26 -0
  26. package/evals/fixtures/kit-conformance-levels/third-party-extension/kit.json +19 -0
  27. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  28. package/evals/integration/test_hook_category_behaviors.sh +51 -0
  29. package/evals/integration/test_kit_conformance_levels.sh +209 -0
  30. package/evals/run.sh +2 -0
  31. package/evals/static/test_universal_bundles.sh +10 -0
  32. package/kits/catalog.json +6 -0
  33. package/kits/knowledge/adapters/default-store/index.js +95 -14
  34. package/kits/knowledge/adapters/flow-runner/entity-extractor.js +194 -0
  35. package/kits/knowledge/adapters/flow-runner/index.js +639 -0
  36. package/kits/knowledge/adapters/obsidian-store/README.md +141 -0
  37. package/kits/knowledge/adapters/obsidian-store/demo.js +181 -0
  38. package/kits/knowledge/adapters/obsidian-store/index.js +868 -0
  39. package/kits/knowledge/adapters/shared/codec.js +325 -0
  40. package/kits/knowledge/adapters/similarity-vector/index.js +284 -0
  41. package/kits/knowledge/docs/README.md +193 -0
  42. package/kits/knowledge/docs/store-contract.md +196 -0
  43. package/kits/knowledge/evals/contract-suite/suite.test.js +10 -5
  44. package/kits/knowledge/evals/entities/demo-acme.js +125 -0
  45. package/kits/knowledge/evals/entities/suite.test.js +722 -0
  46. package/kits/knowledge/evals/retirement/suite.test.js +1173 -0
  47. package/kits/knowledge/evals/similarity-vector/suite.test.js +685 -0
  48. package/kits/knowledge/evals/synthesis/suite.test.js +10 -3
  49. package/kits/knowledge/flows/retire.flow.json +77 -0
  50. package/kits/knowledge/kit.json +31 -1
  51. package/kits/release-evidence/fixtures/claims/README.md +14 -0
  52. package/kits/release-evidence/fixtures/claims/fail-rejected-release.trust.json +22 -0
  53. package/kits/release-evidence/fixtures/claims/pass-trusted-release.trust.json +22 -0
  54. package/kits/release-evidence/flows/release-evidence.flow.json +38 -0
  55. package/kits/release-evidence/kit.json +13 -0
  56. package/package.json +1 -1
  57. package/packaging/conformance/fixtures/config-protection--allow-no-verify-in-string.json +20 -0
  58. package/packaging/conformance/fixtures/config-protection--block-git-no-verify.json +23 -0
  59. package/scripts/hooks/config-protection.js +217 -15
  60. package/src/cli/flow-kit.ts +40 -2
  61. package/src/flow-kit/validate.ts +127 -0
  62. package/src/tools/validate-source-tree.ts +2 -1
@@ -0,0 +1,141 @@
1
+ # Knowledge Kit — Obsidian Store Adapter
2
+
3
+ Spike verdict: **RATIFY**
4
+
5
+ The "file is the record" thesis holds. Each Knowledge Kit record maps to exactly
6
+ one Obsidian-native markdown note. Frontmatter carries the full contract payload;
7
+ the markdown body is human-readable Obsidian rendering. The adapter passes the
8
+ full 48-test contract suite without modifications.
9
+
10
+ ---
11
+
12
+ ## The File-Is-the-Record Thesis
13
+
14
+ A Knowledge Kit record has a canonical identity (`id`), a structured payload
15
+ (type, category, provenance, links, mutation_log), and a human-facing body.
16
+ The Obsidian store places ALL of this in a single `.md` file:
17
+
18
+ - YAML frontmatter holds every contract field including `body`.
19
+ - The markdown section below `---` is rendered for Obsidian readability only —
20
+ it is decorative and not read back on load.
21
+ - The file IS the record. No separate database. No shadow index for content.
22
+
23
+ This is the core claim of the spike: **a single Obsidian note can faithfully
24
+ represent a Knowledge Kit record** with zero fidelity loss.
25
+
26
+ ---
27
+
28
+ ## File Shape
29
+
30
+ ```
31
+ ---
32
+ id: <uuid>
33
+ type: raw | compiled | concept | snapshot
34
+ title: <string>
35
+ category: eng.decisions
36
+ tags: [tag-a, tag-b]
37
+ status: active | implemented | retired
38
+ created_at: <ISO8601>
39
+ updated_at: <ISO8601>
40
+ provenance:
41
+ agent: <string>
42
+ session_id: <optional>
43
+ source_ids: [<optional uuid list>]
44
+ links:
45
+ - target_id: <uuid>
46
+ kind: related | source | proposes | supersedes | refines
47
+ label: <optional>
48
+ mutation_log:
49
+ - op: update
50
+ at: <ISO8601>
51
+ agent: <string>
52
+ evidence:
53
+ fields: [title, body]
54
+ body: "The full contract body text stored here for round-trip fidelity."
55
+ ---
56
+
57
+ <!-- Obsidian-readable section below — decorative, not parsed on load -->
58
+
59
+ > [!note]- Raw Notes ← raw type: collapsed callout
60
+ > Original capture text here.
61
+
62
+ ## Sources ← compiled/concept/snapshot: wikilinks to sources
63
+
64
+ [[source-slug|Source Note]]
65
+
66
+ ## Related
67
+
68
+ [[related-slug]]
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Storage Layout
74
+
75
+ ```
76
+ <storeRoot>/
77
+ <category/as/path>/<title-slug>.md active records
78
+ archive/<category/as/path>/<slug>.md superseded records (moved, not deleted)
79
+ graph-index.json link graph (suite §13 requirement)
80
+ .graph-index.json path index (id → {path, archived})
81
+ ```
82
+
83
+ - Category dots map to directory segments: `eng.decisions` → `eng/decisions/`.
84
+ - Title is slugified for the filename: `My Decision` → `my-decision.md`.
85
+ - Filename collisions (same slug, different id) get a suffix: `my-decision-2.md`.
86
+ - When a record is superseded, its file MOVES to `archive/` (supersede-not-delete
87
+ invariant). The record remains fully queryable via `get(id)`.
88
+
89
+ ---
90
+
91
+ ## Spike Gaps Found
92
+
93
+ 1. **Multi-file wikilink resolution**: Obsidian resolves `[[slug]]` links by
94
+ filename across the entire vault, regardless of folder. The adapter renders
95
+ wikilinks using the filename slug for human readability, but the canonical
96
+ link data (stored as UUIDs in frontmatter) is what the contract uses. If a
97
+ user clicks a wikilink in Obsidian, it navigates by slug — not by UUID —
98
+ which may not match if slugs collide across categories.
99
+
100
+ 2. **Vault-level wikilink display**: Obsidian flattens `[[slug]]` resolution.
101
+ Two records in different categories that happen to share a slug
102
+ (`eng/api/deploy.md` and `ops/api/deploy.md`) would cause Obsidian link
103
+ ambiguity. The contract itself is unaffected (UUIDs in frontmatter win),
104
+ but the human-readable `## Related` section would be ambiguous in Obsidian.
105
+ Mitigation: use category-prefixed slugs in the Obsidian body rendering.
106
+
107
+ 3. **Frontmatter `body` YAML quoting for complex content**: Bodies containing
108
+ Obsidian callout syntax (`> [!note]`) or multi-paragraph content serialize
109
+ correctly via the shared codec's `yamlScalar` quoting, but the result can be
110
+ a long single-line quoted string in frontmatter. Obsidian displays this fine,
111
+ but it is less human-editable than a literal block scalar. A future YAML block
112
+ scalar (`|`) serializer would improve ergonomics.
113
+
114
+ 4. **Obsidian sync conflicts**: If two agents write to the same record
115
+ concurrently, Obsidian Sync may create conflict copies. The adapter uses no
116
+ file locking. This is acceptable for a spike but would need attention in
117
+ production use with live sync.
118
+
119
+ 5. **Archive folder visibility**: The `archive/` subdirectory will appear in the
120
+ Obsidian vault file explorer. This is intentional (superseded records remain
121
+ inspectable) but users may want to exclude it via `.obsidianignore` or a
122
+ dedicated vault folder setting.
123
+
124
+ ---
125
+
126
+ ## Spike Verdict: RATIFY
127
+
128
+ The adapter proves:
129
+
130
+ - A single Obsidian note is a sufficient and non-lossy representation of a
131
+ Knowledge Kit record, including all Addendum A (snapshot/supersede) and
132
+ Addendum B (status/retire) contract extensions.
133
+ - The category hierarchy maps naturally to Obsidian folder organization.
134
+ - Supersede-not-delete is expressible as an archive move within the vault.
135
+ - Human readability (callouts, Sources/Related wikilink sections) coexists with
136
+ machine-readable frontmatter without duplication errors.
137
+ - The contract suite (48/48) passes without modification.
138
+
139
+ **Recommendation**: Proceed with the Obsidian store adapter as a supported adapter
140
+ alongside the default store. Address gap #2 (slug ambiguity) before marking the
141
+ adapter production-ready for multi-category vaults.
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Knowledge Kit — Obsidian Store Demo
4
+ *
5
+ * Generates a small set of realistic sample notes in a temporary vault,
6
+ * then prints the snapshot note verbatim to demonstrate the file-is-the-record
7
+ * thesis.
8
+ *
9
+ * Run:
10
+ * node kits/knowledge/adapters/obsidian-store/demo.js
11
+ */
12
+
13
+ import * as fs from "node:fs";
14
+ import * as os from "node:os";
15
+ import * as path from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
+ const adapterPath = path.join(__dirname, "index.js");
20
+ const { ObsidianKnowledgeStore } = await import(adapterPath);
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Setup: ephemeral vault
24
+ // ---------------------------------------------------------------------------
25
+
26
+ const vaultRoot = fs.mkdtempSync(path.join(os.tmpdir(), "obsidian-demo-"));
27
+ const store = new ObsidianKnowledgeStore({ storeRoot: vaultRoot });
28
+
29
+ console.log(`\nDemo vault: ${vaultRoot}\n`);
30
+ console.log("─".repeat(70));
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // 1. Raw capture: meeting transcript
34
+ // ---------------------------------------------------------------------------
35
+
36
+ const rawId = await store.create({
37
+ type: "raw",
38
+ title: "Q2 Planning Meeting — Transcript",
39
+ body: `Discussed moving deploy pipeline to GitHub Actions.
40
+ Ben: we should retire the Jenkins instance, it's costing us $300/mo.
41
+ Alice: agreed, but we need the secrets migration plan first.
42
+ Action items: Alice to draft secrets migration doc by Friday.`,
43
+ category: "eng.decisions",
44
+ tags: ["meeting", "infra", "q2"],
45
+ provenance: {
46
+ agent: "demo-agent",
47
+ session_id: "demo-sess-001",
48
+ note: "Captured from Zoom transcript",
49
+ },
50
+ });
51
+
52
+ console.log(`\n[1/4] Created raw capture: ${rawId}`);
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // 2. Compiled: distilled from the raw
56
+ // ---------------------------------------------------------------------------
57
+
58
+ const compiledId = await store.create({
59
+ type: "compiled",
60
+ title: "Jenkins Retirement Decision",
61
+ body: `Team agreed to retire the Jenkins CI instance in Q2.
62
+ Prerequisite: secrets migration plan must be completed first (owner: Alice).
63
+ Cost savings: ~$300/month after retirement.`,
64
+ category: "eng.decisions",
65
+ tags: ["infra", "ci", "cost"],
66
+ links: [{ target_id: rawId, kind: "source" }],
67
+ provenance: {
68
+ agent: "demo-agent",
69
+ source_ids: [rawId],
70
+ note: "Compiled from Q2 planning meeting transcript",
71
+ },
72
+ });
73
+
74
+ console.log(`[2/4] Created compiled note: ${compiledId}`);
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // 3. Placeholder snapshot (to be superseded)
78
+ // ---------------------------------------------------------------------------
79
+
80
+ const placeholderSnapshotId = await store.create({
81
+ type: "snapshot",
82
+ title: "CI Strategy Snapshot — Draft",
83
+ body: "Placeholder: CI strategy under discussion. No final decision yet.",
84
+ category: "eng.decisions",
85
+ tags: ["ci", "strategy", "draft"],
86
+ provenance: {
87
+ agent: "demo-agent",
88
+ note: "Placeholder snapshot pending planning meeting outcome",
89
+ },
90
+ });
91
+
92
+ console.log(`[3/4] Created placeholder snapshot: ${placeholderSnapshotId}`);
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // 4. Final snapshot that supersedes the placeholder
96
+ // ---------------------------------------------------------------------------
97
+
98
+ const finalSnapshotId = await store.create({
99
+ type: "snapshot",
100
+ title: "CI Strategy Snapshot — Q2 2026",
101
+ body: `Decision: Migrate from Jenkins to GitHub Actions in Q2 2026.
102
+
103
+ Key constraints:
104
+ - Secrets migration must complete before Jenkins decommission.
105
+ - Target: Jenkins instance retired by end of June 2026.
106
+ - Owner: Platform team (Alice lead).
107
+
108
+ Cost impact: -$300/month after retirement.`,
109
+ category: "eng.decisions",
110
+ tags: ["ci", "strategy", "q2-2026"],
111
+ links: [
112
+ { target_id: compiledId, kind: "source" },
113
+ ],
114
+ provenance: {
115
+ agent: "demo-agent",
116
+ source_ids: [compiledId],
117
+ note: "Final Q2 snapshot after planning meeting",
118
+ },
119
+ });
120
+
121
+ // Supersede the placeholder
122
+ await store.supersede(finalSnapshotId, [placeholderSnapshotId], {
123
+ agent: "demo-agent",
124
+ rationale: "Q2 planning meeting resolved the CI strategy; placeholder superseded by final snapshot.",
125
+ });
126
+
127
+ console.log(`[4/4] Created final snapshot ${finalSnapshotId} (supersedes ${placeholderSnapshotId})\n`);
128
+ console.log("─".repeat(70));
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Print the final snapshot note verbatim
132
+ // ---------------------------------------------------------------------------
133
+
134
+ // Find the file via the path index
135
+ const pathIndexRaw = fs.readFileSync(path.join(vaultRoot, ".graph-index.json"), "utf8");
136
+ const pathIndex = JSON.parse(pathIndexRaw);
137
+ const snapshotEntry = pathIndex.by_id[finalSnapshotId];
138
+ const snapshotPath = path.join(vaultRoot, snapshotEntry.path);
139
+
140
+ console.log(`\nFinal snapshot note on disk: ${snapshotPath}\n`);
141
+ console.log("═".repeat(70));
142
+ console.log(fs.readFileSync(snapshotPath, "utf8"));
143
+ console.log("═".repeat(70));
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // Show the archived placeholder
147
+ // ---------------------------------------------------------------------------
148
+
149
+ const archivedEntry = pathIndex.by_id[placeholderSnapshotId];
150
+ console.log(`\nSuperseded placeholder archived at: ${archivedEntry.path}`);
151
+ console.log(`(archived: ${archivedEntry.archived})`);
152
+
153
+ // Confirm it's still queryable
154
+ const archived = await store.get(placeholderSnapshotId);
155
+ console.log(`\nArchived record still queryable via get(): title = "${archived.title}"`);
156
+ console.log(`Mutation log has ${archived.mutation_log.length} entr${archived.mutation_log.length === 1 ? "y" : "ies"}.`);
157
+ const sbEntry = archived.mutation_log.find((e) => e.op === "superseded-by");
158
+ if (sbEntry) {
159
+ console.log(` - superseded-by: new_id = ${sbEntry.new_id}`);
160
+ }
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // Vault structure
164
+ // ---------------------------------------------------------------------------
165
+
166
+ console.log("\nVault layout:");
167
+ function printTree(dir, prefix = "") {
168
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
169
+ for (const entry of entries) {
170
+ const isLast = entry === entries[entries.length - 1];
171
+ console.log(`${prefix}${isLast ? "└── " : "├── "}${entry.name}`);
172
+ if (entry.isDirectory()) {
173
+ printTree(path.join(dir, entry.name), prefix + (isLast ? " " : "│ "));
174
+ }
175
+ }
176
+ }
177
+ printTree(vaultRoot);
178
+
179
+ // Cleanup
180
+ fs.rmSync(vaultRoot, { recursive: true, force: true });
181
+ console.log(`\nDemo vault cleaned up.\n`);