thumbgate 1.13.0 → 1.14.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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/adapters/README.md +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +15 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +30 -0
- package/config/mcp-allowlists.json +7 -2
- package/package.json +5 -2
- package/public/index.html +2 -2
- package/scripts/cli-schema.js +12 -0
- package/scripts/operator-artifacts.js +608 -0
- package/scripts/pr-manager.js +421 -0
- package/scripts/tool-registry.js +16 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thumbgate-marketplace",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"owner": {
|
|
5
5
|
"name": "Igor Ganapolsky",
|
|
6
6
|
"email": "ig5973700@gmail.com"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"source": "npm",
|
|
14
14
|
"package": "thumbgate"
|
|
15
15
|
},
|
|
16
|
-
"version": "1.
|
|
16
|
+
"version": "1.14.0",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Igor Ganapolsky"
|
|
19
19
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thumbgate",
|
|
3
3
|
"description": "Type 👍 or 👎 on any agent action. ThumbGate captures it, distills a lesson, and blocks the pattern from repeating. One thumbs-down = the agent physically cannot make that mistake again. 33 pre-action gates, budget enforcement, self-protection, and NIST/SOC2 compliance tags.",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.14.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Igor Ganapolsky"
|
|
7
7
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thumbgate",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "ThumbGate — 👍👎 feedback that teaches your AI agent. Thumbs down a mistake, it never happens again.",
|
|
5
5
|
"homepage": "https://thumbgate-production.up.railway.app",
|
|
6
6
|
"transport": "stdio",
|
package/adapters/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
- `chatgpt/openapi.yaml`: import into GPT Actions.
|
|
4
4
|
- `gemini/function-declarations.json`: Gemini function-calling definitions.
|
|
5
5
|
- `mcp/server-stdio.js`: underlying local MCP stdio server implementation.
|
|
6
|
-
- `claude/.mcp.json`: example Claude Code MCP config using `npx --yes --package thumbgate@1.
|
|
6
|
+
- `claude/.mcp.json`: example Claude Code MCP config using `npx --yes --package thumbgate@1.14.0 thumbgate serve`.
|
|
7
7
|
- `codex/config.toml`: example Codex MCP profile section using the same version-pinned portable launcher.
|
|
8
8
|
- `amp/skills/thumbgate-feedback/SKILL.md`: Amp skill template.
|
|
9
9
|
- `opencode/opencode.json`: portable OpenCode MCP profile using the same version-pinned portable launcher.
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"mcpServers": {
|
|
3
3
|
"thumbgate": {
|
|
4
4
|
"command": "npx",
|
|
5
|
-
"args": ["--yes", "--package", "thumbgate@1.
|
|
5
|
+
"args": ["--yes", "--package", "thumbgate@1.14.0", "thumbgate", "serve"]
|
|
6
6
|
}
|
|
7
7
|
},
|
|
8
8
|
"hooks": {
|
|
9
9
|
"preToolUse": {
|
|
10
10
|
"command": "npx",
|
|
11
|
-
"args": ["--yes", "--package", "thumbgate@1.
|
|
11
|
+
"args": ["--yes", "--package", "thumbgate@1.14.0", "thumbgate", "gate-check"]
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -132,6 +132,10 @@ const {
|
|
|
132
132
|
const { exportHfDataset } = require('../../scripts/export-hf-dataset');
|
|
133
133
|
const { distributeContextToAgents } = require('../../scripts/swarm-coordinator');
|
|
134
134
|
const { buildSessionReport } = require('../../scripts/session-report');
|
|
135
|
+
const {
|
|
136
|
+
generateOperatorArtifact,
|
|
137
|
+
formatArtifactMarkdown,
|
|
138
|
+
} = require('../../scripts/operator-artifacts');
|
|
135
139
|
|
|
136
140
|
const PRO_CHECKOUT_URL = 'https://thumbgate-production.up.railway.app/checkout/pro';
|
|
137
141
|
|
|
@@ -153,7 +157,7 @@ const {
|
|
|
153
157
|
finalizeSession: finalizeFeedbackSession,
|
|
154
158
|
} = require('../../scripts/feedback-session');
|
|
155
159
|
|
|
156
|
-
const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.
|
|
160
|
+
const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.14.0' };
|
|
157
161
|
const COMMERCE_CATEGORIES = [
|
|
158
162
|
'product_recommendation',
|
|
159
163
|
'brand_compliance',
|
|
@@ -802,6 +806,16 @@ async function callToolInner(name, args) {
|
|
|
802
806
|
}));
|
|
803
807
|
case 'session_report':
|
|
804
808
|
return toTextResult(buildSessionReport({ windowHours: args.windowHours }));
|
|
809
|
+
case 'generate_operator_artifact': {
|
|
810
|
+
const artifact = await generateOperatorArtifact({
|
|
811
|
+
type: args.type,
|
|
812
|
+
windowHours: args.windowHours,
|
|
813
|
+
});
|
|
814
|
+
if (args.format === 'markdown') {
|
|
815
|
+
return toTextResult(formatArtifactMarkdown(artifact));
|
|
816
|
+
}
|
|
817
|
+
return toTextResult(artifact);
|
|
818
|
+
}
|
|
805
819
|
case 'check_operational_integrity':
|
|
806
820
|
return toTextResult(evaluateOperationalIntegrity({
|
|
807
821
|
repoPath: args.repoPath,
|
package/bin/cli.js
CHANGED
|
@@ -1670,6 +1670,32 @@ function dashboard() {
|
|
|
1670
1670
|
});
|
|
1671
1671
|
}
|
|
1672
1672
|
|
|
1673
|
+
function artifacts() {
|
|
1674
|
+
const argv = process.argv.slice(3);
|
|
1675
|
+
const args = parseArgs(argv);
|
|
1676
|
+
const positionalType = argv.find((arg) => !arg.startsWith('--'));
|
|
1677
|
+
const {
|
|
1678
|
+
generateOperatorArtifact,
|
|
1679
|
+
formatArtifactMarkdown,
|
|
1680
|
+
} = require(path.join(PKG_ROOT, 'scripts', 'operator-artifacts'));
|
|
1681
|
+
|
|
1682
|
+
generateOperatorArtifact({
|
|
1683
|
+
type: args.type || positionalType || 'reliability-pulse',
|
|
1684
|
+
windowHours: args['window-hours'] || args.window,
|
|
1685
|
+
})
|
|
1686
|
+
.then((artifact) => {
|
|
1687
|
+
if (args.json) {
|
|
1688
|
+
console.log(JSON.stringify(artifact, null, 2));
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
process.stdout.write(formatArtifactMarkdown(artifact));
|
|
1692
|
+
})
|
|
1693
|
+
.catch((err) => {
|
|
1694
|
+
console.error(err && err.message ? err.message : err);
|
|
1695
|
+
process.exit(1);
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1673
1699
|
function gateStats() {
|
|
1674
1700
|
const args = parseArgs(process.argv.slice(3));
|
|
1675
1701
|
const { calculateStats, formatStats } = require(path.join(PKG_ROOT, 'scripts', 'gate-stats'));
|
|
@@ -2088,6 +2114,10 @@ switch (COMMAND) {
|
|
|
2088
2114
|
case 'dashboard':
|
|
2089
2115
|
dashboard();
|
|
2090
2116
|
break;
|
|
2117
|
+
case 'artifact':
|
|
2118
|
+
case 'artifacts':
|
|
2119
|
+
artifacts();
|
|
2120
|
+
break;
|
|
2091
2121
|
case 'analytics': {
|
|
2092
2122
|
const { run: runAnalytics } = require(path.join(PKG_ROOT, 'scripts', 'analytics-report'));
|
|
2093
2123
|
runAnalytics();
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"require_evidence_for_claim",
|
|
59
59
|
"distribute_context_to_agents",
|
|
60
60
|
"session_report",
|
|
61
|
+
"generate_operator_artifact",
|
|
61
62
|
"perplexity_search",
|
|
62
63
|
"perplexity_ask",
|
|
63
64
|
"perplexity_research",
|
|
@@ -91,7 +92,8 @@
|
|
|
91
92
|
"estimate_uncertainty",
|
|
92
93
|
"report_product_issue",
|
|
93
94
|
"require_evidence_for_claim",
|
|
94
|
-
"session_report"
|
|
95
|
+
"session_report",
|
|
96
|
+
"generate_operator_artifact"
|
|
95
97
|
],
|
|
96
98
|
"commerce": [
|
|
97
99
|
"capture_feedback",
|
|
@@ -143,6 +145,7 @@
|
|
|
143
145
|
"describe_reliability_entity",
|
|
144
146
|
"require_evidence_for_claim",
|
|
145
147
|
"session_report",
|
|
148
|
+
"generate_operator_artifact",
|
|
146
149
|
"perplexity_search",
|
|
147
150
|
"perplexity_ask"
|
|
148
151
|
],
|
|
@@ -176,6 +179,7 @@
|
|
|
176
179
|
"describe_reliability_entity",
|
|
177
180
|
"require_evidence_for_claim",
|
|
178
181
|
"session_report",
|
|
182
|
+
"generate_operator_artifact",
|
|
179
183
|
"perplexity_search",
|
|
180
184
|
"perplexity_ask"
|
|
181
185
|
],
|
|
@@ -194,7 +198,8 @@
|
|
|
194
198
|
"verify_claim",
|
|
195
199
|
"check_operational_integrity",
|
|
196
200
|
"workflow_sentinel",
|
|
197
|
-
"settings_status"
|
|
201
|
+
"settings_status",
|
|
202
|
+
"generate_operator_artifact"
|
|
198
203
|
]
|
|
199
204
|
}
|
|
200
205
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thumbgate",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "Self-improving agent governance: type thumbs-up or thumbs-down on any AI agent action. ThumbGate turns every mistake into a prevention rule and blocks the pattern from repeating. One thumbs-down, never again. 33 pre-action gates, budget enforcement, and self-protection for Claude Code, Cursor, Codex, Gemini CLI, and Amp.",
|
|
5
5
|
"homepage": "https://thumbgate-production.up.railway.app",
|
|
6
6
|
"repository": {
|
|
@@ -148,11 +148,13 @@
|
|
|
148
148
|
"scripts/operational-dashboard.js",
|
|
149
149
|
"scripts/operational-integrity.js",
|
|
150
150
|
"scripts/operational-summary.js",
|
|
151
|
+
"scripts/operator-artifacts.js",
|
|
151
152
|
"scripts/optimize-context.js",
|
|
152
153
|
"scripts/org-dashboard.js",
|
|
153
154
|
"scripts/partner-orchestration.js",
|
|
154
155
|
"scripts/perplexity-client.js",
|
|
155
156
|
"scripts/predictive-insights.js",
|
|
157
|
+
"scripts/pr-manager.js",
|
|
156
158
|
"scripts/pro-local-dashboard.js",
|
|
157
159
|
"scripts/problem-detail.js",
|
|
158
160
|
"scripts/product-feedback.js",
|
|
@@ -263,7 +265,7 @@
|
|
|
263
265
|
"trace:eval": "node scripts/decision-trace.js eval",
|
|
264
266
|
"social:reply-monitor": "node scripts/social-reply-monitor.js",
|
|
265
267
|
"social:reply-monitor:dry": "node scripts/social-reply-monitor.js --dry-run",
|
|
266
|
-
"test": "npm run test:schema && npm run test:loop && npm run test:dpo && npm run test:kto && npm run test:api && npm run test:proof && npm run test:e2e && npm run test:rlaif && npm run test:attribution && npm run test:quality && npm run test:intelligence && npm run test:training-export && npm run test:deployment && npm run test:operational-integrity && npm run test:workflow && npm run test:billing && npm run test:cli && npm run test:watcher && npm run test:autoresearch && npm run test:ops && npm run test:session-analyzer && npm run test:tessl && npm run test:gates && npm run test:evoskill && npm run test:gates-hardening && npm run test:workers && npm run test:social-analytics && npm run test:memalign && npm run test:xmemory-lite && npm run test:filesystem-search && npm run test:zernio && npm run test:platform-limits && npm run test:post-video && npm run test:post-everywhere-instagram && npm run test:post-everywhere-channels && npm run test:zernio-canonical-pollers && npm run test:zernio-status && npm run test:obsidian-export && npm run test:lesson-db && npm run test:lesson-rotation && npm run test:memory-dedup && npm run test:feedback-quality && npm run test:sync-version && npm run test:check-congruence && npm run test:tool-registry && npm run test:feedback-to-rules && npm run test:memory-firewall && npm run test:belief-update && npm run test:hosted-config && npm run test:operational-summary && npm run test:operator-key-auth && npm run test:cloudflare-sandbox && npm run test:mcp-config && npm run test:plan-gate && npm run test:pulse && npm run test:semantic-layer && npm run test:data-pipeline && npm run test:optimize-context && npm run test:principle-extractor && npm run test:analytics-window && npm run test:funnel-analytics && npm run test:experiment-tracker && npm run test:build-metadata && npm run test:context-engine && npm run test:hf-papers && npm run test:marketing-experiment && npm run test:seo-gsd && npm run test:verify-run && npm run test:export-dpo-pairs && npm run test:export-hf-dataset && npm run test:license && npm run test:bot-detector && npm run test:postinstall && npm run test:funnel-invariants && npm run test:cli-telemetry && npm run test:pro-parity && npm run test:model-tier-router && npm run test:computer-use-firewall && npm run test:skill-exporter && npm run test:statusline && npm run test:evolution && npm run test:org-dashboard && npm run test:multi-hop-recall && npm run test:synthetic-dpo && npm run test:thumbgate-skill && npm run test:learn-hub && npm run test:feedback-fallback && npm run test:metaclaw && npm run test:server-lock && npm run test:control-tower && npm run test:pii-scanner && npm run test:data-governance && npm run test:lesson-inference && npm run test:semantic-dedup && npm run test:fs-utils && npm run test:cli-schema && npm run test:explore && npm run test:lesson-reranker && npm run test:lesson-retrieval && npm run test:cross-encoder && npm run test:reflector-agent && npm run test:feedback-session && npm run test:feedback-history-distiller && npm run test:hallucination-detector && npm run test:history-distiller && npm run test:predictive-insights && npm run test:prove-predictive-insights && npm run test:statusbar-cli && npm run test:generate-instagram-card && npm run test:instagram-thumbgate-post && npm run test:publish-instagram-thumbgate && npm run test:lesson-synthesis && npm run test:background-governance && npm run test:memory-migration && npm run test:prompt-dlp && npm run test:ephemeral-store && npm run test:agent-security && npm run test:skill-progressive && npm run test:per-step-scoring && npm run test:weekly-auto-post && npm run test:social-post-hourly && npm run test:social-quality-gate && npm run test:a2ui-engine && npm run test:gate-satisfy && npm run test:money-watcher && npm run test:budget && npm run test:quick-start && npm run test:utm && npm run test:product-feedback && npm run test:feedback-root-consolidator && npm run test:engagement-audit && npm run test:install-growth-automation && npm run test:publish-thumbgate-launch && npm run test:reconcile-thumbgate-campaign && npm run test:reddit-publisher && npm run test:schedule-thumbgate-campaign && npm run test:social-reply-monitor && npm run test:sync-launch-assets && npm run test:ai-search-visibility && npm run test:perplexity && npm run test:security-scanner && npm run test:llm-client && npm run test:managed-lesson-agent && npm run test:self-distill && npm run test:meta-agent && npm run test:harness-selector && npm run test:thumbgate-bench && npm run test:seo-guides && npm run test:enforcement-loop && npm run test:cli-agent-experience && npm run test:bot-detection && npm run test:checkout-bot-guard && npm run test:session-health && npm run test:session-episodes && npm run test:spec-gate && npm run test:decision-trace && npm run test:dashboard-insights && npm run test:prompt-eval && npm run test:demo-voiceover && npm run test:gate-coherence && npm run test:gate-eval && npm run test:high-roi && npm run test:public-static-assets && npm run test:token-savings && npm run test:workflow-gate-checkpoint && npm run test:lesson-export-import && npm run test:landing-page-claims && npm run test:dashboard-deeplink-e2e && npm run test:public-package-parity && npm run test:token-savings-dashboard && npm run test:cursor-wiring && npm run test:pretooluse-injection && npm run test:recent-corrective-context && npm run test:durability-step && npm run test:mailer && npm run test:brand-assets && npm run test:enforcement-teeth && npm run test:bayes-optimal-gate && npm run test:swarm-coordinator && npm run test:session-report && npm run test:require-evidence-gate",
|
|
268
|
+
"test": "npm run test:schema && npm run test:loop && npm run test:dpo && npm run test:kto && npm run test:api && npm run test:proof && npm run test:e2e && npm run test:rlaif && npm run test:attribution && npm run test:quality && npm run test:intelligence && npm run test:training-export && npm run test:deployment && npm run test:operational-integrity && npm run test:workflow && npm run test:billing && npm run test:cli && npm run test:watcher && npm run test:autoresearch && npm run test:ops && npm run test:session-analyzer && npm run test:tessl && npm run test:gates && npm run test:evoskill && npm run test:gates-hardening && npm run test:workers && npm run test:social-analytics && npm run test:memalign && npm run test:xmemory-lite && npm run test:filesystem-search && npm run test:zernio && npm run test:platform-limits && npm run test:post-video && npm run test:post-everywhere-instagram && npm run test:post-everywhere-channels && npm run test:zernio-canonical-pollers && npm run test:zernio-status && npm run test:obsidian-export && npm run test:lesson-db && npm run test:lesson-rotation && npm run test:memory-dedup && npm run test:feedback-quality && npm run test:sync-version && npm run test:check-congruence && npm run test:tool-registry && npm run test:feedback-to-rules && npm run test:memory-firewall && npm run test:belief-update && npm run test:hosted-config && npm run test:operational-summary && npm run test:operator-artifacts && npm run test:operator-key-auth && npm run test:cloudflare-sandbox && npm run test:mcp-config && npm run test:plan-gate && npm run test:pulse && npm run test:semantic-layer && npm run test:data-pipeline && npm run test:optimize-context && npm run test:principle-extractor && npm run test:analytics-window && npm run test:funnel-analytics && npm run test:experiment-tracker && npm run test:build-metadata && npm run test:context-engine && npm run test:hf-papers && npm run test:marketing-experiment && npm run test:seo-gsd && npm run test:verify-run && npm run test:export-dpo-pairs && npm run test:export-hf-dataset && npm run test:license && npm run test:bot-detector && npm run test:postinstall && npm run test:funnel-invariants && npm run test:cli-telemetry && npm run test:pro-parity && npm run test:model-tier-router && npm run test:computer-use-firewall && npm run test:skill-exporter && npm run test:statusline && npm run test:evolution && npm run test:org-dashboard && npm run test:multi-hop-recall && npm run test:synthetic-dpo && npm run test:thumbgate-skill && npm run test:learn-hub && npm run test:feedback-fallback && npm run test:metaclaw && npm run test:server-lock && npm run test:control-tower && npm run test:pii-scanner && npm run test:data-governance && npm run test:lesson-inference && npm run test:semantic-dedup && npm run test:fs-utils && npm run test:cli-schema && npm run test:explore && npm run test:lesson-reranker && npm run test:lesson-retrieval && npm run test:cross-encoder && npm run test:reflector-agent && npm run test:feedback-session && npm run test:feedback-history-distiller && npm run test:hallucination-detector && npm run test:history-distiller && npm run test:predictive-insights && npm run test:prove-predictive-insights && npm run test:statusbar-cli && npm run test:generate-instagram-card && npm run test:instagram-thumbgate-post && npm run test:publish-instagram-thumbgate && npm run test:lesson-synthesis && npm run test:background-governance && npm run test:memory-migration && npm run test:prompt-dlp && npm run test:ephemeral-store && npm run test:agent-security && npm run test:skill-progressive && npm run test:per-step-scoring && npm run test:weekly-auto-post && npm run test:social-post-hourly && npm run test:social-quality-gate && npm run test:a2ui-engine && npm run test:gate-satisfy && npm run test:money-watcher && npm run test:budget && npm run test:quick-start && npm run test:utm && npm run test:product-feedback && npm run test:feedback-root-consolidator && npm run test:engagement-audit && npm run test:install-growth-automation && npm run test:publish-thumbgate-launch && npm run test:reconcile-thumbgate-campaign && npm run test:reddit-publisher && npm run test:schedule-thumbgate-campaign && npm run test:social-reply-monitor && npm run test:sync-launch-assets && npm run test:ai-search-visibility && npm run test:perplexity && npm run test:security-scanner && npm run test:llm-client && npm run test:managed-lesson-agent && npm run test:self-distill && npm run test:meta-agent && npm run test:harness-selector && npm run test:thumbgate-bench && npm run test:seo-guides && npm run test:enforcement-loop && npm run test:cli-agent-experience && npm run test:bot-detection && npm run test:checkout-bot-guard && npm run test:session-health && npm run test:session-episodes && npm run test:spec-gate && npm run test:decision-trace && npm run test:dashboard-insights && npm run test:prompt-eval && npm run test:demo-voiceover && npm run test:gate-coherence && npm run test:gate-eval && npm run test:high-roi && npm run test:public-static-assets && npm run test:token-savings && npm run test:workflow-gate-checkpoint && npm run test:lesson-export-import && npm run test:landing-page-claims && npm run test:dashboard-deeplink-e2e && npm run test:public-package-parity && npm run test:token-savings-dashboard && npm run test:cursor-wiring && npm run test:pretooluse-injection && npm run test:recent-corrective-context && npm run test:durability-step && npm run test:mailer && npm run test:brand-assets && npm run test:enforcement-teeth && npm run test:bayes-optimal-gate && npm run test:swarm-coordinator && npm run test:session-report && npm run test:require-evidence-gate",
|
|
267
269
|
"test:swarm-coordinator": "node --test tests/swarm-coordinator.test.js",
|
|
268
270
|
"test:session-report": "node --test tests/session-report.test.js",
|
|
269
271
|
"test:require-evidence-gate": "node --test tests/require-evidence-gate.test.js",
|
|
@@ -294,6 +296,7 @@
|
|
|
294
296
|
"test:belief-update": "node --test tests/belief-update.test.js",
|
|
295
297
|
"test:hosted-config": "node --test tests/hosted-config.test.js",
|
|
296
298
|
"test:operational-summary": "node --test tests/operational-summary.test.js",
|
|
299
|
+
"test:operator-artifacts": "node --test tests/operator-artifacts.test.js",
|
|
297
300
|
"test:operator-key-auth": "node --test tests/api-operator-key-auth.test.js",
|
|
298
301
|
"test:cloudflare-sandbox": "node --test tests/cloudflare-dynamic-sandbox.test.js tests/cloudflare-sandbox-api.test.js",
|
|
299
302
|
"test:mcp-config": "node --test tests/mcp-config.test.js",
|
package/public/index.html
CHANGED
|
@@ -974,7 +974,7 @@ __GA_BOOTSTRAP__
|
|
|
974
974
|
<!-- HOW IT WORKS -->
|
|
975
975
|
<section class="how-it-works" id="how-it-works">
|
|
976
976
|
<div class="container">
|
|
977
|
-
<div class="section-label">New in v1.
|
|
977
|
+
<div class="section-label">New in v1.14.0</div>
|
|
978
978
|
<h2 class="section-title">Three steps to stop repeated AI failures</h2>
|
|
979
979
|
<div class="steps">
|
|
980
980
|
<div class="step">
|
|
@@ -1330,7 +1330,7 @@ __GA_BOOTSTRAP__
|
|
|
1330
1330
|
<a href="https://www.linkedin.com/in/igorganapolsky" target="_blank" rel="noopener">LinkedIn</a>
|
|
1331
1331
|
<a href="/blog">Blog</a>
|
|
1332
1332
|
</div>
|
|
1333
|
-
<span class="footer-copy">© 2026 Max Smith KDP LLC · MIT License · v1.
|
|
1333
|
+
<span class="footer-copy">© 2026 Max Smith KDP LLC · MIT License · v1.14.0</span>
|
|
1334
1334
|
</div>
|
|
1335
1335
|
</footer>
|
|
1336
1336
|
|
package/scripts/cli-schema.js
CHANGED
|
@@ -77,6 +77,18 @@ const CLI_COMMANDS = [
|
|
|
77
77
|
{ name: 'json', type: 'boolean', description: 'Output as JSON' },
|
|
78
78
|
],
|
|
79
79
|
},
|
|
80
|
+
{
|
|
81
|
+
name: 'artifacts',
|
|
82
|
+
aliases: ['artifact'],
|
|
83
|
+
description: 'Operator decision artifacts - PR, reliability, revenue, and release pulses',
|
|
84
|
+
group: 'discovery',
|
|
85
|
+
mcpTool: 'generate_operator_artifact',
|
|
86
|
+
flags: [
|
|
87
|
+
{ name: 'type', type: 'string', description: 'pr-pulse | reliability-pulse | revenue-pulse | release-readiness' },
|
|
88
|
+
{ name: 'window-hours', type: 'number', description: 'Lookback window in hours (default 24)' },
|
|
89
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON' },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
80
92
|
{
|
|
81
93
|
name: 'summary',
|
|
82
94
|
description: 'Human-readable feedback summary',
|
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_WINDOW_HOURS = 24;
|
|
8
|
+
const MAX_WINDOW_HOURS = 24 * 30;
|
|
9
|
+
const ARTIFACT_TYPES = [
|
|
10
|
+
'pr-pulse',
|
|
11
|
+
'reliability-pulse',
|
|
12
|
+
'revenue-pulse',
|
|
13
|
+
'release-readiness',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
function normalizeWindowHours(value) {
|
|
17
|
+
if (value === null || value === undefined || value === '') return DEFAULT_WINDOW_HOURS;
|
|
18
|
+
const parsed = Number(value);
|
|
19
|
+
if (!Number.isFinite(parsed)) return DEFAULT_WINDOW_HOURS;
|
|
20
|
+
if (parsed < 1) return 1;
|
|
21
|
+
if (parsed > MAX_WINDOW_HOURS) return MAX_WINDOW_HOURS;
|
|
22
|
+
return Math.floor(parsed);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeArtifactType(type) {
|
|
26
|
+
const normalized = String(type || 'reliability-pulse').trim().toLowerCase();
|
|
27
|
+
const aliases = {
|
|
28
|
+
pr: 'pr-pulse',
|
|
29
|
+
prs: 'pr-pulse',
|
|
30
|
+
pull_requests: 'pr-pulse',
|
|
31
|
+
pullrequests: 'pr-pulse',
|
|
32
|
+
reliability: 'reliability-pulse',
|
|
33
|
+
gates: 'reliability-pulse',
|
|
34
|
+
revenue: 'revenue-pulse',
|
|
35
|
+
growth: 'revenue-pulse',
|
|
36
|
+
acquisition: 'revenue-pulse',
|
|
37
|
+
release: 'release-readiness',
|
|
38
|
+
readiness: 'release-readiness',
|
|
39
|
+
};
|
|
40
|
+
const resolved = aliases[normalized] || normalized;
|
|
41
|
+
if (!ARTIFACT_TYPES.includes(resolved)) {
|
|
42
|
+
throw new Error(`Unknown operator artifact type: ${type}`);
|
|
43
|
+
}
|
|
44
|
+
return resolved;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function safeNumber(value, fallback = 0) {
|
|
48
|
+
const parsed = Number(value);
|
|
49
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getPath(source, parts, fallback = undefined) {
|
|
53
|
+
let cursor = source;
|
|
54
|
+
for (const part of parts) {
|
|
55
|
+
if (!cursor || typeof cursor !== 'object' || !(part in cursor)) return fallback;
|
|
56
|
+
cursor = cursor[part];
|
|
57
|
+
}
|
|
58
|
+
return cursor === undefined ? fallback : cursor;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function formatCurrency(cents) {
|
|
62
|
+
return `$${(safeNumber(cents) / 100).toFixed(2)}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function compactList(items, limit = 5) {
|
|
66
|
+
return (Array.isArray(items) ? items : []).filter(Boolean).slice(0, limit);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getErrorMessage(error) {
|
|
70
|
+
return String(error?.message || error);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function artifactBase(type, options = {}) {
|
|
74
|
+
const generatedAt = options.now instanceof Date
|
|
75
|
+
? options.now.toISOString()
|
|
76
|
+
: options.now || new Date().toISOString();
|
|
77
|
+
return {
|
|
78
|
+
schemaVersion: 1,
|
|
79
|
+
type,
|
|
80
|
+
generatedAt,
|
|
81
|
+
windowHours: normalizeWindowHours(options.windowHours),
|
|
82
|
+
status: 'watch',
|
|
83
|
+
summary: '',
|
|
84
|
+
decision: {
|
|
85
|
+
label: 'Review',
|
|
86
|
+
rationale: '',
|
|
87
|
+
nextActions: [],
|
|
88
|
+
},
|
|
89
|
+
metrics: {},
|
|
90
|
+
sections: [],
|
|
91
|
+
evidence: [],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function buildEvidence(label, value, extra = {}) {
|
|
96
|
+
return {
|
|
97
|
+
label,
|
|
98
|
+
value: value === undefined || value === null ? 'unknown' : value,
|
|
99
|
+
...extra,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function buildReliabilityPulseArtifact(options = {}) {
|
|
104
|
+
const type = 'reliability-pulse';
|
|
105
|
+
const artifact = artifactBase(type, options);
|
|
106
|
+
const dashboard = options.dashboardData || {};
|
|
107
|
+
const session = options.sessionReport || {};
|
|
108
|
+
const gateStats = dashboard.gateStats || {};
|
|
109
|
+
const health = dashboard.health || {};
|
|
110
|
+
const diagnostics = dashboard.diagnostics || {};
|
|
111
|
+
const reviewDelta = dashboard.reviewDelta || {};
|
|
112
|
+
const lessonPipeline = dashboard.lessonPipeline || {};
|
|
113
|
+
|
|
114
|
+
const blocked = safeNumber(gateStats.blocked, safeNumber(getPath(session, ['gates', 'blocked'])));
|
|
115
|
+
const warned = safeNumber(gateStats.warned, safeNumber(getPath(session, ['gates', 'warned'])));
|
|
116
|
+
const feedbackCount = safeNumber(health.feedbackCount);
|
|
117
|
+
const memoryCount = safeNumber(health.memoryCount);
|
|
118
|
+
const negativeAdded = safeNumber(reviewDelta.negativeAdded);
|
|
119
|
+
const staleLessons = safeNumber(lessonPipeline.staleLessons);
|
|
120
|
+
const topDiagnostic = getPath(diagnostics, ['categories', 0, 'key'], null);
|
|
121
|
+
|
|
122
|
+
artifact.title = 'Reliability Pulse';
|
|
123
|
+
artifact.metrics = {
|
|
124
|
+
blocked,
|
|
125
|
+
warned,
|
|
126
|
+
feedbackCount,
|
|
127
|
+
memoryCount,
|
|
128
|
+
negativeAdded,
|
|
129
|
+
staleLessons,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
if (negativeAdded > 0 || staleLessons > 0 || blocked > 0) {
|
|
133
|
+
artifact.status = 'actionable';
|
|
134
|
+
artifact.decision.label = 'Regenerate and inspect gates';
|
|
135
|
+
artifact.decision.rationale = 'Recent negative signal or gate activity means the prevention layer has learnable work to absorb.';
|
|
136
|
+
artifact.decision.nextActions = compactList([
|
|
137
|
+
negativeAdded > 0 ? `Promote ${negativeAdded} new negative signal(s) into prevention rules.` : null,
|
|
138
|
+
staleLessons > 0 ? `Review ${staleLessons} stale lesson(s) before they age out of useful recall.` : null,
|
|
139
|
+
blocked > 0 ? `Inspect the top blocked gate path before the next risky operation.` : null,
|
|
140
|
+
topDiagnostic ? `Address top diagnostic category: ${topDiagnostic}.` : null,
|
|
141
|
+
], 4);
|
|
142
|
+
} else {
|
|
143
|
+
artifact.status = 'healthy';
|
|
144
|
+
artifact.decision.label = 'Keep shipping';
|
|
145
|
+
artifact.decision.rationale = 'No fresh reliability pressure is visible in the current window.';
|
|
146
|
+
artifact.decision.nextActions = ['Keep the Reliability Gateway enabled during PR and release work.'];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
artifact.summary = `${blocked} blocked, ${warned} warned, ${feedbackCount} feedback events, ${memoryCount} memories.`;
|
|
150
|
+
artifact.sections = [
|
|
151
|
+
{
|
|
152
|
+
title: 'Gate Load',
|
|
153
|
+
bullets: [
|
|
154
|
+
`${blocked} blocked actions`,
|
|
155
|
+
`${warned} warnings`,
|
|
156
|
+
`${safeNumber(getPath(session, ['gates', 'pendingApproval']))} pending approvals`,
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
title: 'Learning Queue',
|
|
161
|
+
bullets: compactList([
|
|
162
|
+
`${negativeAdded} new negative signal(s)`,
|
|
163
|
+
`${staleLessons} stale lesson(s)`,
|
|
164
|
+
topDiagnostic ? `Top diagnostic: ${topDiagnostic}` : null,
|
|
165
|
+
]),
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
artifact.evidence = [
|
|
169
|
+
buildEvidence('dashboard.health.feedbackCount', feedbackCount),
|
|
170
|
+
buildEvidence('dashboard.gateStats.blocked', blocked),
|
|
171
|
+
buildEvidence('session_report.windowHours', artifact.windowHours),
|
|
172
|
+
];
|
|
173
|
+
return artifact;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function buildRevenuePulseArtifact(options = {}) {
|
|
177
|
+
const type = 'revenue-pulse';
|
|
178
|
+
const artifact = artifactBase(type, options);
|
|
179
|
+
const dashboard = options.dashboardData || {};
|
|
180
|
+
const analytics = dashboard.analytics || {};
|
|
181
|
+
const funnel = analytics.funnel || {};
|
|
182
|
+
const revenue = analytics.revenue || {};
|
|
183
|
+
const seo = analytics.seo || {};
|
|
184
|
+
const attribution = analytics.attribution || {};
|
|
185
|
+
|
|
186
|
+
const visitors = safeNumber(funnel.visitors);
|
|
187
|
+
const ctaClicks = safeNumber(funnel.ctaClicks);
|
|
188
|
+
const checkoutStarts = safeNumber(funnel.checkoutStarts);
|
|
189
|
+
const acquisitionLeads = safeNumber(funnel.acquisitionLeads);
|
|
190
|
+
const paidOrders = safeNumber(revenue.paidOrders, safeNumber(funnel.paidOrders));
|
|
191
|
+
const bookedRevenueCents = safeNumber(revenue.bookedRevenueCents);
|
|
192
|
+
const topTrafficChannel = funnel.topTrafficChannel || getPath(seo, ['topSurface', 'key'], null);
|
|
193
|
+
const topPaidSource = Object.entries(attribution.paidBySource || {})
|
|
194
|
+
.sort((a, b) => safeNumber(b[1]) - safeNumber(a[1]))[0];
|
|
195
|
+
|
|
196
|
+
artifact.title = 'Revenue Pulse';
|
|
197
|
+
artifact.metrics = {
|
|
198
|
+
visitors,
|
|
199
|
+
ctaClicks,
|
|
200
|
+
checkoutStarts,
|
|
201
|
+
acquisitionLeads,
|
|
202
|
+
paidOrders,
|
|
203
|
+
bookedRevenueCents,
|
|
204
|
+
bookedRevenue: formatCurrency(bookedRevenueCents),
|
|
205
|
+
visitorToPaidRate: safeNumber(funnel.visitorToPaidRate),
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
if (paidOrders > 0) {
|
|
209
|
+
artifact.status = 'actionable';
|
|
210
|
+
artifact.decision.label = 'Double down on converting source';
|
|
211
|
+
artifact.decision.rationale = 'Revenue is visible; the highest-ROI move is to reuse the source and copy that already converted.';
|
|
212
|
+
artifact.decision.nextActions = compactList([
|
|
213
|
+
topPaidSource ? `Create another offer using the paid source: ${topPaidSource[0]}.` : null,
|
|
214
|
+
topTrafficChannel ? `Fan out the winning acquisition angle on ${topTrafficChannel}.` : null,
|
|
215
|
+
'Keep checkout proof and pricing copy unchanged until the next conversion batch is measured.',
|
|
216
|
+
], 3);
|
|
217
|
+
} else if (checkoutStarts > 0 || ctaClicks > 0) {
|
|
218
|
+
artifact.status = 'blocked';
|
|
219
|
+
artifact.decision.label = 'Fix checkout conversion';
|
|
220
|
+
artifact.decision.rationale = 'Intent exists, but the journey is not turning into paid orders.';
|
|
221
|
+
artifact.decision.nextActions = compactList([
|
|
222
|
+
`${checkoutStarts} checkout start(s) and ${ctaClicks} CTA click(s) need buyer-loss review.`,
|
|
223
|
+
'Audit checkout redirects, pricing objections, and proof placement before adding more traffic.',
|
|
224
|
+
topTrafficChannel ? `Inspect source-specific copy for ${topTrafficChannel}.` : null,
|
|
225
|
+
], 4);
|
|
226
|
+
} else {
|
|
227
|
+
artifact.status = 'actionable';
|
|
228
|
+
artifact.decision.label = 'Create more acquisition surface';
|
|
229
|
+
artifact.decision.rationale = 'No paid orders or checkout intent are visible, so traffic and discovery injection beat infrastructure work.';
|
|
230
|
+
artifact.decision.nextActions = compactList([
|
|
231
|
+
'Publish one high-intent ThumbGate proof chunk with DPO, Pre-Action Gates, and Reliability Gateway terms.',
|
|
232
|
+
'Add one outreach or community distribution action tied to the latest verification evidence.',
|
|
233
|
+
topTrafficChannel ? `Reuse current top channel: ${topTrafficChannel}.` : 'Seed a first measurable traffic channel.',
|
|
234
|
+
], 3);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
artifact.summary = `${paidOrders} paid order(s), ${formatCurrency(bookedRevenueCents)} booked, ${visitors} visitors, ${checkoutStarts} checkout starts.`;
|
|
238
|
+
artifact.sections = [
|
|
239
|
+
{
|
|
240
|
+
title: 'Funnel',
|
|
241
|
+
bullets: [
|
|
242
|
+
`${visitors} visitors`,
|
|
243
|
+
`${ctaClicks} CTA clicks`,
|
|
244
|
+
`${checkoutStarts} checkout starts`,
|
|
245
|
+
`${paidOrders} paid orders`,
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
title: 'Acquisition',
|
|
250
|
+
bullets: compactList([
|
|
251
|
+
topTrafficChannel ? `Top traffic channel: ${topTrafficChannel}` : 'No top traffic channel yet',
|
|
252
|
+
`${safeNumber(seo.landingViews)} SEO landing views`,
|
|
253
|
+
`${acquisitionLeads} acquisition leads`,
|
|
254
|
+
]),
|
|
255
|
+
},
|
|
256
|
+
];
|
|
257
|
+
artifact.evidence = [
|
|
258
|
+
buildEvidence('analytics.revenue.paidOrders', paidOrders),
|
|
259
|
+
buildEvidence('analytics.revenue.bookedRevenueCents', bookedRevenueCents),
|
|
260
|
+
buildEvidence('analytics.funnel.checkoutStarts', checkoutStarts),
|
|
261
|
+
];
|
|
262
|
+
return artifact;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function classifyPr(pr, checks) {
|
|
266
|
+
const { summarizeChecks } = require('./pr-manager');
|
|
267
|
+
const summary = summarizeChecks(checks || []);
|
|
268
|
+
const mergeState = String(pr.mergeStateStatus || 'UNKNOWN').toUpperCase();
|
|
269
|
+
const mergeable = String(pr.mergeable || 'UNKNOWN').toUpperCase();
|
|
270
|
+
const reviewDecision = String(pr.reviewDecision || '').toUpperCase();
|
|
271
|
+
if (pr.isDraft) return { state: 'draft', blockers: ['draft'] };
|
|
272
|
+
if (mergeState === 'BEHIND') return { state: 'blocked', blockers: ['BEHIND'] };
|
|
273
|
+
if (mergeState === 'DIRTY' || mergeable === 'CONFLICTING') {
|
|
274
|
+
return { state: 'blocked', blockers: ['conflicts'] };
|
|
275
|
+
}
|
|
276
|
+
if (summary.failing.length > 0) return { state: 'blocked', blockers: summary.failing };
|
|
277
|
+
if (summary.pending.length > 0) return { state: 'pending', blockers: summary.pending };
|
|
278
|
+
if (reviewDecision === 'CHANGES_REQUESTED') {
|
|
279
|
+
return { state: 'blocked', blockers: ['changes_requested'] };
|
|
280
|
+
}
|
|
281
|
+
if (reviewDecision === 'REVIEW_REQUIRED') {
|
|
282
|
+
return { state: 'blocked', blockers: ['review_required'] };
|
|
283
|
+
}
|
|
284
|
+
if (['CLEAN', 'HAS_HOOKS'].includes(mergeState) && ['MERGEABLE', 'UNKNOWN'].includes(mergeable)) {
|
|
285
|
+
return { state: 'ready', blockers: [] };
|
|
286
|
+
}
|
|
287
|
+
return { state: 'pending', blockers: [pr.mergeStateStatus || 'unknown_state'] };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function createPrRow(pr, checks, classification) {
|
|
291
|
+
return {
|
|
292
|
+
number: pr.number,
|
|
293
|
+
title: pr.title,
|
|
294
|
+
url: pr.url,
|
|
295
|
+
draft: Boolean(pr.isDraft),
|
|
296
|
+
mergeStateStatus: pr.mergeStateStatus || null,
|
|
297
|
+
reviewDecision: pr.reviewDecision || null,
|
|
298
|
+
state: classification.state,
|
|
299
|
+
blockers: classification.blockers,
|
|
300
|
+
checkCount: checks.length,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function groupPrRows(rows) {
|
|
305
|
+
return {
|
|
306
|
+
ready: rows.filter((row) => row.state === 'ready'),
|
|
307
|
+
blocked: rows.filter((row) => row.state === 'blocked'),
|
|
308
|
+
pending: rows.filter((row) => row.state === 'pending'),
|
|
309
|
+
drafts: rows.filter((row) => row.state === 'draft'),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function getPrPulseStatus(groups) {
|
|
314
|
+
if (groups.blocked.length > 0) return 'blocked';
|
|
315
|
+
if (groups.ready.length > 0) return 'actionable';
|
|
316
|
+
if (groups.pending.length > 0) return 'watch';
|
|
317
|
+
return 'healthy';
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function getPrPulseDecision(groups) {
|
|
321
|
+
if (groups.ready.length > 0) {
|
|
322
|
+
return {
|
|
323
|
+
label: 'Submit ready PRs through protected merge path',
|
|
324
|
+
rationale: 'Terminal checks and merge state are clean for at least one open PR.',
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
if (groups.blocked.length > 0) {
|
|
328
|
+
return {
|
|
329
|
+
label: 'Fix PR blockers',
|
|
330
|
+
rationale: 'One or more PRs have failing checks, draft state, or merge-state blockers.',
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
if (groups.pending.length > 0) {
|
|
334
|
+
return {
|
|
335
|
+
label: 'Wait for terminal checks',
|
|
336
|
+
rationale: 'Checks are still running; merging now would violate the protected path.',
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
label: 'No PR action',
|
|
341
|
+
rationale: 'No open PRs require operator action.',
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function formatPrNumbers(rows) {
|
|
346
|
+
return rows.map((row) => `#${row.number}`).join(', ');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function formatBlockedPrs(rows) {
|
|
350
|
+
return rows.map((row) => {
|
|
351
|
+
const blocker = row.blockers[0] || 'blocked';
|
|
352
|
+
return `#${row.number} (${blocker})`;
|
|
353
|
+
}).join(', ');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function buildPrNextActions(groups) {
|
|
357
|
+
return compactList([
|
|
358
|
+
groups.ready.length > 0 ? `Run npm run pr:manage for PR(s): ${formatPrNumbers(groups.ready)}.` : null,
|
|
359
|
+
groups.blocked.length > 0 ? `Unblock PR(s): ${formatBlockedPrs(groups.blocked)}.` : null,
|
|
360
|
+
groups.pending.length > 0 ? `Recheck pending PR(s): ${formatPrNumbers(groups.pending)}.` : null,
|
|
361
|
+
groups.drafts.length > 0 ? `Leave draft PR(s) alone until marked ready: ${formatPrNumbers(groups.drafts)}.` : null,
|
|
362
|
+
], 4);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function buildPrPulseArtifact(options = {}) {
|
|
366
|
+
const type = 'pr-pulse';
|
|
367
|
+
const artifact = artifactBase(type, options);
|
|
368
|
+
artifact.title = 'PR Pulse';
|
|
369
|
+
|
|
370
|
+
const prClient = options.prClient || require('./pr-manager');
|
|
371
|
+
const prs = Array.isArray(options.prs) ? options.prs : await prClient.listOpenPrs();
|
|
372
|
+
const checksByPr = options.checksByPr || {};
|
|
373
|
+
const rows = [];
|
|
374
|
+
|
|
375
|
+
for (const pr of prs) {
|
|
376
|
+
const number = pr.number;
|
|
377
|
+
let checks = checksByPr[number];
|
|
378
|
+
let checkError = null;
|
|
379
|
+
if (!Array.isArray(checks)) {
|
|
380
|
+
try {
|
|
381
|
+
checks = await prClient.getPrChecks(number);
|
|
382
|
+
} catch (err) {
|
|
383
|
+
checks = [];
|
|
384
|
+
checkError = getErrorMessage(err);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const classification = checkError
|
|
388
|
+
? { state: 'blocked', blockers: [checkError] }
|
|
389
|
+
: classifyPr(pr, checks);
|
|
390
|
+
rows.push(createPrRow(pr, checks, classification));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const groups = groupPrRows(rows);
|
|
394
|
+
const decision = getPrPulseDecision(groups);
|
|
395
|
+
|
|
396
|
+
artifact.metrics = {
|
|
397
|
+
open: rows.length,
|
|
398
|
+
ready: groups.ready.length,
|
|
399
|
+
blocked: groups.blocked.length,
|
|
400
|
+
pending: groups.pending.length,
|
|
401
|
+
draft: groups.drafts.length,
|
|
402
|
+
};
|
|
403
|
+
artifact.status = getPrPulseStatus(groups);
|
|
404
|
+
artifact.summary = `${rows.length} open PR(s): ${groups.ready.length} ready, ${groups.blocked.length} blocked, ${groups.pending.length} pending, ${groups.drafts.length} draft.`;
|
|
405
|
+
artifact.decision.label = decision.label;
|
|
406
|
+
artifact.decision.rationale = decision.rationale;
|
|
407
|
+
artifact.decision.nextActions = buildPrNextActions(groups);
|
|
408
|
+
artifact.sections = [
|
|
409
|
+
{
|
|
410
|
+
title: 'Open PRs',
|
|
411
|
+
bullets: rows.map((row) => `#${row.number} ${row.state}: ${row.title || 'untitled'}`),
|
|
412
|
+
data: rows,
|
|
413
|
+
},
|
|
414
|
+
];
|
|
415
|
+
artifact.evidence = [
|
|
416
|
+
buildEvidence('openPrs', rows.length),
|
|
417
|
+
buildEvidence('readyPrs', formatPrNumbers(groups.ready) || 'none'),
|
|
418
|
+
buildEvidence('blockedPrs', formatPrNumbers(groups.blocked) || 'none'),
|
|
419
|
+
];
|
|
420
|
+
return artifact;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function buildReleaseReadinessArtifact(options = {}) {
|
|
424
|
+
const type = 'release-readiness';
|
|
425
|
+
const artifact = artifactBase(type, options);
|
|
426
|
+
const dashboard = options.dashboardData || {};
|
|
427
|
+
const packageInfo = options.packageInfo || readPackageInfo(options.packageRoot);
|
|
428
|
+
const health = dashboard.health || {};
|
|
429
|
+
const readiness = dashboard.readiness || {};
|
|
430
|
+
const gateAudit = dashboard.gateAudit || {};
|
|
431
|
+
|
|
432
|
+
const feedbackCount = safeNumber(health.feedbackCount);
|
|
433
|
+
const gateCount = safeNumber(health.gateCount);
|
|
434
|
+
const gateConfigLoaded = health.gateConfigLoaded !== false;
|
|
435
|
+
const warnings = Array.isArray(readiness.warnings) ? readiness.warnings : [];
|
|
436
|
+
const auditWarnings = safeNumber(gateAudit.warnings);
|
|
437
|
+
const version = packageInfo.version || 'unknown';
|
|
438
|
+
|
|
439
|
+
artifact.title = 'Release Readiness';
|
|
440
|
+
artifact.metrics = {
|
|
441
|
+
version,
|
|
442
|
+
feedbackCount,
|
|
443
|
+
gateCount,
|
|
444
|
+
gateConfigLoaded,
|
|
445
|
+
readinessWarnings: warnings.length,
|
|
446
|
+
gateAuditWarnings: auditWarnings,
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
if (!gateConfigLoaded || warnings.length > 0 || auditWarnings > 0) {
|
|
450
|
+
artifact.status = 'blocked';
|
|
451
|
+
artifact.decision.label = 'Hold release until readiness blockers are cleared';
|
|
452
|
+
artifact.decision.rationale = 'Release work needs a loaded gate config and no unresolved readiness warnings.';
|
|
453
|
+
artifact.decision.nextActions = compactList([
|
|
454
|
+
gateConfigLoaded ? null : 'Restore the gate config before release work.',
|
|
455
|
+
warnings[0] ? `Resolve readiness warning: ${warnings[0]}` : null,
|
|
456
|
+
auditWarnings > 0 ? `Clear ${auditWarnings} gate audit warning(s).` : null,
|
|
457
|
+
'Run the clean-worktree verification suite before publishing.',
|
|
458
|
+
], 4);
|
|
459
|
+
} else {
|
|
460
|
+
artifact.status = 'actionable';
|
|
461
|
+
artifact.decision.label = 'Verify in a clean worktree';
|
|
462
|
+
artifact.decision.rationale = 'Local readiness inputs look sane; the next gate is exact-commit verification.';
|
|
463
|
+
artifact.decision.nextActions = [
|
|
464
|
+
'Run npm ci in a dedicated clean verification worktree.',
|
|
465
|
+
'Run npm test, npm run test:coverage, npm run prove:adapters, npm run prove:automation, and npm run self-heal:check.',
|
|
466
|
+
'Submit release PRs through npm run pr:manage after checks are terminal.',
|
|
467
|
+
];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
artifact.summary = `ThumbGate ${version}: ${gateCount} gates, ${feedbackCount} feedback events, ${warnings.length + auditWarnings} readiness warning(s).`;
|
|
471
|
+
artifact.sections = [
|
|
472
|
+
{
|
|
473
|
+
title: 'Release Inputs',
|
|
474
|
+
bullets: [
|
|
475
|
+
`Package version: ${version}`,
|
|
476
|
+
`Gate config: ${gateConfigLoaded ? 'loaded' : 'missing'}`,
|
|
477
|
+
`${gateCount} configured gates`,
|
|
478
|
+
`${feedbackCount} feedback events`,
|
|
479
|
+
],
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
title: 'Verification',
|
|
483
|
+
bullets: artifact.decision.nextActions,
|
|
484
|
+
},
|
|
485
|
+
];
|
|
486
|
+
artifact.evidence = [
|
|
487
|
+
buildEvidence('package.version', version, { path: 'package.json' }),
|
|
488
|
+
buildEvidence('dashboard.health.gateConfigLoaded', gateConfigLoaded),
|
|
489
|
+
buildEvidence('dashboard.readiness.warnings', warnings.length),
|
|
490
|
+
];
|
|
491
|
+
return artifact;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function readPackageInfo(packageRoot) {
|
|
495
|
+
const root = packageRoot || path.join(__dirname, '..');
|
|
496
|
+
try {
|
|
497
|
+
return JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
498
|
+
} catch {
|
|
499
|
+
return {};
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function resolveDashboardData(options) {
|
|
504
|
+
if (options.dashboardData) return options.dashboardData;
|
|
505
|
+
const { generateDashboard } = require('./dashboard');
|
|
506
|
+
const { getFeedbackPaths } = require('./feedback-loop');
|
|
507
|
+
return generateDashboard(options.feedbackDir || getFeedbackPaths().FEEDBACK_DIR, {
|
|
508
|
+
now: options.now,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function resolveSessionReport(options) {
|
|
513
|
+
if (options.sessionReport) return options.sessionReport;
|
|
514
|
+
const { buildSessionReport } = require('./session-report');
|
|
515
|
+
return buildSessionReport({ windowHours: options.windowHours });
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function generateOperatorArtifact(options = {}) {
|
|
519
|
+
const type = normalizeArtifactType(options.type);
|
|
520
|
+
const windowHours = normalizeWindowHours(options.windowHours);
|
|
521
|
+
const sharedOptions = { ...options, type, windowHours };
|
|
522
|
+
|
|
523
|
+
if (type === 'pr-pulse') {
|
|
524
|
+
return buildPrPulseArtifact(sharedOptions);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const dashboardData = await resolveDashboardData(sharedOptions);
|
|
528
|
+
if (type === 'reliability-pulse') {
|
|
529
|
+
const sessionReport = resolveSessionReport(sharedOptions);
|
|
530
|
+
return buildReliabilityPulseArtifact({ ...sharedOptions, dashboardData, sessionReport });
|
|
531
|
+
}
|
|
532
|
+
if (type === 'revenue-pulse') {
|
|
533
|
+
return buildRevenuePulseArtifact({ ...sharedOptions, dashboardData });
|
|
534
|
+
}
|
|
535
|
+
return buildReleaseReadinessArtifact({ ...sharedOptions, dashboardData });
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function formatArtifactMarkdown(artifact) {
|
|
539
|
+
const lines = [
|
|
540
|
+
`# ${artifact.title || artifact.type}`,
|
|
541
|
+
'',
|
|
542
|
+
`Status: ${artifact.status}`,
|
|
543
|
+
`Window: ${artifact.windowHours}h`,
|
|
544
|
+
`Generated: ${artifact.generatedAt}`,
|
|
545
|
+
'',
|
|
546
|
+
`Summary: ${artifact.summary}`,
|
|
547
|
+
'',
|
|
548
|
+
`Decision: ${artifact.decision.label}`,
|
|
549
|
+
'',
|
|
550
|
+
artifact.decision.rationale,
|
|
551
|
+
'',
|
|
552
|
+
'Next actions:',
|
|
553
|
+
];
|
|
554
|
+
for (const action of artifact.decision.nextActions || []) {
|
|
555
|
+
lines.push(`- ${action}`);
|
|
556
|
+
}
|
|
557
|
+
for (const section of artifact.sections || []) {
|
|
558
|
+
lines.push('', `## ${section.title}`);
|
|
559
|
+
for (const bullet of section.bullets || []) {
|
|
560
|
+
lines.push(`- ${bullet}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (Array.isArray(artifact.evidence) && artifact.evidence.length > 0) {
|
|
564
|
+
lines.push('', '## Evidence');
|
|
565
|
+
for (const item of artifact.evidence) {
|
|
566
|
+
lines.push(`- ${item.label}: ${item.value}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return `${lines.join('\n')}\n`;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
module.exports = {
|
|
573
|
+
ARTIFACT_TYPES,
|
|
574
|
+
DEFAULT_WINDOW_HOURS,
|
|
575
|
+
MAX_WINDOW_HOURS,
|
|
576
|
+
buildPrPulseArtifact,
|
|
577
|
+
buildReliabilityPulseArtifact,
|
|
578
|
+
buildRevenuePulseArtifact,
|
|
579
|
+
buildReleaseReadinessArtifact,
|
|
580
|
+
formatArtifactMarkdown,
|
|
581
|
+
generateOperatorArtifact,
|
|
582
|
+
normalizeArtifactType,
|
|
583
|
+
normalizeWindowHours,
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
function isDirectCli() {
|
|
587
|
+
return Boolean(process.argv[1] && path.resolve(process.argv[1]) === __filename);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (isDirectCli()) {
|
|
591
|
+
const args = process.argv.slice(2);
|
|
592
|
+
const typeArg = args.find((arg) => arg.startsWith('--type='));
|
|
593
|
+
const windowArg = args.find((arg) => arg.startsWith('--window-hours='));
|
|
594
|
+
const format = args.includes('--markdown') ? 'markdown' : 'json';
|
|
595
|
+
generateOperatorArtifact({
|
|
596
|
+
type: typeArg ? typeArg.slice('--type='.length) : args.find((arg) => !arg.startsWith('--')),
|
|
597
|
+
windowHours: windowArg ? windowArg.slice('--window-hours='.length) : undefined,
|
|
598
|
+
}).then((artifact) => {
|
|
599
|
+
if (format === 'markdown') {
|
|
600
|
+
process.stdout.write(formatArtifactMarkdown(artifact));
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
process.stdout.write(`${JSON.stringify(artifact, null, 2)}\n`);
|
|
604
|
+
}).catch((err) => {
|
|
605
|
+
console.error(getErrorMessage(err));
|
|
606
|
+
process.exit(1);
|
|
607
|
+
});
|
|
608
|
+
}
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PR Manager — High-Throughput Merge & Blocker Diagnosis
|
|
4
|
+
*
|
|
5
|
+
* Inspired by the 2026 GitHub 'Quick Access' update. Centralizes merge status
|
|
6
|
+
* detection and triggers autonomous self-healing for common blockers.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
const { spawnSync } = require('node:child_process');
|
|
14
|
+
const PR_FIELDS = 'number,state,mergeable,mergeStateStatus,statusCheckRollup,reviewDecision,isDraft,title,url,headRefOid,baseRefName,mergeCommit,mergedAt,mergedBy';
|
|
15
|
+
const PR_CHECK_FIELDS = 'bucket,name,state,workflow,link,event';
|
|
16
|
+
const MERGE_QUALITY_CHECKS = JSON.parse(
|
|
17
|
+
fs.readFileSync(path.join(__dirname, '..', 'config', 'merge-quality-checks.json'), 'utf8')
|
|
18
|
+
);
|
|
19
|
+
const FIXED_GH_BINARIES = [
|
|
20
|
+
'/usr/bin/gh',
|
|
21
|
+
'/usr/local/bin/gh',
|
|
22
|
+
'/opt/homebrew/bin/gh',
|
|
23
|
+
];
|
|
24
|
+
const SUCCESSFUL_CHECK_CONCLUSIONS = new Set(['SUCCESS', 'SKIPPED', 'NEUTRAL']);
|
|
25
|
+
const FAILING_CHECK_CONCLUSIONS = new Set([
|
|
26
|
+
'ACTION_REQUIRED',
|
|
27
|
+
'CANCELLED',
|
|
28
|
+
'FAILURE',
|
|
29
|
+
'STALE',
|
|
30
|
+
'STARTUP_FAILURE',
|
|
31
|
+
'TIMED_OUT',
|
|
32
|
+
]);
|
|
33
|
+
const PASSING_BUCKETS = new Set((MERGE_QUALITY_CHECKS.passingBuckets || []).map((value) => String(value || '').toLowerCase()));
|
|
34
|
+
const PENDING_BUCKETS = new Set((MERGE_QUALITY_CHECKS.pendingBuckets || []).map((value) => String(value || '').toLowerCase()));
|
|
35
|
+
const FAILING_BUCKETS = new Set((MERGE_QUALITY_CHECKS.failingBuckets || []).map((value) => String(value || '').toLowerCase()));
|
|
36
|
+
|
|
37
|
+
function assertSafeGhArgs(args) {
|
|
38
|
+
if (!Array.isArray(args) || args.length === 0) {
|
|
39
|
+
throw new Error('GH CLI args must be a non-empty array.');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return args.map((arg) => {
|
|
43
|
+
const normalized = String(arg ?? '');
|
|
44
|
+
if (!normalized || /\0/.test(normalized)) {
|
|
45
|
+
throw new Error(`Unsafe GH CLI arg: ${arg}`);
|
|
46
|
+
}
|
|
47
|
+
return normalized;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizePrNumber(prNumber, { allowEmpty = true } = {}) {
|
|
52
|
+
const normalized = String(prNumber ?? '').trim();
|
|
53
|
+
if (!normalized) {
|
|
54
|
+
if (allowEmpty) {
|
|
55
|
+
return '';
|
|
56
|
+
}
|
|
57
|
+
throw new Error('PR number is required.');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!/^[1-9]\d*$/.test(normalized)) {
|
|
61
|
+
throw new Error(`Unsafe PR number: ${prNumber}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return normalized;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveGhBinary(options = {}) {
|
|
68
|
+
const accessSync = options.accessSync || fs.accessSync;
|
|
69
|
+
const candidates = [];
|
|
70
|
+
const configuredBinary = String(process.env.THUMBGATE_GH_BIN || '').trim();
|
|
71
|
+
|
|
72
|
+
if (configuredBinary) {
|
|
73
|
+
if (!path.isAbsolute(configuredBinary)) {
|
|
74
|
+
throw new Error(`Unsafe GH binary path: ${configuredBinary}`);
|
|
75
|
+
}
|
|
76
|
+
candidates.push(configuredBinary);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
candidates.push(...FIXED_GH_BINARIES);
|
|
80
|
+
|
|
81
|
+
for (const candidate of candidates) {
|
|
82
|
+
try {
|
|
83
|
+
accessSync(candidate, fs.constants.X_OK);
|
|
84
|
+
return candidate;
|
|
85
|
+
} catch {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
throw new Error(`Unable to locate GH CLI in fixed paths: ${candidates.join(', ')}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function runGh(args, options = {}) {
|
|
94
|
+
return spawnSync(resolveGhBinary(options), assertSafeGhArgs(args), {
|
|
95
|
+
encoding: 'utf-8',
|
|
96
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function formatGhError(result) {
|
|
101
|
+
return (result.stderr || result.stdout || 'Unknown GH CLI failure').trim();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isMissingCurrentBranchPr(result, prNumber) {
|
|
105
|
+
if (prNumber) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return /no pull requests found for branch/i.test(formatGhError(result));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Fetch granular PR status using GH CLI
|
|
114
|
+
*/
|
|
115
|
+
function getPrStatus(prNumber = '', runner = runGh) {
|
|
116
|
+
const normalizedPrNumber = normalizePrNumber(prNumber);
|
|
117
|
+
const args = ['pr', 'view'];
|
|
118
|
+
if (normalizedPrNumber) args.push(normalizedPrNumber);
|
|
119
|
+
args.push('--json', PR_FIELDS);
|
|
120
|
+
|
|
121
|
+
const result = runner(args);
|
|
122
|
+
if (result.status !== 0) {
|
|
123
|
+
if (isMissingCurrentBranchPr(result, normalizedPrNumber)) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
throw new Error(`Failed to fetch PR status: ${formatGhError(result)}`);
|
|
128
|
+
}
|
|
129
|
+
return JSON.parse(result.stdout);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getPrChecks(prNumber = '', runner = runGh) {
|
|
133
|
+
const normalizedPrNumber = normalizePrNumber(prNumber, { allowEmpty: false });
|
|
134
|
+
const result = runner(['pr', 'checks', normalizedPrNumber, '--json', PR_CHECK_FIELDS]);
|
|
135
|
+
if (result.status !== 0) {
|
|
136
|
+
throw new Error(`Failed to fetch PR checks: ${formatGhError(result)}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return JSON.parse(result.stdout || '[]');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function listOpenPrs(runner = runGh) {
|
|
143
|
+
const result = runner(['pr', 'list', '--state', 'open', '--json', PR_FIELDS]);
|
|
144
|
+
if (result.status !== 0) {
|
|
145
|
+
throw new Error(`Failed to list open PRs: ${formatGhError(result)}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return JSON.parse(result.stdout || '[]');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isOpenPr(pr) {
|
|
152
|
+
return Boolean(pr) && String(pr.state || 'OPEN').toUpperCase() === 'OPEN';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function loadManagedPrs(prNumber = '', runner = runGh) {
|
|
156
|
+
if (prNumber) {
|
|
157
|
+
return [getPrStatus(prNumber, runner)];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const currentBranchPr = getPrStatus('', runner);
|
|
161
|
+
if (isOpenPr(currentBranchPr)) {
|
|
162
|
+
return [currentBranchPr];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return listOpenPrs(runner);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function summarizeChecks(checks = []) {
|
|
169
|
+
const failing = [];
|
|
170
|
+
const pending = [];
|
|
171
|
+
|
|
172
|
+
for (const check of checks) {
|
|
173
|
+
const name = check.name || 'unknown-check';
|
|
174
|
+
const bucket = String(check.bucket || '').toLowerCase();
|
|
175
|
+
if (bucket) {
|
|
176
|
+
if (FAILING_BUCKETS.has(bucket)) {
|
|
177
|
+
failing.push(name);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (PENDING_BUCKETS.has(bucket)) {
|
|
182
|
+
pending.push(name);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (PASSING_BUCKETS.has(bucket)) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const conclusion = check.conclusion || null;
|
|
192
|
+
const status = check.status || (conclusion ? 'COMPLETED' : 'UNKNOWN');
|
|
193
|
+
|
|
194
|
+
if (status !== 'COMPLETED') {
|
|
195
|
+
pending.push(name);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (conclusion && FAILING_CHECK_CONCLUSIONS.has(conclusion)) {
|
|
200
|
+
failing.push(name);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (conclusion && !SUCCESSFUL_CHECK_CONCLUSIONS.has(conclusion)) {
|
|
205
|
+
pending.push(name);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { failing, pending };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function sleep(ms) {
|
|
213
|
+
if (!ms || ms <= 0) return;
|
|
214
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Diagnose and resolve blockers autonomously
|
|
219
|
+
*/
|
|
220
|
+
async function resolveBlockers(pr, runner = runGh) {
|
|
221
|
+
const title = pr.title || 'Untitled PR';
|
|
222
|
+
const mergeState = pr.mergeStateStatus || 'UNKNOWN';
|
|
223
|
+
const mergeable = pr.mergeable || 'UNKNOWN';
|
|
224
|
+
|
|
225
|
+
console.log(`[PR Manager] Diagnosing PR #${pr.number}: "${title}"`);
|
|
226
|
+
console.log(`[PR Manager] Merge State: ${mergeState} | Mergeable: ${mergeable}`);
|
|
227
|
+
|
|
228
|
+
if (pr.isDraft) {
|
|
229
|
+
console.log('[PR Manager] PR is a draft. Skipping.');
|
|
230
|
+
return { status: 'skipped', reason: 'draft' };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 1. Handle Outdated Branch (BEHIND)
|
|
234
|
+
if (pr.mergeStateStatus === 'BEHIND') {
|
|
235
|
+
console.log('[PR Manager] PR is behind main. Triggering auto-update...');
|
|
236
|
+
const update = runner(['pr', 'update-branch', pr.number.toString()]);
|
|
237
|
+
if (update.status === 0) {
|
|
238
|
+
return { status: 'healing', action: 'update-branch' };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 2. Handle Merge Conflicts (DIRTY)
|
|
243
|
+
if (pr.mergeStateStatus === 'DIRTY' || pr.mergeable === 'CONFLICTING') {
|
|
244
|
+
console.log('[PR Manager] CRITICAL: Merge conflicts detected. Manual intervention or advanced rebase required.');
|
|
245
|
+
return { status: 'blocked', reason: 'conflicts' };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 3. Handle CI Failures
|
|
249
|
+
let checks = pr.statusCheckRollup || [];
|
|
250
|
+
let checkSource = 'statusCheckRollup';
|
|
251
|
+
|
|
252
|
+
if (pr.number) {
|
|
253
|
+
try {
|
|
254
|
+
checks = getPrChecks(pr.number, runner);
|
|
255
|
+
checkSource = 'gh pr checks';
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.warn(`[PR Manager] Falling back to statusCheckRollup for PR #${pr.number}: ${error.message}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const checkSummary = summarizeChecks(checks);
|
|
262
|
+
const failingChecks = checkSummary.failing;
|
|
263
|
+
|
|
264
|
+
if (failingChecks.length > 0) {
|
|
265
|
+
console.log(`[PR Manager] BLOCKED: ${failingChecks.length} failing quality checks via ${checkSource}.`);
|
|
266
|
+
return { status: 'blocked', reason: 'ci_failure', checks: failingChecks, checkSource };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (checkSummary.pending.length > 0) {
|
|
270
|
+
console.log(`[PR Manager] BLOCKED: ${checkSummary.pending.length} quality checks still pending via ${checkSource}.`);
|
|
271
|
+
return { status: 'blocked', reason: 'ci_pending', checks: checkSummary.pending, checkSource };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 4. Handle Review Blockers
|
|
275
|
+
if (pr.reviewDecision === 'CHANGES_REQUESTED') {
|
|
276
|
+
console.log('[PR Manager] BLOCKED: Changes requested by reviewer.');
|
|
277
|
+
return { status: 'blocked', reason: 'changes_requested' };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (pr.reviewDecision === 'REVIEW_REQUIRED') {
|
|
281
|
+
console.log('[PR Manager] BLOCKED: Required review is still outstanding.');
|
|
282
|
+
return { status: 'blocked', reason: 'review_required' };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 5. Ready to Merge
|
|
286
|
+
if (pr.mergeStateStatus === 'CLEAN' && pr.mergeable === 'MERGEABLE') {
|
|
287
|
+
console.log('[PR Manager] SUCCESS: PR is ready for protected autonomous merge.');
|
|
288
|
+
return { status: 'ready' };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return { status: 'pending', reason: 'unknown_state' };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function waitForMergeCommit(prNumber, runner = runGh, options = {}) {
|
|
295
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 300000;
|
|
296
|
+
const intervalMs = Number.isFinite(options.intervalMs) ? options.intervalMs : 10000;
|
|
297
|
+
const startedAt = Date.now();
|
|
298
|
+
|
|
299
|
+
do {
|
|
300
|
+
const pr = getPrStatus(prNumber, runner);
|
|
301
|
+
if (pr && String(pr.state || '').toUpperCase() === 'MERGED' && pr.mergeCommit && pr.mergeCommit.oid) {
|
|
302
|
+
return {
|
|
303
|
+
finalized: true,
|
|
304
|
+
merged: true,
|
|
305
|
+
mergeCommit: pr.mergeCommit.oid,
|
|
306
|
+
mergedAt: pr.mergedAt || null,
|
|
307
|
+
mergedBy: pr.mergedBy && pr.mergedBy.login ? pr.mergedBy.login : null,
|
|
308
|
+
pr,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (pr && String(pr.state || '').toUpperCase() === 'CLOSED') {
|
|
313
|
+
return {
|
|
314
|
+
finalized: true,
|
|
315
|
+
merged: false,
|
|
316
|
+
reason: 'closed_without_merge',
|
|
317
|
+
pr,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (intervalMs <= 0) {
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if ((Date.now() - startedAt + intervalMs) > timeoutMs) {
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
sleep(intervalMs);
|
|
330
|
+
} while ((Date.now() - startedAt) <= timeoutMs);
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
finalized: false,
|
|
334
|
+
merged: false,
|
|
335
|
+
reason: 'merge_commit_pending',
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Perform autonomous merge
|
|
341
|
+
*/
|
|
342
|
+
function performMerge(prNumber, runner = runGh, options = {}) {
|
|
343
|
+
const normalizedPrNumber = normalizePrNumber(prNumber, { allowEmpty: false });
|
|
344
|
+
const args = ['pr', 'merge', normalizedPrNumber, '--squash', '--delete-branch'];
|
|
345
|
+
console.log(`[PR Manager] Initiating protected squash merge for PR #${normalizedPrNumber}...`);
|
|
346
|
+
const result = runner(args);
|
|
347
|
+
if (result.status === 0) {
|
|
348
|
+
const output = `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
349
|
+
const mode = /merge queue|queued/i.test(output) ? 'queued' : 'merged';
|
|
350
|
+
console.log(`[PR Manager] Merge accepted for PR #${normalizedPrNumber} (${mode}).`);
|
|
351
|
+
const mergeStatus = options.waitForMerge === false
|
|
352
|
+
? { finalized: false, merged: false, reason: 'merge_commit_pending' }
|
|
353
|
+
: waitForMergeCommit(normalizedPrNumber, runner, options);
|
|
354
|
+
return { ok: true, mode, args, ...mergeStatus };
|
|
355
|
+
} else {
|
|
356
|
+
console.error(`[PR Manager] Merge failed: ${formatGhError(result)}`);
|
|
357
|
+
return { ok: false, mode: 'failed', args, error: formatGhError(result) };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function managePrs(prNumber = '', runner = runGh, options = {}) {
|
|
362
|
+
const prs = loadManagedPrs(prNumber, runner).filter(Boolean);
|
|
363
|
+
|
|
364
|
+
if (prs.length === 0) {
|
|
365
|
+
console.log('[PR Manager] No open pull requests found.');
|
|
366
|
+
return { status: 'noop', prs: [] };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const results = [];
|
|
370
|
+
for (const pr of prs) {
|
|
371
|
+
const outcome = await resolveBlockers(pr, runner);
|
|
372
|
+
if (outcome.status === 'ready') {
|
|
373
|
+
const mergeResult = performMerge(pr.number, runner, options);
|
|
374
|
+
outcome.mergeRequested = mergeResult.ok;
|
|
375
|
+
outcome.mergeMode = mergeResult.mode;
|
|
376
|
+
if (mergeResult.mergeCommit) {
|
|
377
|
+
outcome.mergeCommit = mergeResult.mergeCommit;
|
|
378
|
+
}
|
|
379
|
+
if (mergeResult.finalized !== undefined) {
|
|
380
|
+
outcome.mergeFinalized = mergeResult.finalized;
|
|
381
|
+
}
|
|
382
|
+
if (mergeResult.reason) {
|
|
383
|
+
outcome.mergeResolution = mergeResult.reason;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
results.push({
|
|
388
|
+
number: pr.number,
|
|
389
|
+
title: pr.title,
|
|
390
|
+
outcome,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return { status: 'ok', prs: results };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (require.main === module) {
|
|
398
|
+
const prNum = process.argv[2];
|
|
399
|
+
managePrs(prNum).then(() => {
|
|
400
|
+
process.exit(0);
|
|
401
|
+
}).catch((err) => {
|
|
402
|
+
console.error(err.message);
|
|
403
|
+
process.exit(1);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
module.exports = {
|
|
408
|
+
assertSafeGhArgs,
|
|
409
|
+
getPrStatus,
|
|
410
|
+
getPrChecks,
|
|
411
|
+
listOpenPrs,
|
|
412
|
+
isOpenPr,
|
|
413
|
+
loadManagedPrs,
|
|
414
|
+
normalizePrNumber,
|
|
415
|
+
resolveBlockers,
|
|
416
|
+
resolveGhBinary,
|
|
417
|
+
waitForMergeCommit,
|
|
418
|
+
performMerge,
|
|
419
|
+
managePrs,
|
|
420
|
+
summarizeChecks,
|
|
421
|
+
};
|
package/scripts/tool-registry.js
CHANGED
|
@@ -1156,6 +1156,22 @@ const TOOLS = [
|
|
|
1156
1156
|
},
|
|
1157
1157
|
},
|
|
1158
1158
|
}),
|
|
1159
|
+
readOnlyTool({
|
|
1160
|
+
name: 'generate_operator_artifact',
|
|
1161
|
+
description: 'Dynamic operator artifact generator. Turns ThumbGate PR, reliability, revenue, and release data into a decision-ready pulse with metrics, evidence, and next actions.',
|
|
1162
|
+
inputSchema: {
|
|
1163
|
+
type: 'object',
|
|
1164
|
+
properties: {
|
|
1165
|
+
type: {
|
|
1166
|
+
type: 'string',
|
|
1167
|
+
enum: ['pr-pulse', 'reliability-pulse', 'revenue-pulse', 'release-readiness'],
|
|
1168
|
+
description: 'Artifact to generate. Defaults to reliability-pulse.',
|
|
1169
|
+
},
|
|
1170
|
+
windowHours: { type: 'number', description: 'Lookback window in hours (default 24, max 720)' },
|
|
1171
|
+
format: { type: 'string', enum: ['json', 'markdown'], description: 'Response format. Defaults to json.' },
|
|
1172
|
+
},
|
|
1173
|
+
},
|
|
1174
|
+
}),
|
|
1159
1175
|
readOnlyTool({
|
|
1160
1176
|
name: 'context_stuff_lessons',
|
|
1161
1177
|
description: 'Dump ALL prevention lessons into a single text block for context-window injection. Bypasses RAG/search — returns every lesson sorted by confidence. For most projects (20-200 lessons), fits in 1K-10K tokens.',
|