@kontextmind/kxm 0.6.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 (227) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.kxm/README.md +14 -0
  3. package/.kxm/assets/README.md +5 -0
  4. package/.kxm/assets/retrospectives/README.md +5 -0
  5. package/.kxm/config/README.md +5 -0
  6. package/.kxm/config/agents.json +43 -0
  7. package/.kxm/config/env.example +56 -0
  8. package/.kxm/config/update.example.yaml +9 -0
  9. package/.kxm/config/workflows/fix.json +160 -0
  10. package/.kxm/config/workflows/jira-development.json +116 -0
  11. package/.kxm/config/workflows/provenance-quorum.json +150 -0
  12. package/.kxm/config/workflows/v04-dogfood.json +72 -0
  13. package/CHANGELOG.md +465 -0
  14. package/LICENSE +21 -0
  15. package/README.md +306 -0
  16. package/SECURITY.md +72 -0
  17. package/docs/README.md +48 -0
  18. package/docs/agent-communication-envelopes-and-gates.md +553 -0
  19. package/docs/architecture.md +242 -0
  20. package/docs/assignment-runner.md +241 -0
  21. package/docs/configuration.md +361 -0
  22. package/docs/continuous-improvement.md +114 -0
  23. package/docs/getting-started.md +253 -0
  24. package/docs/kxm-handbook.md +1090 -0
  25. package/docs/operations.md +205 -0
  26. package/docs/provenance-gates.md +291 -0
  27. package/docs/skills.md +45 -0
  28. package/docs/templates/README.md +95 -0
  29. package/docs/templates/adr.md +88 -0
  30. package/docs/templates/architecture.md +120 -0
  31. package/docs/templates/bug-fix.md +109 -0
  32. package/docs/templates/feature.md +108 -0
  33. package/docs/templates/handoff.md +72 -0
  34. package/docs/templates/postmortem.md +77 -0
  35. package/docs/templates/research.md +100 -0
  36. package/docs/templates/review.md +85 -0
  37. package/docs/templates/runbook.md +73 -0
  38. package/docs/templates/test-plan.md +87 -0
  39. package/docs/templates/test-report.md +72 -0
  40. package/docs/test-matrix.md +121 -0
  41. package/docs/troubleshooting.md +249 -0
  42. package/docs/vnext/README.md +62 -0
  43. package/docs/vnext/architecture.md +185 -0
  44. package/docs/vnext/effects-and-recovery.md +172 -0
  45. package/docs/vnext/lifecycles.md +235 -0
  46. package/docs/vnext/migration.md +220 -0
  47. package/docs/vnext/routing.md +184 -0
  48. package/docs/vnext/synchronization.md +172 -0
  49. package/docs/vnext/terminology.md +240 -0
  50. package/docs/vnext/validation.md +335 -0
  51. package/docs/webhook-workflows.md +240 -0
  52. package/docs/workflow-guide.md +1150 -0
  53. package/examples/README.md +102 -0
  54. package/examples/provenance-workflow.json +40 -0
  55. package/examples/requester.ts +30 -0
  56. package/examples/reviewer-agent.ts +29 -0
  57. package/examples/roundtrip.ts +46 -0
  58. package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
  59. package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
  60. package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
  61. package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
  62. package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
  63. package/examples/vnext/.kxm/agents/planner.yaml +13 -0
  64. package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
  65. package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
  66. package/examples/vnext/.kxm/gates.yaml +8 -0
  67. package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
  68. package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
  69. package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
  70. package/examples/vnext/.kxm/models/implementation.yaml +14 -0
  71. package/examples/vnext/.kxm/models/primary.yaml +17 -0
  72. package/examples/vnext/.kxm/prices.yaml +111 -0
  73. package/examples/vnext/.kxm/project/env.yaml +7 -0
  74. package/examples/vnext/.kxm/project.yaml +32 -0
  75. package/examples/vnext/.kxm/repo/repo.yaml +8 -0
  76. package/examples/vnext/.kxm/workflows/default.yaml +92 -0
  77. package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
  78. package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
  79. package/examples/vnext/README.md +53 -0
  80. package/examples/vnext/records/assignment-result-recorded.json +63 -0
  81. package/examples/vnext/records/assignment-result.json +46 -0
  82. package/examples/vnext/records/context-candidate.json +42 -0
  83. package/examples/vnext/records/delivery-manifest.json +66 -0
  84. package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
  85. package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
  86. package/examples/vnext/records/run-created.json +54 -0
  87. package/examples/vnext/records/sync-event.json +65 -0
  88. package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
  89. package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
  90. package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
  91. package/examples/workflow-signal.ts +63 -0
  92. package/package.json +129 -0
  93. package/plugins/kxm/.claude-plugin/plugin.json +73 -0
  94. package/plugins/kxm/.mcp.json +19 -0
  95. package/plugins/kxm/README.md +93 -0
  96. package/plugins/kxm/dist/cli.js +42853 -0
  97. package/plugins/kxm/dist/client.js +416 -0
  98. package/plugins/kxm/dist/core.js +1823 -0
  99. package/plugins/kxm/dist/extension.js +3797 -0
  100. package/plugins/kxm/dist/mcp-server.js +17104 -0
  101. package/plugins/kxm/dist/runtime.js +23361 -0
  102. package/plugins/kxm/dist/server.js +13640 -0
  103. package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
  104. package/plugins/kxm/package.json +12 -0
  105. package/plugins/kxm/skills/kxm/SKILL.md +97 -0
  106. package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
  107. package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
  108. package/plugins/kxm/src/arbiter.ts +355 -0
  109. package/plugins/kxm/src/artifacts-exist.ts +62 -0
  110. package/plugins/kxm/src/autocomplete.ts +236 -0
  111. package/plugins/kxm/src/cli.ts +3707 -0
  112. package/plugins/kxm/src/client.ts +614 -0
  113. package/plugins/kxm/src/commands.ts +1063 -0
  114. package/plugins/kxm/src/config.ts +290 -0
  115. package/plugins/kxm/src/context/providers.ts +101 -0
  116. package/plugins/kxm/src/context-packet.ts +332 -0
  117. package/plugins/kxm/src/context.ts +499 -0
  118. package/plugins/kxm/src/core.ts +6 -0
  119. package/plugins/kxm/src/database.ts +563 -0
  120. package/plugins/kxm/src/diagnostics.ts +184 -0
  121. package/plugins/kxm/src/envelope.ts +118 -0
  122. package/plugins/kxm/src/extension.ts +895 -0
  123. package/plugins/kxm/src/external-effects.ts +299 -0
  124. package/plugins/kxm/src/github-watch.ts +255 -0
  125. package/plugins/kxm/src/hub-binding.ts +160 -0
  126. package/plugins/kxm/src/hub.ts +2502 -0
  127. package/plugins/kxm/src/improve.ts +383 -0
  128. package/plugins/kxm/src/inbox.ts +10 -0
  129. package/plugins/kxm/src/kxm-install-kind.ts +113 -0
  130. package/plugins/kxm/src/kxm-update-config.ts +39 -0
  131. package/plugins/kxm/src/kxm-update.ts +238 -0
  132. package/plugins/kxm/src/local-snapshot.ts +406 -0
  133. package/plugins/kxm/src/logger.ts +198 -0
  134. package/plugins/kxm/src/mcp-server.ts +143 -0
  135. package/plugins/kxm/src/memory.ts +385 -0
  136. package/plugins/kxm/src/nous-pi.ts +287 -0
  137. package/plugins/kxm/src/nous-provider.ts +729 -0
  138. package/plugins/kxm/src/price-calc.ts +87 -0
  139. package/plugins/kxm/src/prices.ts +121 -0
  140. package/plugins/kxm/src/protocol.ts +172 -0
  141. package/plugins/kxm/src/recovery.ts +211 -0
  142. package/plugins/kxm/src/redact.ts +26 -0
  143. package/plugins/kxm/src/retrospective.ts +400 -0
  144. package/plugins/kxm/src/routing.ts +830 -0
  145. package/plugins/kxm/src/runtime.ts +9 -0
  146. package/plugins/kxm/src/server.ts +117 -0
  147. package/plugins/kxm/src/session-work.ts +571 -0
  148. package/plugins/kxm/src/session.ts +184 -0
  149. package/plugins/kxm/src/skills.ts +535 -0
  150. package/plugins/kxm/src/state.ts +326 -0
  151. package/plugins/kxm/src/store.ts +637 -0
  152. package/plugins/kxm/src/studio-layout.ts +268 -0
  153. package/plugins/kxm/src/suggest.ts +162 -0
  154. package/plugins/kxm/src/task-manager.ts +244 -0
  155. package/plugins/kxm/src/telemetry.ts +116 -0
  156. package/plugins/kxm/src/tui.ts +1046 -0
  157. package/plugins/kxm/src/vnext-bindings.ts +403 -0
  158. package/plugins/kxm/src/vnext-config.ts +1646 -0
  159. package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
  160. package/plugins/kxm/src/vnext-engine-command.ts +533 -0
  161. package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
  162. package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
  163. package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
  164. package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
  165. package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
  166. package/plugins/kxm/src/vnext-engine.ts +2458 -0
  167. package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
  168. package/plugins/kxm/src/vnext-harness.ts +1142 -0
  169. package/plugins/kxm/src/vnext-init.ts +430 -0
  170. package/plugins/kxm/src/vnext-migrate.ts +1848 -0
  171. package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
  172. package/plugins/kxm/src/vnext-permission.ts +936 -0
  173. package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
  174. package/plugins/kxm/src/vnext-repair.ts +1094 -0
  175. package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
  176. package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
  177. package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
  178. package/plugins/kxm/src/vnext-runtime.ts +663 -0
  179. package/plugins/kxm/src/vnext-template.ts +247 -0
  180. package/plugins/kxm/src/wiki.ts +313 -0
  181. package/plugins/kxm/src/workflow.ts +1548 -0
  182. package/schemas/vnext/README.md +46 -0
  183. package/schemas/vnext/agent.schema.json +40 -0
  184. package/schemas/vnext/assignment-result.schema.json +66 -0
  185. package/schemas/vnext/backup-manifest.schema.json +89 -0
  186. package/schemas/vnext/candidate.schema.json +109 -0
  187. package/schemas/vnext/common.schema.json +422 -0
  188. package/schemas/vnext/context-candidate.schema.json +76 -0
  189. package/schemas/vnext/context-packet.schema.json +192 -0
  190. package/schemas/vnext/delivery-manifest.schema.json +159 -0
  191. package/schemas/vnext/environment.schema.json +66 -0
  192. package/schemas/vnext/gate-registry.schema.json +109 -0
  193. package/schemas/vnext/handoff-manifest.schema.json +146 -0
  194. package/schemas/vnext/init-operation.schema.json +61 -0
  195. package/schemas/vnext/local-repository-bindings.schema.json +30 -0
  196. package/schemas/vnext/memory-record.schema.json +45 -0
  197. package/schemas/vnext/migration-decision.schema.json +26 -0
  198. package/schemas/vnext/migration-plan.schema.json +123 -0
  199. package/schemas/vnext/migration-receipt.schema.json +52 -0
  200. package/schemas/vnext/model.schema.json +42 -0
  201. package/schemas/vnext/permission-diff.schema.json +57 -0
  202. package/schemas/vnext/prices.schema.json +115 -0
  203. package/schemas/vnext/project.schema.json +85 -0
  204. package/schemas/vnext/repository.schema.json +24 -0
  205. package/schemas/vnext/run-event.schema.json +460 -0
  206. package/schemas/vnext/session-brief.schema.json +153 -0
  207. package/schemas/vnext/sync-event.schema.json +234 -0
  208. package/schemas/vnext/template-provenance.schema.json +38 -0
  209. package/schemas/vnext/workflow.schema.json +248 -0
  210. package/scripts/assignment-run.d.mts +354 -0
  211. package/scripts/assignment-run.mjs +4451 -0
  212. package/scripts/build-runtime.mjs +56 -0
  213. package/scripts/check-generated.mjs +77 -0
  214. package/scripts/check-versions.mjs +34 -0
  215. package/scripts/emit-codex-artifacts.d.mts +9 -0
  216. package/scripts/emit-codex-artifacts.mjs +91 -0
  217. package/scripts/harness-run.d.mts +83 -0
  218. package/scripts/harness-run.mjs +2095 -0
  219. package/scripts/kxm-hub.mjs +105 -0
  220. package/scripts/kxm-publish-npm.mjs +327 -0
  221. package/scripts/kxm-release-github.mjs +472 -0
  222. package/scripts/kxm-runtime-supervisor.mjs +7 -0
  223. package/scripts/kxm-worker.mjs +1127 -0
  224. package/scripts/kxm.mjs +27 -0
  225. package/scripts/roster-policy.d.mts +20 -0
  226. package/scripts/roster-policy.mjs +161 -0
  227. package/scripts/smoke-multi-pi.mjs +479 -0
@@ -0,0 +1,400 @@
1
+ import { mkdirSync, renameSync, writeFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { redactSecrets, redactStringList } from "./redact.ts";
4
+ import {
5
+ canonicalWorkflowEvidenceKey,
6
+ journalPromotionState,
7
+ normalizeVerifiedWorkflowEvidence,
8
+ type WorkflowJournalEntry,
9
+ type WorkflowRun,
10
+ } from "./workflow.ts";
11
+
12
+ export interface RetrospectiveEvidenceAuditV1 {
13
+ stageId: string;
14
+ requirementKey: string;
15
+ attempt: number;
16
+ policy: {
17
+ kind: "peer-reply";
18
+ minProducers: number;
19
+ effectiveMinProducers: number;
20
+ acceptedStatuses: ["replied"];
21
+ };
22
+ eligibleProducers: Array<{ id: string; name: string }>;
23
+ verifiedProducerIds: string[];
24
+ verifiedMessages: Array<{
25
+ schema: "pi-mesh.verified-peer-evidence.v1";
26
+ messageId: string;
27
+ producerId: string;
28
+ producerName: string;
29
+ context: {
30
+ schema: "pi-mesh.workflow-message-context.v1";
31
+ runId: string;
32
+ stageId: string;
33
+ requirementKey: string;
34
+ attempt: number;
35
+ };
36
+ status: "replied";
37
+ hashes: {
38
+ requestSha256: string;
39
+ replySha256: string;
40
+ };
41
+ timestamps: {
42
+ createdAt: string;
43
+ replyCreatedAt: string;
44
+ repliedAt: string;
45
+ verifiedAt: string;
46
+ };
47
+ }>;
48
+ degraded: boolean;
49
+ degradationApprovals: Array<{
50
+ schema: "pi-mesh.workflow-degradation-approval.v1";
51
+ id: string;
52
+ requirementKey: string;
53
+ attempt: number;
54
+ policyMinProducers: number;
55
+ approvedMinProducers: number;
56
+ approvedBy: "kxm-admin";
57
+ reason: string;
58
+ approvedAt: string;
59
+ }>;
60
+ }
61
+
62
+ export interface RetrospectiveV1 {
63
+ schema: "pi-mesh.retrospective.v1";
64
+ runId: string;
65
+ definitionId: string;
66
+ status: WorkflowRun["status"];
67
+ exportedAt: string;
68
+ reviewDecision: "proposed";
69
+ stages: Array<{
70
+ id: string;
71
+ area?: string;
72
+ status: string;
73
+ attempts: number;
74
+ startedAt?: string;
75
+ completedAt?: string;
76
+ durationMs?: number;
77
+ updatedAt?: string;
78
+ summary?: string;
79
+ }>;
80
+ counts: {
81
+ byCategory: Record<string, number>;
82
+ byArea: Record<string, number>;
83
+ byClass: Record<string, number>;
84
+ };
85
+ openContradictions: Array<{ id: string; summary: string; area: string }>;
86
+ decisions: Array<{ id: string; summary: string; area: string }>;
87
+ recurringErrorClasses: Array<{ class: string; count: number }>;
88
+ entries: Array<{
89
+ id: string;
90
+ category: string;
91
+ area: string;
92
+ severity: string;
93
+ summary: string;
94
+ evidence: string[];
95
+ relatedEntryIds: string[];
96
+ createdAt: string;
97
+ stageId?: string;
98
+ attempt?: number;
99
+ /** Governed promotion lifecycle state for promotable categories.
100
+ * Absent on v0.4 records and non-promotable categories. */
101
+ promotionState?: string;
102
+ }>;
103
+ proposedImprovements: Array<{ area: string; summary: string; successMeasure: string; status: "proposed" }>;
104
+ /** Additive metadata-only provenance audit. Optional for v1 consumers and
105
+ * absent from runs created before peer-evidence policies existed. */
106
+ evidenceAudit?: RetrospectiveEvidenceAuditV1[];
107
+ /** Stage IDs that completed through an explicit, attempt-bound admin
108
+ * degradation approval. */
109
+ degradedStageIds?: string[];
110
+ }
111
+
112
+ const MAX_RETROSPECTIVE_ENTRIES = 500;
113
+ const SAFE_RUN_ID = /^run_[A-Za-z0-9_-]{1,120}$/;
114
+
115
+ function durationMs(startedAt: string | undefined, completedAt: string | undefined): number | undefined {
116
+ if (!startedAt || !completedAt) return undefined;
117
+ const value = Date.parse(completedAt) - Date.parse(startedAt);
118
+ return Number.isFinite(value) && value >= 0 ? value : undefined;
119
+ }
120
+
121
+ function increment(map: Record<string, number>, key: string): void {
122
+ map[key] = (map[key] ?? 0) + 1;
123
+ }
124
+
125
+ function classFromEvidence(evidence: string[]): string {
126
+ const match = evidence.find((item) => item.startsWith("class:"));
127
+ return match?.slice("class:".length) || "unknown";
128
+ }
129
+
130
+ function buildEvidenceAudit(run: WorkflowRun): RetrospectiveEvidenceAuditV1[] {
131
+ const audit: RetrospectiveEvidenceAuditV1[] = [];
132
+ for (const stage of run.stages) {
133
+ const verifiedEvidence = normalizeVerifiedWorkflowEvidence(stage.verifiedEvidence);
134
+ const degradedRequirements = new Set(
135
+ (stage.degradedRequirements ?? []).map(canonicalWorkflowEvidenceKey),
136
+ );
137
+ const policies = Object.entries(stage.resolvedEvidencePolicies ?? {})
138
+ .sort(([left], [right]) => left.localeCompare(right));
139
+ for (const [rawRequirement, policy] of policies) {
140
+ const requirementKey = canonicalWorkflowEvidenceKey(rawRequirement);
141
+ const attempt = stage.status === "passed" || stage.status === "failed"
142
+ ? Math.max(1, stage.attempts)
143
+ : stage.attempts + 1;
144
+ const verifiedMessages = [...(verifiedEvidence[requirementKey] ?? [])]
145
+ .filter((snapshot) => (
146
+ snapshot.context.runId === run.id
147
+ && snapshot.context.stageId === stage.id
148
+ && snapshot.context.requirementKey === requirementKey
149
+ && snapshot.context.attempt === attempt
150
+ ))
151
+ .sort((left, right) => left.messageId.localeCompare(right.messageId))
152
+ .map((snapshot) => ({
153
+ schema: snapshot.schema,
154
+ messageId: snapshot.messageId,
155
+ producerId: snapshot.producerId,
156
+ producerName: snapshot.producerName,
157
+ context: {
158
+ schema: snapshot.context.schema,
159
+ runId: snapshot.context.runId,
160
+ stageId: snapshot.context.stageId,
161
+ requirementKey: snapshot.context.requirementKey,
162
+ attempt: snapshot.context.attempt,
163
+ },
164
+ status: snapshot.status,
165
+ hashes: {
166
+ requestSha256: snapshot.requestSha256,
167
+ replySha256: snapshot.replySha256,
168
+ },
169
+ timestamps: {
170
+ createdAt: snapshot.createdAt,
171
+ replyCreatedAt: snapshot.replyCreatedAt,
172
+ repliedAt: snapshot.repliedAt,
173
+ verifiedAt: snapshot.verifiedAt,
174
+ },
175
+ }));
176
+ const degradationApprovals = (stage.degradationApprovals ?? [])
177
+ .filter((approval) => (
178
+ approval.schema === "pi-mesh.workflow-degradation-approval.v1"
179
+ && approval.requirementKey === requirementKey
180
+ && approval.attempt === attempt
181
+ ))
182
+ .sort((left, right) => left.attempt - right.attempt
183
+ || left.approvedAt.localeCompare(right.approvedAt)
184
+ || left.id.localeCompare(right.id))
185
+ .map((approval) => ({
186
+ schema: approval.schema,
187
+ id: approval.id,
188
+ requirementKey: approval.requirementKey,
189
+ attempt: approval.attempt,
190
+ policyMinProducers: approval.policyMinProducers,
191
+ approvedMinProducers: approval.approvedMinProducers,
192
+ approvedBy: approval.approvedBy,
193
+ reason: redactSecrets(approval.reason).replace(/\s+/gu, " ").trim(),
194
+ approvedAt: approval.approvedAt,
195
+ }));
196
+ const degraded = Boolean(stage.degraded && degradedRequirements.has(requirementKey));
197
+ const appliedApproval = degradationApprovals.at(-1);
198
+ audit.push({
199
+ stageId: stage.id,
200
+ requirementKey,
201
+ attempt,
202
+ policy: {
203
+ kind: policy.kind,
204
+ minProducers: policy.minProducers,
205
+ effectiveMinProducers: appliedApproval?.approvedMinProducers ?? policy.minProducers,
206
+ acceptedStatuses: ["replied"],
207
+ },
208
+ eligibleProducers: [...policy.eligibleProducers]
209
+ .sort((left, right) => left.id.localeCompare(right.id) || left.name.localeCompare(right.name))
210
+ .map((producer) => ({ id: producer.id, name: producer.name })),
211
+ verifiedProducerIds: [...new Set(verifiedMessages.map((snapshot) => snapshot.producerId))].sort(),
212
+ verifiedMessages,
213
+ degraded,
214
+ degradationApprovals,
215
+ });
216
+ }
217
+ }
218
+ return audit;
219
+ }
220
+
221
+ export function buildRetrospective(
222
+ run: WorkflowRun,
223
+ journal: WorkflowJournalEntry[],
224
+ exportedAt = new Date().toISOString(),
225
+ ): RetrospectiveV1 {
226
+ if (!SAFE_RUN_ID.test(run.id)) throw new Error("invalid retrospective run id");
227
+ const entries = journal
228
+ .filter((entry) => entry.runId === run.id)
229
+ .sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id))
230
+ .slice(-MAX_RETROSPECTIVE_ENTRIES)
231
+ .map((entry) => {
232
+ const promotionState = journalPromotionState(entry);
233
+ const exported: RetrospectiveV1["entries"][number] = {
234
+ id: entry.id,
235
+ category: entry.category,
236
+ area: entry.area,
237
+ severity: entry.severity,
238
+ summary: redactSecrets(entry.summary),
239
+ evidence: redactStringList(entry.evidence),
240
+ relatedEntryIds: entry.relatedEntryIds.slice(0, 16),
241
+ createdAt: entry.createdAt,
242
+ };
243
+ if (entry.stageId !== undefined) exported.stageId = entry.stageId;
244
+ if (entry.attempt !== undefined) exported.attempt = entry.attempt;
245
+ if (promotionState !== undefined) exported.promotionState = promotionState;
246
+ return exported;
247
+ });
248
+ const byCategory: Record<string, number> = {};
249
+ const byArea: Record<string, number> = {};
250
+ const byClass: Record<string, number> = {};
251
+ for (const entry of entries) {
252
+ increment(byCategory, entry.category);
253
+ increment(byArea, entry.area);
254
+ increment(byClass, classFromEvidence(entry.evidence));
255
+ }
256
+ const recurringErrorClasses = Object.entries(byClass)
257
+ .map(([errorClass, count]) => ({ class: errorClass, count }))
258
+ .sort((left, right) => right.count - left.count || left.class.localeCompare(right.class));
259
+ const resolvedContradictions = new Set(entries.filter((entry) => entry.category === "decision" || entry.category === "lesson").flatMap((entry) => entry.relatedEntryIds));
260
+ const openContradictions = entries
261
+ .filter((entry) => entry.category === "contradiction" && !resolvedContradictions.has(entry.id))
262
+ .map((entry) => ({ id: entry.id, summary: entry.summary, area: entry.area }));
263
+ const decisions = entries
264
+ .filter((entry) => entry.category === "decision")
265
+ .map((entry) => ({ id: entry.id, summary: entry.summary, area: entry.area }));
266
+ const proposedImprovements = entries
267
+ .filter((entry) => entry.category === "lesson" || entry.category === "error")
268
+ .slice(0, 12)
269
+ .map((entry) => ({
270
+ area: entry.area,
271
+ summary: entry.summary,
272
+ successMeasure: "reduce recurrence of this class in the next comparable run",
273
+ status: "proposed" as const,
274
+ }));
275
+ const evidenceAudit = buildEvidenceAudit(run);
276
+ const degradedStageIds = run.stages
277
+ .filter((stage) => stage.degraded)
278
+ .map((stage) => stage.id);
279
+ return {
280
+ schema: "pi-mesh.retrospective.v1",
281
+ runId: run.id,
282
+ definitionId: run.definitionId,
283
+ status: run.status,
284
+ exportedAt,
285
+ reviewDecision: "proposed",
286
+ stages: run.stages.map((stage) => {
287
+ const elapsed = durationMs(stage.startedAt, stage.completedAt);
288
+ return { id: stage.id, ...(stage.area ? { area: stage.area } : {}), status: stage.status, attempts: stage.attempts, ...(stage.startedAt ? { startedAt: stage.startedAt } : {}), ...(stage.completedAt ? { completedAt: stage.completedAt } : {}), ...(elapsed !== undefined ? { durationMs: elapsed } : {}), ...(stage.updatedAt ? { updatedAt: stage.updatedAt } : {}), ...(stage.summary ? { summary: redactSecrets(stage.summary) } : {}) };
289
+ }),
290
+ counts: { byCategory, byArea, byClass },
291
+ openContradictions,
292
+ decisions,
293
+ recurringErrorClasses,
294
+ entries,
295
+ proposedImprovements,
296
+ ...(evidenceAudit.length ? { evidenceAudit } : {}),
297
+ ...(degradedStageIds.length ? { degradedStageIds } : {}),
298
+ };
299
+ }
300
+
301
+ export function renderRetrospectiveMarkdown(doc: RetrospectiveV1): string {
302
+ const stageRows = doc.stages
303
+ .map((stage) => `| ${stage.id} | ${stage.area ?? ""} | ${stage.status} | ${stage.attempts} | ${stage.durationMs ?? ""} |`)
304
+ .join("\n");
305
+ const contradictionRows = doc.openContradictions
306
+ .map((entry) => `- ${entry.id} (${entry.area}): ${entry.summary}`)
307
+ .join("\n") || "- none";
308
+ const decisionRows = doc.decisions
309
+ .map((entry) => `- ${entry.id} (${entry.area}): ${entry.summary}`)
310
+ .join("\n") || "- none";
311
+ const classRows = doc.recurringErrorClasses
312
+ .map((entry) => `- ${entry.class}: ${entry.count}`)
313
+ .join("\n") || "- none";
314
+ const entryRows = doc.entries.map((entry) => {
315
+ const related = entry.relatedEntryIds.length > 0 ? `; related: ${entry.relatedEntryIds.join(", ")}` : "";
316
+ const evidence = entry.evidence.length > 0 ? `; evidence: ${entry.evidence.join(", ")}` : "";
317
+ return `- ${entry.id} [${entry.category}/${entry.area}/${entry.severity}]: ${entry.summary}${related}${evidence}`;
318
+ }).join("\n") || "- none";
319
+ const evidenceAuditRows = (doc.evidenceAudit ?? []).flatMap((audit) => {
320
+ const producerNames = audit.eligibleProducers
321
+ .map((producer) => `${producer.name} (${producer.id})`)
322
+ .join(", ") || "none";
323
+ const verified = audit.verifiedMessages.length
324
+ ? audit.verifiedMessages.map((snapshot) => (
325
+ ` - ${snapshot.messageId}: producer ${snapshot.producerName} (${snapshot.producerId}), attempt ${snapshot.context.attempt}, request ${snapshot.hashes.requestSha256}, reply ${snapshot.hashes.replySha256}, replied ${snapshot.timestamps.repliedAt}, verified ${snapshot.timestamps.verifiedAt}`
326
+ ))
327
+ : [" - no verified messages"];
328
+ const approvals = audit.degradationApprovals.map((approval) => (
329
+ ` - approval ${approval.id}: attempt ${approval.attempt}, ${approval.policyMinProducers} -> ${approval.approvedMinProducers} producers, ${approval.approvedBy}, ${approval.approvedAt}; reason: ${approval.reason}`
330
+ ));
331
+ return [
332
+ `- ${audit.stageId} / ${audit.requirementKey} / attempt ${audit.attempt}: ${audit.verifiedProducerIds.length}/${audit.policy.effectiveMinProducers} verified producers; policy minimum ${audit.policy.minProducers}; degraded: ${audit.degraded}`,
333
+ ` - eligible: ${producerNames}`,
334
+ ...verified,
335
+ ...(approvals.length ? [" - degradation approvals:", ...approvals] : []),
336
+ ];
337
+ });
338
+ return [
339
+ `# Workflow retrospective ${doc.runId}`,
340
+ "",
341
+ `- Definition: ${doc.definitionId}`,
342
+ `- Status: ${doc.status}`,
343
+ `- Exported: ${doc.exportedAt}`,
344
+ `- Review decision: ${doc.reviewDecision}`,
345
+ "",
346
+ "## Stages",
347
+ "",
348
+ "| id | area | status | attempts | duration ms |",
349
+ "|---|---|---|---|---|",
350
+ stageRows,
351
+ "",
352
+ "## Decisions",
353
+ "",
354
+ decisionRows,
355
+ "",
356
+ "## Open contradictions",
357
+ "",
358
+ contradictionRows,
359
+ "",
360
+ "## Recurring error classes",
361
+ "",
362
+ classRows,
363
+ "",
364
+ "## Bounded journal evidence",
365
+ "",
366
+ entryRows,
367
+ "",
368
+ ...(doc.evidenceAudit?.length ? [
369
+ "## Peer-evidence audit",
370
+ "",
371
+ "This section contains immutable provenance metadata and content hashes only; prompt and reply bodies are excluded.",
372
+ "",
373
+ ...evidenceAuditRows,
374
+ "",
375
+ ] : []),
376
+ "## Proposed improvements",
377
+ "",
378
+ ...doc.proposedImprovements.map((item) => `- [${item.status}] (${item.area}) ${item.summary}`),
379
+ "",
380
+ "Proposed improvements are evidence, not policy. Do not apply them until an explicit review decision.",
381
+ "",
382
+ ].join("\n");
383
+ }
384
+
385
+ export function writeRetrospective(outDir: string, doc: RetrospectiveV1): { jsonPath: string; mdPath: string } {
386
+ if (!SAFE_RUN_ID.test(doc.runId)) throw new Error("invalid retrospective run id");
387
+ mkdirSync(outDir, { recursive: true });
388
+ const root = resolve(outDir);
389
+ const jsonPath = resolve(root, `${doc.runId}.json`);
390
+ const mdPath = resolve(root, `${doc.runId}.md`);
391
+ const prefix = `${root}${process.platform === "win32" ? "\\" : "/"}`;
392
+ if (!jsonPath.startsWith(prefix) || !mdPath.startsWith(prefix)) throw new Error("retrospective path escaped output directory");
393
+ const jsonTmp = `${jsonPath}.tmp`;
394
+ const mdTmp = `${mdPath}.tmp`;
395
+ writeFileSync(jsonTmp, `${JSON.stringify(doc, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
396
+ writeFileSync(mdTmp, renderRetrospectiveMarkdown(doc), { encoding: "utf8", mode: 0o600 });
397
+ renameSync(jsonTmp, jsonPath);
398
+ renameSync(mdTmp, mdPath);
399
+ return { jsonPath, mdPath };
400
+ }