@kontourai/flow-agents 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/.github/workflows/ci.yml +12 -0
  2. package/CHANGELOG.md +32 -0
  3. package/CONTEXT.md +67 -1
  4. package/README.md +4 -0
  5. package/build/src/cli/assignment-provider.d.ts +188 -0
  6. package/build/src/cli/assignment-provider.js +193 -51
  7. package/build/src/cli/workflow-sidecar.d.ts +16 -6
  8. package/build/src/cli/workflow-sidecar.js +492 -45
  9. package/build/src/lib/flow-resolver.d.ts +12 -6
  10. package/build/src/lib/flow-resolver.js +30 -14
  11. package/build/src/tools/validate-source-tree.js +2 -1
  12. package/context/contracts/assignment-provider-contract.md +10 -1
  13. package/context/contracts/execution-contract.md +78 -0
  14. package/context/contracts/probe-docs-write-contract.md +187 -0
  15. package/context/scripts/hooks/config-protection.js +24 -4
  16. package/context/scripts/hooks/lib/config-protection-remedies.js +9 -0
  17. package/context/scripts/hooks/stop-goal-fit.js +262 -5
  18. package/context/scripts/hooks/workflow-steering.js +42 -0
  19. package/docs/adr/0001-flow-agents-consumes-flow.md +2 -0
  20. package/docs/adr/0002-flow-kits-as-extension-unit.md +2 -0
  21. package/docs/adr/0003-flow-agents-coordinates-kits-and-adapters.md +2 -0
  22. package/docs/adr/0004-gates-expect-surface-claims.md +2 -0
  23. package/docs/adr/0005-kubernetes-inspired-resource-contracts.md +2 -0
  24. package/docs/adr/0006-typescript-first-source-policy.md +2 -0
  25. package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
  26. package/docs/adr/0007-skill-audit.md +2 -0
  27. package/docs/adr/0008-kit-operation-boundary.md +2 -0
  28. package/docs/adr/0009-canonical-hook-core-kit-boundary.md +2 -0
  29. package/docs/adr/0010-workflow-trust-state-as-hachure-bundle.md +2 -0
  30. package/docs/adr/0011-mcp-posture.md +2 -0
  31. package/docs/adr/0012-agent-coordination-as-liveness-claims.md +2 -0
  32. package/docs/adr/0013-context-lifecycle.md +2 -0
  33. package/docs/adr/0014-core-vs-domain-kit-boundary.md +2 -0
  34. package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +2 -0
  35. package/docs/adr/0016-three-hard-boundary-model.md +2 -0
  36. package/docs/adr/0017-anti-gaming-trust-security-model.md +2 -0
  37. package/docs/adr/0018-freeze-local-shell-heuristics.md +2 -0
  38. package/docs/adr/0019-kit-dependency-ownership.md +2 -0
  39. package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +2 -0
  40. package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +3 -1
  41. package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +113 -0
  42. package/docs/adr/README.md +49 -0
  43. package/docs/adr/index.md +34 -0
  44. package/docs/decisions/agent-coordination.md +20 -0
  45. package/docs/decisions/anti-gaming-trust-security.md +20 -0
  46. package/docs/decisions/context-lifecycle.md +18 -0
  47. package/docs/decisions/core-domain-kit-boundary.md +18 -0
  48. package/docs/decisions/flow-flow-agents-boundary.md +18 -0
  49. package/docs/decisions/flow-kit.md +20 -0
  50. package/docs/decisions/flow-skill-kit-tool-boundary.md +20 -0
  51. package/docs/decisions/graph-knowledge-provider.md +63 -0
  52. package/docs/decisions/hook-core-kit-boundary.md +18 -0
  53. package/docs/decisions/index.md +19 -0
  54. package/docs/decisions/kit-dependency-ownership.md +18 -0
  55. package/docs/decisions/kit-operation-boundary.md +18 -0
  56. package/docs/decisions/knowledge-promote-sub-flow.md +59 -0
  57. package/docs/decisions/kontour-resource-contract.md +18 -0
  58. package/docs/decisions/mcp-posture.md +18 -0
  59. package/docs/decisions/three-hard-boundary-model.md +18 -0
  60. package/docs/decisions/trust-reconcile.md +20 -0
  61. package/docs/decisions/typescript-source-policy.md +48 -0
  62. package/docs/decisions/workflow-enforcement.md +18 -0
  63. package/docs/decisions/workflow-trust-state.md +20 -0
  64. package/docs/fixture-ownership.md +1 -1
  65. package/docs/workflow-usage-guide.md +1 -1
  66. package/evals/ci/run-baseline.sh +6 -0
  67. package/evals/fixtures/assignment-provider/hostile-effective-state.json +11 -0
  68. package/evals/integration/test_checkpoint_signing.sh +4 -3
  69. package/evals/integration/test_current_json_per_actor.sh +516 -0
  70. package/evals/integration/test_ensure_session_ownership_guard.sh +435 -0
  71. package/evals/integration/test_gate_lockdown.sh +46 -0
  72. package/evals/integration/test_model_routing_escalation.sh +145 -0
  73. package/evals/integration/test_publish_delivery.sh +14 -6
  74. package/evals/integration/test_pull_work_liveness_preflight.sh +6 -1
  75. package/evals/integration/test_stop_hook_release.sh +552 -0
  76. package/evals/integration/test_trust_reconcile_negatives.sh +170 -0
  77. package/evals/integration/test_workflow_sidecar_writer.sh +12 -3
  78. package/evals/run.sh +10 -0
  79. package/evals/static/test_knowledge_providers.sh +13 -4
  80. package/evals/static/test_model_routing_hints.sh +107 -0
  81. package/evals/static/test_workflow_skills.sh +15 -2
  82. package/kits/builder/skills/builder-shape/SKILL.md +10 -0
  83. package/kits/builder/skills/deliver/SKILL.md +69 -11
  84. package/kits/builder/skills/design-probe/SKILL.md +47 -0
  85. package/kits/builder/skills/execute-plan/SKILL.md +13 -0
  86. package/kits/builder/skills/fix-bug/SKILL.md +17 -0
  87. package/kits/builder/skills/idea-to-backlog/SKILL.md +10 -0
  88. package/kits/builder/skills/pickup-probe/SKILL.md +4 -4
  89. package/kits/builder/skills/plan-work/SKILL.md +9 -0
  90. package/kits/builder/skills/pull-work/SKILL.md +10 -0
  91. package/kits/builder/skills/review-work/SKILL.md +11 -0
  92. package/kits/builder/skills/tdd-workflow/SKILL.md +17 -0
  93. package/kits/builder/skills/verify-work/SKILL.md +11 -0
  94. package/kits/knowledge/adapters/default-store/index.js +147 -18
  95. package/kits/knowledge/adapters/flow-runner/index.js +912 -16
  96. package/kits/knowledge/adapters/obsidian-store/index.js +97 -14
  97. package/kits/knowledge/adapters/shared/codec.js +265 -0
  98. package/kits/knowledge/docs/README.md +121 -2
  99. package/kits/knowledge/docs/store-contract.md +517 -7
  100. package/kits/knowledge/evals/audit-freshness/suite.test.js +92 -1
  101. package/kits/knowledge/evals/consolidate-incremental/suite.test.js +494 -0
  102. package/kits/knowledge/evals/consolidation/suite.test.js +1 -1
  103. package/kits/knowledge/evals/contract-suite/suite.test.js +212 -0
  104. package/kits/knowledge/evals/freshness/suite.test.js +339 -0
  105. package/kits/knowledge/evals/inbound-references/suite.test.js +351 -0
  106. package/kits/knowledge/evals/retirement/suite.test.js +1 -1
  107. package/kits/knowledge/evals/supersede-propagation/suite.test.js +384 -0
  108. package/kits/knowledge/flows/promote.flow.json +84 -0
  109. package/kits/knowledge/kit.json +15 -0
  110. package/kits/knowledge/promote/distill.js +96 -0
  111. package/kits/knowledge/promote/fixtures/contradiction-registry/CONTEXT.md +7 -0
  112. package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction-policy.md +12 -0
  113. package/kits/knowledge/promote/fixtures/contradiction-registry/docs/decisions/cache-eviction.md +12 -0
  114. package/kits/knowledge/promote/fixtures/repo/CONTEXT.md +11 -0
  115. package/kits/knowledge/promote/fixtures/repo/docs/decisions/index.md +9 -0
  116. package/kits/knowledge/promote/fixtures/repo/docs/decisions/liveness-claim.md +12 -0
  117. package/kits/knowledge/promote/fixtures/session/acceptance.json +9 -0
  118. package/kits/knowledge/promote/fixtures/session/agents/tool-planner/events.jsonl +1 -0
  119. package/kits/knowledge/promote/fixtures/session/agents/tool-verifier/events.jsonl +1 -0
  120. package/kits/knowledge/promote/fixtures/session/agents/tool-worker-1-1/events.jsonl +1 -0
  121. package/kits/knowledge/promote/fixtures/session/kontourai-flow-agents-287--deliver.md +47 -0
  122. package/kits/knowledge/promote/fixtures/session/learning.json +35 -0
  123. package/kits/knowledge/promote/fixtures/session/state.json +19 -0
  124. package/kits/knowledge/promote/health.js +137 -0
  125. package/kits/knowledge/promote/index.js +176 -0
  126. package/kits/knowledge/promote/ingest.js +103 -0
  127. package/kits/knowledge/promote/lib.js +132 -0
  128. package/kits/knowledge/promote/link.js +84 -0
  129. package/kits/knowledge/promote/promote.test.js +174 -0
  130. package/kits/knowledge/providers/conformance/suite.test.js +18 -0
  131. package/kits/knowledge/providers/index.js +1 -0
  132. package/kits/knowledge/providers/neo4j/connection.js +121 -0
  133. package/kits/knowledge/providers/neo4j/cypher.js +190 -0
  134. package/kits/knowledge/providers/neo4j/fake-driver.js +123 -0
  135. package/kits/knowledge/providers/neo4j/fixtures/spike-ground-truth.json +29 -0
  136. package/kits/knowledge/providers/neo4j/graph-queries.js +210 -0
  137. package/kits/knowledge/providers/neo4j/index.js +280 -0
  138. package/kits/knowledge/providers/neo4j/integration.test.js +122 -0
  139. package/kits/knowledge/providers/neo4j/neo4j.test.js +219 -0
  140. package/kits/knowledge/providers/neo4j/sync.js +235 -0
  141. package/package.json +4 -2
  142. package/schemas/workflow-handoff.schema.json +6 -0
  143. package/scripts/README.md +1 -0
  144. package/scripts/ci/mint-attestation.js +33 -6
  145. package/scripts/ci/trust-reconcile.js +144 -26
  146. package/scripts/freeze-adrs.mjs +364 -0
  147. package/scripts/hooks/config-protection.js +24 -4
  148. package/scripts/hooks/evidence-capture.js +4 -1
  149. package/scripts/hooks/lib/config-protection-remedies.js +9 -0
  150. package/scripts/hooks/lib/current-pointer.js +123 -0
  151. package/scripts/hooks/lib/liveness-heartbeat.js +75 -35
  152. package/scripts/hooks/stop-goal-fit.js +262 -5
  153. package/scripts/hooks/workflow-steering.js +42 -0
  154. package/scripts/statusline/flow-agents-statusline.js +3 -1
  155. package/src/cli/assignment-provider.ts +239 -59
  156. package/src/cli/workflow-sidecar.ts +529 -43
  157. package/src/lib/flow-resolver.ts +35 -14
  158. package/src/tools/validate-source-tree.ts +2 -1
@@ -0,0 +1,12 @@
1
+ ---
2
+ status: current
3
+ subject: Liveness Claim
4
+ decided: 2026-06-20
5
+ evidence:
6
+ - kind: issue
7
+ ref: https://github.com/kontourai/flow-agents/issues/166
8
+ ---
9
+
10
+ # Liveness Claim
11
+
12
+ Coordination is an advisory TTL-bounded liveness claim, not a hard lock.
@@ -0,0 +1,9 @@
1
+ {
2
+ "schema_version": "1.0",
3
+ "task_slug": "kontourai-flow-agents-287",
4
+ "status": "accepted",
5
+ "verdict": "PASS",
6
+ "criteria": [
7
+ { "id": "ac1", "status": "pass", "summary": "distinct actors per concurrent session; unset actor fails loud" }
8
+ ]
9
+ }
@@ -0,0 +1 @@
1
+ {"ts":"2026-07-02T19:55:00Z","event":"plan","summary":"actor struct + resolver plan"}
@@ -0,0 +1 @@
1
+ {"ts":"2026-07-02T22:10:00Z","event":"verify","summary":"test_actor_identity.sh green; distinct actors confirmed"}
@@ -0,0 +1 @@
1
+ {"ts":"2026-07-02T20:30:00Z","event":"implement","summary":"actor resolver + fail-closed liveness write"}
@@ -0,0 +1,47 @@
1
+ # kontourai-flow-agents-287
2
+
3
+ branch: agent/287-actor-identity
4
+ worktree: /tmp/wt-287
5
+ created: 2026-07-02T19:35:52Z
6
+ status: delivered
7
+ type: deliver
8
+ iteration: 3
9
+
10
+ ## Plan
11
+
12
+ Actor struct {runtime, session_id, host, human?} with a stable serialized id; SessionStart derives FLOW_AGENTS_ACTOR; the sidecar fails loudly on a missing/local actor for liveness writes.
13
+
14
+ ## Definition Of Done
15
+
16
+ - **User outcome:** Actor identity is a stable serialized struct so two concurrent sessions on one host never collide in the liveness stream.
17
+ - **Scope:** Workflow session artifacts and sidecars.
18
+ - **Acceptance criteria:**
19
+ - [x] Two concurrent sessions on one host produce distinct actors; liveness status shows two holders; an unset actor on a liveness write fails with remediation.
20
+ - **Durable docs target:** docs/decisions (actor identity)
21
+ - **Sandbox mode:** local-edit
22
+
23
+ ## Decisions
24
+
25
+ - **Actor identity** — Agent identity is a serialized struct {runtime, session_id, host, human?} with a stable id; the sidecar fails closed (loud) on a missing or `local` actor for any liveness write, so concurrent same-host sessions never collide.
26
+
27
+ ## Execution Progress
28
+
29
+ - [x] Actor struct + resolver implemented; SessionStart derives FLOW_AGENTS_ACTOR.
30
+ - [x] Liveness writes fail closed on unset/local actor.
31
+
32
+ ## Verification Report
33
+
34
+ Build: [PASS]
35
+
36
+ ### Acceptance Criteria
37
+ - [PASS] Two concurrent sessions produce distinct actors; unset actor fails loud - Evidence: integration eval test_actor_identity.sh.
38
+
39
+ ### Verdict: PASS
40
+
41
+ ## Goal Fit Gate
42
+
43
+ - [x] Original user goal restated and met.
44
+
45
+ ## Final Acceptance
46
+
47
+ - [x] CI/relevant checks passed.
@@ -0,0 +1,35 @@
1
+ {
2
+ "schema_version": "1.0",
3
+ "task_slug": "kontourai-flow-agents-287",
4
+ "repo": "kontourai/flow-agents",
5
+ "status": "learned",
6
+ "updated_at": "2026-07-02T22:40:06Z",
7
+ "records": [
8
+ {
9
+ "recorded_at": "2026-07-02T22:40:06Z",
10
+ "id": "learn-287-ci-eval-gap",
11
+ "outcome": "mixed",
12
+ "summary": "Integration eval suite not wired into CI; resume-roundtrip eval broken at baseline invisibly. Manifest registration is what makes agent evidence reconcilable.",
13
+ "source_refs": ["evals/run.sh", "evals/ci/run-baseline.sh"],
14
+ "correction": {
15
+ "needed": true,
16
+ "type": "tooling",
17
+ "recurrence_key": "integration-evals-not-in-ci",
18
+ "prevention": { "target": "backlog", "action": "wire integration evals into CI required lanes", "status": "completed" }
19
+ }
20
+ },
21
+ {
22
+ "recorded_at": "2026-07-02T22:40:06Z",
23
+ "id": "learn-287-env-drift",
24
+ "outcome": "mixed",
25
+ "summary": "node_modules drifted from lockfile: phantom eval failures and record-evidence hard-blocked until npm ci.",
26
+ "source_refs": ["package-lock.json"],
27
+ "correction": {
28
+ "needed": true,
29
+ "type": "process",
30
+ "recurrence_key": "node-modules-lockfile-drift",
31
+ "prevention": { "target": "docs", "action": "document npm ci before verification phases", "status": "completed" }
32
+ }
33
+ }
34
+ ]
35
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "schema_version": "1.0",
3
+ "task_slug": "kontourai-flow-agents-287",
4
+ "repo": "kontourai/flow-agents",
5
+ "status": "accepted",
6
+ "phase": "learning",
7
+ "created_at": "2026-07-02T19:50:44Z",
8
+ "updated_at": "2026-07-02T22:40:06Z",
9
+ "artifact_paths": [
10
+ "acceptance.json",
11
+ "kontourai-flow-agents-287--deliver.md",
12
+ "learning.json",
13
+ "state.json"
14
+ ],
15
+ "next_action": {
16
+ "status": "continue",
17
+ "summary": "287 delivered clean after 2 fix iterations; process learnings routed"
18
+ }
19
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Step 4 — HEALTH.
3
+ *
4
+ * Contradiction detection over the decision-registry topic files, via the #317
5
+ * knowledge-store provider interface. The git-repo provider is the SAME reader
6
+ * every provider shares (no forked reader): its graph is folded together with
7
+ * the proposed draft decisions, and the provider-agnostic duplicate-detection
8
+ * health verb flags topics whose subject nouns overlap. Where two CURRENT
9
+ * decisions collide over one subject with divergent content, the step emits a
10
+ * merge-repair PROPOSAL naming BOTH topics and a merge target.
11
+ *
12
+ * Merge-repair is a PROPOSAL, never an auto-edit. It is a schema-valid
13
+ * knowledge-store proposal (proposals-only by construction) the operator applies
14
+ * through the registry's own supersede/merge edit.
15
+ *
16
+ * @module promote/health
17
+ */
18
+
19
+ import { GitRepoProvider } from "../providers/git-repo/index.js";
20
+ import { detectDuplicates } from "../providers/health/index.js";
21
+ import { node, proposal, provenance } from "../providers/lib/model.js";
22
+ import { loadSchemas } from "../providers/lib/model.js";
23
+ import { assertValid } from "../providers/lib/schema-validate.js";
24
+
25
+ const { proposal: PROPOSAL_SCHEMA } = loadSchemas();
26
+
27
+ /** Build draft decision nodes (same id scheme as the git-repo provider) from linked deltas. */
28
+ function draftNodes(draftDecisions, agent) {
29
+ const now = new Date().toISOString();
30
+ return draftDecisions.map((d) =>
31
+ node({
32
+ id: `decision:${d.slug}`,
33
+ type: "decision",
34
+ title: d.subject,
35
+ body: d.body || "",
36
+ attributes: { registry_status: d.status || "current", draft: true },
37
+ provenance: provenance({ provider: "git-repo", source: `docs/decisions/${d.slug}.md`, locator: "draft", retrievedAt: now, agent }),
38
+ }),
39
+ );
40
+ }
41
+
42
+ function nodeStatus(n) {
43
+ return (n.attributes && n.attributes.registry_status) || "current";
44
+ }
45
+
46
+ /**
47
+ * @param {object} options
48
+ * @param {string} options.repoRoot repo whose registry is read via the git-repo provider.
49
+ * @param {object[]} [options.draftDecisions] linked draft decision deltas to fold in.
50
+ * @param {string} [options.agent]
51
+ * @param {number} [options.threshold] duplicate similarity threshold (default 0.7).
52
+ * @returns {Promise<{report:object, mergeProposals:object[]}>}
53
+ */
54
+ export async function health(options = {}) {
55
+ const { repoRoot, draftDecisions = [], agent, threshold = 0.7 } = options;
56
+ const provider = new GitRepoProvider({ repoRoot, agent });
57
+ const existing = await provider.readGraph();
58
+
59
+ const nodes = [...existing.nodes, ...draftNodes(draftDecisions, agent)];
60
+ const graph = { nodes, edges: existing.edges };
61
+
62
+ // Reuse the provider-agnostic duplicate-detection verb: overlapping subject
63
+ // nouns => a schema-valid duplicate-detection report naming BOTH topics.
64
+ const report = detectDuplicates(graph, { provider: "git-repo", threshold });
65
+
66
+ const byId = new Map(nodes.map((n) => [n.id, n]));
67
+ const mergeProposals = [];
68
+
69
+ for (const finding of report.findings) {
70
+ const [idA, idB] = finding.node_ids;
71
+ const a = byId.get(idA);
72
+ const b = byId.get(idB);
73
+ if (!a || !b || a.type !== "decision" || b.type !== "decision") continue;
74
+ // Contradiction = both current AND divergent content. A resolved pair (one
75
+ // already a merged/superseded tombstone) is not a contradiction.
76
+ const aCurrent = nodeStatus(a) === "current";
77
+ const bCurrent = nodeStatus(b) === "current";
78
+ if (!aCurrent || !bCurrent) continue;
79
+ if ((a.body || "").trim() === (b.body || "").trim()) continue;
80
+
81
+ // Merge target: keep the more-recently-decided topic; the other is folded in.
82
+ const decidedA = (a.attributes && a.attributes.decided) || "";
83
+ const decidedB = (b.attributes && b.attributes.decided) || "";
84
+ let survivor = a, absorbed = b;
85
+ if (decidedB > decidedA) { survivor = b; absorbed = a; }
86
+ else if (decidedB === decidedA && b.id < a.id) { survivor = b; absorbed = a; }
87
+
88
+ const survivorSlug = survivor.id.replace(/^decision:/, "");
89
+ const absorbedSlug = absorbed.id.replace(/^decision:/, "");
90
+
91
+ const rendered = [
92
+ "---",
93
+ "status: merged",
94
+ `subject: ${absorbed.title}`,
95
+ `decided: ${new Date().toISOString().slice(0, 10)}`,
96
+ `merged_into: ${survivorSlug}`,
97
+ "evidence:",
98
+ " - kind: doc",
99
+ ` ref: docs/decisions/${survivorSlug}.md`,
100
+ "---",
101
+ "",
102
+ `# ${absorbed.title}`,
103
+ "",
104
+ `Merged into [${survivorSlug}](./${survivorSlug}.md). This subject was carried by two`,
105
+ `current topics with divergent content; the current answer now lives in ${survivorSlug}.`,
106
+ ].join("\n");
107
+
108
+ const prop = proposal({
109
+ provider: "git-repo",
110
+ kind: "decision-topic",
111
+ target: {
112
+ merge_repair: true,
113
+ merge_into: survivorSlug,
114
+ tombstone: absorbedSlug,
115
+ topics: [survivorSlug, absorbedSlug],
116
+ path: `docs/decisions/${absorbedSlug}.md`,
117
+ },
118
+ payload: {
119
+ contradiction: {
120
+ topics: [survivor.id, absorbed.id],
121
+ subject_a: survivor.title,
122
+ subject_b: absorbed.title,
123
+ divergence: { survivor: survivor.body || "", absorbed: absorbed.body || "" },
124
+ similarity: finding.evidence && finding.evidence.similarity,
125
+ },
126
+ merge_target: survivorSlug,
127
+ },
128
+ rendered,
129
+ rationale: `Two CURRENT decision topics ('${survivor.title}' @ ${survivor.id}, '${absorbed.title}' @ ${absorbed.id}) share a subject noun with divergent content — a registry contradiction. Proposed merge target: ${survivorSlug}; fold ${absorbedSlug} in as a merged tombstone. Merge-repair is a PROPOSAL: apply it via the registry's own edit, never auto-written.`,
130
+ provenance: provenance({ provider: "git-repo", source: `docs/decisions/${absorbedSlug}.md`, locator: "merge-repair", agent }),
131
+ });
132
+ assertValid(prop, PROPOSAL_SCHEMA, `merge-repair proposal (${absorbedSlug} -> ${survivorSlug})`);
133
+ mergeProposals.push(prop);
134
+ }
135
+
136
+ return { report, mergeProposals };
137
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Knowledge promote sub-flow — orchestrator (the "flow within a flow").
3
+ *
4
+ * Runs ingest -> distill -> link -> health over a completed session directory
5
+ * and lands every output as a DRAFT under `<session>/proposals/` for the promote
6
+ * step's human/agent to apply. PROPOSALS-ONLY (R4): this module writes ONLY under
7
+ * `outDir` (default `<session>/proposals`) — never into docs/, CONTEXT.md, or any
8
+ * path outside the session directory. The Builder promote step names this sub-flow
9
+ * as its assisted path; the `promote` CLI stays the recording mechanism.
10
+ *
11
+ * FlowDefinition: kits/knowledge/flows/promote.flow.json (id `knowledge.promote`).
12
+ * See context/contracts/knowledge-store-contract.md and
13
+ * context/contracts/decision-registry-contract.md.
14
+ *
15
+ * @module promote
16
+ */
17
+
18
+ import * as fs from "node:fs";
19
+ import * as path from "node:path";
20
+ import { ingestSession } from "./ingest.js";
21
+ import { distill } from "./distill.js";
22
+ import { link } from "./link.js";
23
+ import { health } from "./health.js";
24
+
25
+ export { ingestSession } from "./ingest.js";
26
+ export { distill } from "./distill.js";
27
+ export { link } from "./link.js";
28
+ export { health } from "./health.js";
29
+ export { snapshotDir, validateDecisionDelta } from "./lib.js";
30
+
31
+ function renderVocabulary(vocabulary) {
32
+ const lines = [
33
+ "# Proposed CONTEXT.md vocabulary additions",
34
+ "",
35
+ "Add each subject noun to the `## Glossary` section of CONTEXT.md before its",
36
+ "decision topic is filed (the registry slug rule keys off CONTEXT.md vocabulary).",
37
+ "",
38
+ ];
39
+ for (const v of vocabulary) {
40
+ lines.push(`### ${v.term}`, "", v.definition || "(define this term)", "");
41
+ }
42
+ if (vocabulary.length === 0) lines.push("_No new vocabulary — every decision subject already exists in CONTEXT.md._", "");
43
+ return lines.join("\n");
44
+ }
45
+
46
+ function renderLearning(rec) {
47
+ const lines = [
48
+ "---",
49
+ `id: ${rec.id}`,
50
+ rec.outcome ? `outcome: ${rec.outcome}` : "outcome: neutral",
51
+ "---",
52
+ "",
53
+ `# ${rec.title}`,
54
+ "",
55
+ rec.body || "",
56
+ "",
57
+ ];
58
+ if (rec.source_refs && rec.source_refs.length) {
59
+ lines.push("## Source refs", "");
60
+ for (const r of rec.source_refs) lines.push(`- ${r}`);
61
+ lines.push("");
62
+ }
63
+ if (rec.correction) {
64
+ lines.push("## Correction", "", `- type: ${rec.correction.type || "n/a"}`, `- recurrence_key: ${rec.correction.recurrence_key || "n/a"}`, "");
65
+ }
66
+ return lines.join("\n");
67
+ }
68
+
69
+ function renderReadme(result) {
70
+ return [
71
+ `# Promote proposals — session ${result.slug}`,
72
+ "",
73
+ "Generated by the Knowledge Kit promote sub-flow (`knowledge.promote`).",
74
+ "Everything here is a DRAFT PROPOSAL. Nothing was written to docs/ or CONTEXT.md.",
75
+ "The promote step's human/agent applies these, then records them with the `promote` CLI.",
76
+ "",
77
+ "## Apply order",
78
+ "",
79
+ "1. `vocabulary.md` — add any new subject nouns to CONTEXT.md `## Glossary`.",
80
+ "2. `decisions/*.md` — file each as `docs/decisions/<slug>.md`, then `npm run gen:decisions-index` + `npm run check:decisions`.",
81
+ "3. `learnings/*.md` — record via the learning sink / `docs/learnings/`.",
82
+ "4. `health/` — if a contradiction report + merge-repair proposal is present, apply the merge target edit.",
83
+ "",
84
+ "## Contents",
85
+ "",
86
+ `- decisions: ${result.decisions.map((d) => d.slug).join(", ") || "(none)"}`,
87
+ `- vocabulary additions: ${result.vocabulary.map((v) => v.term).join(", ") || "(none)"}`,
88
+ `- learnings: ${result.learnings.length}`,
89
+ `- touched topics: ${result.manifest.touched_topics.join(", ") || "(none)"}`,
90
+ `- contradiction findings: ${result.health.report.summary.finding_count}`,
91
+ `- merge-repair proposals: ${result.health.mergeProposals.length}`,
92
+ "",
93
+ ].join("\n");
94
+ }
95
+
96
+ /** Write every draft ONLY under outDir. Returns the list of relative paths written. */
97
+ function writeProposals(outDir, linked, healthResult, result) {
98
+ const written = [];
99
+ const write = (rel, content) => {
100
+ const abs = path.join(outDir, rel);
101
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
102
+ fs.writeFileSync(abs, content, "utf8");
103
+ written.push(rel);
104
+ };
105
+
106
+ for (const d of linked.decisions) write(path.join("decisions", `${d.slug}.md`), d.rendered);
107
+ write("vocabulary.md", renderVocabulary(linked.vocabulary));
108
+ for (const rec of linked.learnings) write(path.join("learnings", `${rec.id}.md`), renderLearning(rec));
109
+ write("link-manifest.json", `${JSON.stringify(linked.manifest, null, 2)}\n`);
110
+ write(path.join("health", "contradiction-report.json"), `${JSON.stringify(healthResult.report, null, 2)}\n`);
111
+ if (healthResult.mergeProposals.length) {
112
+ write(path.join("health", "merge-proposals.json"), `${JSON.stringify(healthResult.mergeProposals, null, 2)}\n`);
113
+ for (const mp of healthResult.mergeProposals) {
114
+ write(path.join("health", `merge-repair-${mp.target.tombstone}.md`), mp.rendered);
115
+ }
116
+ }
117
+ write("promote-result.json", `${JSON.stringify(result, null, 2)}\n`);
118
+ write("README.md", renderReadme(result));
119
+ return written;
120
+ }
121
+
122
+ /**
123
+ * Run the promote sub-flow over a completed session directory.
124
+ *
125
+ * @param {object} opts
126
+ * @param {string} opts.sessionDir absolute path to the completed session dir.
127
+ * @param {string} opts.repoRoot repo root (registry + CONTEXT.md live here).
128
+ * @param {object} [opts.provenance] { pr, mergeSha, sessionArchivePath }.
129
+ * @param {string} [opts.outDir] proposals dir (default <sessionDir>/proposals).
130
+ * @param {string} [opts.decided] ISO date stamped on drafts.
131
+ * @param {string} [opts.agent]
132
+ * @param {boolean} [opts.write=true] write drafts to disk (still only under outDir).
133
+ * @returns {Promise<object>} the promote result.
134
+ */
135
+ export async function runPromote(opts = {}) {
136
+ const { sessionDir, repoRoot, provenance = {}, decided, agent, write = true } = opts;
137
+ if (!sessionDir) throw new Error("runPromote requires { sessionDir }");
138
+ if (!repoRoot) throw new Error("runPromote requires { repoRoot }");
139
+ const outDir = opts.outDir || path.join(sessionDir, "proposals");
140
+
141
+ const residue = ingestSession(sessionDir, { agent });
142
+ const distilled = distill(residue, { repoRoot, decided });
143
+ const linked = await link(distilled, provenance, { repoRoot, agent });
144
+ const healthResult = await health({ repoRoot, draftDecisions: linked.decisions, agent });
145
+
146
+ const result = {
147
+ schema_version: "1.0",
148
+ flow: "knowledge.promote",
149
+ slug: residue.slug,
150
+ repo: residue.repo,
151
+ out_dir: outDir,
152
+ ingested: {
153
+ session_markdown: residue.sessionMarkdown,
154
+ status: residue.status,
155
+ decision_candidates: residue.decisions.length,
156
+ learning_records: residue.learnings.length,
157
+ transcript_refs: residue.transcriptRefs,
158
+ touched_files: residue.touchedFiles,
159
+ },
160
+ decisions: linked.decisions.map((d) => ({ slug: d.slug, subject: d.subject, evidence: d.evidence, new_vocabulary: d.newVocabulary })),
161
+ vocabulary: linked.vocabulary,
162
+ learnings: linked.learnings,
163
+ manifest: linked.manifest,
164
+ health: {
165
+ report: healthResult.report,
166
+ mergeProposals: healthResult.mergeProposals,
167
+ },
168
+ warnings: distilled.warnings,
169
+ };
170
+
171
+ const written = write ? writeProposals(outDir, linked, healthResult, result) : [];
172
+ result.written = written;
173
+ return result;
174
+ }
175
+
176
+ export default runPromote;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Step 1 — INGEST.
3
+ *
4
+ * Read a completed session directory (read-only) and collect its durable
5
+ * residue: the plan / Definition Of Done, verification evidence, review
6
+ * critique, learning records, and delegate transcripts. Ingest never mutates
7
+ * the session and never reads outside `sessionDir`.
8
+ *
9
+ * @module promote/ingest
10
+ */
11
+
12
+ import * as fs from "node:fs";
13
+ import * as path from "node:path";
14
+ import { readIfExists, markdownSection, parseDecisionList } from "./lib.js";
15
+
16
+ function readJson(file) {
17
+ const raw = readIfExists(file);
18
+ if (!raw) return null;
19
+ try { return JSON.parse(raw); } catch { return null; }
20
+ }
21
+
22
+ function findSessionMarkdown(sessionDir, slug) {
23
+ // Prefer the deliver session file, then plan-work, then any <slug>--*.md.
24
+ const preferred = [`${slug}--deliver.md`, `${slug}--plan-work.md`];
25
+ for (const name of preferred) {
26
+ const p = path.join(sessionDir, name);
27
+ if (fs.existsSync(p)) return p;
28
+ }
29
+ let names = [];
30
+ try { names = fs.readdirSync(sessionDir); } catch { return null; }
31
+ const md = names.find((n) => n.endsWith(".md") && n.startsWith(`${slug}--`)) || names.find((n) => n.endsWith(".md"));
32
+ return md ? path.join(sessionDir, md) : null;
33
+ }
34
+
35
+ function collectTranscriptRefs(sessionDir) {
36
+ const agentsDir = path.join(sessionDir, "agents");
37
+ const refs = [];
38
+ let agents = [];
39
+ try { agents = fs.readdirSync(agentsDir); } catch { return refs; }
40
+ for (const agent of agents) {
41
+ const events = path.join(agentsDir, agent, "events.jsonl");
42
+ if (fs.existsSync(events)) refs.push(path.join("agents", agent, "events.jsonl"));
43
+ }
44
+ return refs.sort();
45
+ }
46
+
47
+ /**
48
+ * @param {string} sessionDir absolute path to a completed session directory.
49
+ * @param {object} [options]
50
+ * @param {string} [options.slug] override the derived slug.
51
+ * @returns residue object consumed by distill/link/health.
52
+ */
53
+ export function ingestSession(sessionDir, options = {}) {
54
+ if (!sessionDir || !fs.existsSync(sessionDir)) {
55
+ throw new Error(`ingestSession: session directory not found: ${sessionDir}`);
56
+ }
57
+ const state = readJson(path.join(sessionDir, "state.json")) || {};
58
+ const slug = options.slug || state.task_slug || path.basename(sessionDir);
59
+ const repo = state.repo || null;
60
+
61
+ const mdPath = findSessionMarkdown(sessionDir, slug);
62
+ const md = mdPath ? readIfExists(mdPath) : "";
63
+ const planSummary = markdownSection(md, "Plan");
64
+ const definitionOfDone = markdownSection(md, "Definition Of Done");
65
+ const decisionsSection = markdownSection(md, "Decisions");
66
+
67
+ const learning = readJson(path.join(sessionDir, "learning.json")) || {};
68
+ const learnings = Array.isArray(learning.records) ? learning.records : [];
69
+
70
+ const acceptance = readJson(path.join(sessionDir, "acceptance.json"));
71
+ const critique = readJson(path.join(sessionDir, "critique.json"));
72
+
73
+ // Structured decisions: explicit `## Decisions` list first, else derive one
74
+ // durable decision from the Definition Of Done outcome so a delivered session
75
+ // always yields at least one candidate delta (the operator refines the draft).
76
+ let decisions = parseDecisionList(decisionsSection);
77
+ if (decisions.length === 0 && definitionOfDone) {
78
+ const outcome = definitionOfDone.match(/User outcome:\*\*\s*(.+?)\s*$/im);
79
+ if (outcome) decisions = [{ subject: null, body: outcome[1].trim(), derived: true }];
80
+ }
81
+
82
+ const touchedFiles = new Set();
83
+ for (const rec of learnings) {
84
+ for (const ref of Array.isArray(rec.source_refs) ? rec.source_refs : []) touchedFiles.add(ref);
85
+ }
86
+
87
+ return {
88
+ sessionDir,
89
+ sessionMarkdown: mdPath ? path.basename(mdPath) : null,
90
+ slug,
91
+ repo,
92
+ status: state.status || null,
93
+ phase: state.phase || null,
94
+ planSummary,
95
+ definitionOfDone,
96
+ decisions,
97
+ learnings,
98
+ evidence: acceptance || (fs.existsSync(path.join(sessionDir, "trust.bundle")) ? { source: "trust.bundle" } : null),
99
+ critiqueSummary: critique && (critique.summary || critique.status) ? { status: critique.status, summary: critique.summary } : null,
100
+ transcriptRefs: collectTranscriptRefs(sessionDir),
101
+ touchedFiles: [...touchedFiles].sort(),
102
+ };
103
+ }