phewsh 0.15.64 → 0.15.66

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.
@@ -27,6 +27,11 @@ const CLAUDE_SETTINGS = path.join(os.homedir(), '.claude', 'settings.json');
27
27
 
28
28
  const HOOK_START = { type: 'command', command: 'phewsh hook session-start' };
29
29
  const HOOK_END = { type: 'command', command: 'phewsh hook session-end' };
30
+ // Opt-in Decision Gate enforcement (separate from ambient context sync). The
31
+ // matcher scopes the hook to the tools the policy actually judges, so it never
32
+ // fires on harmless reads.
33
+ const HOOK_PRETOOL = { type: 'command', command: 'phewsh hook pre-tool' };
34
+ const PRETOOL_MATCHER = 'Write|Edit|MultiEdit|NotebookEdit|Bash';
30
35
 
31
36
  // ANSI helpers (256-color per cli/lib/ui.js palette rules)
32
37
  const b = (s) => `\x1b[1m${s}\x1b[0m`;
@@ -97,6 +102,35 @@ function removeClaudeHooks() {
97
102
  return removed;
98
103
  }
99
104
 
105
+ // ── Decision Gate enforcement (PreToolUse) — opt-in, reversible ──────────────
106
+ function preToolGateApplied() {
107
+ const s = loadClaudeSettings();
108
+ return !!s && hasHook(s, 'PreToolUse', HOOK_PRETOOL.command);
109
+ }
110
+
111
+ function enablePreToolGate() {
112
+ const settings = loadClaudeSettings() || {};
113
+ settings.hooks = settings.hooks || {};
114
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse || [];
115
+ if (hasHook(settings, 'PreToolUse', HOOK_PRETOOL.command)) return false;
116
+ settings.hooks.PreToolUse.push({ matcher: PRETOOL_MATCHER, hooks: [HOOK_PRETOOL] });
117
+ fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2));
118
+ return true;
119
+ }
120
+
121
+ function disablePreToolGate() {
122
+ const settings = loadClaudeSettings();
123
+ if (!settings?.hooks?.PreToolUse) return false;
124
+ const before = settings.hooks.PreToolUse.length;
125
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse
126
+ .map(e => ({ ...e, hooks: (e.hooks || []).filter(h => h.command !== HOOK_PRETOOL.command) }))
127
+ .filter(e => e.hooks.length > 0);
128
+ if ((settings.hooks.PreToolUse || []).length === 0) delete settings.hooks.PreToolUse;
129
+ const changed = before !== (settings.hooks.PreToolUse?.length ?? 0);
130
+ if (changed) fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2));
131
+ return changed;
132
+ }
133
+
100
134
  function showConsentScreen(harnesses) {
101
135
  console.log('');
102
136
  console.log(` ${b(cream('PHEWSH ambient'))} ${sage('— continuity without launching phewsh')}`);
@@ -365,3 +399,6 @@ async function main() {
365
399
 
366
400
  module.exports = main;
367
401
  module.exports.ensureAuto = ensureAuto;
402
+ module.exports.enablePreToolGate = enablePreToolGate;
403
+ module.exports.disablePreToolGate = disablePreToolGate;
404
+ module.exports.preToolGateApplied = preToolGateApplied;
package/commands/gate.js CHANGED
@@ -373,6 +373,7 @@ async function main() {
373
373
  if (subcommand === 'update' || subcommand === 'edit') { await update(); return; }
374
374
  if (subcommand === 'pause') { pause(); return; }
375
375
  if (subcommand === 'resume') { resume(); return; }
376
+ if (subcommand === 'enforce') { enforce(args[1]); return; }
376
377
  if (subcommand === 'reset') {
377
378
  let removed = false;
378
379
  const project = loadProjectJson();
@@ -391,6 +392,34 @@ async function main() {
391
392
  console.log(`\n Unknown: ${subcommand}. Run \`phewsh gate --help\`.\n`);
392
393
  }
393
394
 
395
+ // Opt-in deterministic enforcement: register/unregister the Claude Code
396
+ // PreToolUse hook that makes the Decision Gate act BEFORE a tool runs. Reversible,
397
+ // Claude-Code-only for now (it's the one provider with a real veto hook).
398
+ function enforce(action) {
399
+ let amb;
400
+ try { amb = require('./ambient'); } catch { console.log('\n Enforcement unavailable.\n'); return; }
401
+ const on = (action || 'status').toLowerCase();
402
+ if (on === 'on' || on === 'enable') {
403
+ const changed = amb.enablePreToolGate();
404
+ console.log(changed
405
+ ? `\n ${green('●')} Gate enforcement ${green('ON')} — Claude Code asks/denies on protected-path writes and high-blast-radius commands before they run.\n ${g('Reversible:')} phewsh gate enforce off\n`
406
+ : `\n Gate enforcement already on.\n`);
407
+ return;
408
+ }
409
+ if (on === 'off' || on === 'disable') {
410
+ const changed = amb.disablePreToolGate();
411
+ console.log(changed ? `\n Gate enforcement ${yellow('OFF')} — PreToolUse hook removed.\n` : `\n Gate enforcement was not on.\n`);
412
+ return;
413
+ }
414
+ // status
415
+ const applied = amb.preToolGateApplied();
416
+ console.log(`\n Gate enforcement: ${applied ? green('ON') : g('off')} ${g('(Claude Code PreToolUse)')}`);
417
+ console.log(` ${g('Turn on:')} phewsh gate enforce on ${g('· off:')} phewsh gate enforce off`);
418
+ console.log(` ${g('What it does: deny writes to protected paths (.env, keys, .git/…),')}`);
419
+ console.log(` ${g('ask before high-blast-radius shell (rm -rf, force-push, sudo…).')}`);
420
+ console.log(` ${g('Opt-in, local-only, fail-open. Other tools: advisory only for now.')}\n`);
421
+ }
422
+
394
423
  module.exports = main;
395
424
 
396
425
  main().catch(err => {
package/commands/hook.js CHANGED
@@ -163,10 +163,63 @@ function sessionEnd() {
163
163
  setTimeout(() => { appendBreadcrumb('session-end'); process.exit(0); }, 1500);
164
164
  }
165
165
 
166
+ // Load the Decision Gate context (constraints + protected files) for the project
167
+ // being worked in. Best-effort: missing/garbled → empty context (gate allows).
168
+ function loadGateContext(cwd) {
169
+ try {
170
+ const pj = JSON.parse(fs.readFileSync(path.join(cwd, '.intent', 'project.json'), 'utf-8'));
171
+ const dg = pj.decisionGate || {};
172
+ return {
173
+ constraints: dg.constraints || {},
174
+ protectedFiles: dg.protectedFiles || pj.protectedFiles || [],
175
+ };
176
+ } catch { return { constraints: {}, protectedFiles: [] }; }
177
+ }
178
+
179
+ // PreToolUse adapter — the Decision Gate acting BEFORE a tool runs. Reads Claude
180
+ // Code's PreToolUse JSON from stdin, evaluates against the project's constraints
181
+ // + protected files, and emits a permission decision. FAIL-SAFE: any error, no
182
+ // stdin, or no policy hit → exit 0 (allow). A broken gate must never trap you.
183
+ // It logs only a redacted decision line (never the tool payload).
184
+ function preTool() {
185
+ try {
186
+ if (process.stdin.isTTY) process.exit(0); // not a real hook invocation
187
+ const { evaluateAction, auditLine } = require('../lib/gate-policy');
188
+ let raw = '';
189
+ try { raw = fs.readFileSync(0, 'utf-8'); } catch { process.exit(0); }
190
+ let payload = {};
191
+ try { payload = JSON.parse(raw || '{}'); } catch { process.exit(0); }
192
+
193
+ const cwd = payload.cwd || process.cwd();
194
+ const envelope = {
195
+ toolName: payload.tool_name || payload.toolName,
196
+ toolInput: payload.tool_input || payload.toolInput || {},
197
+ ...loadGateContext(cwd),
198
+ };
199
+ let result = { decision: 'allow', reason: '' };
200
+ try { result = evaluateAction(envelope); } catch { process.exit(0); }
201
+
202
+ if (result.decision !== 'allow') {
203
+ try { appendBreadcrumb('pre-tool', { decision: result.decision, action: auditLine(envelope, result) }); } catch { /* audit is best-effort */ }
204
+ process.stdout.write(JSON.stringify({
205
+ hookSpecificOutput: {
206
+ hookEventName: 'PreToolUse',
207
+ permissionDecision: result.decision, // 'deny' | 'ask'
208
+ permissionDecisionReason: result.reason,
209
+ },
210
+ }));
211
+ }
212
+ process.exit(0);
213
+ } catch {
214
+ process.exit(0); // fail open, always
215
+ }
216
+ }
217
+
166
218
  function main() {
167
219
  const event = process.argv[3];
168
220
  if (event === 'session-start') return sessionStart();
169
221
  if (event === 'session-end') return sessionEnd();
222
+ if (event === 'pre-tool') return preTool();
170
223
  // Unknown event: exit silently — hooks must never error the host tool.
171
224
  process.exit(0);
172
225
  }
package/commands/pack.js CHANGED
@@ -52,7 +52,7 @@ async function install(name) {
52
52
  console.log('');
53
53
 
54
54
  if (p.kind === 'linked') {
55
- console.log(` ${sage('This is a separate tool — phewsh doesn\'t vendor it. Install it with:')}`);
55
+ console.log(` ${sage('This is a separate tool — phewsh doesn\'t vendor it. Start here:')}`);
56
56
  console.log(` ${cream(p.install)}`);
57
57
  console.log('');
58
58
  return;
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ // Deterministic pre-action policy — the enforcement core of the Decision Gate.
4
+ //
5
+ // Pure and provider-neutral: given an action envelope it returns a decision.
6
+ // No I/O, no provider coupling — the hook adapter (commands/hook.js pre-tool)
7
+ // feeds it the action and renders the provider-specific response. This is the
8
+ // "deterministic enforcement" layer from cli/docs/pre-action-architecture.md;
9
+ // it does NOT replace the Decision Gate, it is how the gate acts before a tool
10
+ // runs. Decisions: 'allow' | 'ask' (require human) | 'deny' (block).
11
+ //
12
+ // Design rules: fail OPEN (an empty/garbled envelope returns allow — a policy
13
+ // must never trap the user), and never inspect file CONTENTS (we judge the
14
+ // action's target/shape, not your code or secrets).
15
+
16
+ const path = require('path');
17
+
18
+ // Tools that write/modify the filesystem — common names across providers.
19
+ const WRITE_TOOLS = new Set([
20
+ 'Write', 'Edit', 'MultiEdit', 'NotebookEdit',
21
+ 'create_file', 'apply_patch', 'str_replace_editor', 'edit_file',
22
+ ]);
23
+
24
+ // Shell/command tools.
25
+ const SHELL_TOOLS = new Set(['Bash', 'shell', 'run_command', 'run_terminal_cmd']);
26
+
27
+ // Paths an agent should never silently write — credentials, keys, VCS internals.
28
+ const DEFAULT_PROTECTED = [
29
+ '.env', '.env.*', '*.pem', '*.key', 'id_rsa', 'id_ed25519',
30
+ '.git/', '.npmrc', '.aws/', '.ssh/', '.phewsh/config.json',
31
+ ];
32
+
33
+ // High-blast-radius shell patterns that warrant a human OK.
34
+ const DESTRUCTIVE = [
35
+ { re: /\brm\s+-[a-z]*r[a-z]*f|\brm\s+-[a-z]*f[a-z]*r/, label: 'recursive force delete' },
36
+ { re: /\bgit\s+push\b[^\n]*--force/, label: 'git force-push' },
37
+ { re: /\bgit\s+reset\s+--hard\b/, label: 'git hard reset' },
38
+ { re: /\bDROP\s+(TABLE|DATABASE)\b/i, label: 'SQL DROP' },
39
+ { re: /\bsudo\b/, label: 'sudo' },
40
+ { re: /\bchmod\s+-R\b/, label: 'recursive chmod' },
41
+ { re: /\bmkfs\b|\bdd\s+if=/, label: 'disk write' },
42
+ { re: /\bcurl\b[^\n]*\|\s*(sh|bash)\b|\bwget\b[^\n]*\|\s*(sh|bash)\b/, label: 'pipe-to-shell' },
43
+ ];
44
+
45
+ function globToRegExp(glob) {
46
+ const esc = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
47
+ return new RegExp('^' + esc + '$');
48
+ }
49
+
50
+ // Does `target` match any protected entry? Directory entries (trailing '/')
51
+ // match anywhere in the path; file globs match the basename or full path.
52
+ function isProtected(target, protectedList) {
53
+ if (!target || typeof target !== 'string') return false;
54
+ const norm = target.replace(/^\.\//, '');
55
+ const base = path.basename(norm);
56
+ return protectedList.some(p => {
57
+ if (p.endsWith('/')) return norm.split('/').includes(p.slice(0, -1)) || norm.startsWith(p);
58
+ const re = globToRegExp(p);
59
+ return re.test(base) || re.test(norm);
60
+ });
61
+ }
62
+
63
+ function writeTarget(toolInput = {}) {
64
+ return toolInput.file_path || toolInput.path || toolInput.notebook_path
65
+ || toolInput.filename || toolInput.target_file || null;
66
+ }
67
+
68
+ // envelope: { toolName, toolInput, constraints, protectedFiles }
69
+ function evaluateAction(envelope = {}) {
70
+ try {
71
+ const { toolName, toolInput = {}, constraints = {}, protectedFiles = [] } = envelope;
72
+ if (!toolName) return { decision: 'allow', reason: '' };
73
+ const protectedList = [...DEFAULT_PROTECTED, ...(Array.isArray(protectedFiles) ? protectedFiles : [])];
74
+
75
+ // 1. Writes to a protected path → deny (only a deliberate human act should).
76
+ if (WRITE_TOOLS.has(toolName)) {
77
+ const target = writeTarget(toolInput);
78
+ if (isProtected(target, protectedList)) {
79
+ return { decision: 'deny', reason: `phewsh gate: ${toolName} targets a protected path (${target}). Credentials/keys/VCS internals are off-limits to automated edits.` };
80
+ }
81
+ }
82
+
83
+ // 2. High-blast-radius shell → ask (require human confirmation).
84
+ if (SHELL_TOOLS.has(toolName)) {
85
+ const cmd = String(toolInput.command || toolInput.cmd || '');
86
+ const hit = DESTRUCTIVE.find(d => d.re.test(cmd));
87
+ if (hit) {
88
+ return { decision: 'ask', reason: `phewsh gate: high-blast-radius command (${hit.label}) — confirm before it runs.` };
89
+ }
90
+ }
91
+
92
+ // 3. Strict autonomy asks before any write. "delegated"/"guided" do not —
93
+ // that would turn every edit into ceremony.
94
+ const autonomy = String(constraints.autonomy || '').toLowerCase();
95
+ if ((autonomy === 'manual' || autonomy === 'review') && WRITE_TOOLS.has(toolName)) {
96
+ return { decision: 'ask', reason: `phewsh gate: your autonomy is "${autonomy}" — confirm this ${toolName} before it runs.` };
97
+ }
98
+
99
+ return { decision: 'allow', reason: '' };
100
+ } catch {
101
+ return { decision: 'allow', reason: '' }; // fail OPEN: never trap the user
102
+ }
103
+ }
104
+
105
+ // A redacted one-line audit record — decision + target shape only, never the
106
+ // tool payload (which may contain code or secrets).
107
+ function auditLine(envelope = {}, result = {}) {
108
+ const target = WRITE_TOOLS.has(envelope.toolName)
109
+ ? writeTarget(envelope.toolInput || {})
110
+ : (SHELL_TOOLS.has(envelope.toolName) ? '<command>' : '');
111
+ return `${result.decision || 'allow'} ${envelope.toolName || '?'}${target ? ' ' + target : ''}`;
112
+ }
113
+
114
+ module.exports = {
115
+ evaluateAction, isProtected, auditLine, writeTarget,
116
+ WRITE_TOOLS, SHELL_TOOLS, DEFAULT_PROTECTED, DESTRUCTIVE,
117
+ };
package/lib/packs.js CHANGED
@@ -12,7 +12,7 @@
12
12
  // • vendored — phewsh writes a clearly-marked block into the project's agent
13
13
  // files (e.g. Karpathy's coding guidelines into CLAUDE.md/AGENTS.md).
14
14
  // • linked — phewsh doesn't vendor it; it points you at the upstream
15
- // installer (e.g. GSD is its own tool — we hand you its command).
15
+ // installer or review page (e.g. GSD is its own tool — we hand you its command).
16
16
 
17
17
  const fs = require('fs');
18
18
  const path = require('path');
@@ -52,6 +52,110 @@ const PACKS = {
52
52
  license: 'see the GSD repo',
53
53
  install: 'npx @gsd-build/get-shit-done init # follow the GSD repo for current instructions',
54
54
  },
55
+ 'loop-library': {
56
+ kind: 'linked',
57
+ title: 'Loop Library',
58
+ desc: 'Copy-ready agent loops with checks and stopping conditions. Good Work-layer inspiration; phewsh links to it without becoming a scheduler or runner.',
59
+ source: 'github.com/Forward-Future/loop-library · signals.forwardfuture.ai/loop-library',
60
+ license: 'MIT',
61
+ install: 'npx skills add Forward-Future/loop-library --skill loop-library -g # install through upstream skills tooling',
62
+ },
63
+ 'matt-skills': {
64
+ kind: 'linked',
65
+ title: 'Matt Pocock Skills',
66
+ desc: 'Engineering-focused skills for debugging, TDD, domain modeling, handoffs, git guardrails, and design reviews.',
67
+ source: 'github.com/mattpocock/skills',
68
+ license: 'MIT',
69
+ install: 'npx skills@latest add mattpocock/skills # install through upstream skills tooling',
70
+ },
71
+ 'cybersecurity-skills': {
72
+ kind: 'linked',
73
+ title: 'Anthropic Cybersecurity Skills',
74
+ desc: 'Large cybersecurity skill library mapped to security frameworks. Review scope carefully before using on real targets.',
75
+ source: 'github.com/mukul975/Anthropic-Cybersecurity-Skills',
76
+ license: 'Apache-2.0',
77
+ install: 'review https://github.com/mukul975/Anthropic-Cybersecurity-Skills # choose upstream install path and scope deliberately',
78
+ },
79
+ skillspector: {
80
+ kind: 'linked',
81
+ title: 'NVIDIA SkillSpector',
82
+ desc: 'Security scanner for AI-agent skills. Useful candidate for future Phewsh pack vetting before install or distribution.',
83
+ source: 'github.com/nvidia/skillspector',
84
+ license: 'see the SkillSpector repo',
85
+ install: 'review https://github.com/nvidia/skillspector # run scanner only after reading upstream docs',
86
+ },
87
+ 'codebase-memory-mcp': {
88
+ kind: 'linked',
89
+ title: 'Codebase Memory MCP',
90
+ desc: 'Persistent code-intelligence MCP server for repository memory and fast code search. Linked only; configure per harness.',
91
+ source: 'github.com/DeusData/codebase-memory-mcp',
92
+ license: 'see the codebase-memory-mcp repo',
93
+ install: 'review https://github.com/DeusData/codebase-memory-mcp # install/configure upstream MCP manually',
94
+ },
95
+ 'unlimited-ocr': {
96
+ kind: 'linked',
97
+ title: 'Unlimited OCR',
98
+ desc: 'Local/GPU OCR model candidate for images and PDFs. Relevant to RecipeFlower-style recipe capture, but heavy and not auto-installed.',
99
+ source: 'github.com/baidu/Unlimited-OCR',
100
+ license: 'MIT',
101
+ install: 'review https://github.com/baidu/Unlimited-OCR # inspect CUDA/SGLang requirements before installing',
102
+ },
103
+ 'palmier-pro': {
104
+ kind: 'linked',
105
+ title: 'Palmier Pro',
106
+ desc: 'Open-source macOS video editor with an MCP surface. Useful media-production inspiration; platform-gated and not bundled.',
107
+ source: 'github.com/palmier-io/palmier-pro',
108
+ license: 'GPLv3',
109
+ install: 'review https://github.com/palmier-io/palmier-pro # macOS/Apple Silicon requirements apply',
110
+ },
111
+ openmontage: {
112
+ kind: 'linked',
113
+ title: 'OpenMontage',
114
+ desc: 'Agentic video-production system with pipelines, tools, and skills. Treat as an upstream media lab, not a core Phewsh dependency.',
115
+ source: 'github.com/calesthio/OpenMontage',
116
+ license: 'see the OpenMontage repo',
117
+ install: 'review https://github.com/calesthio/OpenMontage # install only in a project that needs agentic video production',
118
+ },
119
+ hyperframes: {
120
+ kind: 'linked',
121
+ title: 'Hyperframes',
122
+ desc: 'HTML-to-video rendering for agents. Good fit for future product demos, walkthroughs, and generated media packs.',
123
+ source: 'github.com/heygen-com/hyperframes',
124
+ license: 'see the Hyperframes repo',
125
+ install: 'review https://github.com/heygen-com/hyperframes # inspect current renderer/runtime requirements',
126
+ },
127
+ 'deer-flow': {
128
+ kind: 'linked',
129
+ title: 'DeerFlow',
130
+ desc: 'Open-source agent harness with sub-agents, memory, sandboxes, and skills. Compare as an executor/harness, not a Phewsh replacement.',
131
+ source: 'github.com/bytedance/deer-flow',
132
+ license: 'MIT',
133
+ install: 'review https://github.com/bytedance/deer-flow # evaluate as a separate harness before configuring',
134
+ },
135
+ 'hermes-agent': {
136
+ kind: 'linked',
137
+ title: 'Hermes Agent',
138
+ desc: 'Nous Research agent project. Linked as a possible compatible harness to evaluate before any deeper integration.',
139
+ source: 'github.com/nousresearch/hermes-agent',
140
+ license: 'see the Hermes Agent repo',
141
+ install: 'review https://github.com/nousresearch/hermes-agent # evaluate upstream capabilities first',
142
+ },
143
+ voicebox: {
144
+ kind: 'linked',
145
+ title: 'Voicebox',
146
+ desc: 'Open-source AI voice studio. Candidate for future audio/voice workflows and Keylink-adjacent media experiments.',
147
+ source: 'github.com/jamiepine/voicebox',
148
+ license: 'see the Voicebox repo',
149
+ install: 'review https://github.com/jamiepine/voicebox # install only for voice workflows that need it',
150
+ },
151
+ gstack: {
152
+ kind: 'linked',
153
+ title: 'gstack',
154
+ desc: 'Opinionated Claude Code setup with role/tooling patterns. Useful reference, but Phewsh should preserve its own four-word model.',
155
+ source: 'github.com/garrytan/gstack',
156
+ license: 'see the gstack repo',
157
+ install: 'review https://github.com/garrytan/gstack # inspect before mixing with existing agent files',
158
+ },
55
159
  };
56
160
 
57
161
  function markers(name) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phewsh",
3
- "version": "0.15.64",
3
+ "version": "0.15.66",
4
4
  "description": "Turn intent into action. Structure your thinking, execute your next step.",
5
5
  "bin": {
6
6
  "phewsh": "bin/phewsh.js"