@kontourai/flow-agents 0.2.0 → 0.4.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 (53) hide show
  1. package/.github/workflows/release-please.yml +13 -1
  2. package/.github/workflows/runtime-compat.yml +1 -1
  3. package/AGENTS.md +8 -1
  4. package/CHANGELOG.md +41 -0
  5. package/README.md +38 -19
  6. package/build/src/cli/flow-kit.js +9 -4
  7. package/build/src/cli/runtime-adapter.js +9 -5
  8. package/build/src/cli/telemetry-doctor.js +4 -1
  9. package/build/src/runtime-adapters.js +34 -0
  10. package/build/src/tools/build-universal-bundles.js +18 -1
  11. package/console.telemetry.json +115 -20
  12. package/docs/_layouts/default.html +2 -0
  13. package/docs/index.md +8 -0
  14. package/docs/integrations/index.md +4 -0
  15. package/docs/integrations/knowledge-kit-live.md +211 -0
  16. package/docs/kit-authoring-guide.md +169 -0
  17. package/docs/spec/runtime-hook-surface.md +56 -3
  18. package/evals/acceptance/run.sh +10 -1
  19. package/evals/acceptance/test_knowledge_kit_live.sh +221 -0
  20. package/evals/acceptance/test_pi_harness.sh +15 -0
  21. package/evals/integration/test_runtime_adapter_activation.sh +113 -1
  22. package/evals/static/test_universal_bundles.sh +10 -0
  23. package/integrations/strands/examples/knowledge_kit_live.py +461 -0
  24. package/integrations/strands/flow_agents_strands/steering.py +54 -1
  25. package/integrations/strands/tests/test_hooks.py +88 -0
  26. package/integrations/strands-ts/src/hooks.ts +104 -0
  27. package/integrations/strands-ts/test/test-steering.ts +159 -0
  28. package/kits/catalog.json +6 -0
  29. package/kits/knowledge/adapters/default-store/index.js +902 -0
  30. package/kits/knowledge/adapters/flow-runner/index.js +1469 -0
  31. package/kits/knowledge/adapters/flow-runner/telemetry.js +174 -0
  32. package/kits/knowledge/adapters/similarity-vector/index.js +284 -0
  33. package/kits/knowledge/docs/README.md +328 -0
  34. package/kits/knowledge/docs/store-contract.md +650 -0
  35. package/kits/knowledge/evals/consolidation/suite.test.js +1234 -0
  36. package/kits/knowledge/evals/contract-suite/suite.test.js +675 -0
  37. package/kits/knowledge/evals/ingest-compile/suite.test.js +574 -0
  38. package/kits/knowledge/evals/retirement/suite.test.js +1173 -0
  39. package/kits/knowledge/evals/similarity-vector/suite.test.js +685 -0
  40. package/kits/knowledge/evals/synthesis/suite.test.js +916 -0
  41. package/kits/knowledge/flows/compile.flow.json +60 -0
  42. package/kits/knowledge/flows/consolidate.flow.json +77 -0
  43. package/kits/knowledge/flows/ingest.flow.json +60 -0
  44. package/kits/knowledge/flows/retire.flow.json +77 -0
  45. package/kits/knowledge/flows/store-contract.flow.json +48 -0
  46. package/kits/knowledge/flows/synthesize.flow.json +77 -0
  47. package/kits/knowledge/kit.json +98 -0
  48. package/package.json +1 -1
  49. package/src/cli/flow-kit.ts +10 -4
  50. package/src/cli/runtime-adapter.ts +10 -5
  51. package/src/cli/telemetry-doctor.ts +4 -1
  52. package/src/runtime-adapters.ts +35 -0
  53. package/src/tools/build-universal-bundles.ts +18 -1
@@ -0,0 +1,60 @@
1
+ {
2
+ "id": "knowledge.compile",
3
+ "version": "1.0",
4
+ "steps": [
5
+ { "id": "select-raws", "next": "compile" },
6
+ { "id": "compile", "next": "link" },
7
+ { "id": "link", "next": "done" },
8
+ { "id": "done", "next": null }
9
+ ],
10
+ "gates": {
11
+ "select-raws-gate": {
12
+ "step": "select-raws",
13
+ "expects": [
14
+ {
15
+ "id": "raw-ids-selected",
16
+ "kind": "surface.claim",
17
+ "required": true,
18
+ "description": "One or more raw record IDs have been selected for compilation. Each ID must resolve to an existing raw record in the store.",
19
+ "claim": {
20
+ "type": "knowledge.compile.selection",
21
+ "subject": "artifact",
22
+ "accepted_statuses": ["trusted", "accepted"]
23
+ }
24
+ }
25
+ ]
26
+ },
27
+ "compile-gate": {
28
+ "step": "compile",
29
+ "expects": [
30
+ {
31
+ "id": "compiled-record-provenance",
32
+ "kind": "surface.claim",
33
+ "required": true,
34
+ "description": "The compiled record has been created with provenance.source_ids listing every consumed raw ID, and source links (kind='source') from the compiled record to each raw record — satisfying the store contract's compiled record requirements.",
35
+ "claim": {
36
+ "type": "knowledge.compile.provenance",
37
+ "subject": "artifact",
38
+ "accepted_statuses": ["trusted", "accepted"]
39
+ }
40
+ }
41
+ ]
42
+ },
43
+ "link-gate": {
44
+ "step": "link",
45
+ "expects": [
46
+ {
47
+ "id": "provenance-refs-resolve",
48
+ "kind": "surface.claim",
49
+ "required": true,
50
+ "description": "Every provenance ref in the compiled record resolves to a retrievable raw record via the store's get() operation. The graph index reflects all source links.",
51
+ "claim": {
52
+ "type": "knowledge.compile.link-integrity",
53
+ "subject": "artifact",
54
+ "accepted_statuses": ["trusted", "accepted"]
55
+ }
56
+ }
57
+ ]
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,77 @@
1
+ {
2
+ "id": "knowledge.consolidate",
3
+ "version": "1.0",
4
+ "steps": [
5
+ { "id": "related-event", "next": "propose" },
6
+ { "id": "propose", "next": "evidence-gate" },
7
+ { "id": "evidence-gate", "next": "apply-or-reject" },
8
+ { "id": "apply-or-reject", "next": "done" },
9
+ { "id": "done", "next": null }
10
+ ],
11
+ "gates": {
12
+ "related-event-gate": {
13
+ "step": "related-event",
14
+ "expects": [
15
+ {
16
+ "id": "related-compiled-records-found",
17
+ "kind": "surface.claim",
18
+ "required": true,
19
+ "description": "One or more compiled records linked to the snapshot's topic have been identified. The trigger fires when new compiled records matching the snapshot topic (by category or explicit topic tag) are present. The result is a cluster of compiled record IDs that form the consolidation input.",
20
+ "claim": {
21
+ "type": "knowledge.consolidate.trigger",
22
+ "subject": "artifact",
23
+ "accepted_statuses": ["trusted", "accepted"]
24
+ }
25
+ }
26
+ ]
27
+ },
28
+ "propose-gate": {
29
+ "step": "propose",
30
+ "expects": [
31
+ {
32
+ "id": "consolidation-proposal-recorded",
33
+ "kind": "surface.claim",
34
+ "required": true,
35
+ "description": "A consolidation proposal has been recorded. The proposal carries: the updated snapshot body (latest decisions), source_ids referencing every compiled record that contributed, and supersedes_ids referencing any prior snapshot(s) for the same topic. The snapshot record is NOT mutated at this step — only a consolidation proposal record and proposes link are created.",
36
+ "claim": {
37
+ "type": "knowledge.consolidate.proposal",
38
+ "subject": "artifact",
39
+ "accepted_statuses": ["trusted", "accepted"]
40
+ }
41
+ }
42
+ ]
43
+ },
44
+ "evidence-gate": {
45
+ "step": "evidence-gate",
46
+ "expects": [
47
+ {
48
+ "id": "proposal-carries-source-refs",
49
+ "kind": "surface.claim",
50
+ "required": true,
51
+ "description": "The consolidation proposal evidence includes source_ids referencing every compiled record that contributed to the proposed snapshot body. Gate rejects if source_ids is empty or any referenced record does not exist. Also verifies that the proposer record has a 'proposes' link to the snapshot target.",
52
+ "claim": {
53
+ "type": "knowledge.consolidate.evidence",
54
+ "subject": "artifact",
55
+ "accepted_statuses": ["trusted", "accepted"]
56
+ }
57
+ }
58
+ ]
59
+ },
60
+ "apply-gate": {
61
+ "step": "apply-or-reject",
62
+ "expects": [
63
+ {
64
+ "id": "consolidation-gate-decision",
65
+ "kind": "surface.claim",
66
+ "required": true,
67
+ "description": "A gate decision (apply or reject) has been recorded. If applied: snapshot body is updated via the store update op with the proposed body; supersede op links the new snapshot to any prior snapshots for the same topic; all superseded snapshots remain queryable with provenance intact. If rejected: the snapshot body is byte-identical to its pre-proposal state.",
68
+ "claim": {
69
+ "type": "knowledge.consolidate.gate-decision",
70
+ "subject": "artifact",
71
+ "accepted_statuses": ["trusted", "accepted"]
72
+ }
73
+ }
74
+ ]
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,60 @@
1
+ {
2
+ "id": "knowledge.ingest",
3
+ "version": "1.0",
4
+ "steps": [
5
+ { "id": "capture", "next": "classify" },
6
+ { "id": "classify", "next": "route" },
7
+ { "id": "route", "next": "done" },
8
+ { "id": "done", "next": null }
9
+ ],
10
+ "gates": {
11
+ "capture-gate": {
12
+ "step": "capture",
13
+ "expects": [
14
+ {
15
+ "id": "raw-text-received",
16
+ "kind": "surface.claim",
17
+ "required": true,
18
+ "description": "Raw text and metadata have been received and are non-empty.",
19
+ "claim": {
20
+ "type": "knowledge.ingest.capture",
21
+ "subject": "artifact",
22
+ "accepted_statuses": ["trusted", "accepted"]
23
+ }
24
+ }
25
+ ]
26
+ },
27
+ "classify-gate": {
28
+ "step": "classify",
29
+ "expects": [
30
+ {
31
+ "id": "classification-recorded",
32
+ "kind": "surface.claim",
33
+ "required": true,
34
+ "description": "The raw record has been classified and stored with a non-empty category and type='raw', satisfying the store contract's create op provenance requirements.",
35
+ "claim": {
36
+ "type": "knowledge.ingest.classification",
37
+ "subject": "artifact",
38
+ "accepted_statuses": ["trusted", "accepted"]
39
+ }
40
+ }
41
+ ]
42
+ },
43
+ "route-gate": {
44
+ "step": "route",
45
+ "expects": [
46
+ {
47
+ "id": "routing-decision",
48
+ "kind": "surface.claim",
49
+ "required": true,
50
+ "description": "A routing decision has been recorded for the classified raw record (e.g. queue for compile, discard, or hold).",
51
+ "claim": {
52
+ "type": "knowledge.ingest.routing",
53
+ "subject": "decision",
54
+ "accepted_statuses": ["trusted", "accepted"]
55
+ }
56
+ }
57
+ ]
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,77 @@
1
+ {
2
+ "id": "knowledge.retire",
3
+ "version": "1.0",
4
+ "steps": [
5
+ { "id": "identify", "next": "propose-retirement" },
6
+ { "id": "propose-retirement", "next": "evidence-gate" },
7
+ { "id": "evidence-gate", "next": "apply-or-reject" },
8
+ { "id": "apply-or-reject", "next": "done" },
9
+ { "id": "done", "next": null }
10
+ ],
11
+ "gates": {
12
+ "identify-gate": {
13
+ "step": "identify",
14
+ "expects": [
15
+ {
16
+ "id": "target-record-found",
17
+ "kind": "surface.claim",
18
+ "required": true,
19
+ "description": "The record to be retired has been identified and is in a retirable status (active or implemented). The record ID and current status are surfaced for the proposal step.",
20
+ "claim": {
21
+ "type": "knowledge.retire.identify",
22
+ "subject": "artifact",
23
+ "accepted_statuses": ["trusted", "accepted"]
24
+ }
25
+ }
26
+ ]
27
+ },
28
+ "propose-retirement-gate": {
29
+ "step": "propose-retirement",
30
+ "expects": [
31
+ {
32
+ "id": "retirement-proposal-recorded",
33
+ "kind": "surface.claim",
34
+ "required": true,
35
+ "description": "A retirement proposal has been recorded. The proposal carries: the target status (implemented or retired), the retirement rationale, and — when targetStatus is 'implemented' — an implementedByRef pointing to the implementing artifact (commit SHA, PR URL, issue number, etc.). The record is NOT mutated at this step — only a retirement proposal record and proposes link are created.",
36
+ "claim": {
37
+ "type": "knowledge.retire.proposal",
38
+ "subject": "artifact",
39
+ "accepted_statuses": ["trusted", "accepted"]
40
+ }
41
+ }
42
+ ]
43
+ },
44
+ "evidence-gate": {
45
+ "step": "evidence-gate",
46
+ "expects": [
47
+ {
48
+ "id": "retirement-evidence-valid",
49
+ "kind": "surface.claim",
50
+ "required": true,
51
+ "description": "The retirement proposal evidence is valid: rationale is non-empty; implementedByRef is non-empty when targetStatus is 'implemented'; the target record exists and is in a state that allows the requested transition (active→implemented, active→retired, implemented→retired). Gate rejects if any required evidence field is missing or the transition is invalid.",
52
+ "claim": {
53
+ "type": "knowledge.retire.evidence",
54
+ "subject": "artifact",
55
+ "accepted_statuses": ["trusted", "accepted"]
56
+ }
57
+ }
58
+ ]
59
+ },
60
+ "apply-gate": {
61
+ "step": "apply-or-reject",
62
+ "expects": [
63
+ {
64
+ "id": "retirement-gate-decision",
65
+ "kind": "surface.claim",
66
+ "required": true,
67
+ "description": "A gate decision (apply or reject) has been recorded. If applied: the record status is updated to the target status via the store retire op; the retirement evidence (rationale, implementedByRef, supersededByRef) is appended to the mutation log; the record body, links, and provenance remain intact; the record is excluded from default working-set queries (listByType, listByCategory, similarity detection). If rejected: the record status is byte-identical to its pre-proposal state.",
68
+ "claim": {
69
+ "type": "knowledge.retire.gate-decision",
70
+ "subject": "artifact",
71
+ "accepted_statuses": ["trusted", "accepted"]
72
+ }
73
+ }
74
+ ]
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,48 @@
1
+ {
2
+ "id": "knowledge.store-contract",
3
+ "version": "1.0",
4
+ "steps": [
5
+ { "id": "verify-contract", "next": "done" },
6
+ { "id": "done", "next": null }
7
+ ],
8
+ "gates": {
9
+ "verify-contract-gate": {
10
+ "step": "verify-contract",
11
+ "expects": [
12
+ {
13
+ "id": "contract-suite-passed",
14
+ "kind": "surface.claim",
15
+ "required": true,
16
+ "description": "The contract test suite has passed 100% against the target adapter, verifying round-trip integrity, required-evidence enforcement, and graph-index consistency.",
17
+ "claim": {
18
+ "type": "knowledge.store-contract.suite-result",
19
+ "subject": "artifact",
20
+ "accepted_statuses": ["trusted", "accepted"]
21
+ }
22
+ },
23
+ {
24
+ "id": "mutation-provenance-enforced",
25
+ "kind": "surface.claim",
26
+ "required": true,
27
+ "description": "Every mutation operation (create, update, link, propose, apply, reject) rejects requests missing required provenance fields, as verified by the contract suite.",
28
+ "claim": {
29
+ "type": "knowledge.store-contract.provenance-enforcement",
30
+ "subject": "artifact",
31
+ "accepted_statuses": ["trusted", "accepted"]
32
+ }
33
+ },
34
+ {
35
+ "id": "round-trip-integrity",
36
+ "kind": "surface.claim",
37
+ "required": true,
38
+ "description": "Records round-trip raw to stored to queried with category and links intact, as verified by the contract suite.",
39
+ "claim": {
40
+ "type": "knowledge.store-contract.round-trip",
41
+ "subject": "artifact",
42
+ "accepted_statuses": ["trusted", "accepted"]
43
+ }
44
+ }
45
+ ]
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,77 @@
1
+ {
2
+ "id": "knowledge.synthesize",
3
+ "version": "1.0",
4
+ "steps": [
5
+ { "id": "detect-cluster", "next": "propose" },
6
+ { "id": "propose", "next": "evidence-gate" },
7
+ { "id": "evidence-gate", "next": "apply-or-reject" },
8
+ { "id": "apply-or-reject", "next": "done" },
9
+ { "id": "done", "next": null }
10
+ ],
11
+ "gates": {
12
+ "detect-cluster-gate": {
13
+ "step": "detect-cluster",
14
+ "expects": [
15
+ {
16
+ "id": "similar-sources-found",
17
+ "kind": "surface.claim",
18
+ "required": true,
19
+ "description": "At least one compiled record similar to the target concept has been identified via the similarity detector. Similarity v1 uses category match + link-overlap heuristic. The result is a cluster of source record IDs to synthesize from.",
20
+ "claim": {
21
+ "type": "knowledge.synthesize.cluster",
22
+ "subject": "artifact",
23
+ "accepted_statuses": ["trusted", "accepted"]
24
+ }
25
+ }
26
+ ]
27
+ },
28
+ "propose-gate": {
29
+ "step": "propose",
30
+ "expects": [
31
+ {
32
+ "id": "proposal-recorded",
33
+ "kind": "surface.claim",
34
+ "required": true,
35
+ "description": "A proposal carrying source refs (proposer_id + source_ids in evidence) has been recorded via the store propose op. The concept body is NOT modified at this step — only a proposes link and mutation log entry are created.",
36
+ "claim": {
37
+ "type": "knowledge.synthesize.proposal",
38
+ "subject": "artifact",
39
+ "accepted_statuses": ["trusted", "accepted"]
40
+ }
41
+ }
42
+ ]
43
+ },
44
+ "evidence-gate": {
45
+ "step": "evidence-gate",
46
+ "expects": [
47
+ {
48
+ "id": "proposal-carries-source-refs",
49
+ "kind": "surface.claim",
50
+ "required": true,
51
+ "description": "The proposal evidence includes source_ids referencing every compiled record that contributed to the proposed summary. Gate rejects if source_ids is empty or any referenced record does not exist.",
52
+ "claim": {
53
+ "type": "knowledge.synthesize.evidence",
54
+ "subject": "artifact",
55
+ "accepted_statuses": ["trusted", "accepted"]
56
+ }
57
+ }
58
+ ]
59
+ },
60
+ "apply-gate": {
61
+ "step": "apply-or-reject",
62
+ "expects": [
63
+ {
64
+ "id": "mutation-gate-decision",
65
+ "kind": "surface.claim",
66
+ "required": true,
67
+ "description": "A gate decision (apply or reject) has been recorded. If applied: concept body is updated via the store apply op with rationale and all contributing source_ids in provenance. If rejected: the store reject op is called, the concept body is byte-identical to its pre-proposal state, and only a rejection log entry is appended.",
68
+ "claim": {
69
+ "type": "knowledge.synthesize.gate-decision",
70
+ "subject": "artifact",
71
+ "accepted_statuses": ["trusted", "accepted"]
72
+ }
73
+ }
74
+ ]
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,98 @@
1
+ {
2
+ "schema_version": "1.0",
3
+ "id": "knowledge",
4
+ "name": "Knowledge Kit",
5
+ "product_name": "Knowledge Kit",
6
+ "description": "A Flow Kit for durable, gated knowledge storage. Provides a store contract with defined record types, mutation operations, and provenance rules \u2014 plus a default adapter backed by markdown files, YAML frontmatter, wikilinks, and a graph index.",
7
+ "flows": [
8
+ {
9
+ "id": "knowledge.store-contract",
10
+ "path": "flows/store-contract.flow.json",
11
+ "description": "Gate that requires contract-suite evidence before a store implementation is accepted."
12
+ },
13
+ {
14
+ "id": "knowledge.ingest",
15
+ "path": "flows/ingest.flow.json",
16
+ "description": "Ingest raw captures: capture \u2192 classify \u2192 route. Gate on classify requires category + type='raw' classification evidence per the store contract create op."
17
+ },
18
+ {
19
+ "id": "knowledge.compile",
20
+ "path": "flows/compile.flow.json",
21
+ "description": "Compile classified raws into durable notes: select-raws \u2192 compile \u2192 link. Gate on compile requires provenance refs to every consumed raw record."
22
+ },
23
+ {
24
+ "id": "knowledge.synthesize",
25
+ "path": "flows/synthesize.flow.json",
26
+ "description": "Synthesize compiled sources into concept summaries: detect-cluster \u2192 propose \u2192 evidence-gate \u2192 apply-or-reject. Mutation-gate policy: no summary mutates without an approved proposal carrying source refs; rejection leaves store untouched."
27
+ },
28
+ {
29
+ "id": "knowledge.consolidate",
30
+ "path": "flows/consolidate.flow.json",
31
+ "description": "Consolidate related compiled records into a living decision snapshot: related-event trigger -> consolidation proposal -> evidence gate -> apply-or-reject. Creates or updates a snapshot record; supersedes prior snapshots via supersede op (never deletes). Reuses S3 mutation-gate machinery."
32
+ },
33
+ {
34
+ "id": "knowledge.retire",
35
+ "path": "flows/retire.flow.json",
36
+ "description": "Retire implemented or obsolete records from the working set via gated lifecycle: identify \u2192 propose-retirement \u2192 evidence-gate \u2192 apply-or-reject. Evidence required: retirement rationale + implementedByRef (when targeting 'implemented' status) or supersededByRef (optional, for 'retired'). Rejection leaves record status byte-identical. Retired records remain fully queryable with provenance via includeRetired flag."
37
+ }
38
+ ],
39
+ "docs": [
40
+ {
41
+ "id": "knowledge.readme",
42
+ "path": "docs/README.md"
43
+ },
44
+ {
45
+ "id": "knowledge.store-contract-doc",
46
+ "path": "docs/store-contract.md"
47
+ }
48
+ ],
49
+ "adapters": [
50
+ {
51
+ "id": "knowledge.default-store",
52
+ "path": "adapters/default-store/index.js",
53
+ "description": "Default store adapter: markdown files + YAML frontmatter + wikilinks + JSON graph index."
54
+ },
55
+ {
56
+ "id": "knowledge.flow-runner",
57
+ "path": "adapters/flow-runner/index.js",
58
+ "description": "Executable flow logic: capture(rawText, meta) \u2192 classified raw record; compile(rawIds[]) \u2192 compiled record with provenance links; synthesize(conceptId | topicSelector, options) \u2192 concept summary proposal with mutation gate; consolidate(snapshotId | topicSelector, options) \u2192 decision snapshot consolidation with supersede-not-delete; retire(recordId, options) \u2192 gated status lifecycle transition (active\u2192implemented\u2192retired) with working-set exclusion. Emits canonical telemetry events at gate points."
59
+ },
60
+ {
61
+ "id": "knowledge.similarity-vector",
62
+ "path": "adapters/similarity-vector/index.js",
63
+ "description": "Vector similarity detector: createVectorSimilarityDetector(options) returns a drop-in SimilarityDetector backed by dense embeddings and cosine similarity. Supports injectable embed fn (tests) or ollama /api/embed (default). Fail-closed: throws EMBED_FAILURE on infrastructure errors rather than silently masking them as empty clusters."
64
+ }
65
+ ],
66
+ "evals": [
67
+ {
68
+ "id": "knowledge.contract-suite",
69
+ "path": "evals/contract-suite/suite.test.js",
70
+ "description": "Parameterized contract test suite for any Knowledge Kit store adapter."
71
+ },
72
+ {
73
+ "id": "knowledge.ingest-compile-suite",
74
+ "path": "evals/ingest-compile/suite.test.js",
75
+ "description": "Eval cases for ingest (AC-mapped), compile provenance gate, telemetry emission, and ref resolution."
76
+ },
77
+ {
78
+ "id": "knowledge.synthesis-suite",
79
+ "path": "evals/synthesis/suite.test.js",
80
+ "description": "Eval cases for synthesize: AC1 (similar source \u2192 proposal not mutation), AC2 (rejection leaves concept byte-identical), AC3 (apply updates with provenance to all contributing sources), pluggable-similarity interface test, gate telemetry."
81
+ },
82
+ {
83
+ "id": "knowledge.consolidation-suite",
84
+ "path": "evals/consolidation/suite.test.js",
85
+ "description": "Eval cases for consolidate: AC1 (related event -> proposal, not mutation); AC2 (apply updates exactly ONE snapshot, links supersedes refs, superseded sources still queryable); AC3 fixture (decision changed across 3 events -> snapshot reflects ONLY latest decision, provenance to all 3); supersede-not-delete invariant; gate telemetry; contract suite extensions for snapshot semantics."
86
+ },
87
+ {
88
+ "id": "knowledge.similarity-vector-suite",
89
+ "path": "evals/similarity-vector/suite.test.js",
90
+ "description": "Eval cases for the vector similarity adapter: unit tests (cosineSimilarity math, injectable embed, threshold, fail-closed), drop-in proof (runner.synthesize with injected embed produces valid proposals), and live-gated tests (real ollama nomic-embed-text round-trip \u2014 skipped when ollama unavailable)."
91
+ },
92
+ {
93
+ "id": "knowledge.retirement-suite",
94
+ "path": "evals/retirement/suite.test.js",
95
+ "description": "Eval cases for retire: AC1 (retirement only via approved proposal with rationale/ref, transition table enforcement); AC2 (retired excluded from listByType/listByCategory/similarity defaults, returned with includeRetired flag, provenance intact via get()); AC3 (consolidation after retirement reflects pruned working set; retired record still reachable from snapshot provenance history); rejection leaves status byte-identical; gate telemetry."
96
+ }
97
+ ]
98
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/flow-agents",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Flow Agents — a Kontour product that applies Flow and Veritas discipline as a portable process layer inside the agent tools you already use: Claude Code, Codex, Kiro, opencode, pi, and GitHub Actions — with framework adapters (AWS Strands preview) on the same policy-engine contract.",
5
5
  "keywords": [
6
6
  "agents",
@@ -4,7 +4,7 @@ import * as path from "node:path";
4
4
  import { parseArgs, flagBool, flagString } from "../lib/args.js";
5
5
  import { assertPathContained, copyDir, isoNow, readJson, walkFiles, writeJson } from "../lib/fs.js";
6
6
  import { assertKitRepository } from "../flow-kit/validate.js";
7
- import { activateCodexLocal } from "../runtime-adapters.js";
7
+ import { activateCodexLocal, activateStrandsLocal } from "../runtime-adapters.js";
8
8
 
9
9
  const REGISTRY_REL = path.join("kits", "local", "installed-kits.json");
10
10
  const REPOSITORIES_REL = path.join("kits", "local", "repositories");
@@ -112,16 +112,22 @@ function status(argv: string[]): number {
112
112
  return 0;
113
113
  }
114
114
 
115
+ // Available adapters for the activate subcommand (Issue #32: added strands-local).
116
+ const AVAILABLE_ADAPTERS = ["codex-local", "strands-local"];
117
+
115
118
  function activate(argv: string[]): number {
116
119
  const args = parseArgs(argv);
117
120
  const dest = path.resolve(flagString(args.flags, "dest", ".") ?? ".");
118
121
  const sourceRoot = path.resolve(flagString(args.flags, "source-root", path.resolve(path.dirname(process.argv[1]), "..")) ?? ".");
119
122
  const adapter = flagString(args.flags, "adapter");
120
- if (adapter && adapter !== "codex-local") {
121
- console.log(JSON.stringify({ selected_adapter: null, available_adapters: ["codex-local"], supported_asset_classes: [], generated_runtime_files: [], skipped_assets: [], warnings: [], errors: [`unknown runtime adapter '${adapter}'; available adapters: codex-local`] }, null, 2));
123
+ if (adapter && !AVAILABLE_ADAPTERS.includes(adapter)) {
124
+ console.log(JSON.stringify({ selected_adapter: null, available_adapters: AVAILABLE_ADAPTERS, supported_asset_classes: [], generated_runtime_files: [], skipped_assets: [], warnings: [], errors: [`unknown runtime adapter '${adapter}'; available adapters: ${AVAILABLE_ADAPTERS.join(", ")}`] }, null, 2));
122
125
  return 2;
123
126
  }
124
- const result = activateCodexLocal(sourceRoot, dest);
127
+ // Default to codex-local for backward compatibility; strands-local is opt-in via --adapter.
128
+ const result = adapter === "strands-local"
129
+ ? activateStrandsLocal(sourceRoot, dest)
130
+ : activateCodexLocal(sourceRoot, dest);
125
131
  console.log(JSON.stringify(result, null, 2));
126
132
  return Array.isArray(result.errors) && result.errors.length ? 1 : 0;
127
133
  }
@@ -1,22 +1,27 @@
1
1
  import * as path from "node:path";
2
2
  import { parseArgs, flagString } from "../lib/args.js";
3
- import { activateCodexLocal } from "../runtime-adapters.js";
3
+ import { activateCodexLocal, activateStrandsLocal } from "../runtime-adapters.js";
4
+
5
+ const AVAILABLE_ADAPTERS = ["codex-local", "strands-local"];
4
6
 
5
7
  export function main(argv = process.argv.slice(2)): number {
6
8
  const [command, ...rest] = argv;
7
9
  if (command !== "activate") {
8
- console.error("usage: runtime-adapter activate [--dest DIR] [--source-root DIR] [--adapter codex-local]");
10
+ console.error("usage: runtime-adapter activate [--dest DIR] [--source-root DIR] [--adapter codex-local|strands-local]");
9
11
  return 2;
10
12
  }
11
13
  const args = parseArgs(rest);
12
14
  const dest = path.resolve(flagString(args.flags, "dest", ".") ?? ".");
13
15
  const sourceRoot = path.resolve(flagString(args.flags, "source-root", path.resolve(path.dirname(process.argv[1]), "..")) ?? ".");
14
16
  const adapter = flagString(args.flags, "adapter");
15
- if (adapter && adapter !== "codex-local") {
16
- console.log(JSON.stringify({ selected_adapter: null, available_adapters: ["codex-local"], supported_asset_classes: [], generated_runtime_files: [], skipped_assets: [], warnings: [], errors: [`unknown runtime adapter '${adapter}'; available adapters: codex-local`] }, null, 2));
17
+ if (adapter && !AVAILABLE_ADAPTERS.includes(adapter)) {
18
+ console.log(JSON.stringify({ selected_adapter: null, available_adapters: AVAILABLE_ADAPTERS, supported_asset_classes: [], generated_runtime_files: [], skipped_assets: [], warnings: [], errors: [`unknown runtime adapter '${adapter}'; available adapters: ${AVAILABLE_ADAPTERS.join(", ")}`] }, null, 2));
17
19
  return 2;
18
20
  }
19
- const result = activateCodexLocal(sourceRoot, dest);
21
+ // Default to codex-local for backward compatibility; strands-local is opt-in via --adapter.
22
+ const result = adapter === "strands-local"
23
+ ? activateStrandsLocal(sourceRoot, dest)
24
+ : activateCodexLocal(sourceRoot, dest);
20
25
  console.log(JSON.stringify(result, null, 2));
21
26
  return Array.isArray(result.errors) && result.errors.length ? 1 : 0;
22
27
  }
@@ -82,7 +82,10 @@ function channelConfigValue(config: Config, channel: string, key: string, fallba
82
82
 
83
83
  function telemetryDataDir(dest: string): string {
84
84
  const configured = process.env.TELEMETRY_DATA_DIR;
85
- return configured ? path.resolve(dest, configured) : path.resolve(dest, "..", ".telemetry");
85
+ // Must mirror scripts/telemetry/lib/config.sh: the sink lives INSIDE the
86
+ // workspace at <dest>/.telemetry. The previous "../.telemetry" duplicated
87
+ // the parent-escape bug fixed in config.sh on 2026-06-11.
88
+ return configured ? path.resolve(dest, configured) : path.resolve(dest, ".telemetry");
86
89
  }
87
90
 
88
91
  function deriveConsoleEndpoint(consoleUrl: string, explicitEndpoint: string): string {
@@ -152,3 +152,38 @@ export function activateCodexLocal(sourceRoot: string, dest: string): Record<str
152
152
  generated.push({ asset_class: "activation-manifest", path: relPath(dest, manifestPath), kit_id: "runtime", asset_id: "codex-local.activation", source_path: manifestPath.split(path.sep).join("/") });
153
153
  return { selected_adapter: "codex-local", supported_asset_classes: ["flows"], generated_runtime_files: generated, skipped_assets: skipped, warnings: inventory.warnings, errors: inventory.errors };
154
154
  }
155
+
156
+ // Decision Q3 (Issue #32): Option (a) — new adapter id "strands-local" rather than
157
+ // loading kit flows inside FlowAgentsHooks. Rationale: activation is a workspace-prep
158
+ // concern (reads catalog + installed kits, writes runtime files, produces diagnostics).
159
+ // Keeping it in the CLI adapter layer maximises reuse of readKitInventory and safeSegment,
160
+ // mirrors the codex-local pattern exactly, and keeps framework adapters free of catalog-
161
+ // layout knowledge. The Strands steering layer then reads the written runtime files.
162
+ export function activateStrandsLocal(sourceRoot: string, dest: string): Record<string, unknown> {
163
+ const inventory = readKitInventory(sourceRoot, dest);
164
+ // Runtime flows land at .flow-agents/runtime/strands/flows/<kit-id>/<asset-id>.flow.json
165
+ // so the Strands steering context can glob for *.flow.json under this path.
166
+ const runtimeDir = path.join(dest, ".flow-agents", "runtime", "strands");
167
+ const generated: Record<string, string>[] = [];
168
+ const skipped: Record<string, string | null>[] = [];
169
+ for (const asset of inventory.assets) {
170
+ if (asset.asset_class !== "flows") {
171
+ skipped.push({ asset_class: asset.asset_class, path: asset.relative_path, kit_id: asset.kit_id, asset_id: asset.asset_id, reason: "asset class is diagnostic-only for strands-local" });
172
+ continue;
173
+ }
174
+ if (!asset.asset_id) {
175
+ skipped.push({ asset_class: asset.asset_class, path: asset.relative_path, kit_id: asset.kit_id, asset_id: null, reason: "flow asset is missing an id" });
176
+ continue;
177
+ }
178
+ const output = path.join(runtimeDir, "flows", safeSegment(asset.kit_id), `${safeSegment(asset.asset_id)}.flow.json`);
179
+ fs.mkdirSync(path.dirname(output), { recursive: true });
180
+ fs.copyFileSync(asset.source_path, output);
181
+ generated.push({ asset_class: asset.asset_class, path: relPath(dest, output), kit_id: asset.kit_id, asset_id: asset.asset_id, source_path: asset.source_path.split(path.sep).join("/") });
182
+ }
183
+ fs.mkdirSync(runtimeDir, { recursive: true });
184
+ const manifest = { schema_version: "1.0", adapter: "strands-local", supported_asset_classes: ["flows"], generated_runtime_files: generated, skipped_assets: skipped, warnings: inventory.warnings, errors: inventory.errors };
185
+ const manifestPath = path.join(runtimeDir, "activation.json");
186
+ writeJson(manifestPath, manifest);
187
+ generated.push({ asset_class: "activation-manifest", path: relPath(dest, manifestPath), kit_id: "runtime", asset_id: "strands-local.activation", source_path: manifestPath.split(path.sep).join("/") });
188
+ return { selected_adapter: "strands-local", supported_asset_classes: ["flows"], generated_runtime_files: generated, skipped_assets: skipped, warnings: inventory.warnings, errors: inventory.errors };
189
+ }