thumbgate 1.34.3 → 1.37.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.
Files changed (200) hide show
  1. package/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
  2. package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
  3. package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
  4. package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
  5. package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
  6. package/.claude-plugin/plugin.json +1 -1
  7. package/.well-known/llms.txt +1 -0
  8. package/.well-known/mcp/server-card.json +1 -1
  9. package/CONTRIBUTING.md +95 -0
  10. package/README.md +195 -632
  11. package/THIRD_PARTY_NOTICES.md +89 -0
  12. package/adapters/claude/.mcp.json +2 -2
  13. package/adapters/forge/forge.yaml +3 -3
  14. package/adapters/future-agi/.mcp.json +8 -0
  15. package/adapters/future-agi/FUTURE_AGI.md +23 -0
  16. package/adapters/future-agi/config.toml +3 -0
  17. package/adapters/future-agi/future-agi-bridge.js +9 -0
  18. package/adapters/future-agi/opencode.json +8 -0
  19. package/adapters/herdr/herdr-plugin.toml +18 -0
  20. package/adapters/mcp/server-stdio.js +238 -25
  21. package/adapters/opencode/opencode.json +1 -1
  22. package/adapters/workos/WORKOS.md +52 -0
  23. package/bin/cli.js +373 -5
  24. package/bin/futureagi-bridge +9 -0
  25. package/config/gate-templates.json +653 -4
  26. package/config/gates/actor-critic-audit.json +34 -0
  27. package/config/gates/default.json +21 -2
  28. package/config/gates/five-walls-governance.json +34 -0
  29. package/config/gates/future-agi-guardrails.json +34 -0
  30. package/config/gates/radware-threat-defense-2026.json +61 -0
  31. package/config/gates/simatree-data-governance.json +33 -0
  32. package/config/mcp-allowlists.json +4 -0
  33. package/config/merge-quality-checks.json +10 -1
  34. package/config/model-candidates.json +382 -24
  35. package/config/model-tiers.json +18 -0
  36. package/config/post-deploy-marketing-pages.json +10 -0
  37. package/config/progressive/01-wire-only.json +11 -0
  38. package/config/progressive/02-dashboard-empty-ok.json +10 -0
  39. package/config/progressive/03-one-lesson.json +10 -0
  40. package/config/progressive/04-warn-fires.json +11 -0
  41. package/config/progressive/05-strict-optional.json +11 -0
  42. package/config/progressive/README.md +15 -0
  43. package/config/schemas/broker-execution-receipt.schema.json +139 -0
  44. package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
  45. package/conformance/provider-attestation/vectors.json +320 -0
  46. package/docs/specs/provider-execution-attestation-v1.md +69 -0
  47. package/openapi/openapi.yaml +15 -0
  48. package/package.json +401 -147
  49. package/public/about.html +2 -2
  50. package/public/ai-malpractice-prevention.html +7 -7
  51. package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
  52. package/public/blog/a-receipt-is-not-world-state.html +388 -0
  53. package/public/blog/git-at-agent-scale.html +374 -0
  54. package/public/blog/no-llm-in-the-gate.html +133 -0
  55. package/public/blog.html +80 -0
  56. package/public/case-studies.html +16 -1
  57. package/public/compare.html +28 -0
  58. package/public/diagnostic.html +216 -7
  59. package/public/docs/connectors.html +39 -0
  60. package/public/federal.html +2 -2
  61. package/public/founders.html +639 -0
  62. package/public/index.html +87 -9
  63. package/public/install.html +8 -8
  64. package/public/learn.html +39 -0
  65. package/public/numbers.html +2 -2
  66. package/public/peter.html +310 -0
  67. package/public/platform-partners.html +119 -0
  68. package/public/pricing.html +24 -3
  69. package/public/privacy.html +117 -0
  70. package/public/pro.html +17 -0
  71. package/public/support.html +62 -0
  72. package/public/terms.html +130 -0
  73. package/public/third-party-notices.html +95 -0
  74. package/public/yt.html +351 -0
  75. package/scripts/action-receipts.js +133 -3
  76. package/scripts/adaptive-governance-arena.js +349 -0
  77. package/scripts/admin-override.js +205 -0
  78. package/scripts/agent-action-inventory.js +869 -0
  79. package/scripts/agent-audit-trace.js +42 -2
  80. package/scripts/agent-egress-policy.js +1117 -0
  81. package/scripts/agent-memory-lifecycle.js +141 -2
  82. package/scripts/agent-operations-planner.js +441 -1
  83. package/scripts/agent-readiness.js +68 -0
  84. package/scripts/agent-security-central.js +647 -0
  85. package/scripts/allowlist-bridge-honesty.js +417 -0
  86. package/scripts/async-job-runner.js +102 -11
  87. package/scripts/audit-trail.js +212 -0
  88. package/scripts/auto-promote-gates.js +178 -27
  89. package/scripts/billing.js +1 -1
  90. package/scripts/broker-execution-receipts.js +719 -0
  91. package/scripts/budget-aware-gates-proof.js +423 -0
  92. package/scripts/claude-feedback-sync.js +29 -3
  93. package/scripts/claw-harness-production.js +237 -0
  94. package/scripts/cli-progress.js +111 -0
  95. package/scripts/cli-schema.js +163 -1
  96. package/scripts/codex-runbook-flywheel.js +318 -0
  97. package/scripts/context-footprint.js +186 -0
  98. package/scripts/contextfs.js +143 -61
  99. package/scripts/dashboard-limits.js +27 -0
  100. package/scripts/dashboard.js +279 -9
  101. package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
  102. package/scripts/docker-sandbox-planner.js +18 -0
  103. package/scripts/double-blind-eval-protocol.js +252 -0
  104. package/scripts/edotenv-rl-gateway.js +259 -0
  105. package/scripts/ensure-production-search-corpus.js +162 -0
  106. package/scripts/eval-holdout.js +311 -0
  107. package/scripts/feedback-aggregate.js +21 -2
  108. package/scripts/feedback-loop.js +87 -5
  109. package/scripts/feedback-quality.js +9 -0
  110. package/scripts/file-ledger-lock.js +4 -1
  111. package/scripts/financial-control-plane.js +41 -1
  112. package/scripts/find-dormant-requires.js +118 -0
  113. package/scripts/fs-utils.js +84 -8
  114. package/scripts/gate-stats.js +2 -2
  115. package/scripts/gates-engine.js +859 -58
  116. package/scripts/generate-case-study-outreach.js +24 -15
  117. package/scripts/git-at-scale.js +628 -0
  118. package/scripts/governance-conflict-audit.js +1650 -0
  119. package/scripts/governance-difficulty-curriculum.js +328 -0
  120. package/scripts/graphrag-retrieval.js +275 -0
  121. package/scripts/gurobi-optimizer.js +324 -0
  122. package/scripts/gurobi_optimizer.py +485 -0
  123. package/scripts/harness-selector.js +82 -1
  124. package/scripts/hidden-entry-points.js +284 -0
  125. package/scripts/human-escalation.js +199 -1
  126. package/scripts/hybrid-feedback-context.js +152 -19
  127. package/scripts/intent-governed-execution.js +602 -0
  128. package/scripts/intervention-policy.js +123 -20
  129. package/scripts/jit-harness-compose.js +628 -0
  130. package/scripts/jsonl-watcher.js +10 -0
  131. package/scripts/lesson-embedding-index.js +95 -12
  132. package/scripts/lesson-retrieval.js +105 -19
  133. package/scripts/local-model-profile.js +19 -2
  134. package/scripts/mailer/resend-mailer.js +1 -1
  135. package/scripts/matryoshka-embedding.js +235 -0
  136. package/scripts/mcp-oauth.js +42 -4
  137. package/scripts/mcp-session-handles.js +1016 -0
  138. package/scripts/mcp-wiring-doctor.js +314 -0
  139. package/scripts/memory-firewall.js +115 -2
  140. package/scripts/memory-scope-readiness.js +299 -0
  141. package/scripts/memory-vs-rag-route.js +161 -0
  142. package/scripts/model-tier-router.js +148 -21
  143. package/scripts/nvidia-specdecode-al-doctor.js +536 -0
  144. package/scripts/openui-catalog-compose-honesty.js +593 -0
  145. package/scripts/operational-integrity.js +19 -1
  146. package/scripts/override-audit.js +213 -0
  147. package/scripts/package-manager-honesty-doctor.js +458 -0
  148. package/scripts/pr-manager.js +63 -1
  149. package/scripts/prove-herdr-adapter.js +52 -0
  150. package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
  151. package/scripts/prove-workos.js +73 -0
  152. package/scripts/provider-attestation-conformance.js +192 -0
  153. package/scripts/provider-receipt-contract.js +136 -0
  154. package/scripts/qwen38-max-cost-optimizer.js +401 -0
  155. package/scripts/radware-threat-defense.js +280 -0
  156. package/scripts/rag-embedding-identity.js +221 -0
  157. package/scripts/rag-precision-guardrails.js +112 -2
  158. package/scripts/remote-feedback-capture.js +159 -0
  159. package/scripts/research-agent-harness.js +256 -0
  160. package/scripts/rsi-safety-hillclimb.js +200 -0
  161. package/scripts/rule-sprawl.js +188 -0
  162. package/scripts/schedule-manager.js +147 -0
  163. package/scripts/self-heal.js +8 -0
  164. package/scripts/session-lease.js +415 -0
  165. package/scripts/simatree-data-governance.js +347 -0
  166. package/scripts/slo-alert-engine.js +172 -7
  167. package/scripts/solver-parity.js +539 -0
  168. package/scripts/stealth-memory-injection-gate.js +333 -0
  169. package/scripts/switchyard-router.js +366 -0
  170. package/scripts/telemetry-analytics.js +84 -27
  171. package/scripts/temporal-decay-weighting.js +138 -0
  172. package/scripts/test-all.js +165 -0
  173. package/scripts/token-savings.js +42 -0
  174. package/scripts/tool-kpi-tracker.js +108 -5
  175. package/scripts/tool-registry.js +193 -5
  176. package/scripts/universal-claim-evaluator.js +14 -2
  177. package/scripts/vector-store.js +279 -9
  178. package/scripts/workflow-notebook.js +391 -0
  179. package/scripts/workflow-sentinel.js +111 -12
  180. package/scripts/workos-production-guard.js +260 -0
  181. package/scripts/workspace-search-route.js +515 -0
  182. package/server.json +2 -2
  183. package/src/agent-identity-boundary.js +76 -0
  184. package/src/agent-retrieval-cache.js +155 -0
  185. package/src/alert-noise-ledger.js +502 -0
  186. package/src/api/server.js +802 -185
  187. package/src/git-fast-cache.js +220 -0
  188. package/src/git-wal-sync.js +156 -0
  189. package/src/hash-anchored-edit.js +82 -0
  190. package/src/hermes-platform-protocol.js +475 -0
  191. package/src/hermes-sync-plane.js +241 -0
  192. package/src/index.js +30 -1
  193. package/src/iso42001-compliance-guard.js +97 -0
  194. package/src/latency-budget.js +244 -0
  195. package/src/mcp-writeguard.js +316 -0
  196. package/src/miminions-adapter.js +106 -0
  197. package/src/pipeline-compass.js +104 -0
  198. package/src/ppl-alert-pipeline.js +284 -0
  199. package/src/rendezvous-router.js +90 -0
  200. package/src/security-questionnaire.js +195 -0
@@ -0,0 +1,213 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * override-audit.js — typed, first-class audit records for gate overrides.
5
+ *
6
+ * WHY THIS EXISTS
7
+ * ---------------
8
+ * Before this module, an override left one of two traces:
9
+ *
10
+ * 1. Via the MCP tool -> a generic tool-call row in audit-trail.jsonl with
11
+ * toolName "satisfy_gate", indistinguishable by TYPE from every Bash call.
12
+ * 2. Via the CLI -> `satisfyCondition()` wrote ONLY to the gate state
13
+ * store. Nothing reached the audit trail at all. The override was invisible.
14
+ *
15
+ * Path 2 is the dangerous one: it is exactly the path an agent falls back to
16
+ * when the MCP tool is unavailable, so the least-supervised override was also
17
+ * the least-recorded one. An override log that misses overrides is worse than
18
+ * none, because it reads as completeness.
19
+ *
20
+ * This module gives overrides a typed shape (decision "override") that can be
21
+ * filtered, counted, and rendered, no matter which path produced them.
22
+ */
23
+
24
+ const fs = require('node:fs');
25
+ const path = require('node:path');
26
+
27
+ const { recordAuditEvent, getAuditLogPath } = require('./audit-trail');
28
+
29
+ /** Marker written into every override record. */
30
+ const OVERRIDE_DECISION = 'override';
31
+ const OVERRIDE_TOOL_NAME = 'gate-override';
32
+
33
+ /** Where the override came from. Unknown callers are recorded, never dropped. */
34
+ const SOURCES = Object.freeze(['mcp', 'cli', 'break-glass', 'api', 'unknown']);
35
+
36
+ function normalizeSource(source) {
37
+ if (typeof source !== 'string') return 'unknown';
38
+ const lower = source.trim().toLowerCase();
39
+ return SOURCES.includes(lower) ? lower : 'unknown';
40
+ }
41
+
42
+ /**
43
+ * Truncate free text so a pathological evidence blob cannot bloat the log.
44
+ * Truncation is marked so a reader never mistakes a cut string for the whole.
45
+ */
46
+ function clip(value, max = 2000) {
47
+ if (typeof value !== 'string') return null;
48
+ if (value.length <= max) return value;
49
+ return `${value.slice(0, max)}…[truncated ${value.length - max} chars]`;
50
+ }
51
+
52
+ function normalizeReasoning(reasoning) {
53
+ if (!reasoning || typeof reasoning !== 'object' || Array.isArray(reasoning)) return null;
54
+ const out = {
55
+ premise: clip(reasoning.premise, 1000),
56
+ evidence: clip(reasoning.evidence, 1000),
57
+ risk: clip(reasoning.risk, 1000),
58
+ conclusion: clip(reasoning.conclusion, 1000),
59
+ };
60
+ // An object of four nulls is noise, not reasoning.
61
+ return Object.values(out).some((v) => v !== null) ? out : null;
62
+ }
63
+
64
+ /**
65
+ * Record one gate override.
66
+ *
67
+ * Never throws: an audit failure must not also break the caller's control flow,
68
+ * or a broken log would become a reason to skip recording entirely.
69
+ *
70
+ * @returns {object|null} the stored record, or null if recording failed
71
+ */
72
+ function recordOverride(params = {}) {
73
+ const gateId = typeof params.gateId === 'string' && params.gateId.trim() ? params.gateId.trim() : null;
74
+ if (!gateId) return null; // an override with no gate is not attributable
75
+
76
+ const override = {
77
+ gateId,
78
+ source: normalizeSource(params.source),
79
+ actor: clip(params.actor, 200) || 'agent',
80
+ reason: clip(params.reason),
81
+ evidence: clip(params.evidence),
82
+ structuredReasoning: normalizeReasoning(params.structuredReasoning),
83
+ // A reasoned override is materially different from a bare one. Surface it
84
+ // as a first-class field so the panel can rank unreasoned overrides first.
85
+ reasoned: Boolean(normalizeReasoning(params.structuredReasoning)),
86
+ ttlMs: Number.isFinite(params.ttlMs) ? params.ttlMs : null,
87
+ pathGlobs: Array.isArray(params.pathGlobs) ? params.pathGlobs.slice(0, 50).map(String) : null,
88
+ };
89
+
90
+ try {
91
+ return recordAuditEvent({
92
+ toolName: OVERRIDE_TOOL_NAME,
93
+ toolInput: { gateId, source: override.source },
94
+ decision: OVERRIDE_DECISION,
95
+ gateId,
96
+ message: override.reason || `Override of ${gateId}`,
97
+ severity: 'high',
98
+ source: 'override-audit',
99
+ override,
100
+ });
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ function resolveLogPath(logPath) {
107
+ if (logPath) return logPath;
108
+ try {
109
+ return getAuditLogPath();
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Read typed override records, newest first.
117
+ *
118
+ * Reads the tail of the file rather than the whole thing: audit logs reach
119
+ * hundreds of MB and a panel must not allocate the entire history to show 50 rows.
120
+ */
121
+ function readOverrides(options = {}) {
122
+ const logPath = resolveLogPath(options.logPath);
123
+ if (!logPath || !fs.existsSync(logPath)) return [];
124
+
125
+ const limit = Number.isFinite(options.limit) ? Math.max(0, options.limit) : 100;
126
+ if (limit === 0) return [];
127
+ const sinceMs = Number.isFinite(options.sinceMs) ? options.sinceMs : null;
128
+ const maxBytes = Number.isFinite(options.maxBytes) ? options.maxBytes : 8 * 1024 * 1024;
129
+
130
+ let text = '';
131
+ let fd;
132
+ try {
133
+ const { size } = fs.statSync(logPath);
134
+ if (size === 0) return [];
135
+ const readBytes = Math.min(size, maxBytes);
136
+ const buf = Buffer.alloc(readBytes);
137
+ fd = fs.openSync(logPath, 'r');
138
+ fs.readSync(fd, buf, 0, readBytes, size - readBytes);
139
+ text = buf.toString('utf8');
140
+ if (readBytes < size) {
141
+ const nl = text.indexOf('\n');
142
+ text = nl === -1 ? '' : text.slice(nl + 1); // drop the partial first record
143
+ }
144
+ } catch {
145
+ return [];
146
+ } finally {
147
+ if (fd !== undefined) {
148
+ try { fs.closeSync(fd); } catch { /* already closed */ }
149
+ }
150
+ }
151
+
152
+ const out = [];
153
+ const lines = text.split('\n');
154
+ for (let i = lines.length - 1; i >= 0 && out.length < limit; i -= 1) {
155
+ const line = lines[i];
156
+ if (!line) continue;
157
+ // Cheap reject before JSON.parse — most rows are not overrides.
158
+ if (!line.includes(`"${OVERRIDE_DECISION}"`)) continue;
159
+ let rec;
160
+ try {
161
+ rec = JSON.parse(line);
162
+ } catch {
163
+ continue; // a corrupt line must not abort the scan
164
+ }
165
+ if (!rec || rec.decision !== OVERRIDE_DECISION || !rec.override) continue;
166
+ if (sinceMs !== null) {
167
+ const ts = Date.parse(rec.timestamp);
168
+ if (Number.isFinite(ts) && ts < sinceMs) continue;
169
+ }
170
+ out.push(rec);
171
+ }
172
+ return out;
173
+ }
174
+
175
+ /**
176
+ * Aggregate overrides for the dashboard panel.
177
+ * Unreasoned overrides are counted separately because they are the ones a
178
+ * reviewer should look at first.
179
+ */
180
+ function summarizeOverrides(options = {}) {
181
+ const records = readOverrides({ ...options, limit: options.limit ?? 500 });
182
+ const byGate = {};
183
+ const bySource = {};
184
+ const byActor = {};
185
+ let unreasoned = 0;
186
+
187
+ for (const rec of records) {
188
+ const o = rec.override || {};
189
+ byGate[o.gateId] = (byGate[o.gateId] || 0) + 1;
190
+ bySource[o.source] = (bySource[o.source] || 0) + 1;
191
+ byActor[o.actor] = (byActor[o.actor] || 0) + 1;
192
+ if (!o.reasoned) unreasoned += 1;
193
+ }
194
+
195
+ return {
196
+ total: records.length,
197
+ unreasoned,
198
+ byGate,
199
+ bySource,
200
+ byActor,
201
+ mostRecent: records[0] || null,
202
+ records,
203
+ };
204
+ }
205
+
206
+ module.exports = {
207
+ recordOverride,
208
+ readOverrides,
209
+ summarizeOverrides,
210
+ OVERRIDE_DECISION,
211
+ OVERRIDE_TOOL_NAME,
212
+ SOURCES,
213
+ };
@@ -0,0 +1,458 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Package-manager honesty doctor (InfoQ pnpm 12 process steal).
6
+ *
7
+ * Source: https://www.infoq.com/news/2026/09/pnpm-12-rust/
8
+ *
9
+ * Transfers:
10
+ * - Single lockfile + packageManager pin honesty
11
+ * - CI install command must match the lockfile manager
12
+ * - Lifecycle scripts are spicy by default on npm — prefer --ignore-scripts
13
+ * evidence for agent/untrusted installs
14
+ * - Fail-closed when an agent proposes switching managers without CI rewrite
15
+ * - pnpm 12 compat checklist (resolution-only → peers check, etc.) when
16
+ * switching TO pnpm — without migrating this repo off npm
17
+ *
18
+ * Does NOT migrate ThumbGate from npm → pnpm. CI stays on npm ci.
19
+ */
20
+
21
+ const fs = require('node:fs');
22
+ const path = require('node:path');
23
+
24
+ const SOURCE_URL = 'https://www.infoq.com/news/2026/09/pnpm-12-rust/';
25
+ const LOCKFILES = [
26
+ { file: 'package-lock.json', manager: 'npm' },
27
+ { file: 'pnpm-lock.yaml', manager: 'pnpm' },
28
+ { file: 'yarn.lock', manager: 'yarn' },
29
+ { file: 'bun.lockb', manager: 'bun' },
30
+ { file: 'bun.lock', manager: 'bun' },
31
+ ];
32
+
33
+ const PNPM12_COMPAT_CHECKS = [
34
+ {
35
+ id: 'resolution-only-removed',
36
+ legacy: 'pnpm install --resolution-only',
37
+ replacement: 'pnpm peers check',
38
+ note: 'pnpm 12 removed --resolution-only; CI that still uses it will break.',
39
+ },
40
+ {
41
+ id: 'workspace-unknown-keys',
42
+ legacy: 'unknown keys silently ignored in pnpm-workspace.yaml',
43
+ replacement: 'unknown keys reported as errors',
44
+ note: 'Audit pnpm-workspace.yaml before upgrading; unknown keys now fail closed.',
45
+ },
46
+ {
47
+ id: 'git-canonical-https',
48
+ legacy: 'git+ssh://github.com/... style git deps',
49
+ replacement: 'canonical HTTPS + Git URL rewrite for private SSH',
50
+ note: 'GitHub/GitLab/Bitbucket git deps resolve via HTTPS; configure URL rewriting for private SSH.',
51
+ },
52
+ ];
53
+
54
+ function normalizeBoolean(value) {
55
+ if (value === true) return true;
56
+ if (value === false || value === undefined || value === null) return false;
57
+ return /^(1|true|yes|on)$/i.test(String(value).trim());
58
+ }
59
+
60
+ function detectLockfiles(rootDir) {
61
+ return LOCKFILES
62
+ .filter((entry) => fs.existsSync(path.join(rootDir, entry.file)))
63
+ .map((entry) => ({ ...entry, path: path.join(rootDir, entry.file) }));
64
+ }
65
+
66
+ function readPackageManagerPin(rootDir) {
67
+ const pkgPath = path.join(rootDir, 'package.json');
68
+ if (!fs.existsSync(pkgPath)) return { path: pkgPath, pin: null, raw: null };
69
+ let pkg;
70
+ try {
71
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
72
+ } catch {
73
+ return { path: pkgPath, pin: null, raw: null, parseError: true };
74
+ }
75
+ const raw = pkg.packageManager ? String(pkg.packageManager) : null;
76
+ if (!raw) return { path: pkgPath, pin: null, raw: null };
77
+ const m = /^(npm|pnpm|yarn|bun)@/.exec(raw);
78
+ return { path: pkgPath, pin: m ? m[1] : raw.split('@')[0], raw };
79
+ }
80
+
81
+ function listWorkflowFiles(workflowsDir) {
82
+ if (!fs.existsSync(workflowsDir)) return [];
83
+ const out = [];
84
+ for (const name of fs.readdirSync(workflowsDir)) {
85
+ if (!/\.ya?ml$/i.test(name)) continue;
86
+ out.push(path.join(workflowsDir, name));
87
+ }
88
+ return out;
89
+ }
90
+
91
+ /**
92
+ * Extract install-ish commands from workflow YAML text (line-oriented, not a YAML parser).
93
+ * Only inspects `run:` steps (including folded `|` / `>` blocks) and splits shell
94
+ * chains (`&&`, `;`, `|`) so each manager+flags pair is recorded separately.
95
+ */
96
+ function extractInstallCommands(text, filePath) {
97
+ const lines = String(text).split(/\r?\n/);
98
+ const commands = [];
99
+ const installRe = /\b(npm\s+ci|npm\s+install|npm\s+i\b|pnpm\s+(?:i|install|fetch|import)|yarn\s+(?:install|immutable)|bun\s+install)\b/i;
100
+ const runStartRe = /^(\s*)-\s*run:\s*(.*)$/;
101
+
102
+ function recordCommand(cmdText, lineNo) {
103
+ const trimmed = String(cmdText || '').trim();
104
+ if (!trimmed || trimmed.startsWith('#') || !installRe.test(trimmed)) return;
105
+ let manager = 'npm';
106
+ if (/\bpnpm\b/i.test(trimmed)) manager = 'pnpm';
107
+ else if (/\byarn\b/i.test(trimmed)) manager = 'yarn';
108
+ else if (/\bbun\b/i.test(trimmed)) manager = 'bun';
109
+ commands.push({
110
+ file: filePath,
111
+ line: lineNo,
112
+ text: trimmed,
113
+ manager,
114
+ ignoreScripts: /--ignore-scripts\b/.test(trimmed),
115
+ });
116
+ }
117
+
118
+ function splitShellChain(raw) {
119
+ // Split on &&, ;, and bare | (not ||). Keep install segments intact.
120
+ return String(raw)
121
+ .split(/(?:&&|;|(?<!\|)\|(?!\|))/)
122
+ .map((s) => s.trim())
123
+ .filter(Boolean);
124
+ }
125
+
126
+ for (let i = 0; i < lines.length; i += 1) {
127
+ const match = runStartRe.exec(lines[i]);
128
+ if (!match) continue;
129
+ const indent = match[1].length;
130
+ let rest = match[2].trim();
131
+ let startLine = i + 1;
132
+
133
+ if (rest === '|' || rest === '>' || rest === '|-' || rest === '>-') {
134
+ // Multiline run block: each deeper-indented line is its own shell statement
135
+ // (and may itself contain && / ; chains).
136
+ let j = i + 1;
137
+ while (j < lines.length) {
138
+ const next = lines[j];
139
+ if (!next.trim()) { j += 1; continue; }
140
+ const nextIndent = next.match(/^(\s*)/)[1].length;
141
+ if (nextIndent <= indent) break;
142
+ const lineNo = j + 1;
143
+ for (const segment of splitShellChain(next.trim())) {
144
+ recordCommand(segment, lineNo);
145
+ }
146
+ j += 1;
147
+ }
148
+ i = j - 1;
149
+ continue;
150
+ }
151
+ if (!rest) continue;
152
+
153
+ for (const segment of splitShellChain(rest)) {
154
+ recordCommand(segment, startLine);
155
+ }
156
+ }
157
+ return commands;
158
+ }
159
+
160
+ function scanWorkflowInstalls(rootDir) {
161
+ const workflowsDir = path.join(rootDir, '.github', 'workflows');
162
+ const files = listWorkflowFiles(workflowsDir);
163
+ const commands = [];
164
+ for (const file of files) {
165
+ const text = fs.readFileSync(file, 'utf8');
166
+ commands.push(...extractInstallCommands(text, path.relative(rootDir, file)));
167
+ }
168
+ return commands;
169
+ }
170
+
171
+ function inferCanonicalManager(lockfiles, pin) {
172
+ if (lockfiles.length === 1) return lockfiles[0].manager;
173
+ if (pin && lockfiles.some((l) => l.manager === pin)) return pin;
174
+ if (lockfiles.some((l) => l.manager === 'npm')) return 'npm';
175
+ return lockfiles[0] ? lockfiles[0].manager : null;
176
+ }
177
+
178
+ function buildFindings({
179
+ lockfiles,
180
+ pinInfo,
181
+ installs,
182
+ canonical,
183
+ proposeSwitch,
184
+ allowIgnoreScriptsGaps,
185
+ }) {
186
+ const findings = [];
187
+
188
+ if (lockfiles.length === 0) {
189
+ findings.push({
190
+ id: 'no_lockfile',
191
+ severity: 'fail',
192
+ gateId: 'require-package-manager-lockfile-ci-parity',
193
+ message: 'No package-lock.json / pnpm-lock.yaml / yarn.lock / bun.lock found. Installs are not reproducible.',
194
+ });
195
+ }
196
+
197
+ if (lockfiles.length > 1) {
198
+ findings.push({
199
+ id: 'multiple_lockfiles',
200
+ severity: 'fail',
201
+ gateId: 'require-package-manager-lockfile-ci-parity',
202
+ message: `Multiple lockfiles present (${lockfiles.map((l) => l.file).join(', ')}). Keep exactly one manager+lockfile pair.`,
203
+ });
204
+ }
205
+
206
+ if (pinInfo.pin && canonical && pinInfo.pin !== canonical) {
207
+ findings.push({
208
+ id: 'package_manager_pin_mismatch',
209
+ severity: 'fail',
210
+ gateId: 'require-package-manager-lockfile-ci-parity',
211
+ message: `package.json packageManager=${pinInfo.raw} disagrees with lockfile manager=${canonical}.`,
212
+ });
213
+ }
214
+
215
+ if (pinInfo.parseError) {
216
+ findings.push({
217
+ id: 'package_json_parse_error',
218
+ severity: 'fail',
219
+ gateId: 'require-package-manager-lockfile-ci-parity',
220
+ message: 'package.json could not be parsed. Fix JSON before trusting packageManager pin / install commands.',
221
+ });
222
+ } else if (!pinInfo.pin && canonical) {
223
+ findings.push({
224
+ id: 'package_manager_pin_missing',
225
+ severity: 'info',
226
+ gateId: null,
227
+ message: `No packageManager pin in package.json. Canonical lockfile manager is ${canonical}. Pinning is optional for npm-default repos but required before switching managers.`,
228
+ });
229
+ }
230
+
231
+ const mismatchedCi = installs.filter((c) => canonical && c.manager !== canonical);
232
+ if (mismatchedCi.length > 0) {
233
+ const sample = mismatchedCi.slice(0, 3)
234
+ .map((c) => `${c.file}:${c.line} (${c.manager})`)
235
+ .join('; ');
236
+ findings.push({
237
+ id: 'ci_install_manager_mismatch',
238
+ severity: 'fail',
239
+ gateId: 'require-package-manager-lockfile-ci-parity',
240
+ message: `${mismatchedCi.length} CI install command(s) use a different manager than lockfile ${canonical}. Examples: ${sample}`,
241
+ });
242
+ }
243
+
244
+ const withoutIgnore = installs.filter((c) => !c.ignoreScripts);
245
+ if (withoutIgnore.length > 0 && !allowIgnoreScriptsGaps) {
246
+ findings.push({
247
+ id: 'ci_install_without_ignore_scripts',
248
+ severity: 'warn',
249
+ gateId: 'block-package-lifecycle-secret-harvest',
250
+ message: `${withoutIgnore.length}/${installs.length} CI install lines lack --ignore-scripts. npm runs lifecycle scripts by default (spicy); prefer --ignore-scripts unless prepare/build scripts are required and reviewed.`,
251
+ });
252
+ }
253
+
254
+ if (proposeSwitch) {
255
+ const target = String(proposeSwitch).trim().toLowerCase();
256
+ if (!['npm', 'pnpm', 'yarn', 'bun'].includes(target)) {
257
+ findings.push({
258
+ id: 'unknown_switch_target',
259
+ severity: 'fail',
260
+ gateId: 'checkpoint-package-manager-switch',
261
+ message: `Unknown --propose-switch=${proposeSwitch}. Expected npm|pnpm|yarn|bun.`,
262
+ });
263
+ } else if (canonical && target === canonical) {
264
+ findings.push({
265
+ id: 'switch_noop',
266
+ severity: 'info',
267
+ gateId: null,
268
+ message: `Proposed switch target ${target} already matches canonical manager.`,
269
+ });
270
+ } else {
271
+ findings.push({
272
+ id: 'switch_without_migration_plan',
273
+ severity: 'fail',
274
+ gateId: 'checkpoint-package-manager-switch',
275
+ message: `Refuse silent switch ${canonical || 'unknown'} → ${target}. Require: single new lockfile, packageManager pin, rewrite every CI install, delete the old lockfile, and re-run npm pack/CI green. ThumbGate stays on npm unless that plan is complete.`,
276
+ });
277
+ if (target === 'pnpm') {
278
+ for (const check of PNPM12_COMPAT_CHECKS) {
279
+ findings.push({
280
+ id: `pnpm12_${check.id}`,
281
+ severity: 'warn',
282
+ gateId: 'checkpoint-package-manager-switch',
283
+ message: `pnpm 12 compat: replace "${check.legacy}" with "${check.replacement}". ${check.note}`,
284
+ });
285
+ }
286
+ }
287
+ }
288
+ }
289
+
290
+ return findings;
291
+ }
292
+
293
+ function normalizeOptions(options = {}) {
294
+ const rootDir = path.resolve(
295
+ String(options.root || options.cwd || options['root-dir'] || process.cwd())
296
+ );
297
+ return {
298
+ rootDir,
299
+ proposeSwitch: options['propose-switch'] || options.switch || options.propose || null,
300
+ allowIgnoreScriptsGaps: normalizeBoolean(options['allow-ignore-scripts-gaps']),
301
+ strict: normalizeBoolean(options.strict),
302
+ json: normalizeBoolean(options.json),
303
+ };
304
+ }
305
+
306
+ function buildPackageManagerHonestyReport(rawOptions = {}) {
307
+ const options = normalizeOptions(rawOptions);
308
+ const lockfiles = detectLockfiles(options.rootDir);
309
+ const pinInfo = readPackageManagerPin(options.rootDir);
310
+ const installs = scanWorkflowInstalls(options.rootDir);
311
+ const canonical = inferCanonicalManager(lockfiles, pinInfo.pin);
312
+ const findings = buildFindings({
313
+ lockfiles,
314
+ pinInfo,
315
+ installs,
316
+ canonical,
317
+ proposeSwitch: options.proposeSwitch,
318
+ allowIgnoreScriptsGaps: options.allowIgnoreScriptsGaps,
319
+ });
320
+
321
+ const failCount = findings.filter((f) => f.severity === 'fail').length;
322
+ const warnCount = findings.filter((f) => f.severity === 'warn').length;
323
+ let status = 'ready';
324
+ if (failCount > 0) status = 'fail';
325
+ else if (warnCount > 0) status = 'actionable';
326
+
327
+ const ignoreScriptsCount = installs.filter((c) => c.ignoreScripts).length;
328
+
329
+ return {
330
+ name: 'thumbgate-package-manager-honesty-doctor',
331
+ status,
332
+ source: SOURCE_URL,
333
+ disclaimer:
334
+ 'Process steal from InfoQ pnpm 12 coverage (Rust rewrite, lockfile honesty, lifecycle defaults). Does not migrate this repo to pnpm and is not affiliated with pnpm.',
335
+ rootDir: options.rootDir,
336
+ metrics: {
337
+ lockfileCount: lockfiles.length,
338
+ lockfiles: lockfiles.map((l) => l.file),
339
+ canonicalManager: canonical,
340
+ packageManagerPin: pinInfo.raw,
341
+ ciInstallCount: installs.length,
342
+ ciIgnoreScriptsCount: ignoreScriptsCount,
343
+ ciIgnoreScriptsRatio: installs.length
344
+ ? Number((ignoreScriptsCount / installs.length).toFixed(4))
345
+ : null,
346
+ proposeSwitch: options.proposeSwitch || null,
347
+ },
348
+ findings,
349
+ summary: {
350
+ failCount,
351
+ warnCount,
352
+ findingCount: findings.length,
353
+ recommendedGateCount: [...new Set(findings.map((f) => f.gateId).filter(Boolean))].length,
354
+ },
355
+ recommendedGates: [...new Set(findings.map((f) => f.gateId).filter(Boolean))],
356
+ nextActions: [
357
+ 'Keep exactly one lockfile; do not add pnpm-lock.yaml beside package-lock.json.',
358
+ 'CI install manager must match the lockfile (ThumbGate: npm ci).',
359
+ 'Prefer --ignore-scripts on agent/untrusted installs; npm runs lifecycle scripts by default.',
360
+ 'Refuse package-manager switches without pin + CI rewrite + old lockfile deletion.',
361
+ 'If evaluating pnpm 12: use peers check (not --resolution-only) and audit workspace unknown keys — do not rewrite this public npm package casually.',
362
+ ],
363
+ exampleCommand:
364
+ 'npx thumbgate package-manager-honesty-doctor --propose-switch=pnpm --json',
365
+ };
366
+ }
367
+
368
+ function formatPackageManagerHonestyReport(report) {
369
+ const lines = [
370
+ '',
371
+ 'ThumbGate Package-Manager Honesty Doctor',
372
+ '-'.repeat(40),
373
+ `Status : ${report.status}`,
374
+ `Root : ${report.rootDir}`,
375
+ `Canonical: ${report.metrics.canonicalManager || 'none'}`,
376
+ `Lockfiles: ${report.metrics.lockfiles.join(', ') || 'none'}`,
377
+ `Pin : ${report.metrics.packageManagerPin || '(unset)'}`,
378
+ `CI installs: ${report.metrics.ciInstallCount} (ignore-scripts ${report.metrics.ciIgnoreScriptsCount})`,
379
+ `Findings : ${report.summary.findingCount} (fail=${report.summary.failCount}, warn=${report.summary.warnCount})`,
380
+ `Source : ${report.source}`,
381
+ ];
382
+ if (report.findings.length) {
383
+ lines.push('', 'Findings:');
384
+ for (const f of report.findings) {
385
+ const gate = f.gateId ? ` [${f.gateId}]` : '';
386
+ lines.push(` - [${f.severity}] ${f.id}${gate}`);
387
+ lines.push(` ${f.message}`);
388
+ }
389
+ }
390
+ lines.push('', 'Next actions:');
391
+ for (const a of report.nextActions) lines.push(` - ${a}`);
392
+ lines.push('', `Example: ${report.exampleCommand}`);
393
+ lines.push(`Note: ${report.disclaimer}`, '');
394
+ return `${lines.join('\n')}\n`;
395
+ }
396
+
397
+ function parseCliArgs(argv) {
398
+ const options = {};
399
+ for (const arg of argv) {
400
+ if (arg === '--json') { options.json = true; continue; }
401
+ if (arg === '--strict') { options.strict = true; continue; }
402
+ if (arg === '--help' || arg === '-h') { options.help = true; continue; }
403
+ const m = /^--([^=]+)(?:=(.*))?$/.exec(arg);
404
+ if (!m) continue;
405
+ options[m[1]] = m[2] === undefined ? true : m[2];
406
+ }
407
+ return options;
408
+ }
409
+
410
+ function printHelp() {
411
+ process.stdout.write(`Usage: node scripts/package-manager-honesty-doctor.js [flags]
412
+
413
+ Flags:
414
+ --root=DIR Repo root to scan (default: cwd)
415
+ --propose-switch=pnpm|yarn|bun|npm
416
+ Fail-closed checklist for a manager switch
417
+ --allow-ignore-scripts-gaps
418
+ Downgrade missing --ignore-scripts to silence
419
+ --strict Exit 1 on fail/actionable
420
+ --json
421
+
422
+ Source: ${SOURCE_URL}
423
+ `);
424
+ }
425
+
426
+ function runCli(argv = process.argv.slice(2)) {
427
+ const args = parseCliArgs(argv);
428
+ if (args.help) {
429
+ printHelp();
430
+ return 0;
431
+ }
432
+ const report = buildPackageManagerHonestyReport(args);
433
+ if (args.json) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
434
+ else process.stdout.write(formatPackageManagerHonestyReport(report));
435
+ if (args.strict && report.status !== 'ready') return 1;
436
+ if (report.status === 'fail') return 1;
437
+ return 0;
438
+ }
439
+
440
+ module.exports = {
441
+ SOURCE_URL,
442
+ LOCKFILES,
443
+ PNPM12_COMPAT_CHECKS,
444
+ detectLockfiles,
445
+ readPackageManagerPin,
446
+ extractInstallCommands,
447
+ scanWorkflowInstalls,
448
+ inferCanonicalManager,
449
+ buildPackageManagerHonestyReport,
450
+ formatPackageManagerHonestyReport,
451
+ normalizeOptions,
452
+ parseCliArgs,
453
+ runCli,
454
+ };
455
+
456
+ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(__filename)) {
457
+ process.exitCode = runCli(process.argv.slice(2));
458
+ }