@smartmemory/compose 0.1.1-beta → 0.1.2-beta

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 (124) hide show
  1. package/.claude/skills/bug-fix/SKILL.md +143 -0
  2. package/.claude/skills/compose/SKILL.md +604 -0
  3. package/.compose-deps.json +89 -0
  4. package/README.md +14 -3
  5. package/bin/compose.js +473 -0
  6. package/contracts/comp-obs-contract.schema.json +362 -0
  7. package/contracts/cross-model-review-result.json +78 -0
  8. package/contracts/review-result.json +126 -0
  9. package/dist/assets/{_baseUniq-CQwX6VLz.js → _baseUniq-D-avYfn5.js} +1 -1
  10. package/dist/assets/{arc-SxJ2J1sh.js → arc-BC4dfQ-X.js} +1 -1
  11. package/dist/assets/{architectureDiagram-Q4EWVU46-BykunY1F.js → architectureDiagram-Q4EWVU46-BZmFXnGI.js} +1 -1
  12. package/dist/assets/{blockDiagram-DXYQGD6D-ohAKBOUw.js → blockDiagram-DXYQGD6D-DlfWSuux.js} +1 -1
  13. package/dist/assets/{c4Diagram-AHTNJAMY-DBDC3ENB.js → c4Diagram-AHTNJAMY-Y__uJrRx.js} +1 -1
  14. package/dist/assets/channel-LRG9kHqJ.js +1 -0
  15. package/dist/assets/{chunk-4BX2VUAB-Cv93Z7uM.js → chunk-4BX2VUAB-BfMePfTp.js} +1 -1
  16. package/dist/assets/{chunk-4TB4RGXK-DE0WBDkj.js → chunk-4TB4RGXK-BdlMSdEA.js} +1 -1
  17. package/dist/assets/{chunk-55IACEB6-CE1EXenG.js → chunk-55IACEB6-vrQHZTdv.js} +1 -1
  18. package/dist/assets/{chunk-EDXVE4YY-DA7Ana6H.js → chunk-EDXVE4YY-B8wioVlW.js} +1 -1
  19. package/dist/assets/{chunk-FMBD7UC4-CTDIPA3p.js → chunk-FMBD7UC4-Cd6Hrux2.js} +1 -1
  20. package/dist/assets/{chunk-OYMX7WX6-uGBaPaTX.js → chunk-OYMX7WX6-CfrhdQXY.js} +1 -1
  21. package/dist/assets/{chunk-QZHKN3VN-CYlnXuUO.js → chunk-QZHKN3VN-B9JQerOU.js} +1 -1
  22. package/dist/assets/{chunk-YZCP3GAM-ojGkzcZK.js → chunk-YZCP3GAM-DFN9X99H.js} +1 -1
  23. package/dist/assets/classDiagram-6PBFFD2Q-BC9a6pDE.js +1 -0
  24. package/dist/assets/classDiagram-v2-HSJHXN6E-BC9a6pDE.js +1 -0
  25. package/dist/assets/clone-dRxgFrBv.js +1 -0
  26. package/dist/assets/{cose-bilkent-S5V4N54A-Bktn9hL-.js → cose-bilkent-S5V4N54A-BAn0ap_E.js} +1 -1
  27. package/dist/assets/{dagre-KV5264BT-DFaSzuRF.js → dagre-KV5264BT-DyxnVq1g.js} +1 -1
  28. package/dist/assets/{diagram-5BDNPKRD-DnfmDzEm.js → diagram-5BDNPKRD-XCrzqski.js} +1 -1
  29. package/dist/assets/{diagram-G4DWMVQ6-Bm8W9YnG.js → diagram-G4DWMVQ6-MBCAXft_.js} +1 -1
  30. package/dist/assets/{diagram-MMDJMWI5-B5-TSKvp.js → diagram-MMDJMWI5-DbtB2yS6.js} +1 -1
  31. package/dist/assets/{diagram-TYMM5635-ls4rqlky.js → diagram-TYMM5635-Bb5NzX61.js} +1 -1
  32. package/dist/assets/{erDiagram-SMLLAGMA-giG6WO-r.js → erDiagram-SMLLAGMA-CpIeCOh2.js} +1 -1
  33. package/dist/assets/{flowDiagram-DWJPFMVM-XvlUuz-7.js → flowDiagram-DWJPFMVM-CHyoKnhW.js} +1 -1
  34. package/dist/assets/{ganttDiagram-T4ZO3ILL-hLBV57oV.js → ganttDiagram-T4ZO3ILL-DErKteO_.js} +1 -1
  35. package/dist/assets/{gitGraphDiagram-UUTBAWPF-BHu3s_Gn.js → gitGraphDiagram-UUTBAWPF-KFVAtj2F.js} +1 -1
  36. package/dist/assets/{graph-D0Cfv00Y.js → graph-CRnO_ifT.js} +1 -1
  37. package/dist/assets/index-DKBsEUJ-.css +1 -0
  38. package/dist/assets/index-DkRKLuNr.js +1144 -0
  39. package/dist/assets/{infoDiagram-42DDH7IO-DbqRsOo3.js → infoDiagram-42DDH7IO-BZFnuSp5.js} +1 -1
  40. package/dist/assets/{ishikawaDiagram-UXIWVN3A-DnCdx7zb.js → ishikawaDiagram-UXIWVN3A-4Xe2Szde.js} +1 -1
  41. package/dist/assets/{journeyDiagram-VCZTEJTY-CfD7eNcP.js → journeyDiagram-VCZTEJTY-CZRByfS-.js} +1 -1
  42. package/dist/assets/{kanban-definition-6JOO6SKY-BYaO9-mK.js → kanban-definition-6JOO6SKY-B95sk6Fk.js} +1 -1
  43. package/dist/assets/{layout-Bj72wOEB.js → layout-BqNQzxWT.js} +1 -1
  44. package/dist/assets/{linear-BRFo114D.js → linear-CUh7qb64.js} +1 -1
  45. package/dist/assets/{min-GCHnKlJS.js → min-wXgOS3ig.js} +1 -1
  46. package/dist/assets/{mindmap-definition-QFDTVHPH-n0PMebY4.js → mindmap-definition-QFDTVHPH-DB6iaAbO.js} +1 -1
  47. package/dist/assets/{pieDiagram-DEJITSTG-pN4CljHF.js → pieDiagram-DEJITSTG-CHkZHrTW.js} +1 -1
  48. package/dist/assets/{quadrantDiagram-34T5L4WZ-DNoAy8-D.js → quadrantDiagram-34T5L4WZ-DoTEO8e3.js} +1 -1
  49. package/dist/assets/{requirementDiagram-MS252O5E-BhtY05PT.js → requirementDiagram-MS252O5E-Dn8peXYp.js} +1 -1
  50. package/dist/assets/{sankeyDiagram-XADWPNL6-B6AD-16A.js → sankeyDiagram-XADWPNL6-DRXs6Ipb.js} +1 -1
  51. package/dist/assets/{sequenceDiagram-FGHM5R23-DShHM-uk.js → sequenceDiagram-FGHM5R23-wBBYZ0aq.js} +1 -1
  52. package/dist/assets/{stateDiagram-FHFEXIEX-DMxn7HTo.js → stateDiagram-FHFEXIEX-DPlBNGmf.js} +1 -1
  53. package/dist/assets/stateDiagram-v2-QKLJ7IA2-BW0ezXb4.js +1 -0
  54. package/dist/assets/{timeline-definition-GMOUNBTQ-Cdu6uq52.js → timeline-definition-GMOUNBTQ-CbbyTlHk.js} +1 -1
  55. package/dist/assets/{vennDiagram-DHZGUBPP-CpK29iRe.js → vennDiagram-DHZGUBPP-Bj4GaFfj.js} +1 -1
  56. package/dist/assets/{wardley-RL74JXVD-BQgSkdcO.js → wardley-RL74JXVD-RtNzq8KU.js} +55 -55
  57. package/dist/assets/{wardleyDiagram-NUSXRM2D-DJHYev6O.js → wardleyDiagram-NUSXRM2D-CDfE3zSj.js} +1 -1
  58. package/dist/assets/{xychartDiagram-5P7HB3ND-1d75pbaO.js → xychartDiagram-5P7HB3ND-CZXHHYD5.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/lib/budget-ledger.js +45 -0
  61. package/lib/bug-bisect.js +292 -0
  62. package/lib/bug-checkpoint.js +191 -0
  63. package/lib/bug-escalation.js +306 -0
  64. package/lib/bug-index-gen.js +136 -0
  65. package/lib/bug-ledger.js +126 -0
  66. package/lib/build-stream-schema.js +176 -0
  67. package/lib/build-stream-writer.js +3 -1
  68. package/lib/build.js +854 -284
  69. package/lib/connector-factory-shim.js +167 -0
  70. package/lib/constants.js +18 -0
  71. package/lib/debug-discipline.js +176 -27
  72. package/lib/deps.js +205 -0
  73. package/lib/health-score.js +4 -4
  74. package/lib/import.js +26 -13
  75. package/lib/inject-schema.js +21 -0
  76. package/lib/new.js +27 -53
  77. package/lib/result-normalizer.js +160 -144
  78. package/lib/review-lenses.js +5 -5
  79. package/lib/review-normalize.js +413 -0
  80. package/lib/review-prompt.js +163 -0
  81. package/lib/sections.js +325 -0
  82. package/lib/step-prompt.js +21 -1
  83. package/lib/step-validator.js +5 -3
  84. package/lib/stratum-mcp-client.js +172 -7
  85. package/package.json +14 -3
  86. package/pipelines/bug-fix.stratum.yaml +39 -1
  87. package/pipelines/build.stratum.yaml +28 -45
  88. package/pipelines/review-fix.stratum.yaml +1 -1
  89. package/presets/team-review.stratum.yaml +21 -14
  90. package/server/build-stream-bridge.js +28 -0
  91. package/server/cc-session-feature-resolver.js +111 -0
  92. package/server/cc-session-reader.js +327 -0
  93. package/server/cc-session-watcher.js +318 -0
  94. package/server/compose-mcp-tools.js +0 -125
  95. package/server/compose-mcp.js +2 -4
  96. package/server/contract-diff.js +192 -0
  97. package/server/decision-event-emit.js +175 -0
  98. package/server/decision-event-id.js +64 -0
  99. package/server/decision-events-snapshot.js +166 -0
  100. package/server/design-routes.js +92 -49
  101. package/server/drift-axes.js +365 -0
  102. package/server/drift-emit.js +121 -0
  103. package/server/gate-log-store.js +102 -0
  104. package/server/lifecycle-phase-history.js +44 -0
  105. package/server/open-loops-store.js +102 -0
  106. package/server/schema-validator.js +49 -0
  107. package/server/status-emit.js +27 -0
  108. package/server/status-snapshot.js +218 -0
  109. package/server/vision-routes.js +332 -4
  110. package/server/vision-server.js +104 -12
  111. package/server/vision-store.js +21 -0
  112. package/dist/assets/channel-DGElom1e.js +0 -1
  113. package/dist/assets/classDiagram-6PBFFD2Q-KqWP9wWZ.js +0 -1
  114. package/dist/assets/classDiagram-v2-HSJHXN6E-KqWP9wWZ.js +0 -1
  115. package/dist/assets/clone-DUJKJXd7.js +0 -1
  116. package/dist/assets/index-CUd6pFGF.css +0 -1
  117. package/dist/assets/index-DReRlzZI.js +0 -1144
  118. package/dist/assets/stateDiagram-v2-QKLJ7IA2-o6PnCs4e.js +0 -1
  119. package/server/connectors/agent-connector.js +0 -78
  120. package/server/connectors/claude-sdk-connector.js +0 -198
  121. package/server/connectors/codex-connector.js +0 -240
  122. package/server/connectors/connector-discovery.js +0 -18
  123. package/server/connectors/connector-runtime.js +0 -13
  124. package/server/connectors/opencode-connector.js +0 -200
@@ -69,6 +69,13 @@ contracts:
69
69
  discipline_score: {type: number}
70
70
  summary: {type: string}
71
71
 
72
+ BisectResult:
73
+ skipped: {type: boolean}
74
+ bisect_commit: {type: string} # nullable; empty string when skipped
75
+ estimate_minutes: {type: number}
76
+ log_path: {type: string} # nullable; empty string when skipped
77
+ summary: {type: string}
78
+
72
79
  functions:
73
80
  reproduce:
74
81
  mode: compute
@@ -99,6 +106,30 @@ functions:
99
106
  - "result.scope_hint in ('single', 'cross-layer', 'unknown')"
100
107
  retries: 2
101
108
 
109
+ bisect:
110
+ mode: compute
111
+ intent: >
112
+ Decide whether the bug is a regression worth bisecting; if so,
113
+ classify → estimate cost → gate the human → run git bisect.
114
+ The runtime helper `lib/bug-bisect.js` does the work:
115
+ 1. classifyRegression(): repro test exists in main AND files in
116
+ diagnosis.affected_layers were touched in the last 10 commits.
117
+ 2. estimateBisectCost(): test_runs = ceil(log2(commits in range));
118
+ seconds_per_run sampled by running the test once.
119
+ 3. Gate: "Looks like a regression. Run git bisect? Estimate:
120
+ <test_runs> runs × <seconds_per_run>s = ~<total_minutes> min.
121
+ approve / skip / kill"
122
+ 4. On approve: runBisect() drives `git bisect start && bad HEAD
123
+ && good <baseline> && bisect run <testCmd>`, captures log to
124
+ docs/bugs/<bug-code>/bisect.log, always resets in finally.
125
+ 5. On skip OR not-a-regression: return {skipped: true,
126
+ bisect_commit: null, estimate_minutes: 0} and continue.
127
+ input:
128
+ task: {type: string}
129
+ diagnosis: {type: object}
130
+ output: BisectResult
131
+ retries: 1
132
+
102
133
  scope_check:
103
134
  mode: compute
104
135
  intent: >
@@ -191,12 +222,19 @@ flows:
191
222
  task: "$.input.task"
192
223
  depends_on: [reproduce]
193
224
 
225
+ - id: bisect
226
+ function: bisect
227
+ inputs:
228
+ task: "$.input.task"
229
+ diagnosis: "$.steps.diagnose.output"
230
+ depends_on: [diagnose]
231
+
194
232
  - id: scope_check
195
233
  function: scope_check
196
234
  inputs:
197
235
  task: "$.input.task"
198
236
  diagnosis: "$.steps.diagnose.output"
199
- depends_on: [diagnose]
237
+ depends_on: [bisect]
200
238
 
201
239
  - id: fix
202
240
  function: fix
@@ -26,10 +26,16 @@ contracts:
26
26
  outcome: {type: string, values: [complete, skipped, failed]}
27
27
  summary: {type: string}
28
28
 
29
+ # Canonical review output — produced by both review_check (Codex) and parallel_review (Claude).
30
+ # Schema source: compose/contracts/review-result.json (_roadmap: STRAT-CLAUDE-EFFORT-PARITY)
29
31
  ReviewResult:
30
- clean: {type: boolean}
31
- summary: {type: string}
32
- findings: {type: array}
32
+ clean: {type: boolean}
33
+ summary: {type: string}
34
+ findings: {type: array}
35
+ meta: {type: object}
36
+ lenses_run: {type: array}
37
+ auto_fixes: {type: array}
38
+ asks: {type: array}
33
39
 
34
40
  TestResult:
35
41
  passing: {type: boolean}
@@ -39,14 +45,6 @@ contracts:
39
45
  TaskGraph:
40
46
  tasks: {type: array}
41
47
 
42
- LensFinding:
43
- lens: {type: string}
44
- file: {type: string}
45
- line: {type: number}
46
- severity: {type: string, values: [must-fix, should-fix, nit]}
47
- finding: {type: string}
48
- confidence: {type: number}
49
-
50
48
  LensTask:
51
49
  id: {type: string}
52
50
  lens_name: {type: string}
@@ -57,18 +55,6 @@ contracts:
57
55
  TriageResult:
58
56
  tasks: {type: array}
59
57
 
60
- LensResult:
61
- clean: {type: boolean}
62
- findings: {type: array, items: LensFinding}
63
-
64
- MergedReviewResult:
65
- clean: {type: boolean}
66
- summary: {type: string}
67
- findings: {type: array, items: LensFinding}
68
- lenses_run: {type: array}
69
- auto_fixes: {type: array, items: LensFinding}
70
- asks: {type: array, items: LensFinding}
71
-
72
58
  functions:
73
59
  design_gate:
74
60
  mode: gate
@@ -96,12 +82,12 @@ flows:
96
82
  - id: review
97
83
  agent: codex
98
84
  intent: >
99
- Review the implementation against the blueprint. Return structured JSON:
100
- { "clean": boolean, "summary": string, "findings": string[] }.
101
- Set clean=true only if no actionable findings with confidence >= 80 remain.
85
+ Review the implementation against the blueprint and task.
102
86
  inputs:
103
87
  task: "$.input.task"
104
88
  blueprint: "$.input.blueprint"
89
+ review_mode: "true"
90
+ confidence_gate: "7"
105
91
  output_contract: ReviewResult
106
92
  ensure:
107
93
  - "result.clean == True"
@@ -117,7 +103,7 @@ flows:
117
103
  blueprint: {type: string}
118
104
  diff: {type: string}
119
105
  prior_dirty_lenses: {type: array, optional: true}
120
- output: MergedReviewResult
106
+ output: ReviewResult
121
107
  steps:
122
108
  - id: triage
123
109
  agent: "claude:orchestrator"
@@ -150,34 +136,31 @@ flows:
150
136
  source: "$.steps.triage.output.tasks"
151
137
  max_concurrent: 4
152
138
  isolation: none
139
+ output_contract: ReviewResult
153
140
  intent_template: >
154
- You are a {lens_name} reviewer. Review ONLY through the {lens_name} lens.
155
- Focus: {lens_focus}
156
- Confidence gate: only report findings with confidence >= {confidence_gate}.
157
- False-positive exclusions: {exclusions}
158
- Return JSON: { "clean": boolean, "findings": LensFinding[] }
159
- where each finding has: lens, file, line, severity (must-fix|should-fix|nit),
160
- finding (description), confidence (1-10).
141
+ Lens: {lens_name}. Focus: {lens_focus}.
161
142
  require: all
162
143
 
163
144
  - id: merge
164
145
  agent: "claude:orchestrator"
165
146
  intent: >
166
- Merge findings from all review lenses. The input contains the parallel
167
- dispatch aggregate with tasks[].result for each lens.
168
- 1. Collect all findings arrays from completed task results.
169
- 2. Deduplicate: same file + same issue = one finding, keep highest confidence.
170
- 3. Assign severity: must-fix (blocks ship), should-fix (next iteration), nit (logged).
171
- 4. Classify: auto_fixes (mechanical: formatting, simple tests, obvious typos)
172
- vs asks (requires judgment).
173
- 5. lenses_run = IDs of lenses that produced must-fix or should-fix findings.
174
- Return JSON: MergedReviewResult with clean, summary, findings, lenses_run,
175
- auto_fixes, asks. Set clean=true only if zero must-fix and zero should-fix findings.
147
+ Merge ReviewResult arrays from all lens runs into a single canonical ReviewResult.
148
+ The input contains the parallel dispatch aggregate with tasks[].result for each lens.
149
+ 1. Concatenate all findings arrays from completed task results.
150
+ Deduplicate by file+line+lens: keep finding with highest confidence;
151
+ carry over its applied_gate.
152
+ 2. Recompute clean: zero must-fix AND zero should-fix findings (post-dedupe, post-gate).
153
+ 3. lenses_run = IDs of lenses that produced any must-fix or should-fix finding.
154
+ 4. auto_fixes = mechanical fixes (formatting, typos, simple tests).
155
+ 5. asks = findings requiring human judgment.
156
+ Return canonical ReviewResult with clean, summary, findings, lenses_run,
157
+ auto_fixes, asks, meta.
176
158
  inputs:
177
159
  results: "$.steps.review_lenses.output"
178
160
  task: "$.input.task"
179
161
  blueprint: "$.input.blueprint"
180
- output_contract: MergedReviewResult
162
+ reduce_mode: "true"
163
+ output_contract: ReviewResult
181
164
  depends_on: [review_lenses]
182
165
 
183
166
  # --- Sub-flow: coverage ---
@@ -73,7 +73,7 @@ functions:
73
73
  - the task description
74
74
  - the execute_summary (what was implemented)
75
75
  - the blueprint content
76
- - instruction: "List actionable findings with confidence >= 80.
76
+ - instruction: "List actionable findings with confidence >= 7.
77
77
  Set clean=true only if no actionable findings remain."
78
78
  Pass the schema to agent_run so it returns structured JSON.
79
79
 
@@ -17,10 +17,15 @@ contracts:
17
17
  type: array
18
18
  values: [{type: object}]
19
19
  diff: {type: string}
20
- MergedReviewResult:
21
- findings: {type: string}
22
- has_critical: {type: string}
23
- summary: {type: string}
20
+ # Canonical schema — STRAT-CLAUDE-EFFORT-PARITY. Local MergedReviewResult removed.
21
+ ReviewResult:
22
+ clean: {type: boolean}
23
+ summary: {type: string}
24
+ findings: {type: array}
25
+ meta: {type: object}
26
+ lenses_run: {type: array}
27
+ auto_fixes: {type: array}
28
+ asks: {type: array}
24
29
 
25
30
  workflow:
26
31
  name: team-review
@@ -38,7 +43,7 @@ flows:
38
43
  input:
39
44
  featureCode: {type: string}
40
45
  description: {type: string}
41
- output: MergedReviewResult
46
+ output: ReviewResult
42
47
  steps:
43
48
  - id: triage
44
49
  agent: "claude:orchestrator"
@@ -76,9 +81,9 @@ flows:
76
81
  Confidence gate: only report findings with confidence >= {confidence_gate}/10.
77
82
  Exclusions: do NOT flag {exclusions}.
78
83
 
79
- Return JSON: { "clean": boolean, "findings": LensFinding[] }
84
+ Return JSON: { "clean": boolean, "findings": <ReviewResult finding items> }
80
85
  where each finding has: lens, file, line, severity (must-fix|should-fix|nit),
81
- finding (description), confidence (1-10).
86
+ finding (description), confidence (1-10), applied_gate (integer).
82
87
 
83
88
  If no issues found, return { "clean": true, "findings": [] }.
84
89
  output_fields:
@@ -92,15 +97,17 @@ flows:
92
97
  agent: "claude:orchestrator"
93
98
  depends_on: [review_lenses]
94
99
  intent: >
95
- Consolidate findings from all review lenses. Deduplicate findings
96
- that appear in multiple passes (same file:line, same issue). Rank
97
- by severity (must-fix first, then should-fix, then nit).
98
- Set has_critical to "true" if any finding has severity "must-fix",
99
- "false" otherwise. Write a one-sentence summary.
100
+ Consolidate ReviewResult arrays from all review lenses into a single
101
+ canonical ReviewResult. Deduplicate findings (same file:line+lens,
102
+ keep highest confidence; carry over its applied_gate). Rank by
103
+ severity (must-fix first, then should-fix, then nit). Set clean=true
104
+ only if zero must-fix AND zero should-fix findings remain. Write a
105
+ one-sentence summary.
100
106
  inputs:
101
107
  review_results: "$.steps.review_lenses.output"
102
- output_contract: MergedReviewResult
108
+ reduce_mode: "true"
109
+ output_contract: ReviewResult
103
110
  ensure:
104
- - "result.has_critical == 'false'"
111
+ - "result.clean == True"
105
112
  - "result.summary != ''"
106
113
  retries: 2
@@ -455,6 +455,22 @@ export class BuildStreamBridge {
455
455
  _source: 'build',
456
456
  };
457
457
 
458
+ // STRAT-PAR-STREAM: typed BuildStreamEvent envelope (schema v0.2.5+) wrapped
459
+ // by build.js as { type: 'build_stream_event', event: {...} }. Pass the
460
+ // inner envelope through to the cockpit unchanged so renderers can
461
+ // discriminate by `kind`.
462
+ case 'build_stream_event': {
463
+ const inner = event.event;
464
+ if (!inner || typeof inner !== 'object' || typeof inner.kind !== 'string') {
465
+ return null;
466
+ }
467
+ return {
468
+ type: 'buildStreamEvent',
469
+ event: inner,
470
+ _source: 'build',
471
+ };
472
+ }
473
+
458
474
  // COMP-HEALTH item 118: health score after build completion
459
475
  case 'health_score':
460
476
  return {
@@ -465,6 +481,18 @@ export class BuildStreamBridge {
465
481
  _source: 'build',
466
482
  };
467
483
 
484
+ // COMP-AGENT-CAPS-5: capability violation audit events
485
+ case 'capability_violation':
486
+ return {
487
+ type: 'system', subtype: 'capability_violation',
488
+ stepId: event.stepId,
489
+ agent: event.agent,
490
+ template: event.template,
491
+ detail: event.detail,
492
+ severity: event.severity ?? 'violation',
493
+ _source: 'build',
494
+ };
495
+
468
496
  default:
469
497
  return null; // unknown event type — skip
470
498
  }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * CC-session → feature_code resolver — COMP-OBS-BRANCH T2.
3
+ *
4
+ * Forge session ids (`session-<ts>-<hex>` from SessionManager) and Claude Code
5
+ * session uuids live in different namespaces. The only join key is
6
+ * `sessions.json[i].transcriptPath` — its basename is `<cc_session_id>.jsonl`.
7
+ *
8
+ * Resolution tiers (per blueprint §5):
9
+ * 1. Primary — sessions.json scan, match basename(transcriptPath)
10
+ * 2. Fallback — probe <featureRoot>/<CODE>/sessions/<cc_session_id>.*
11
+ * 3. Unbound — null (caller skips emission)
12
+ *
13
+ * Results cache per cc_session_id; invalidated on mtime bump of either
14
+ * sessions.json or the matched feature's sessions/ directory.
15
+ */
16
+
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+
20
+ function statMtime(p) {
21
+ try { return fs.statSync(p).mtimeMs; } catch { return null; }
22
+ }
23
+
24
+ function loadSessionsJson(sessionsFile) {
25
+ try {
26
+ const raw = fs.readFileSync(sessionsFile, 'utf8');
27
+ const arr = JSON.parse(raw);
28
+ return Array.isArray(arr) ? arr : [];
29
+ } catch {
30
+ return [];
31
+ }
32
+ }
33
+
34
+ export class CCSessionFeatureResolver {
35
+ constructor({ sessionsFile, featureRoot }) {
36
+ if (!sessionsFile) throw new Error('sessionsFile required');
37
+ if (!featureRoot) throw new Error('featureRoot required');
38
+ this.sessionsFile = sessionsFile;
39
+ this.featureRoot = featureRoot;
40
+ this._cache = new Map(); // cc_session_id → { feature_code, sessionsMtime, featureDirMtime }
41
+ this._sessionsIndex = null;
42
+ this._sessionsMtime = null;
43
+ this.stats = { unbound_count: 0 };
44
+ }
45
+
46
+ _refreshIndex() {
47
+ const mtime = statMtime(this.sessionsFile);
48
+ if (mtime === this._sessionsMtime && this._sessionsIndex) return;
49
+ this._sessionsMtime = mtime;
50
+ const sessions = loadSessionsJson(this.sessionsFile);
51
+ const idx = new Map();
52
+ for (const s of sessions) {
53
+ const tp = s?.transcriptPath;
54
+ if (!tp || !s?.featureCode) continue;
55
+ const base = path.basename(tp);
56
+ const ccId = base.replace(/\.jsonl$/, '');
57
+ if (!idx.has(ccId)) idx.set(ccId, s.featureCode);
58
+ }
59
+ this._sessionsIndex = idx;
60
+ this._cache.clear();
61
+ }
62
+
63
+ _fallbackProbe(cc_session_id) {
64
+ let entries;
65
+ try { entries = fs.readdirSync(this.featureRoot, { withFileTypes: true }); }
66
+ catch { return null; }
67
+ for (const ent of entries) {
68
+ if (!ent.isDirectory()) continue;
69
+ const sessionsDir = path.join(this.featureRoot, ent.name, 'sessions');
70
+ let files;
71
+ try { files = fs.readdirSync(sessionsDir); } catch { continue; }
72
+ if (files.some(f => f.startsWith(cc_session_id + '.'))) {
73
+ return ent.name;
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+
79
+ resolve(cc_session_id) {
80
+ if (!cc_session_id) return null;
81
+ this._refreshIndex();
82
+
83
+ const cached = this._cache.get(cc_session_id);
84
+ if (cached && cached.sessionsMtime === this._sessionsMtime) {
85
+ if (cached.feature_code == null) return null;
86
+ const curDirMtime = statMtime(path.join(this.featureRoot, cached.feature_code, 'sessions'));
87
+ if (curDirMtime === cached.featureDirMtime) return cached.feature_code;
88
+ }
89
+
90
+ let fc = this._sessionsIndex.get(cc_session_id);
91
+ if (!fc) fc = this._fallbackProbe(cc_session_id);
92
+
93
+ if (fc) {
94
+ const dirMtime = statMtime(path.join(this.featureRoot, fc, 'sessions'));
95
+ this._cache.set(cc_session_id, {
96
+ feature_code: fc,
97
+ sessionsMtime: this._sessionsMtime,
98
+ featureDirMtime: dirMtime,
99
+ });
100
+ return fc;
101
+ }
102
+
103
+ this._cache.set(cc_session_id, {
104
+ feature_code: null,
105
+ sessionsMtime: this._sessionsMtime,
106
+ featureDirMtime: null,
107
+ });
108
+ this.stats.unbound_count++;
109
+ return null;
110
+ }
111
+ }