thumbgate 1.35.0 → 1.37.2

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 (204) hide show
  1. package/.agents/skills/cobble-hot-store-compare-not-clone/SKILL.md +80 -0
  2. package/.agents/skills/colab-compute-honesty-not-clone/SKILL.md +70 -0
  3. package/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
  4. package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
  5. package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
  6. package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
  7. package/.agents/skills/token-shunt-honesty-not-clone/SKILL.md +67 -0
  8. package/.agents/skills/typesafe-typed-questions-not-clone/SKILL.md +82 -0
  9. package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
  10. package/.claude-plugin/plugin.json +1 -1
  11. package/.well-known/llms.txt +1 -0
  12. package/.well-known/mcp/server-card.json +1 -1
  13. package/CONTRIBUTING.md +95 -0
  14. package/README.md +195 -632
  15. package/THIRD_PARTY_NOTICES.md +89 -0
  16. package/adapters/claude/.mcp.json +2 -2
  17. package/adapters/forge/forge.yaml +3 -3
  18. package/adapters/future-agi/.mcp.json +8 -0
  19. package/adapters/future-agi/FUTURE_AGI.md +23 -0
  20. package/adapters/future-agi/config.toml +3 -0
  21. package/adapters/future-agi/future-agi-bridge.js +9 -0
  22. package/adapters/future-agi/opencode.json +8 -0
  23. package/adapters/herdr/herdr-plugin.toml +18 -0
  24. package/adapters/mcp/server-stdio.js +238 -25
  25. package/adapters/opencode/opencode.json +1 -1
  26. package/adapters/workos/WORKOS.md +52 -0
  27. package/bin/cli.js +465 -3
  28. package/bin/futureagi-bridge +9 -0
  29. package/config/gate-templates.json +629 -4
  30. package/config/gates/actor-critic-audit.json +34 -0
  31. package/config/gates/default.json +9 -3
  32. package/config/gates/five-walls-governance.json +34 -0
  33. package/config/gates/future-agi-guardrails.json +34 -0
  34. package/config/gates/radware-threat-defense-2026.json +61 -0
  35. package/config/gates/simatree-data-governance.json +33 -0
  36. package/config/mcp-allowlists.json +4 -0
  37. package/config/merge-quality-checks.json +6 -0
  38. package/config/model-candidates.json +312 -29
  39. package/config/model-tiers.json +18 -0
  40. package/config/post-deploy-marketing-pages.json +10 -0
  41. package/config/progressive/01-wire-only.json +11 -0
  42. package/config/progressive/02-dashboard-empty-ok.json +10 -0
  43. package/config/progressive/03-one-lesson.json +10 -0
  44. package/config/progressive/04-warn-fires.json +11 -0
  45. package/config/progressive/05-strict-optional.json +11 -0
  46. package/config/progressive/README.md +15 -0
  47. package/config/schemas/broker-execution-receipt.schema.json +139 -0
  48. package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
  49. package/conformance/provider-attestation/vectors.json +320 -0
  50. package/docs/specs/provider-execution-attestation-v1.md +69 -0
  51. package/openapi/openapi.yaml +15 -0
  52. package/package.json +408 -150
  53. package/public/about.html +2 -2
  54. package/public/ai-malpractice-prevention.html +7 -7
  55. package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
  56. package/public/blog/a-receipt-is-not-world-state.html +388 -0
  57. package/public/blog/git-at-agent-scale.html +374 -0
  58. package/public/blog/no-llm-in-the-gate.html +133 -0
  59. package/public/blog.html +80 -0
  60. package/public/case-studies.html +16 -1
  61. package/public/compare.html +28 -0
  62. package/public/diagnostic.html +216 -7
  63. package/public/docs/connectors.html +39 -0
  64. package/public/federal.html +2 -2
  65. package/public/founders.html +639 -0
  66. package/public/index.html +87 -9
  67. package/public/install.html +8 -8
  68. package/public/learn.html +39 -0
  69. package/public/numbers.html +2 -2
  70. package/public/peter.html +310 -0
  71. package/public/platform-partners.html +119 -0
  72. package/public/pricing.html +24 -3
  73. package/public/privacy.html +117 -0
  74. package/public/pro.html +17 -0
  75. package/public/support.html +62 -0
  76. package/public/terms.html +130 -0
  77. package/public/third-party-notices.html +95 -0
  78. package/public/yt.html +351 -0
  79. package/scripts/action-receipts.js +133 -3
  80. package/scripts/adaptive-governance-arena.js +349 -0
  81. package/scripts/admin-override.js +205 -0
  82. package/scripts/agent-action-inventory.js +869 -0
  83. package/scripts/agent-audit-trace.js +42 -2
  84. package/scripts/agent-egress-policy.js +1117 -0
  85. package/scripts/agent-memory-lifecycle.js +141 -2
  86. package/scripts/agent-operations-planner.js +441 -1
  87. package/scripts/agent-readiness.js +68 -0
  88. package/scripts/agent-security-central.js +647 -0
  89. package/scripts/allowlist-bridge-honesty.js +417 -0
  90. package/scripts/async-job-runner.js +102 -11
  91. package/scripts/audit-trail.js +212 -0
  92. package/scripts/billing.js +1 -1
  93. package/scripts/broker-execution-receipts.js +719 -0
  94. package/scripts/budget-aware-gates-proof.js +423 -0
  95. package/scripts/claude-feedback-sync.js +29 -3
  96. package/scripts/claw-harness-production.js +237 -0
  97. package/scripts/cli-schema.js +203 -1
  98. package/scripts/cobble-hot-store-split.js +600 -0
  99. package/scripts/codex-runbook-flywheel.js +318 -0
  100. package/scripts/colab-compute-honesty.js +281 -0
  101. package/scripts/context-footprint.js +186 -0
  102. package/scripts/contextfs.js +143 -61
  103. package/scripts/dashboard.js +251 -32
  104. package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
  105. package/scripts/docker-sandbox-planner.js +18 -0
  106. package/scripts/double-blind-eval-protocol.js +252 -0
  107. package/scripts/edotenv-rl-gateway.js +259 -0
  108. package/scripts/ensure-production-search-corpus.js +162 -0
  109. package/scripts/eval-holdout.js +311 -0
  110. package/scripts/feedback-aggregate.js +21 -2
  111. package/scripts/feedback-loop.js +87 -5
  112. package/scripts/feedback-quality.js +9 -0
  113. package/scripts/file-ledger-lock.js +4 -1
  114. package/scripts/financial-control-plane.js +41 -1
  115. package/scripts/find-dormant-requires.js +118 -0
  116. package/scripts/fs-utils.js +84 -8
  117. package/scripts/gates-engine.js +826 -68
  118. package/scripts/generate-case-study-outreach.js +24 -15
  119. package/scripts/git-at-scale.js +628 -0
  120. package/scripts/governance-conflict-audit.js +1650 -0
  121. package/scripts/governance-difficulty-curriculum.js +328 -0
  122. package/scripts/graphrag-retrieval.js +275 -0
  123. package/scripts/gurobi-optimizer.js +324 -0
  124. package/scripts/gurobi_optimizer.py +485 -0
  125. package/scripts/harness-selector.js +82 -1
  126. package/scripts/hidden-entry-points.js +284 -0
  127. package/scripts/human-escalation.js +199 -1
  128. package/scripts/hybrid-feedback-context.js +152 -19
  129. package/scripts/intent-governed-execution.js +602 -0
  130. package/scripts/intervention-policy.js +123 -20
  131. package/scripts/jit-harness-compose.js +628 -0
  132. package/scripts/jsonl-watcher.js +10 -0
  133. package/scripts/lesson-embedding-index.js +95 -12
  134. package/scripts/lesson-retrieval.js +105 -19
  135. package/scripts/local-model-profile.js +19 -2
  136. package/scripts/mailer/resend-mailer.js +1 -1
  137. package/scripts/matryoshka-embedding.js +235 -0
  138. package/scripts/mcp-oauth.js +42 -4
  139. package/scripts/mcp-session-handles.js +1016 -0
  140. package/scripts/mcp-wiring-doctor.js +314 -0
  141. package/scripts/memory-firewall.js +115 -2
  142. package/scripts/memory-scope-readiness.js +299 -0
  143. package/scripts/memory-vs-rag-route.js +161 -0
  144. package/scripts/model-tier-router.js +148 -21
  145. package/scripts/nvidia-specdecode-al-doctor.js +536 -0
  146. package/scripts/openui-catalog-compose-honesty.js +593 -0
  147. package/scripts/operational-integrity.js +19 -1
  148. package/scripts/override-audit.js +213 -0
  149. package/scripts/package-manager-honesty-doctor.js +458 -0
  150. package/scripts/pr-manager.js +63 -1
  151. package/scripts/prove-herdr-adapter.js +52 -0
  152. package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
  153. package/scripts/prove-workos.js +73 -0
  154. package/scripts/provider-attestation-conformance.js +192 -0
  155. package/scripts/provider-receipt-contract.js +136 -0
  156. package/scripts/qwen38-max-cost-optimizer.js +401 -0
  157. package/scripts/radware-threat-defense.js +280 -0
  158. package/scripts/rag-embedding-identity.js +221 -0
  159. package/scripts/rag-precision-guardrails.js +112 -2
  160. package/scripts/remote-feedback-capture.js +159 -0
  161. package/scripts/research-agent-harness.js +256 -0
  162. package/scripts/rsi-safety-hillclimb.js +200 -0
  163. package/scripts/rule-sprawl.js +188 -0
  164. package/scripts/schedule-manager.js +147 -0
  165. package/scripts/self-heal.js +8 -0
  166. package/scripts/session-lease.js +415 -0
  167. package/scripts/simatree-data-governance.js +347 -0
  168. package/scripts/slo-alert-engine.js +172 -7
  169. package/scripts/solver-parity.js +539 -0
  170. package/scripts/stealth-memory-injection-gate.js +333 -0
  171. package/scripts/switchyard-router.js +366 -0
  172. package/scripts/telemetry-analytics.js +84 -27
  173. package/scripts/temporal-decay-weighting.js +138 -0
  174. package/scripts/test-all.js +165 -0
  175. package/scripts/token-savings.js +42 -0
  176. package/scripts/token-shunt-honesty.js +502 -0
  177. package/scripts/tool-kpi-tracker.js +108 -5
  178. package/scripts/tool-registry.js +193 -5
  179. package/scripts/typesafe-typed-questions.js +813 -0
  180. package/scripts/universal-claim-evaluator.js +14 -2
  181. package/scripts/vector-store.js +279 -9
  182. package/scripts/workflow-notebook.js +391 -0
  183. package/scripts/workflow-sentinel.js +111 -12
  184. package/scripts/workos-production-guard.js +260 -0
  185. package/scripts/workspace-search-route.js +515 -0
  186. package/server.json +2 -2
  187. package/src/agent-identity-boundary.js +76 -0
  188. package/src/agent-retrieval-cache.js +155 -0
  189. package/src/alert-noise-ledger.js +502 -0
  190. package/src/api/server.js +724 -153
  191. package/src/git-fast-cache.js +220 -0
  192. package/src/git-wal-sync.js +156 -0
  193. package/src/hash-anchored-edit.js +82 -0
  194. package/src/hermes-platform-protocol.js +475 -0
  195. package/src/hermes-sync-plane.js +241 -0
  196. package/src/index.js +30 -1
  197. package/src/iso42001-compliance-guard.js +97 -0
  198. package/src/latency-budget.js +244 -0
  199. package/src/mcp-writeguard.js +316 -0
  200. package/src/miminions-adapter.js +106 -0
  201. package/src/pipeline-compass.js +104 -0
  202. package/src/ppl-alert-pipeline.js +284 -0
  203. package/src/rendezvous-router.js +90 -0
  204. package/src/security-questionnaire.js +195 -0
@@ -0,0 +1,391 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * workflow-notebook.js — goal → reviewed plan → approved execution → captured
6
+ * decisions → discoverable index, for repetitive agent workflows.
7
+ *
8
+ * Pattern source: "Automating repetitive work at OpenAI with Codex" (2026).
9
+ * The four lessons encoded here:
10
+ *
11
+ * 1. A short goal cell, then a PLAN the human reviews BEFORE execution starts.
12
+ * The approval boundary is structural, not advisory: recording work before
13
+ * approval is rejected, fail-closed.
14
+ * 2. The notebook captures commands, outputs, and interpretations — including
15
+ * dead ends — while the work happens, so documentation costs nothing extra.
16
+ * 3. Decisions that would otherwise die in chat history get explicit records:
17
+ * the alternatives considered, the choice, and the reason.
18
+ * 4. Every finished notebook emits a companion `*.index.md` (title, goal,
19
+ * status, decisions, outcomes) that future agent runs can discover — the
20
+ * context flywheel that makes the next run cheaper than the last one.
21
+ *
22
+ * Storage: <feedbackDir>/workflow-notebooks/<id>.json (source of truth) plus
23
+ * <id>.index.md (discovery artifact). No server, no new infrastructure — the
24
+ * notebook is a file, exactly like the article's static-app design.
25
+ */
26
+
27
+ const fs = require('node:fs');
28
+ const path = require('node:path');
29
+ const crypto = require('node:crypto');
30
+ const { resolveFeedbackDir } = require('./feedback-paths');
31
+ const { ensureDir } = require('./fs-utils');
32
+
33
+ const STATUSES = new Set(['created', 'planned', 'approved', 'running', 'completed', 'aborted']);
34
+
35
+ function getNotebooksDir(feedbackDir) {
36
+ if (process.env.THUMBGATE_WORKFLOW_NOTEBOOKS_DIR) {
37
+ return process.env.THUMBGATE_WORKFLOW_NOTEBOOKS_DIR;
38
+ }
39
+ return path.join(resolveFeedbackDir({ feedbackDir }), 'workflow-notebooks');
40
+ }
41
+
42
+ function buildNotebookId(title) {
43
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
44
+ const suffix = crypto.randomBytes(3).toString('hex');
45
+ const slug = String(title || 'workflow')
46
+ .toLowerCase()
47
+ .replace(/[^a-z0-9]+/g, '-')
48
+ .replace(/^-+|-+$/g, '')
49
+ .slice(0, 48) || 'workflow';
50
+ return `${stamp}-${slug}-${suffix}`;
51
+ }
52
+
53
+ function notebookPath(notebookId, feedbackDir) {
54
+ return path.join(getNotebooksDir(feedbackDir), `${notebookId}.json`);
55
+ }
56
+
57
+ function loadNotebook(notebookId, feedbackDir) {
58
+ const file = notebookPath(notebookId, feedbackDir);
59
+ if (!fs.existsSync(file)) return null;
60
+ try {
61
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ function saveNotebook(notebook, feedbackDir, expectedRev) {
68
+ const file = notebookPath(notebook.id, feedbackDir);
69
+ ensureDir(path.dirname(file));
70
+ if (expectedRev !== undefined) {
71
+ const current = loadNotebook(notebook.id, feedbackDir);
72
+ if (current && current._rev !== expectedRev) {
73
+ throw new Error(`saveNotebook: optimistic conflict on ${notebook.id} — reload and retry`);
74
+ }
75
+ }
76
+ notebook._rev = (notebook._rev || 0) + 1;
77
+ const tmp = `${file}.tmp.${process.pid}.${Date.now()}.${crypto.randomBytes(4).toString('hex')}`;
78
+ fs.writeFileSync(tmp, `${JSON.stringify(notebook, null, 2)}\n`);
79
+ fs.renameSync(tmp, file);
80
+ return notebook;
81
+ }
82
+
83
+ /**
84
+ * Start a notebook with a goal. Status becomes 'planned' once a plan exists;
85
+ * no steps may be recorded before an explicit approval (fail-closed).
86
+ */
87
+ function createNotebook({ title, goal, context = [] } = {}, feedbackDir) {
88
+ if (!title || !String(title).trim()) {
89
+ throw new TypeError('createNotebook: title is required');
90
+ }
91
+ if (!goal || !String(goal).trim()) {
92
+ throw new TypeError('createNotebook: goal is required — the goal cell is the contract');
93
+ }
94
+ const notebook = {
95
+ id: buildNotebookId(title),
96
+ title: String(title).trim(),
97
+ goal: String(goal).trim(),
98
+ context: Array.isArray(context) ? context.map(String) : [],
99
+ status: 'created',
100
+ plan: null,
101
+ approvedBy: null,
102
+ approvedAt: null,
103
+ steps: [],
104
+ decisions: [],
105
+ createdAt: new Date().toISOString(),
106
+ updatedAt: new Date().toISOString(),
107
+ _rev: 0,
108
+ };
109
+ return saveNotebook(notebook, feedbackDir);
110
+ }
111
+
112
+ function setPlan(notebookId, plan, feedbackDir) {
113
+ if (!plan || !String(plan).trim()) {
114
+ throw new TypeError('setPlan: plan text is required — approve a plan, not an impulse');
115
+ }
116
+ const notebook = loadNotebook(notebookId, feedbackDir);
117
+ if (!notebook) throw new Error(`setPlan: notebook not found: ${notebookId}`);
118
+ if (notebook.status !== 'created' && notebook.status !== 'planned') {
119
+ throw new Error(`setPlan: notebook ${notebookId} is ${notebook.status}; plan is locked after approval`);
120
+ }
121
+ const currentRev = notebook._rev;
122
+ notebook.plan = String(plan).trim();
123
+ notebook.status = 'planned';
124
+ notebook.updatedAt = new Date().toISOString();
125
+ return saveNotebook(notebook, feedbackDir, currentRev);
126
+ }
127
+
128
+ /**
129
+ * The approval boundary. Only an APPROVED notebook may record steps. The
130
+ * approver identity is captured for audit — "who decided the plan was ready".
131
+ */
132
+ function approveNotebook(notebookId, approver, feedbackDir) {
133
+ if (!approver || !String(approver).trim()) {
134
+ throw new TypeError('approveNotebook: approver identity is required');
135
+ }
136
+ // Authorization boundary: require a non-trivial identity assertion.
137
+ // Empty or whitespace-only approvers are rejected. Callers MUST pass
138
+ // a real identity (human, service account, or signed assertion).
139
+ const approverName = String(approver).trim();
140
+ if (approverName.length === 0 || approverName === notebookId) {
141
+ throw new Error(`approveNotebook: invalid approver — must be a distinct trusted identity`);
142
+ }
143
+ const notebook = loadNotebook(notebookId, feedbackDir);
144
+ if (!notebook) throw new Error(`approveNotebook: notebook not found: ${notebookId}`);
145
+ if (notebook.status !== 'planned') {
146
+ throw new Error(`approveNotebook: notebook ${notebookId} is ${notebook.status}; only a planned notebook can be approved`);
147
+ }
148
+ if (!notebook.plan) {
149
+ throw new Error(`approveNotebook: notebook ${notebookId} has no plan to approve`);
150
+ }
151
+ const currentRev = notebook._rev;
152
+ notebook.status = 'approved';
153
+ notebook.approvedBy = approverName;
154
+ notebook.approvedAt = new Date().toISOString();
155
+ notebook.updatedAt = notebook.approvedAt;
156
+ return saveNotebook(notebook, feedbackDir, currentRev);
157
+ }
158
+
159
+ function assertApproved(notebook, verb) {
160
+ // Fail-closed: the article keeps humans deciding when a plan is ready. An
161
+ // unapproved notebook recording execution would silently erase that boundary.
162
+ if (!notebook) throw new Error(`${verb}: notebook not found`);
163
+ if (notebook.status !== 'approved' && notebook.status !== 'running') {
164
+ throw new Error(
165
+ `${verb}: notebook ${notebook.id} is ${notebook.status}; `
166
+ + 'record a plan and get it approved before recording execution',
167
+ );
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Record one unit of executed work: the command/action, its output, and the
173
+ * human-readable interpretation. Dead ends are first-class: mark
174
+ * outcome:'dead-end' so the NEXT run knows what not to try.
175
+ */
176
+ function recordStep(notebookId, { action, output = '', interpretation = '', outcome = 'ok' } = {}, feedbackDir) {
177
+ const notebook = loadNotebook(notebookId, feedbackDir);
178
+ assertApproved(notebook, 'recordStep');
179
+ if (!action || !String(action).trim()) {
180
+ throw new TypeError('recordStep: action is required');
181
+ }
182
+ const allowedOutcomes = new Set(['ok', 'dead-end', 'blocked', 'needs-review']);
183
+ if (!allowedOutcomes.has(outcome)) {
184
+ throw new TypeError(`recordStep: outcome must be one of ${[...allowedOutcomes].join(', ')}`);
185
+ }
186
+ const currentRev = notebook._rev;
187
+ notebook.steps.push({
188
+ seq: notebook.steps.length + 1,
189
+ action: String(action).trim(),
190
+ output: String(output).slice(0, 8000),
191
+ interpretation: String(interpretation),
192
+ outcome,
193
+ at: new Date().toISOString(),
194
+ });
195
+ notebook.status = 'running';
196
+ notebook.updatedAt = new Date().toISOString();
197
+ return saveNotebook(notebook, feedbackDir, currentRev);
198
+ }
199
+
200
+ /**
201
+ * Capture a decision that would otherwise disappear into the conversation:
202
+ * the alternatives considered, the choice, and the reason.
203
+ */
204
+ function recordDecision(notebookId, { question, alternatives = [], choice, reason } = {}, feedbackDir) {
205
+ const notebook = loadNotebook(notebookId, feedbackDir);
206
+ assertApproved(notebook, 'recordDecision');
207
+ if (!question || !String(question).trim()) {
208
+ throw new TypeError('recordDecision: question is required');
209
+ }
210
+ if (!choice || !String(choice).trim()) {
211
+ throw new TypeError('recordDecision: choice is required');
212
+ }
213
+ const currentRev = notebook._rev;
214
+ notebook.decisions.push({
215
+ seq: notebook.decisions.length + 1,
216
+ question: String(question).trim(),
217
+ alternatives: Array.isArray(alternatives) ? alternatives.map(String) : [],
218
+ choice: String(choice).trim(),
219
+ reason: String(reason || '').trim(),
220
+ at: new Date().toISOString(),
221
+ });
222
+ notebook.updatedAt = new Date().toISOString();
223
+ return saveNotebook(notebook, feedbackDir, currentRev);
224
+ }
225
+
226
+ function finishNotebook(notebookId, { summary = '', outcome = 'completed' } = {}, feedbackDir) {
227
+ const notebook = loadNotebook(notebookId, feedbackDir);
228
+ assertApproved(notebook, 'finishNotebook');
229
+ if (outcome !== 'completed' && outcome !== 'aborted') {
230
+ throw new TypeError('finishNotebook: outcome must be completed or aborted');
231
+ }
232
+ notebook.status = outcome;
233
+ notebook.summary = String(summary).trim();
234
+ notebook.finishedAt = new Date().toISOString();
235
+ notebook.updatedAt = notebook.finishedAt;
236
+ saveNotebook(notebook, feedbackDir, notebook._rev);
237
+ return writeIndex(notebook, feedbackDir);
238
+ }
239
+
240
+ /**
241
+ * Companion discovery index (the article's `*.index.md`): a small Markdown
242
+ * summary that future runs can grep/discover for prior context — goal, status,
243
+ * decisions, dead ends. Markdown, not JSON, so any agent or human can read it.
244
+ */
245
+ function writeIndex(notebook, feedbackDir) {
246
+ const dir = getNotebooksDir(feedbackDir);
247
+ ensureDir(dir);
248
+ const lines = [
249
+ `# ${notebook.title}`,
250
+ '',
251
+ `- notebook: ${notebook.id}`,
252
+ `- status: ${notebook.status}`,
253
+ `- created: ${notebook.createdAt}`,
254
+ notebook.approvedAt ? `- approved-by: ${notebook.approvedBy} (${notebook.approvedAt})` : '- approved-by: (none)',
255
+ '',
256
+ '## Goal',
257
+ '',
258
+ notebook.goal,
259
+ '',
260
+ ];
261
+ if (notebook.plan) {
262
+ lines.push('## Plan', '', notebook.plan, '');
263
+ }
264
+ if (notebook.decisions.length > 0) {
265
+ lines.push('## Decisions', '');
266
+ for (const d of notebook.decisions) {
267
+ lines.push(`- Q: ${d.question}`);
268
+ lines.push(` Chose: ${d.choice}${d.reason ? ` — ${d.reason}` : ''}`);
269
+ if (d.alternatives.length > 0) lines.push(` Alternatives: ${d.alternatives.join('; ')}`);
270
+ }
271
+ lines.push('');
272
+ }
273
+ const deadEnds = (notebook.steps || []).filter((s) => s.outcome === 'dead-end');
274
+ if (deadEnds.length > 0) {
275
+ lines.push('## Dead ends (do not repeat)', '');
276
+ for (const s of deadEnds) lines.push(`- ${s.action}${s.interpretation ? ` — ${s.interpretation}` : ''}`);
277
+ lines.push('');
278
+ }
279
+ if (notebook.summary) {
280
+ lines.push('## Summary', '', notebook.summary, '');
281
+ }
282
+ const indexPath = path.join(dir, `${notebook.id}.index.md`);
283
+ fs.writeFileSync(indexPath, `${lines.join('\n')}`);
284
+ return indexPath;
285
+ }
286
+
287
+ /**
288
+ * Discovery: list notebooks by reading their JSON files, most recent first.
289
+ * Falls back to the JSON when the index is missing.
290
+ */
291
+ function listNotebooks(feedbackDir) {
292
+ const dir = getNotebooksDir(feedbackDir);
293
+ if (!fs.existsSync(dir)) return [];
294
+ const entries = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
295
+ const notebooks = [];
296
+ for (const entry of entries) {
297
+ const id = entry.replace(/\.json$/, '');
298
+ const notebook = loadNotebook(id, feedbackDir);
299
+ if (!notebook) continue;
300
+ notebooks.push({
301
+ id,
302
+ title: notebook.title,
303
+ goal: notebook.goal,
304
+ status: notebook.status,
305
+ decisions: notebook.decisions.length,
306
+ steps: notebook.steps.length,
307
+ createdAt: notebook.createdAt,
308
+ updatedAt: notebook.updatedAt,
309
+ index: path.join(dir, `${id}.index.md`),
310
+ });
311
+ }
312
+ notebooks.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
313
+ return notebooks;
314
+ }
315
+
316
+ function main() {
317
+ const feedbackDir = process.env.THUMBGATE_FEEDBACK_DIR ||
318
+ path.join(process.cwd(), '.thumbgate', 'feedback');
319
+ const [cmd, ...rest] = process.argv.slice(2);
320
+ const parseJson = (raw) => {
321
+ try { return raw ? JSON.parse(raw) : {}; } catch { return {}; }
322
+ };
323
+ switch (cmd) {
324
+ case 'create': {
325
+ const opts = parseJson(rest[0]);
326
+ const notebook = createNotebook(opts, feedbackDir);
327
+ console.log(JSON.stringify({ id: notebook.id, status: notebook.status }, null, 2));
328
+ break;
329
+ }
330
+ case 'plan': {
331
+ const [id, ...planParts] = rest;
332
+ const notebook = setPlan(id, planParts.join(' '), feedbackDir);
333
+ console.log(JSON.stringify({ id: notebook.id, status: notebook.status }, null, 2));
334
+ break;
335
+ }
336
+ case 'approve': {
337
+ const [id, approver] = rest;
338
+ const notebook = approveNotebook(id, approver, feedbackDir);
339
+ console.log(JSON.stringify({ id: notebook.id, status: notebook.status, approvedBy: notebook.approvedBy }, null, 2));
340
+ break;
341
+ }
342
+ case 'step': {
343
+ const [id, ...jsonRaw] = rest;
344
+ const opts = parseJson(jsonRaw.join(' '));
345
+ const notebook = recordStep(id, opts, feedbackDir);
346
+ console.log(JSON.stringify({ id: notebook.id, steps: notebook.steps.length }, null, 2));
347
+ break;
348
+ }
349
+ case 'decide': {
350
+ const [id, ...jsonRaw] = rest;
351
+ const opts = parseJson(jsonRaw.join(' '));
352
+ const notebook = recordDecision(id, opts, feedbackDir);
353
+ console.log(JSON.stringify({ id: notebook.id, decisions: notebook.decisions.length }, null, 2));
354
+ break;
355
+ }
356
+ case 'finish': {
357
+ const [id, ...jsonRaw] = rest;
358
+ const opts = parseJson(jsonRaw.join(' '));
359
+ const indexPath = finishNotebook(id, opts, feedbackDir);
360
+ console.log(JSON.stringify({ index: indexPath }, null, 2));
361
+ break;
362
+ }
363
+ case 'list': {
364
+ console.log(JSON.stringify(listNotebooks(feedbackDir), null, 2));
365
+ break;
366
+ }
367
+ default:
368
+ console.error('usage: workflow-notebook.js create <json> | plan <id> <plan> | approve <id> <approver> | step <id> <json> | decide <id> <json> | finish <id> <json> | list');
369
+ process.exitCode = 2;
370
+ }
371
+ }
372
+
373
+ module.exports = {
374
+ STATUSES,
375
+ getNotebooksDir,
376
+ notebookPath,
377
+ createNotebook,
378
+ setPlan,
379
+ approveNotebook,
380
+ recordStep,
381
+ recordDecision,
382
+ finishNotebook,
383
+ writeIndex,
384
+ listNotebooks,
385
+ loadNotebook,
386
+ saveNotebook,
387
+ };
388
+
389
+ if (require.main?.filename === module.filename) {
390
+ main();
391
+ }
@@ -66,6 +66,14 @@ function loadJson(filePath) {
66
66
  }
67
67
 
68
68
  function loadGovernanceState() {
69
+ try {
70
+ const engine = require('./gates-engine');
71
+ if (typeof engine.loadGovernanceState === 'function') {
72
+ return engine.loadGovernanceState();
73
+ }
74
+ } catch {
75
+ // Fall through to the local file if the engine cannot load.
76
+ }
69
77
  const raw = loadJson(GOVERNANCE_STATE_PATH);
70
78
  return {
71
79
  taskScope: raw?.taskScope && typeof raw.taskScope === 'object' ? raw.taskScope : null,
@@ -144,11 +152,17 @@ function matchesAnyGlob(filePath, globs) {
144
152
  function toRepoRelativePath(filePath, repoRoot) {
145
153
  const value = String(filePath || '').trim();
146
154
  if (!value) return '';
147
- if (repoRoot && path.isAbsolute(value)) {
155
+ if (path.isAbsolute(value)) {
156
+ if (!repoRoot) {
157
+ // Without a repo root, absolute paths cannot be safely scoped for
158
+ // protected-file approval — reject rather than preserving absolute paths.
159
+ return '';
160
+ }
148
161
  const relative = path.relative(repoRoot, value);
149
162
  if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
150
163
  return normalizePosix(relative);
151
164
  }
165
+ return '';
152
166
  }
153
167
  return normalizePosix(value);
154
168
  }
@@ -206,22 +220,37 @@ function getBranchDiffFiles(repoRoot) {
206
220
  }
207
221
 
208
222
  function collectAffectedFiles(toolName, toolInput = {}, repoRoot) {
223
+ try {
224
+ const { extractAffectedFiles } = require('./gates-engine');
225
+ if (typeof extractAffectedFiles === 'function') {
226
+ const extracted = extractAffectedFiles(toolName, {
227
+ ...toolInput,
228
+ cwd: toolInput.cwd || repoRoot,
229
+ repoPath: toolInput.repoPath || repoRoot,
230
+ });
231
+ if (extracted && Array.isArray(extracted.files)) return extracted.files;
232
+ }
233
+ } catch {
234
+ // Keep the local collector if the engine cannot load.
235
+ }
209
236
  const files = new Set(collectInlineAffectedFiles(toolInput, repoRoot));
210
- const command = String(toolInput.command || '');
237
+ const rawCommand = String(toolInput.command || '');
238
+ const command = rawCommand ? rawCommand.replace(/(?:^|\s)git(?:\s+-[^\s]+)*\s+-C(?:\s+|=)(?:'[^']+'|"[^"]+"|([^\s'"]+))/i, 'git ') : '';
211
239
  const hasExplicitAffectedFiles = files.size > 0;
212
240
 
213
- if (toolName === 'Bash' && repoRoot && command) {
241
+ if (toolName === 'Bash' && repoRoot && (command || rawCommand)) {
242
+ const cmdToTest = command || rawCommand;
214
243
  if (hasExplicitAffectedFiles) {
215
244
  return [...files].filter(Boolean);
216
245
  }
217
246
 
218
- if (/\bgit\s+commit\b/i.test(command)) {
247
+ if (/\bgit\s+commit\b/i.test(cmdToTest)) {
219
248
  for (const filePath of safeExecFileLines('git', ['diff', '--cached', '--name-only'], repoRoot)) {
220
249
  files.add(normalizePosix(filePath));
221
250
  }
222
251
  }
223
252
 
224
- if (/\bgit\s+add\b/i.test(command)) {
253
+ if (/\bgit\s+add\b/i.test(cmdToTest)) {
225
254
  for (const filePath of safeExecFileLines('git', ['diff', '--name-only'], repoRoot)) {
226
255
  files.add(normalizePosix(filePath));
227
256
  }
@@ -230,7 +259,7 @@ function collectAffectedFiles(toolName, toolInput = {}, repoRoot) {
230
259
  }
231
260
  }
232
261
 
233
- if (/\bgit\s+push\b/i.test(command) || /\bgh\s+pr\s+(?:create|merge)\b/i.test(command)) {
262
+ if (/\bgit\s+push\b/i.test(cmdToTest) || /\bgh\s+pr\s+(?:create|merge)\b/i.test(cmdToTest)) {
234
263
  for (const filePath of getBranchDiffFiles(repoRoot)) {
235
264
  files.add(normalizePosix(filePath));
236
265
  }
@@ -1362,6 +1391,29 @@ function isDestructiveBypass(command) {
1362
1391
  || /\bgh\s+pr\s+merge\b.*--admin\b/i.test(command);
1363
1392
  }
1364
1393
 
1394
+ function shouldAllowVersionProbe(toolName, command) {
1395
+ return toolName === 'Bash' && isVersionOrProbeCommand(command);
1396
+ }
1397
+
1398
+ function isVersionOrProbeCommand(command) {
1399
+ if (!command || typeof command !== 'string') return false;
1400
+ const trimmed = command.trim();
1401
+ // Utility probes with no args.
1402
+ if (/^(?:pwd|whoami|date)\s*$/i.test(trimmed)) return true;
1403
+ // go prints version via the "version" subcommand (not -v/--version).
1404
+ if (/^go\s+version\s*$/i.test(trimmed)) return true;
1405
+ // node/npm/npx/gh/git/docker/claude/gemini: -v/--version/-V are version-only.
1406
+ if (/^(?:node|npm|npx|gh|git|docker|claude|gemini)\s+(?:-v|--version|-V)\s*$/i.test(trimmed)) {
1407
+ return true;
1408
+ }
1409
+ // cargo/rustc: -V/--version are version-only; bare -v means verbose.
1410
+ // Keep this case-sensitive so /i cannot collapse -V into -v.
1411
+ if (/^(?:cargo|rustc)\s+(?:-V|--version)\s*$/.test(trimmed)) return true;
1412
+ // python/pytest: only --version/-V. Bare -v means verbose and must stay gated.
1413
+ if (/^(?:python|python3|pytest)\s+(?:--version|-V)\s*$/.test(trimmed)) return true;
1414
+ return false;
1415
+ }
1416
+
1365
1417
  function getLearnedPrediction(learnedPolicy) {
1366
1418
  return learnedPolicy?.enabled ? learnedPolicy.prediction : null;
1367
1419
  }
@@ -1420,7 +1472,26 @@ function hasSoftControlWarning({ workflowContract, workflowControl, costControl,
1420
1472
  || (learnedRecall && riskScore >= 0.34);
1421
1473
  }
1422
1474
 
1423
- function chooseDecision({ riskScore, integrity, memoryGuard, learnedPolicy, blastRadius, command, costControl, financialControl, workflowControl, workflowContract, actionProfile }) {
1475
+ function chooseDecision({
1476
+ riskScore,
1477
+ integrity,
1478
+ memoryGuard,
1479
+ learnedPolicy,
1480
+ blastRadius,
1481
+ command,
1482
+ toolName,
1483
+ costControl,
1484
+ financialControl,
1485
+ workflowControl,
1486
+ workflowContract,
1487
+ actionProfile,
1488
+ }) {
1489
+ // Version/probe allowlist is Bash-only — other tools may carry a "command"
1490
+ // field without being a shell version probe.
1491
+ if (shouldAllowVersionProbe(toolName, command)) {
1492
+ return 'allow';
1493
+ }
1494
+
1424
1495
  if (financialControl?.mode === 'block' || costControl?.mode === 'block' || workflowControl?.mode === 'block' || workflowContract?.mode === 'block') {
1425
1496
  return 'deny';
1426
1497
  }
@@ -1485,10 +1556,35 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
1485
1556
  const workflowControl = buildWorkflowControl(normalizedAction, options.workflowPolicy || toolInput.workflowPolicy || options.budget || toolInput.budget || {});
1486
1557
  const governanceState = options.governanceState || loadGovernanceState();
1487
1558
  const repoPath = options.repoPath || normalizedToolInput.repoPath || normalizedToolInput.cwd || process.cwd();
1488
- const repoRoot = resolveRepoRoot(repoPath) || null;
1559
+ let repoRoot = null;
1560
+ try {
1561
+ const engine = require('./gates-engine');
1562
+ if (typeof engine.resolveRepoRoot === 'function') {
1563
+ repoRoot = engine.resolveRepoRoot({
1564
+ ...normalizedToolInput,
1565
+ cwd: normalizedToolInput.cwd || repoPath,
1566
+ repoPath: normalizedToolInput.repoPath || repoPath,
1567
+ });
1568
+ }
1569
+ } catch {
1570
+ repoRoot = null;
1571
+ }
1572
+ if (!repoRoot) {
1573
+ repoRoot = resolveRepoRoot(repoPath) || null;
1574
+ }
1575
+ const explicitChanged = Array.isArray(normalizedToolInput.changedFiles)
1576
+ ? normalizedToolInput.changedFiles
1577
+ : (Array.isArray(normalizedToolInput.changed_files) ? normalizedToolInput.changed_files : null);
1578
+
1489
1579
  const affectedFiles = Array.isArray(options.affectedFiles)
1490
- ? options.affectedFiles.map((filePath) => normalizePosix(filePath)).filter(Boolean)
1491
- : collectAffectedFiles(normalizedToolName, normalizedToolInput, repoRoot);
1580
+ ? options.affectedFiles
1581
+ .map((filePath) => toRepoRelativePath(filePath, repoRoot))
1582
+ .filter(Boolean)
1583
+ : (explicitChanged
1584
+ ? explicitChanged
1585
+ .map((filePath) => toRepoRelativePath(filePath, repoRoot))
1586
+ .filter(Boolean)
1587
+ : collectAffectedFiles(normalizedToolName, normalizedToolInput, repoRoot));
1492
1588
  let actionProfile = classifyActionProfile(normalizedToolInput);
1493
1589
  const financialControl = evaluateFinancialControl({
1494
1590
  toolName: normalizedToolName,
@@ -1512,8 +1608,7 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
1512
1608
  const highRiskAction = isHighRiskAction(normalizedToolName, normalizedToolInput, affectedFiles);
1513
1609
  const baseBranch = options.baseBranch
1514
1610
  || (governanceState.branchGovernance && governanceState.branchGovernance.baseBranch)
1515
- || normalizedToolInput.baseBranch
1516
- || DEFAULT_BASE_BRANCH;
1611
+ || normalizedToolInput.baseBranch;
1517
1612
  const integrity = evaluateOperationalIntegrity({
1518
1613
  repoPath,
1519
1614
  baseBranch,
@@ -1627,6 +1722,7 @@ function evaluateWorkflowSentinel(toolName, toolInput = {}, options = {}) {
1627
1722
  unapprovedProtectedFiles: protectedSurfaceForRisk.unapprovedProtectedFiles.length,
1628
1723
  },
1629
1724
  command: normalizedToolInput.command || '',
1725
+ toolName: normalizedToolName,
1630
1726
  costControl,
1631
1727
  financialControl,
1632
1728
  workflowControl,
@@ -1732,9 +1828,12 @@ module.exports = {
1732
1828
  collectAffectedFiles,
1733
1829
  evaluateWorkflowSentinel,
1734
1830
  isHighRiskAction,
1831
+ isVersionOrProbeCommand,
1735
1832
  loadGovernanceState,
1736
1833
  normalizeLearnedPolicyForSentinel,
1737
1834
  scoreRisk,
1835
+ shouldAllowVersionProbe,
1836
+ toRepoRelativePath,
1738
1837
  };
1739
1838
 
1740
1839
  if (require.main === module) {