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.
- package/.agents/skills/cobble-hot-store-compare-not-clone/SKILL.md +80 -0
- package/.agents/skills/colab-compute-honesty-not-clone/SKILL.md +70 -0
- package/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
- package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
- package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
- package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
- package/.agents/skills/token-shunt-honesty-not-clone/SKILL.md +67 -0
- package/.agents/skills/typesafe-typed-questions-not-clone/SKILL.md +82 -0
- package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/llms.txt +1 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/CONTRIBUTING.md +95 -0
- package/README.md +195 -632
- package/THIRD_PARTY_NOTICES.md +89 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/future-agi/.mcp.json +8 -0
- package/adapters/future-agi/FUTURE_AGI.md +23 -0
- package/adapters/future-agi/config.toml +3 -0
- package/adapters/future-agi/future-agi-bridge.js +9 -0
- package/adapters/future-agi/opencode.json +8 -0
- package/adapters/herdr/herdr-plugin.toml +18 -0
- package/adapters/mcp/server-stdio.js +238 -25
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/workos/WORKOS.md +52 -0
- package/bin/cli.js +465 -3
- package/bin/futureagi-bridge +9 -0
- package/config/gate-templates.json +629 -4
- package/config/gates/actor-critic-audit.json +34 -0
- package/config/gates/default.json +9 -3
- package/config/gates/five-walls-governance.json +34 -0
- package/config/gates/future-agi-guardrails.json +34 -0
- package/config/gates/radware-threat-defense-2026.json +61 -0
- package/config/gates/simatree-data-governance.json +33 -0
- package/config/mcp-allowlists.json +4 -0
- package/config/merge-quality-checks.json +6 -0
- package/config/model-candidates.json +312 -29
- package/config/model-tiers.json +18 -0
- package/config/post-deploy-marketing-pages.json +10 -0
- package/config/progressive/01-wire-only.json +11 -0
- package/config/progressive/02-dashboard-empty-ok.json +10 -0
- package/config/progressive/03-one-lesson.json +10 -0
- package/config/progressive/04-warn-fires.json +11 -0
- package/config/progressive/05-strict-optional.json +11 -0
- package/config/progressive/README.md +15 -0
- package/config/schemas/broker-execution-receipt.schema.json +139 -0
- package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
- package/conformance/provider-attestation/vectors.json +320 -0
- package/docs/specs/provider-execution-attestation-v1.md +69 -0
- package/openapi/openapi.yaml +15 -0
- package/package.json +408 -150
- package/public/about.html +2 -2
- package/public/ai-malpractice-prevention.html +7 -7
- package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
- package/public/blog/a-receipt-is-not-world-state.html +388 -0
- package/public/blog/git-at-agent-scale.html +374 -0
- package/public/blog/no-llm-in-the-gate.html +133 -0
- package/public/blog.html +80 -0
- package/public/case-studies.html +16 -1
- package/public/compare.html +28 -0
- package/public/diagnostic.html +216 -7
- package/public/docs/connectors.html +39 -0
- package/public/federal.html +2 -2
- package/public/founders.html +639 -0
- package/public/index.html +87 -9
- package/public/install.html +8 -8
- package/public/learn.html +39 -0
- package/public/numbers.html +2 -2
- package/public/peter.html +310 -0
- package/public/platform-partners.html +119 -0
- package/public/pricing.html +24 -3
- package/public/privacy.html +117 -0
- package/public/pro.html +17 -0
- package/public/support.html +62 -0
- package/public/terms.html +130 -0
- package/public/third-party-notices.html +95 -0
- package/public/yt.html +351 -0
- package/scripts/action-receipts.js +133 -3
- package/scripts/adaptive-governance-arena.js +349 -0
- package/scripts/admin-override.js +205 -0
- package/scripts/agent-action-inventory.js +869 -0
- package/scripts/agent-audit-trace.js +42 -2
- package/scripts/agent-egress-policy.js +1117 -0
- package/scripts/agent-memory-lifecycle.js +141 -2
- package/scripts/agent-operations-planner.js +441 -1
- package/scripts/agent-readiness.js +68 -0
- package/scripts/agent-security-central.js +647 -0
- package/scripts/allowlist-bridge-honesty.js +417 -0
- package/scripts/async-job-runner.js +102 -11
- package/scripts/audit-trail.js +212 -0
- package/scripts/billing.js +1 -1
- package/scripts/broker-execution-receipts.js +719 -0
- package/scripts/budget-aware-gates-proof.js +423 -0
- package/scripts/claude-feedback-sync.js +29 -3
- package/scripts/claw-harness-production.js +237 -0
- package/scripts/cli-schema.js +203 -1
- package/scripts/cobble-hot-store-split.js +600 -0
- package/scripts/codex-runbook-flywheel.js +318 -0
- package/scripts/colab-compute-honesty.js +281 -0
- package/scripts/context-footprint.js +186 -0
- package/scripts/contextfs.js +143 -61
- package/scripts/dashboard.js +251 -32
- package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
- package/scripts/docker-sandbox-planner.js +18 -0
- package/scripts/double-blind-eval-protocol.js +252 -0
- package/scripts/edotenv-rl-gateway.js +259 -0
- package/scripts/ensure-production-search-corpus.js +162 -0
- package/scripts/eval-holdout.js +311 -0
- package/scripts/feedback-aggregate.js +21 -2
- package/scripts/feedback-loop.js +87 -5
- package/scripts/feedback-quality.js +9 -0
- package/scripts/file-ledger-lock.js +4 -1
- package/scripts/financial-control-plane.js +41 -1
- package/scripts/find-dormant-requires.js +118 -0
- package/scripts/fs-utils.js +84 -8
- package/scripts/gates-engine.js +826 -68
- package/scripts/generate-case-study-outreach.js +24 -15
- package/scripts/git-at-scale.js +628 -0
- package/scripts/governance-conflict-audit.js +1650 -0
- package/scripts/governance-difficulty-curriculum.js +328 -0
- package/scripts/graphrag-retrieval.js +275 -0
- package/scripts/gurobi-optimizer.js +324 -0
- package/scripts/gurobi_optimizer.py +485 -0
- package/scripts/harness-selector.js +82 -1
- package/scripts/hidden-entry-points.js +284 -0
- package/scripts/human-escalation.js +199 -1
- package/scripts/hybrid-feedback-context.js +152 -19
- package/scripts/intent-governed-execution.js +602 -0
- package/scripts/intervention-policy.js +123 -20
- package/scripts/jit-harness-compose.js +628 -0
- package/scripts/jsonl-watcher.js +10 -0
- package/scripts/lesson-embedding-index.js +95 -12
- package/scripts/lesson-retrieval.js +105 -19
- package/scripts/local-model-profile.js +19 -2
- package/scripts/mailer/resend-mailer.js +1 -1
- package/scripts/matryoshka-embedding.js +235 -0
- package/scripts/mcp-oauth.js +42 -4
- package/scripts/mcp-session-handles.js +1016 -0
- package/scripts/mcp-wiring-doctor.js +314 -0
- package/scripts/memory-firewall.js +115 -2
- package/scripts/memory-scope-readiness.js +299 -0
- package/scripts/memory-vs-rag-route.js +161 -0
- package/scripts/model-tier-router.js +148 -21
- package/scripts/nvidia-specdecode-al-doctor.js +536 -0
- package/scripts/openui-catalog-compose-honesty.js +593 -0
- package/scripts/operational-integrity.js +19 -1
- package/scripts/override-audit.js +213 -0
- package/scripts/package-manager-honesty-doctor.js +458 -0
- package/scripts/pr-manager.js +63 -1
- package/scripts/prove-herdr-adapter.js +52 -0
- package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
- package/scripts/prove-workos.js +73 -0
- package/scripts/provider-attestation-conformance.js +192 -0
- package/scripts/provider-receipt-contract.js +136 -0
- package/scripts/qwen38-max-cost-optimizer.js +401 -0
- package/scripts/radware-threat-defense.js +280 -0
- package/scripts/rag-embedding-identity.js +221 -0
- package/scripts/rag-precision-guardrails.js +112 -2
- package/scripts/remote-feedback-capture.js +159 -0
- package/scripts/research-agent-harness.js +256 -0
- package/scripts/rsi-safety-hillclimb.js +200 -0
- package/scripts/rule-sprawl.js +188 -0
- package/scripts/schedule-manager.js +147 -0
- package/scripts/self-heal.js +8 -0
- package/scripts/session-lease.js +415 -0
- package/scripts/simatree-data-governance.js +347 -0
- package/scripts/slo-alert-engine.js +172 -7
- package/scripts/solver-parity.js +539 -0
- package/scripts/stealth-memory-injection-gate.js +333 -0
- package/scripts/switchyard-router.js +366 -0
- package/scripts/telemetry-analytics.js +84 -27
- package/scripts/temporal-decay-weighting.js +138 -0
- package/scripts/test-all.js +165 -0
- package/scripts/token-savings.js +42 -0
- package/scripts/token-shunt-honesty.js +502 -0
- package/scripts/tool-kpi-tracker.js +108 -5
- package/scripts/tool-registry.js +193 -5
- package/scripts/typesafe-typed-questions.js +813 -0
- package/scripts/universal-claim-evaluator.js +14 -2
- package/scripts/vector-store.js +279 -9
- package/scripts/workflow-notebook.js +391 -0
- package/scripts/workflow-sentinel.js +111 -12
- package/scripts/workos-production-guard.js +260 -0
- package/scripts/workspace-search-route.js +515 -0
- package/server.json +2 -2
- package/src/agent-identity-boundary.js +76 -0
- package/src/agent-retrieval-cache.js +155 -0
- package/src/alert-noise-ledger.js +502 -0
- package/src/api/server.js +724 -153
- package/src/git-fast-cache.js +220 -0
- package/src/git-wal-sync.js +156 -0
- package/src/hash-anchored-edit.js +82 -0
- package/src/hermes-platform-protocol.js +475 -0
- package/src/hermes-sync-plane.js +241 -0
- package/src/index.js +30 -1
- package/src/iso42001-compliance-guard.js +97 -0
- package/src/latency-budget.js +244 -0
- package/src/mcp-writeguard.js +316 -0
- package/src/miminions-adapter.js +106 -0
- package/src/pipeline-compass.js +104 -0
- package/src/ppl-alert-pipeline.js +284 -0
- package/src/rendezvous-router.js +90 -0
- 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 (
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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({
|
|
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
|
-
|
|
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
|
|
1491
|
-
|
|
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) {
|