@nforma.ai/nforma 0.2.1
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/LICENSE +22 -0
- package/README.md +1024 -0
- package/agents/qgsd-codebase-mapper.md +764 -0
- package/agents/qgsd-debugger.md +1201 -0
- package/agents/qgsd-executor.md +472 -0
- package/agents/qgsd-integration-checker.md +443 -0
- package/agents/qgsd-phase-researcher.md +502 -0
- package/agents/qgsd-plan-checker.md +643 -0
- package/agents/qgsd-planner.md +1182 -0
- package/agents/qgsd-project-researcher.md +621 -0
- package/agents/qgsd-quorum-orchestrator.md +628 -0
- package/agents/qgsd-quorum-slot-worker.md +41 -0
- package/agents/qgsd-quorum-synthesizer.md +133 -0
- package/agents/qgsd-quorum-test-worker.md +37 -0
- package/agents/qgsd-quorum-worker.md +161 -0
- package/agents/qgsd-research-synthesizer.md +239 -0
- package/agents/qgsd-roadmapper.md +660 -0
- package/agents/qgsd-verifier.md +628 -0
- package/bin/accept-debug-invariant.cjs +165 -0
- package/bin/account-manager.cjs +719 -0
- package/bin/aggregate-requirements.cjs +466 -0
- package/bin/analyze-assumptions.cjs +757 -0
- package/bin/analyze-state-space.cjs +921 -0
- package/bin/attribute-trace-divergence.cjs +150 -0
- package/bin/auth-drivers/gh-cli.cjs +93 -0
- package/bin/auth-drivers/index.cjs +46 -0
- package/bin/auth-drivers/pool.cjs +67 -0
- package/bin/auth-drivers/simple.cjs +95 -0
- package/bin/autoClosePtoF.cjs +110 -0
- package/bin/blessed-terminal.cjs +350 -0
- package/bin/build-phase-index.cjs +472 -0
- package/bin/call-quorum-slot.cjs +541 -0
- package/bin/ccr-secure-config.cjs +99 -0
- package/bin/ccr-secure-start.cjs +83 -0
- package/bin/check-bundled-sdks.cjs +177 -0
- package/bin/check-coverage-guard.cjs +112 -0
- package/bin/check-liveness-fairness.cjs +95 -0
- package/bin/check-mcp-health.cjs +123 -0
- package/bin/check-provider-health.cjs +395 -0
- package/bin/check-results-exit.cjs +24 -0
- package/bin/check-spec-sync.cjs +360 -0
- package/bin/check-trace-redaction.cjs +271 -0
- package/bin/check-trace-schema-drift.cjs +99 -0
- package/bin/compareDrift.cjs +21 -0
- package/bin/conformance-schema.cjs +12 -0
- package/bin/count-scenarios.cjs +420 -0
- package/bin/debt-dedup.cjs +144 -0
- package/bin/debt-ledger.cjs +61 -0
- package/bin/debt-retention.cjs +76 -0
- package/bin/debt-state-machine.cjs +80 -0
- package/bin/detect-coverage-gaps.cjs +204 -0
- package/bin/detect-project-intent.cjs +362 -0
- package/bin/export-prism-constants.cjs +164 -0
- package/bin/extract-annotations.cjs +633 -0
- package/bin/extractFormalExpected.cjs +104 -0
- package/bin/fingerprint-drift.cjs +24 -0
- package/bin/fingerprint-issue.cjs +46 -0
- package/bin/formal-core.cjs +519 -0
- package/bin/formal-ref-linker.cjs +141 -0
- package/bin/formal-test-sync.cjs +788 -0
- package/bin/generate-formal-specs.cjs +588 -0
- package/bin/generate-petri-net.cjs +397 -0
- package/bin/generate-phase-spec.cjs +249 -0
- package/bin/generate-proposed-changes.cjs +194 -0
- package/bin/generate-tla-cfg.cjs +122 -0
- package/bin/generate-traceability-matrix.cjs +701 -0
- package/bin/generate-triage-bundle.cjs +300 -0
- package/bin/gh-account-rotate.cjs +34 -0
- package/bin/initialize-model-registry.cjs +105 -0
- package/bin/install-formal-tools.cjs +382 -0
- package/bin/install.js +2424 -0
- package/bin/isNumericThreshold.cjs +34 -0
- package/bin/issue-classifier.cjs +151 -0
- package/bin/levenshtein.cjs +74 -0
- package/bin/lint-formal-models.cjs +580 -0
- package/bin/load-baseline-requirements.cjs +275 -0
- package/bin/manage-agents-core.cjs +815 -0
- package/bin/migrate-formal-dir.cjs +172 -0
- package/bin/migrate-planning.cjs +206 -0
- package/bin/migrate-to-slots.cjs +255 -0
- package/bin/nForma.cjs +2726 -0
- package/bin/observe-config.cjs +353 -0
- package/bin/observe-debt-writer.cjs +140 -0
- package/bin/observe-handler-grafana.cjs +128 -0
- package/bin/observe-handler-internal.cjs +301 -0
- package/bin/observe-handler-logstash.cjs +153 -0
- package/bin/observe-handler-prometheus.cjs +185 -0
- package/bin/observe-handlers.cjs +436 -0
- package/bin/observe-registry.cjs +131 -0
- package/bin/observe-render.cjs +168 -0
- package/bin/planning-paths.cjs +167 -0
- package/bin/polyrepo.cjs +560 -0
- package/bin/prism-priority.cjs +153 -0
- package/bin/probe-quorum-slots.cjs +167 -0
- package/bin/promote-model.cjs +225 -0
- package/bin/propose-debug-invariants.cjs +165 -0
- package/bin/providers.json +392 -0
- package/bin/pty-proxy.py +129 -0
- package/bin/qgsd-solve.cjs +2477 -0
- package/bin/quorum-consensus-gate.cjs +238 -0
- package/bin/quorum-formal-context.cjs +183 -0
- package/bin/quorum-slot-dispatch.cjs +934 -0
- package/bin/read-policy.cjs +60 -0
- package/bin/requirement-map.cjs +63 -0
- package/bin/requirements-core.cjs +247 -0
- package/bin/resolve-cli.cjs +101 -0
- package/bin/review-mcp-logs.cjs +294 -0
- package/bin/run-account-manager-tlc.cjs +188 -0
- package/bin/run-account-pool-alloy.cjs +158 -0
- package/bin/run-alloy.cjs +153 -0
- package/bin/run-audit-alloy.cjs +187 -0
- package/bin/run-breaker-tlc.cjs +181 -0
- package/bin/run-formal-check.cjs +395 -0
- package/bin/run-formal-verify.cjs +701 -0
- package/bin/run-installer-alloy.cjs +188 -0
- package/bin/run-oauth-rotation-prism.cjs +132 -0
- package/bin/run-oscillation-tlc.cjs +202 -0
- package/bin/run-phase-tlc.cjs +228 -0
- package/bin/run-prism.cjs +446 -0
- package/bin/run-protocol-tlc.cjs +201 -0
- package/bin/run-quorum-composition-alloy.cjs +155 -0
- package/bin/run-sensitivity-sweep.cjs +231 -0
- package/bin/run-stop-hook-tlc.cjs +188 -0
- package/bin/run-tlc.cjs +467 -0
- package/bin/run-transcript-alloy.cjs +173 -0
- package/bin/run-uppaal.cjs +264 -0
- package/bin/secrets.cjs +134 -0
- package/bin/sensitivity-report.cjs +219 -0
- package/bin/sensitivity-sweep-feedback.cjs +194 -0
- package/bin/set-secret.cjs +29 -0
- package/bin/setup-telemetry-cron.sh +36 -0
- package/bin/sweepPtoF.cjs +63 -0
- package/bin/sync-baseline-requirements.cjs +290 -0
- package/bin/task-envelope.cjs +360 -0
- package/bin/telemetry-collector.cjs +229 -0
- package/bin/unified-mcp-server.mjs +735 -0
- package/bin/update-agents.cjs +369 -0
- package/bin/update-scoreboard.cjs +1134 -0
- package/bin/validate-debt-entry.cjs +207 -0
- package/bin/validate-invariant.cjs +419 -0
- package/bin/validate-memory.cjs +389 -0
- package/bin/validate-requirements-haiku.cjs +435 -0
- package/bin/validate-traces.cjs +438 -0
- package/bin/verify-formal-results.cjs +124 -0
- package/bin/verify-quorum-health.cjs +273 -0
- package/bin/write-check-result.cjs +106 -0
- package/bin/xstate-to-tla.cjs +483 -0
- package/bin/xstate-trace-walker.cjs +205 -0
- package/commands/qgsd/add-phase.md +43 -0
- package/commands/qgsd/add-requirement.md +24 -0
- package/commands/qgsd/add-todo.md +47 -0
- package/commands/qgsd/audit-milestone.md +37 -0
- package/commands/qgsd/check-todos.md +45 -0
- package/commands/qgsd/cleanup.md +18 -0
- package/commands/qgsd/close-formal-gaps.md +33 -0
- package/commands/qgsd/complete-milestone.md +136 -0
- package/commands/qgsd/debug.md +166 -0
- package/commands/qgsd/discuss-phase.md +83 -0
- package/commands/qgsd/execute-phase.md +117 -0
- package/commands/qgsd/fix-tests.md +27 -0
- package/commands/qgsd/formal-test-sync.md +32 -0
- package/commands/qgsd/health.md +22 -0
- package/commands/qgsd/help.md +22 -0
- package/commands/qgsd/insert-phase.md +32 -0
- package/commands/qgsd/join-discord.md +18 -0
- package/commands/qgsd/list-phase-assumptions.md +46 -0
- package/commands/qgsd/map-codebase.md +71 -0
- package/commands/qgsd/map-requirements.md +20 -0
- package/commands/qgsd/mcp-restart.md +176 -0
- package/commands/qgsd/mcp-set-model.md +134 -0
- package/commands/qgsd/mcp-setup.md +1371 -0
- package/commands/qgsd/mcp-status.md +274 -0
- package/commands/qgsd/mcp-update.md +238 -0
- package/commands/qgsd/new-milestone.md +44 -0
- package/commands/qgsd/new-project.md +42 -0
- package/commands/qgsd/observe.md +260 -0
- package/commands/qgsd/pause-work.md +38 -0
- package/commands/qgsd/plan-milestone-gaps.md +34 -0
- package/commands/qgsd/plan-phase.md +44 -0
- package/commands/qgsd/polyrepo.md +50 -0
- package/commands/qgsd/progress.md +24 -0
- package/commands/qgsd/queue.md +54 -0
- package/commands/qgsd/quick.md +133 -0
- package/commands/qgsd/quorum-test.md +275 -0
- package/commands/qgsd/quorum.md +707 -0
- package/commands/qgsd/reapply-patches.md +110 -0
- package/commands/qgsd/remove-phase.md +31 -0
- package/commands/qgsd/research-phase.md +189 -0
- package/commands/qgsd/resume-work.md +40 -0
- package/commands/qgsd/set-profile.md +34 -0
- package/commands/qgsd/settings.md +39 -0
- package/commands/qgsd/solve.md +565 -0
- package/commands/qgsd/sync-baselines.md +119 -0
- package/commands/qgsd/triage.md +233 -0
- package/commands/qgsd/update.md +37 -0
- package/commands/qgsd/verify-work.md +38 -0
- package/hooks/dist/config-loader.js +297 -0
- package/hooks/dist/conformance-schema.cjs +12 -0
- package/hooks/dist/gsd-context-monitor.js +64 -0
- package/hooks/dist/qgsd-check-update.js +62 -0
- package/hooks/dist/qgsd-circuit-breaker.js +682 -0
- package/hooks/dist/qgsd-precompact.js +156 -0
- package/hooks/dist/qgsd-prompt.js +653 -0
- package/hooks/dist/qgsd-session-start.js +122 -0
- package/hooks/dist/qgsd-slot-correlator.js +58 -0
- package/hooks/dist/qgsd-spec-regen.js +86 -0
- package/hooks/dist/qgsd-statusline.js +91 -0
- package/hooks/dist/qgsd-stop.js +553 -0
- package/hooks/dist/qgsd-token-collector.js +133 -0
- package/hooks/dist/unified-mcp-server.mjs +669 -0
- package/package.json +95 -0
- package/scripts/build-hooks.js +46 -0
- package/scripts/postinstall.js +48 -0
- package/scripts/secret-audit.sh +45 -0
- package/templates/qgsd.json +49 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source handler implementations for /qgsd:observe
|
|
3
|
+
* GitHub, Sentry, sentry-feedback, and bash handlers
|
|
4
|
+
*
|
|
5
|
+
* ALL handlers return the SAME schema:
|
|
6
|
+
* { source_label, source_type, status: "ok"|"error"|"pending_mcp", issues: [...], error?, _mcp_instruction? }
|
|
7
|
+
*
|
|
8
|
+
* GitHub and bash handlers do their work directly (CLI calls via execFileSync).
|
|
9
|
+
* Sentry and sentry-feedback return status: "pending_mcp" with _mcp_instruction.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { execFileSync } = require('node:child_process');
|
|
13
|
+
|
|
14
|
+
// Severity labels recognized from GitHub labels (ordered by priority)
|
|
15
|
+
const SEVERITY_LABELS = ['critical', 'error', 'bug', 'warning', 'enhancement', 'info'];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse a duration string like "7d", "24h", "30m" into milliseconds
|
|
19
|
+
* @param {string} duration - Duration string
|
|
20
|
+
* @returns {number} Milliseconds
|
|
21
|
+
*/
|
|
22
|
+
function parseDuration(duration) {
|
|
23
|
+
if (!duration) return 0;
|
|
24
|
+
const match = String(duration).match(/^(\d+)([dhms])$/);
|
|
25
|
+
if (!match) return 0;
|
|
26
|
+
const num = parseInt(match[1], 10);
|
|
27
|
+
const unit = match[2];
|
|
28
|
+
const multipliers = { d: 86400000, h: 3600000, m: 60000, s: 1000 };
|
|
29
|
+
return num * (multipliers[unit] || 0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Format age from ISO date to human-readable string
|
|
34
|
+
* @param {string} isoDate - ISO8601 date string
|
|
35
|
+
* @returns {string} Human-readable age like "5m", "2h", "3d"
|
|
36
|
+
*/
|
|
37
|
+
function formatAge(isoDate) {
|
|
38
|
+
if (!isoDate) return 'unknown';
|
|
39
|
+
const diffMs = Date.now() - new Date(isoDate).getTime();
|
|
40
|
+
if (diffMs < 0) return 'future';
|
|
41
|
+
const minutes = Math.floor(diffMs / 60000);
|
|
42
|
+
if (minutes < 60) return `${minutes}m`;
|
|
43
|
+
const hours = Math.floor(minutes / 60);
|
|
44
|
+
if (hours < 24) return `${hours}h`;
|
|
45
|
+
const days = Math.floor(hours / 24);
|
|
46
|
+
return `${days}d`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Classify severity from GitHub labels
|
|
51
|
+
* @param {Array} labels - Array of label objects with 'name' field, or strings
|
|
52
|
+
* @returns {string} Severity string
|
|
53
|
+
*/
|
|
54
|
+
function classifySeverityFromLabels(labels) {
|
|
55
|
+
if (!Array.isArray(labels)) return 'info';
|
|
56
|
+
const labelNames = labels.map(l => (typeof l === 'string' ? l : l.name || '').toLowerCase());
|
|
57
|
+
for (const sev of SEVERITY_LABELS) {
|
|
58
|
+
if (labelNames.some(name => name.includes(sev))) {
|
|
59
|
+
return sev;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return 'info';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Detect repo from git remote
|
|
67
|
+
* @param {Function} [execFn] - execFileSync function for testing
|
|
68
|
+
* @returns {string|null} owner/repo string or null
|
|
69
|
+
*/
|
|
70
|
+
function detectRepoFromGit(execFn) {
|
|
71
|
+
const execFile = execFn || execFileSync;
|
|
72
|
+
try {
|
|
73
|
+
const url = execFile('git', ['remote', 'get-url', 'origin'], { encoding: 'utf8' }).trim();
|
|
74
|
+
// Parse SSH: git@github.com:owner/repo.git or HTTPS: https://github.com/owner/repo.git
|
|
75
|
+
const sshMatch = url.match(/github\.com[:/]([^/]+\/[^/.]+)/);
|
|
76
|
+
if (sshMatch) return sshMatch[1];
|
|
77
|
+
return null;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* GitHub source handler
|
|
85
|
+
* Uses gh CLI via execFileSync (no shell injection risk) to fetch issues
|
|
86
|
+
*
|
|
87
|
+
* @param {object} sourceConfig - { type, label, repo?, filter?: { state, labels, since, limit } }
|
|
88
|
+
* @param {object} options - { sinceOverride?, limitOverride?, execFn? }
|
|
89
|
+
* @returns {object} Standard schema result
|
|
90
|
+
*/
|
|
91
|
+
function handleGitHub(sourceConfig, options) {
|
|
92
|
+
const label = sourceConfig.label || 'GitHub';
|
|
93
|
+
const execFile = options.execFn || execFileSync;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const repo = sourceConfig.repo || detectRepoFromGit(execFile);
|
|
97
|
+
if (!repo) {
|
|
98
|
+
return {
|
|
99
|
+
source_label: label,
|
|
100
|
+
source_type: 'github',
|
|
101
|
+
status: 'error',
|
|
102
|
+
error: 'Could not determine repo — set repo in config or ensure git remote is configured',
|
|
103
|
+
issues: []
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const filter = sourceConfig.filter || {};
|
|
108
|
+
const state = filter.state || 'open';
|
|
109
|
+
const limit = options.limitOverride || filter.limit || 10;
|
|
110
|
+
const since = options.sinceOverride || filter.since;
|
|
111
|
+
const labels = filter.labels || [];
|
|
112
|
+
|
|
113
|
+
// Build gh CLI args — using execFileSync (array args, no shell interpolation)
|
|
114
|
+
const args = ['issue', 'list', '--repo', repo, '--state', state,
|
|
115
|
+
'--limit', String(limit), '--json', 'number,title,url,labels,createdAt,assignees'];
|
|
116
|
+
|
|
117
|
+
for (const lbl of labels) {
|
|
118
|
+
args.push('--label', lbl);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const output = execFile('gh', args, { encoding: 'utf8' });
|
|
122
|
+
let issues = JSON.parse(output);
|
|
123
|
+
|
|
124
|
+
// Apply since filter
|
|
125
|
+
if (since) {
|
|
126
|
+
const cutoffMs = parseDuration(since);
|
|
127
|
+
if (cutoffMs > 0) {
|
|
128
|
+
const cutoff = Date.now() - cutoffMs;
|
|
129
|
+
issues = issues.filter(i => new Date(i.createdAt).getTime() > cutoff);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
source_label: label,
|
|
135
|
+
source_type: 'github',
|
|
136
|
+
status: 'ok',
|
|
137
|
+
issues: issues.map(issue => ({
|
|
138
|
+
id: `gh-${issue.number}`,
|
|
139
|
+
title: issue.title,
|
|
140
|
+
severity: classifySeverityFromLabels(issue.labels),
|
|
141
|
+
url: issue.url || '',
|
|
142
|
+
age: formatAge(issue.createdAt),
|
|
143
|
+
created_at: issue.createdAt,
|
|
144
|
+
meta: `#${issue.number} · ${(issue.assignees || []).length} assignee(s)`,
|
|
145
|
+
source_type: 'github',
|
|
146
|
+
issue_type: sourceConfig.issue_type || 'issue'
|
|
147
|
+
}))
|
|
148
|
+
};
|
|
149
|
+
} catch (err) {
|
|
150
|
+
return {
|
|
151
|
+
source_label: label,
|
|
152
|
+
source_type: 'github',
|
|
153
|
+
status: 'error',
|
|
154
|
+
error: `GitHub fetch failed: ${err.message}`,
|
|
155
|
+
issues: []
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Sentry source handler
|
|
162
|
+
* Returns pending_mcp status with instruction for the observe command to execute MCP call
|
|
163
|
+
*
|
|
164
|
+
* @param {object} sourceConfig - { type, label, project?, filter?: { status, since } }
|
|
165
|
+
* @param {object} options - { sinceOverride? }
|
|
166
|
+
* @returns {object} Standard schema result with pending_mcp status
|
|
167
|
+
*/
|
|
168
|
+
function handleSentry(sourceConfig, options) {
|
|
169
|
+
const label = sourceConfig.label || 'Sentry';
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const project = sourceConfig.project || '';
|
|
173
|
+
const parts = project.split('/');
|
|
174
|
+
const organization_slug = parts[0] || '';
|
|
175
|
+
const project_slug = parts[1] || '';
|
|
176
|
+
|
|
177
|
+
const filter = sourceConfig.filter || {};
|
|
178
|
+
const status = filter.status || 'unresolved';
|
|
179
|
+
const since = options.sinceOverride || filter.since || '24h';
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
source_label: label,
|
|
183
|
+
source_type: 'sentry',
|
|
184
|
+
status: 'pending_mcp',
|
|
185
|
+
issues: [],
|
|
186
|
+
_mcp_instruction: {
|
|
187
|
+
type: 'mcp',
|
|
188
|
+
tool: 'list_project_issues',
|
|
189
|
+
params: {
|
|
190
|
+
organization_slug,
|
|
191
|
+
project_slug,
|
|
192
|
+
query: `is:${status} firstSeen:>${since}`
|
|
193
|
+
},
|
|
194
|
+
mapper: 'mapSentryIssuesToSchema'
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
} catch (err) {
|
|
198
|
+
return {
|
|
199
|
+
source_label: label,
|
|
200
|
+
source_type: 'sentry',
|
|
201
|
+
status: 'error',
|
|
202
|
+
error: `Sentry handler failed: ${err.message}`,
|
|
203
|
+
issues: []
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Map raw Sentry MCP result to standard schema
|
|
210
|
+
* @param {Array} mcpResult - Array of Sentry issue objects from MCP
|
|
211
|
+
* @param {object} sourceConfig - Source config for labels
|
|
212
|
+
* @returns {object} Standard schema result with mapped issues
|
|
213
|
+
*/
|
|
214
|
+
function mapSentryIssuesToSchema(mcpResult, sourceConfig) {
|
|
215
|
+
const label = sourceConfig.label || 'Sentry';
|
|
216
|
+
const levelMap = { fatal: 'error', error: 'error', warning: 'warning', info: 'info' };
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const issues = (Array.isArray(mcpResult) ? mcpResult : []).map(issue => ({
|
|
220
|
+
id: `sentry-${issue.id}`,
|
|
221
|
+
title: issue.title || issue.culprit || 'Unknown Sentry issue',
|
|
222
|
+
severity: levelMap[issue.level] || 'info',
|
|
223
|
+
url: issue.permalink || '',
|
|
224
|
+
age: formatAge(issue.firstSeen || issue.dateCreated),
|
|
225
|
+
created_at: issue.firstSeen || issue.dateCreated || new Date().toISOString(),
|
|
226
|
+
meta: `${issue.count || 0} events · ${issue.userCount || 0} users`,
|
|
227
|
+
source_type: 'sentry',
|
|
228
|
+
issue_type: sourceConfig.issue_type || 'issue'
|
|
229
|
+
}));
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
source_label: label,
|
|
233
|
+
source_type: 'sentry',
|
|
234
|
+
status: 'ok',
|
|
235
|
+
issues
|
|
236
|
+
};
|
|
237
|
+
} catch (err) {
|
|
238
|
+
return {
|
|
239
|
+
source_label: label,
|
|
240
|
+
source_type: 'sentry',
|
|
241
|
+
status: 'error',
|
|
242
|
+
error: `Sentry mapping failed: ${err.message}`,
|
|
243
|
+
issues: []
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Sentry feedback source handler
|
|
250
|
+
* Returns pending_mcp status with instruction
|
|
251
|
+
*
|
|
252
|
+
* @param {object} sourceConfig - { type, label, project?, filter?: { since } }
|
|
253
|
+
* @param {object} options - { sinceOverride? }
|
|
254
|
+
* @returns {object} Standard schema result with pending_mcp status
|
|
255
|
+
*/
|
|
256
|
+
function handleSentryFeedback(sourceConfig, options) {
|
|
257
|
+
const label = sourceConfig.label || 'Sentry Feedback';
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const project = sourceConfig.project || '';
|
|
261
|
+
const parts = project.split('/');
|
|
262
|
+
const organization_slug = parts[0] || '';
|
|
263
|
+
const project_slug = parts[1] || '';
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
source_label: label,
|
|
267
|
+
source_type: 'sentry-feedback',
|
|
268
|
+
status: 'pending_mcp',
|
|
269
|
+
issues: [],
|
|
270
|
+
_mcp_instruction: {
|
|
271
|
+
type: 'mcp',
|
|
272
|
+
tool: 'list_user_feedback',
|
|
273
|
+
params: {
|
|
274
|
+
organization_slug,
|
|
275
|
+
project_slug
|
|
276
|
+
},
|
|
277
|
+
mapper: 'mapSentryFeedbackToSchema'
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
} catch (err) {
|
|
281
|
+
return {
|
|
282
|
+
source_label: label,
|
|
283
|
+
source_type: 'sentry-feedback',
|
|
284
|
+
status: 'error',
|
|
285
|
+
error: `Sentry feedback handler failed: ${err.message}`,
|
|
286
|
+
issues: []
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Map raw Sentry feedback MCP result to standard schema
|
|
293
|
+
* @param {Array} mcpResult - Array of feedback objects from MCP
|
|
294
|
+
* @param {object} sourceConfig - Source config for labels
|
|
295
|
+
* @returns {object} Standard schema result with mapped feedback
|
|
296
|
+
*/
|
|
297
|
+
function mapSentryFeedbackToSchema(mcpResult, sourceConfig) {
|
|
298
|
+
const label = sourceConfig.label || 'Sentry Feedback';
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const issues = (Array.isArray(mcpResult) ? mcpResult : []).map((fb, idx) => {
|
|
302
|
+
const comment = fb.comments || fb.message || 'No comment';
|
|
303
|
+
const truncated = comment.length > 80 ? comment.slice(0, 80) + '...' : comment;
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
id: `feedback-${fb.id || idx}`,
|
|
307
|
+
title: `[Feedback] ${truncated}`,
|
|
308
|
+
severity: 'info',
|
|
309
|
+
url: fb.url || '',
|
|
310
|
+
age: formatAge(fb.dateCreated),
|
|
311
|
+
created_at: fb.dateCreated || new Date().toISOString(),
|
|
312
|
+
meta: fb.email ? `by ${fb.email}` : 'anonymous',
|
|
313
|
+
source_type: 'sentry-feedback',
|
|
314
|
+
issue_type: sourceConfig.issue_type || 'issue'
|
|
315
|
+
};
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
source_label: label,
|
|
320
|
+
source_type: 'sentry-feedback',
|
|
321
|
+
status: 'ok',
|
|
322
|
+
issues
|
|
323
|
+
};
|
|
324
|
+
} catch (err) {
|
|
325
|
+
return {
|
|
326
|
+
source_label: label,
|
|
327
|
+
source_type: 'sentry-feedback',
|
|
328
|
+
status: 'error',
|
|
329
|
+
error: `Sentry feedback mapping failed: ${err.message}`,
|
|
330
|
+
issues: []
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Bash source handler
|
|
337
|
+
* Uses execFileSync with ['sh', '-c', command] — command comes from user's own config
|
|
338
|
+
*
|
|
339
|
+
* @param {object} sourceConfig - { type, label, command, parser?: "lines"|"json" }
|
|
340
|
+
* @param {object} options - { execFn? }
|
|
341
|
+
* @returns {object} Standard schema result
|
|
342
|
+
*/
|
|
343
|
+
function handleBash(sourceConfig, options) {
|
|
344
|
+
const label = sourceConfig.label || 'Bash';
|
|
345
|
+
const execFile = options.execFn || execFileSync;
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
if (!sourceConfig.command) {
|
|
349
|
+
return {
|
|
350
|
+
source_label: label,
|
|
351
|
+
source_type: 'bash',
|
|
352
|
+
status: 'error',
|
|
353
|
+
error: 'No command configured for bash source',
|
|
354
|
+
issues: []
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// execFileSync with ['sh', '-c', command] — command is from trusted config file
|
|
359
|
+
const output = execFile('sh', ['-c', sourceConfig.command], { encoding: 'utf8' });
|
|
360
|
+
const parser = sourceConfig.parser || 'lines';
|
|
361
|
+
|
|
362
|
+
let issues;
|
|
363
|
+
if (parser === 'json') {
|
|
364
|
+
const parsed = JSON.parse(output);
|
|
365
|
+
const items = Array.isArray(parsed) ? parsed : [];
|
|
366
|
+
issues = items.map((item, idx) => ({
|
|
367
|
+
id: `bash-${idx}`,
|
|
368
|
+
title: item.title || String(item),
|
|
369
|
+
severity: item.severity || 'info',
|
|
370
|
+
url: item.url || '',
|
|
371
|
+
age: '',
|
|
372
|
+
created_at: new Date().toISOString(),
|
|
373
|
+
meta: '',
|
|
374
|
+
source_type: 'bash',
|
|
375
|
+
issue_type: sourceConfig.issue_type || 'issue'
|
|
376
|
+
}));
|
|
377
|
+
} else {
|
|
378
|
+
// lines parser
|
|
379
|
+
const lines = output.split('\n').filter(l => l.trim() !== '');
|
|
380
|
+
issues = lines.map((line, idx) => ({
|
|
381
|
+
id: `bash-${idx}`,
|
|
382
|
+
title: line.trim(),
|
|
383
|
+
severity: 'info',
|
|
384
|
+
url: '',
|
|
385
|
+
age: '',
|
|
386
|
+
created_at: new Date().toISOString(),
|
|
387
|
+
meta: '',
|
|
388
|
+
source_type: 'bash',
|
|
389
|
+
issue_type: sourceConfig.issue_type || 'issue'
|
|
390
|
+
}));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
source_label: label,
|
|
395
|
+
source_type: 'bash',
|
|
396
|
+
status: 'ok',
|
|
397
|
+
issues
|
|
398
|
+
};
|
|
399
|
+
} catch (err) {
|
|
400
|
+
return {
|
|
401
|
+
source_label: label,
|
|
402
|
+
source_type: 'bash',
|
|
403
|
+
status: 'error',
|
|
404
|
+
error: `Bash command failed: ${err.message}`,
|
|
405
|
+
issues: []
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Production source handlers (v0.27-04)
|
|
411
|
+
const { handlePrometheus } = require('./observe-handler-prometheus.cjs');
|
|
412
|
+
const { handleGrafana } = require('./observe-handler-grafana.cjs');
|
|
413
|
+
const { handleLogstash } = require('./observe-handler-logstash.cjs');
|
|
414
|
+
|
|
415
|
+
// Internal work detection handler
|
|
416
|
+
const { handleInternal } = require('./observe-handler-internal.cjs');
|
|
417
|
+
|
|
418
|
+
module.exports = {
|
|
419
|
+
handleGitHub,
|
|
420
|
+
handleSentry,
|
|
421
|
+
handleSentryFeedback,
|
|
422
|
+
handleBash,
|
|
423
|
+
mapSentryIssuesToSchema,
|
|
424
|
+
mapSentryFeedbackToSchema,
|
|
425
|
+
// Production handlers (v0.27-04)
|
|
426
|
+
handlePrometheus,
|
|
427
|
+
handleGrafana,
|
|
428
|
+
handleLogstash,
|
|
429
|
+
// Internal work detection (quick-168)
|
|
430
|
+
handleInternal,
|
|
431
|
+
// Exported for testing
|
|
432
|
+
parseDuration,
|
|
433
|
+
formatAge,
|
|
434
|
+
classifySeverityFromLabels,
|
|
435
|
+
detectRepoFromGit
|
|
436
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source handler registry with pluggable dispatch
|
|
3
|
+
* Provides register/get/list/dispatch with timeout wrapping and Promise.allSettled parallel dispatch
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Internal handler map
|
|
7
|
+
const handlers = new Map();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Register a source handler function
|
|
11
|
+
* @param {string} sourceType - Source type identifier (e.g., "github", "sentry")
|
|
12
|
+
* @param {Function} handlerFn - Async handler: (sourceConfig, options) -> { source_label, source_type, status, issues[], error? }
|
|
13
|
+
* @throws {Error} If sourceType is already registered
|
|
14
|
+
*/
|
|
15
|
+
function registerHandler(sourceType, handlerFn) {
|
|
16
|
+
if (handlers.has(sourceType)) {
|
|
17
|
+
throw new Error(`Handler already registered for source type: ${sourceType}`);
|
|
18
|
+
}
|
|
19
|
+
if (typeof handlerFn !== 'function') {
|
|
20
|
+
throw new Error(`Handler must be a function, got: ${typeof handlerFn}`);
|
|
21
|
+
}
|
|
22
|
+
handlers.set(sourceType, handlerFn);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get the registered handler for a source type
|
|
27
|
+
* @param {string} sourceType - Source type identifier
|
|
28
|
+
* @returns {Function|null} Handler function or null if not registered
|
|
29
|
+
*/
|
|
30
|
+
function getHandler(sourceType) {
|
|
31
|
+
return handlers.get(sourceType) || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* List all registered source types
|
|
36
|
+
* @returns {string[]} Array of registered source type strings
|
|
37
|
+
*/
|
|
38
|
+
function listHandlers() {
|
|
39
|
+
return Array.from(handlers.keys());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Dispatch a single source with timeout wrapping
|
|
44
|
+
* Looks up handler, wraps in Promise.race with timeout, returns standard schema on error
|
|
45
|
+
*
|
|
46
|
+
* @param {object} sourceConfig - Source configuration object with type, label, etc.
|
|
47
|
+
* @param {object} options - Options passed to handler (sinceOverride, limitOverride)
|
|
48
|
+
* @param {number} [timeoutSeconds=10] - Timeout in seconds
|
|
49
|
+
* @returns {Promise<object>} Standard schema result { source_label, source_type, status, issues[], error? }
|
|
50
|
+
*/
|
|
51
|
+
async function dispatchSource(sourceConfig, options, timeoutSeconds) {
|
|
52
|
+
const timeout = timeoutSeconds ?? 10;
|
|
53
|
+
const label = sourceConfig.label || sourceConfig.type || 'unknown';
|
|
54
|
+
const type = sourceConfig.type || 'unknown';
|
|
55
|
+
|
|
56
|
+
const handlerFn = getHandler(type);
|
|
57
|
+
if (!handlerFn) {
|
|
58
|
+
return {
|
|
59
|
+
source_label: label,
|
|
60
|
+
source_type: type,
|
|
61
|
+
status: 'error',
|
|
62
|
+
error: `No handler registered for type: ${type}`,
|
|
63
|
+
issues: []
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const handlerPromise = handlerFn(sourceConfig, options || {});
|
|
69
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
70
|
+
setTimeout(() => reject(new Error(`Timeout after ${timeout}s`)), timeout * 1000)
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const result = await Promise.race([handlerPromise, timeoutPromise]);
|
|
74
|
+
return result;
|
|
75
|
+
} catch (err) {
|
|
76
|
+
return {
|
|
77
|
+
source_label: label,
|
|
78
|
+
source_type: type,
|
|
79
|
+
status: 'error',
|
|
80
|
+
error: err.message || 'Unknown error',
|
|
81
|
+
issues: []
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Dispatch all sources in parallel via Promise.allSettled
|
|
88
|
+
* One failing source does not block others (OBS-08)
|
|
89
|
+
*
|
|
90
|
+
* @param {object[]} sources - Array of source config objects
|
|
91
|
+
* @param {object} options - Options passed to each handler
|
|
92
|
+
* @returns {Promise<object[]>} Array of results (standard schema)
|
|
93
|
+
*/
|
|
94
|
+
async function dispatchAll(sources, options) {
|
|
95
|
+
const promises = sources.map(source =>
|
|
96
|
+
dispatchSource(source, options, source.timeout)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const settled = await Promise.allSettled(promises);
|
|
100
|
+
|
|
101
|
+
return settled.map((result, idx) => {
|
|
102
|
+
if (result.status === 'fulfilled') {
|
|
103
|
+
return result.value;
|
|
104
|
+
}
|
|
105
|
+
// Rejected — should not happen since dispatchSource catches errors,
|
|
106
|
+
// but handle defensively
|
|
107
|
+
return {
|
|
108
|
+
source_label: sources[idx].label || sources[idx].type || 'unknown',
|
|
109
|
+
source_type: sources[idx].type || 'unknown',
|
|
110
|
+
status: 'error',
|
|
111
|
+
error: `Dispatch failed: ${result.reason?.message || 'Unknown error'}`,
|
|
112
|
+
issues: []
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Clear all registered handlers (for testing)
|
|
119
|
+
*/
|
|
120
|
+
function clearHandlers() {
|
|
121
|
+
handlers.clear();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
registerHandler,
|
|
126
|
+
getHandler,
|
|
127
|
+
listHandlers,
|
|
128
|
+
dispatchSource,
|
|
129
|
+
dispatchAll,
|
|
130
|
+
clearHandlers
|
|
131
|
+
};
|