switchman-dev 0.1.5 → 0.1.7
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.
- package/.cursor/mcp.json +8 -0
- package/.mcp.json +8 -0
- package/README.md +173 -4
- package/examples/README.md +28 -0
- package/package.json +1 -1
- package/src/cli/index.js +2941 -314
- package/src/core/ci.js +204 -0
- package/src/core/db.js +822 -26
- package/src/core/enforcement.js +18 -5
- package/src/core/git.js +286 -1
- package/src/core/merge-gate.js +17 -2
- package/src/core/outcome.js +1 -1
- package/src/core/pipeline.js +2399 -59
- package/src/core/planner.js +25 -5
- package/src/core/policy.js +105 -0
- package/src/core/queue.js +643 -27
- package/src/core/semantic.js +71 -5
- package/src/core/telemetry.js +210 -0
package/src/core/ci.js
CHANGED
|
@@ -40,6 +40,66 @@ export function formatCiGateMarkdown(result) {
|
|
|
40
40
|
].join('\n');
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function getPipelineLandingCheckInfo(result) {
|
|
44
|
+
if (result.queue_state?.status === 'merged') {
|
|
45
|
+
return {
|
|
46
|
+
status: 'success',
|
|
47
|
+
title: 'Pipeline merged to target branch',
|
|
48
|
+
summary: result.queue_state.merged_commit
|
|
49
|
+
? `Merged as ${result.queue_state.merged_commit}.`
|
|
50
|
+
: 'Merged successfully.',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (['queued', 'validating', 'rebasing', 'merging', 'retrying'].includes(result.queue_state?.status)) {
|
|
55
|
+
return {
|
|
56
|
+
status: result.queue_state.status === 'retrying' ? 'action_required' : 'pending',
|
|
57
|
+
title: result.queue_state.status === 'retrying'
|
|
58
|
+
? 'Pipeline is retrying in the landing queue'
|
|
59
|
+
: 'Pipeline is in the landing queue',
|
|
60
|
+
summary: result.queue_state.next_action || result.next_action,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (result.ready_to_queue) {
|
|
65
|
+
return {
|
|
66
|
+
status: 'success',
|
|
67
|
+
title: 'Pipeline ready to queue',
|
|
68
|
+
summary: result.queue_state?.next_action || result.next_action,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if ((result.stale_clusters?.length || 0) > 0) {
|
|
73
|
+
return {
|
|
74
|
+
status: 'action_required',
|
|
75
|
+
title: 'Pipeline has stale work to revalidate',
|
|
76
|
+
summary: result.stale_clusters[0]?.next_action || result.queue_state?.next_action || result.next_action,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (result.landing_error || result.landing?.last_failure) {
|
|
81
|
+
return {
|
|
82
|
+
status: 'action_required',
|
|
83
|
+
title: 'Pipeline landing needs attention',
|
|
84
|
+
summary: result.queue_state?.next_action || result.next_action,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (result.landing?.stale) {
|
|
89
|
+
return {
|
|
90
|
+
status: 'action_required',
|
|
91
|
+
title: 'Pipeline landing is stale',
|
|
92
|
+
summary: result.queue_state?.next_action || result.next_action,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
status: 'action_required',
|
|
98
|
+
title: 'Pipeline not ready to queue',
|
|
99
|
+
summary: result.queue_state?.next_action || result.next_action,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
43
103
|
export function writeGitHubCiStatus({ result, stepSummaryPath = null, outputPath = null }) {
|
|
44
104
|
const markdown = formatCiGateMarkdown(result);
|
|
45
105
|
|
|
@@ -67,6 +127,126 @@ export function writeGitHubCiStatus({ result, stepSummaryPath = null, outputPath
|
|
|
67
127
|
};
|
|
68
128
|
}
|
|
69
129
|
|
|
130
|
+
export function formatPipelineLandingMarkdown(result) {
|
|
131
|
+
const checkInfo = getPipelineLandingCheckInfo(result);
|
|
132
|
+
const landingStateLines = result.landing_error
|
|
133
|
+
? [`- ${result.landing_error}`]
|
|
134
|
+
: result.landing?.last_failure
|
|
135
|
+
? [
|
|
136
|
+
`- Failure: ${result.landing.last_failure.reason_code || 'landing_branch_materialization_failed'}`,
|
|
137
|
+
...(result.landing.last_failure.failed_branch ? [`- Failed branch: ${result.landing.last_failure.failed_branch}`] : []),
|
|
138
|
+
...(result.landing.last_failure.conflicting_files?.length
|
|
139
|
+
? [`- Conflicts: ${result.landing.last_failure.conflicting_files.join(', ')}`]
|
|
140
|
+
: []),
|
|
141
|
+
]
|
|
142
|
+
: result.landing?.stale
|
|
143
|
+
? (result.landing.stale_reasons?.length
|
|
144
|
+
? result.landing.stale_reasons.map((reason) => `- Stale: ${reason.summary}`)
|
|
145
|
+
: ['- Landing branch is stale and needs refresh'])
|
|
146
|
+
: ['- Current and ready for queueing'];
|
|
147
|
+
return [
|
|
148
|
+
'# Switchman Pipeline Landing',
|
|
149
|
+
'',
|
|
150
|
+
'## Check Summary',
|
|
151
|
+
`- Name: Switchman Pipeline Landing`,
|
|
152
|
+
`- Status: ${checkInfo.status}`,
|
|
153
|
+
`- Title: ${checkInfo.title}`,
|
|
154
|
+
`- Summary: ${checkInfo.summary}`,
|
|
155
|
+
'',
|
|
156
|
+
'## Pipeline',
|
|
157
|
+
`- Pipeline: ${result.pipeline_id}`,
|
|
158
|
+
`- Ready to queue: ${result.ready_to_queue ? 'yes' : 'no'}`,
|
|
159
|
+
`- Landing branch: ${result.landing.branch}`,
|
|
160
|
+
`- Strategy: ${result.landing.strategy}`,
|
|
161
|
+
`- Synthetic: ${result.landing.synthetic ? 'yes' : 'no'}`,
|
|
162
|
+
'',
|
|
163
|
+
'## Component Branches',
|
|
164
|
+
...(result.landing.component_branches?.length
|
|
165
|
+
? result.landing.component_branches.map((branch) => `- ${branch}`)
|
|
166
|
+
: ['- None']),
|
|
167
|
+
'',
|
|
168
|
+
'## Landing State',
|
|
169
|
+
...landingStateLines,
|
|
170
|
+
'',
|
|
171
|
+
'## Recovery State',
|
|
172
|
+
...(result.recovery_state
|
|
173
|
+
? [
|
|
174
|
+
`- Status: ${result.recovery_state.status}`,
|
|
175
|
+
...(result.recovery_state.recovery_path ? [`- Path: ${result.recovery_state.recovery_path}`] : []),
|
|
176
|
+
...(result.recovery_state.resume_command ? [`- Resume: ${result.recovery_state.resume_command}`] : []),
|
|
177
|
+
]
|
|
178
|
+
: ['- No active recovery worktree']),
|
|
179
|
+
'',
|
|
180
|
+
'## Stale Clusters',
|
|
181
|
+
...(result.stale_clusters?.length
|
|
182
|
+
? result.stale_clusters.map((cluster) => `- ${cluster.title}: ${cluster.detail} -> ${cluster.next_action}`)
|
|
183
|
+
: ['- None']),
|
|
184
|
+
'',
|
|
185
|
+
'## Stale Waves',
|
|
186
|
+
...(result.stale_causal_waves?.length
|
|
187
|
+
? result.stale_causal_waves.map((wave) => `- ${wave.summary}: affects ${wave.affected_pipeline_ids.join(', ') || 'unknown'} -> ${wave.cluster_count} cluster(s), ${wave.invalidation_count} invalidation(s)`)
|
|
188
|
+
: ['- None']),
|
|
189
|
+
'',
|
|
190
|
+
'## Policy & Stale Audit',
|
|
191
|
+
...(result.trust_audit?.length
|
|
192
|
+
? result.trust_audit.map((entry) => `- ${entry.created_at}: [${entry.category}] ${entry.summary} -> ${entry.next_action}`)
|
|
193
|
+
: ['- No recent policy or stale-wave audit entries']),
|
|
194
|
+
'',
|
|
195
|
+
'## Queue State',
|
|
196
|
+
`- Status: ${result.queue_state?.status || 'not_queued'}`,
|
|
197
|
+
...(result.queue_state?.item_id ? [`- Item: ${result.queue_state.item_id}`] : []),
|
|
198
|
+
`- Target branch: ${result.queue_state?.target_branch || 'main'}`,
|
|
199
|
+
...(result.queue_state?.merged_commit ? [`- Merged commit: ${result.queue_state.merged_commit}`] : []),
|
|
200
|
+
...(result.queue_state?.last_error_summary ? [`- Queue error: ${result.queue_state.last_error_summary}`] : []),
|
|
201
|
+
...(result.queue_state?.policy_override_summary ? [`- Policy override: ${result.queue_state.policy_override_summary}`] : []),
|
|
202
|
+
'',
|
|
203
|
+
'## Next Action',
|
|
204
|
+
`- ${result.next_action}`,
|
|
205
|
+
].join('\n');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function writeGitHubPipelineLandingStatus({ result, stepSummaryPath = null, outputPath = null }) {
|
|
209
|
+
const markdown = formatPipelineLandingMarkdown(result);
|
|
210
|
+
const checkInfo = getPipelineLandingCheckInfo(result);
|
|
211
|
+
|
|
212
|
+
if (stepSummaryPath) {
|
|
213
|
+
writeFileSync(stepSummaryPath, `${markdown}\n`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (outputPath) {
|
|
217
|
+
const lines = [
|
|
218
|
+
`switchman_pipeline_id=${result.pipeline_id}`,
|
|
219
|
+
`switchman_pipeline_ready=${result.ready_to_queue ? 'true' : 'false'}`,
|
|
220
|
+
`switchman_landing_branch=${result.landing.branch}`,
|
|
221
|
+
`switchman_landing_strategy=${result.landing.strategy}`,
|
|
222
|
+
`switchman_landing_synthetic=${result.landing.synthetic ? 'true' : 'false'}`,
|
|
223
|
+
`switchman_queue_status=${result.queue_state?.status || 'not_queued'}`,
|
|
224
|
+
`switchman_queue_item_id=${result.queue_state?.item_id || ''}`,
|
|
225
|
+
`switchman_queue_target_branch=${result.queue_state?.target_branch || 'main'}`,
|
|
226
|
+
`switchman_queue_merged_commit=${result.queue_state?.merged_commit || ''}`,
|
|
227
|
+
`switchman_stale_cluster_count=${result.stale_clusters?.length || 0}`,
|
|
228
|
+
`switchman_stale_cluster_summary=${JSON.stringify((result.stale_clusters || []).map((cluster) => `${cluster.affected_pipeline_id || cluster.affected_task_ids[0]}:${cluster.invalidation_count}`).join(' | '))}`,
|
|
229
|
+
`switchman_stale_wave_count=${result.stale_causal_waves?.length || 0}`,
|
|
230
|
+
`switchman_stale_wave_summary=${JSON.stringify((result.stale_causal_waves || []).map((wave) => `${wave.summary}:${wave.affected_pipeline_ids.join(',')}`).join(' | '))}`,
|
|
231
|
+
`switchman_trust_audit_count=${result.trust_audit?.length || 0}`,
|
|
232
|
+
`switchman_trust_audit_summary=${JSON.stringify((result.trust_audit || []).slice(0, 3).map((entry) => `${entry.category}:${entry.summary}`).join(' | '))}`,
|
|
233
|
+
`switchman_policy_override_summary=${JSON.stringify(result.policy_override_summary || '')}`,
|
|
234
|
+
`switchman_landing_next_action=${JSON.stringify(result.next_action)}`,
|
|
235
|
+
`switchman_check_name=${JSON.stringify('Switchman Pipeline Landing')}`,
|
|
236
|
+
`switchman_check_status=${checkInfo.status}`,
|
|
237
|
+
`switchman_check_title=${JSON.stringify(checkInfo.title)}`,
|
|
238
|
+
`switchman_check_summary=${JSON.stringify(checkInfo.summary)}`,
|
|
239
|
+
];
|
|
240
|
+
writeFileSync(outputPath, `${lines.join('\n')}\n`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
markdown,
|
|
245
|
+
wrote_step_summary: Boolean(stepSummaryPath),
|
|
246
|
+
wrote_output: Boolean(outputPath),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
70
250
|
export function installGitHubActionsWorkflow(repoRoot, workflowName = 'switchman-gate.yml') {
|
|
71
251
|
const workflowsDir = join(repoRoot, '.github', 'workflows');
|
|
72
252
|
if (!existsSync(workflowsDir)) {
|
|
@@ -84,6 +264,7 @@ on:
|
|
|
84
264
|
|
|
85
265
|
jobs:
|
|
86
266
|
switchman-gate:
|
|
267
|
+
name: Switchman Gate
|
|
87
268
|
runs-on: ubuntu-latest
|
|
88
269
|
steps:
|
|
89
270
|
- name: Checkout
|
|
@@ -98,7 +279,30 @@ jobs:
|
|
|
98
279
|
run: npm install -g switchman-dev
|
|
99
280
|
|
|
100
281
|
- name: Run Switchman CI gate
|
|
282
|
+
id: switchman_gate
|
|
283
|
+
continue-on-error: true
|
|
101
284
|
run: switchman gate ci --github
|
|
285
|
+
|
|
286
|
+
- name: Summarize Switchman result
|
|
287
|
+
if: always()
|
|
288
|
+
run: |
|
|
289
|
+
echo "Switchman status: \${{ steps.switchman_gate.outputs.switchman_ok }}"
|
|
290
|
+
echo "Switchman summary: \${{ steps.switchman_gate.outputs.switchman_summary }}"
|
|
291
|
+
|
|
292
|
+
- name: Enforce Switchman gate
|
|
293
|
+
if: always()
|
|
294
|
+
run: |
|
|
295
|
+
if [ "\${{ steps.switchman_gate.outputs.switchman_ok }}" != "true" ]; then
|
|
296
|
+
echo "Switchman blocked this change."
|
|
297
|
+
echo "\${{ steps.switchman_gate.outputs.switchman_summary }}"
|
|
298
|
+
exit 1
|
|
299
|
+
fi
|
|
300
|
+
|
|
301
|
+
- name: Sync Switchman PR state
|
|
302
|
+
if: github.event_name == 'pull_request'
|
|
303
|
+
continue-on-error: true
|
|
304
|
+
run: |
|
|
305
|
+
switchman pipeline sync-pr --pipeline-from-env --skip-missing-pipeline --pr-from-env --github
|
|
102
306
|
`;
|
|
103
307
|
|
|
104
308
|
writeFileSync(workflowPath, workflow);
|