claude-code-session-manager 0.95.0 → 0.97.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{DataModel-DZK9QZpV.js → DataModel-D93FoaOm.js} +1 -1
- package/dist/assets/History-CXxxqNP1.js +2 -0
- package/dist/assets/{Hooks-CmK6l0uf.js → Hooks-CzeYFQjq.js} +3 -3
- package/dist/assets/{HostBilko-D8TIx-Bx.js → HostBilko-nESrTRIg.js} +1 -1
- package/dist/assets/{Library-DTxpOjx8.js → Library-D2zPkwOC.js} +1 -1
- package/dist/assets/{MarkdownEditor-BPJyrevq.js → MarkdownEditor-DbinP78A.js} +1 -1
- package/dist/assets/{McpServers-B_rvIYDs.js → McpServers-Bu_sVGLr.js} +2 -2
- package/dist/assets/Memory-BgiI6LZ_.js +8 -0
- package/dist/assets/{Permissions-DhMMp27n.js → Permissions-C1MMzZRm.js} +3 -3
- package/dist/assets/Plugins-Dhu6wAZi.js +2 -0
- package/dist/assets/{ProvenanceBadge-ptlItdCK.js → ProvenanceBadge-C3AcWKMG.js} +1 -1
- package/dist/assets/{SaveBar-CSbNSl2W.js → SaveBar-plwYm0iO.js} +1 -1
- package/dist/assets/Scheduler-BVYTeh39.js +16 -0
- package/dist/assets/{ScopeSwitcher-BJof-GWH.js → ScopeSwitcher-DFy0vv_o.js} +1 -1
- package/dist/assets/Settings-DIhGgdfs.js +3 -0
- package/dist/assets/{SkillReferenceGraph-CGl4B89-.js → SkillReferenceGraph-DsI7hRPB.js} +1 -1
- package/dist/assets/Skills-3E748UfB.js +3 -0
- package/dist/assets/{SystemPrompt-DKj-q39J.js → SystemPrompt-BqmfUJOd.js} +1 -1
- package/dist/assets/TagLibrary-CPz6fO8Q.js +1 -0
- package/dist/assets/{TiptapBody-BbcJL9az.js → TiptapBody-D9EoBQ1P.js} +1 -1
- package/dist/assets/{Toggle-DAslvnnv.js → Toggle-BHAmZNDn.js} +1 -1
- package/dist/assets/index-tn5JIVPj.js +3079 -0
- package/dist/assets/{settingsSchema-pzp0TgvN.js → settingsSchema-Crqy_aNz.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +8 -2
- package/plugins/CLAUDE.md +3 -3
- package/plugins/session-manager-dev/skills/develop/SKILL.md +90 -67
- package/plugins/session-manager-dev/skills/develop/standards.md +5 -11
- package/plugins/session-manager-dev/skills/requesting-code-review/SKILL.md +5 -15
- package/scripts/README.md +1 -0
- package/scripts/hooks/guard-destructive-git.cjs +9 -374
- package/scripts/hooks/guard-inline-implementation.cjs +9 -128
- package/scripts/hooks/guard-prd-writes.cjs +9 -74
- package/scripts/hooks/guard-self-schedule.cjs +9 -61
- package/scripts/hooks/lib/guard-destructive-git-policy.cjs +410 -0
- package/scripts/hooks/lib/guard-inline-implementation-policy.cjs +159 -0
- package/scripts/hooks/lib/guard-prd-writes-policy.cjs +100 -0
- package/scripts/hooks/lib/guard-self-schedule-policy.cjs +89 -0
- package/scripts/scheduler-mcp-server.cjs +8 -1
- package/src/main/__tests__/configWriteBoundaryOwners.test.cjs +59 -0
- package/src/main/__tests__/exchanges.test.cjs +41 -96
- package/src/main/__tests__/exchangesPromptId.test.cjs +16 -25
- package/src/main/__tests__/files-reject-credentials.test.cjs +1 -1
- package/src/main/__tests__/health-credentials.test.cjs +81 -0
- package/src/main/__tests__/ipcSchemas-dependsOn.test.cjs +39 -0
- package/src/main/__tests__/memoryAggregate.test.cjs +40 -1
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +1 -1
- package/src/main/__tests__/planValidator.test.cjs +125 -0
- package/src/main/__tests__/prdCreate.test.cjs +3 -1
- package/src/main/__tests__/prdSizing.test.cjs +106 -0
- package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +46 -0
- package/src/main/__tests__/reconcileFlatPrdSweep.test.cjs +10 -0
- package/src/main/__tests__/scheduler-looks-done.test.cjs +40 -0
- package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +81 -0
- package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +6 -1
- package/src/main/__tests__/schedulerStateSidecarRestore.test.cjs +110 -0
- package/src/main/__tests__/seedAgentPersonas.test.cjs +19 -1
- package/src/main/__tests__/seedValidatorPersona.test.cjs +116 -0
- package/src/main/__tests__/telemetryClient.test.cjs +2 -2
- package/src/main/__tests__/telemetryContract.test.cjs +2 -2
- package/src/main/__tests__/usageSingleFlight.test.cjs +10 -0
- package/src/main/__tests__/validationSentinels.test.cjs +84 -0
- package/src/main/build-info.json +4 -4
- package/src/main/config.cjs +26 -14
- package/src/main/exchanges.cjs +26 -21
- package/src/main/health.cjs +66 -4
- package/src/main/index.cjs +1 -1
- package/src/main/ipcSchemas.cjs +15 -7
- package/src/main/lib/__tests__/crashTelemetry.test.cjs +1 -1
- package/src/main/lib/__tests__/credentials-futile-refresh.test.cjs +115 -0
- package/src/main/lib/__tests__/epicWorktreeProjectConfig.test.cjs +85 -27
- package/src/main/lib/__tests__/gitWorktree.test.cjs +21 -27
- package/src/main/lib/__tests__/guardShims.test.cjs +10 -3
- package/src/main/lib/__tests__/loadGate.test.cjs +44 -1
- package/src/main/lib/__tests__/opsOwnership.test.cjs +14 -0
- package/src/main/lib/__tests__/telemetryBacklog.test.cjs +4 -4
- package/src/main/lib/__tests__/telemetryConsent.test.cjs +2 -2
- package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +1 -1
- package/src/main/lib/agentModelResolve.cjs +1 -0
- package/src/main/lib/credentials.cjs +62 -0
- package/src/main/lib/cwdClassify.cjs +15 -0
- package/src/main/lib/definitionOfDone.cjs +9 -13
- package/src/main/lib/effectiveModelInfo.cjs +2 -9
- package/src/main/lib/epicWorktreeProjectConfig.cjs +75 -31
- package/src/main/lib/loadGate.cjs +29 -5
- package/src/main/lib/opsOwnership.cjs +23 -0
- package/src/main/lib/planValidator.cjs +80 -0
- package/src/main/lib/prdCreate.cjs +8 -4
- package/src/main/lib/prdSizing.cjs +83 -0
- package/src/main/lib/runLogRetention.cjs +2 -8
- package/src/main/lib/telemetryBacklog.cjs +9 -3
- package/src/main/lib/telemetryClient.cjs +12 -3
- package/src/main/lib/telemetrySettings.cjs +8 -2
- package/src/main/lib/validationSentinels.cjs +29 -0
- package/src/main/memoryAggregate.cjs +36 -11
- package/src/main/otelSettings.cjs +8 -2
- package/src/main/scheduler.cjs +215 -31
- package/src/main/seedAgentPersonas.cjs +1 -1
- package/src/main/sessionsStore.cjs +8 -2
- package/src/main/supervisor.cjs +3 -3
- package/src/main/templates/PRD_AUTHORING.md +9 -9
- package/src/main/voiceSettings.cjs +8 -2
- package/src/preload/api.d.ts +3 -3
- package/src/preload/index.cjs +5 -5
- package/src/seed/agents/dev-lead.md +24 -15
- package/src/seed/agents/validator.md +33 -0
- package/dist/assets/History-BDyJUGtR.js +0 -2
- package/dist/assets/Memory-B-9GlHqe.js +0 -8
- package/dist/assets/Plugins-DKfAlmMY.js +0 -2
- package/dist/assets/Scheduler--rn1QBYc.js +0 -16
- package/dist/assets/Settings-Dsx6XZP5.js +0 -3
- package/dist/assets/Skills-D522Zfol.js +0 -3
- package/dist/assets/TagLibrary-BH7CG7-6.js +0 -1
- package/dist/assets/index-Yf8e7VCB.js +0 -3079
|
@@ -90,11 +90,7 @@
|
|
|
90
90
|
*/
|
|
91
91
|
'use strict';
|
|
92
92
|
|
|
93
|
-
const
|
|
94
|
-
const BACKGROUNDABLE_AGENT_TOOLS = new Set(['Task', 'Agent']);
|
|
95
|
-
|
|
96
|
-
const STANDARDS_QUOTE = 'You ARE the executor — never re-queue or self-schedule.';
|
|
97
|
-
const STANDARDS_CITE = 'plugins/session-manager-dev/skills/develop/standards.md:85 (Execution discipline)';
|
|
93
|
+
const { decide, parsePayload } = require('./lib/guard-self-schedule-policy.cjs');
|
|
98
94
|
|
|
99
95
|
function readStdin() {
|
|
100
96
|
return new Promise((resolve) => {
|
|
@@ -107,69 +103,21 @@ function readStdin() {
|
|
|
107
103
|
});
|
|
108
104
|
}
|
|
109
105
|
|
|
110
|
-
function
|
|
111
|
-
process.stdout.write(JSON.stringify(
|
|
112
|
-
process.exit(0);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function deny(reason) {
|
|
116
|
-
process.stdout.write(JSON.stringify({
|
|
117
|
-
continue: true,
|
|
118
|
-
decision: 'block',
|
|
119
|
-
reason,
|
|
120
|
-
hookSpecificOutput: {
|
|
121
|
-
hookEventName: 'PreToolUse',
|
|
122
|
-
permissionDecision: 'deny',
|
|
123
|
-
permissionDecisionReason: reason,
|
|
124
|
-
},
|
|
125
|
-
}));
|
|
106
|
+
function emit(result) {
|
|
107
|
+
process.stdout.write(JSON.stringify(result));
|
|
126
108
|
process.exit(0);
|
|
127
109
|
}
|
|
128
110
|
|
|
129
111
|
async function main() {
|
|
130
112
|
const raw = await readStdin();
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
payload = raw ? JSON.parse(raw) : {};
|
|
134
|
-
} catch (e) {
|
|
135
|
-
console.error(`[guard-self-schedule] malformed stdin JSON, failing open: ${e?.message}`);
|
|
136
|
-
return allow();
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
const jobSlug = process.env.SM_SCHEDULER_JOB_SLUG;
|
|
141
|
-
if (!jobSlug) return allow(); // interactive session — this guard is inert here
|
|
142
|
-
|
|
143
|
-
const toolName = payload.tool_name;
|
|
144
|
-
|
|
145
|
-
if (ALWAYS_DENY_TOOLS.has(toolName)) {
|
|
146
|
-
const reason = [
|
|
147
|
-
`Blocked: \`${toolName}\` from inside a headless scheduler-spawned run (job '${jobSlug}').`,
|
|
148
|
-
`${STANDARDS_CITE}: "${STANDARDS_QUOTE}"`,
|
|
149
|
-
'A headless run has no next turn — nothing will ever deliver this wakeup, so the job exits with its work stranded and parks in needs_review even if it was already correct.',
|
|
150
|
-
'Do it now instead: commit what you have, then run any required review synchronously inline (call the reviewer and read its result in this same turn) before the finish protocol — never schedule it for later.',
|
|
151
|
-
].join(' ');
|
|
152
|
-
return deny(reason);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (BACKGROUNDABLE_AGENT_TOOLS.has(toolName) && payload.tool_input && payload.tool_input.run_in_background) {
|
|
156
|
-
const reason = [
|
|
157
|
-
`Blocked: a backgrounded ${toolName} call (run_in_background: true) from inside a headless scheduler-spawned run (job '${jobSlug}').`,
|
|
158
|
-
`${STANDARDS_CITE}: "${STANDARDS_QUOTE}"`,
|
|
159
|
-
'A headless run has no later turn to collect a background agent\'s result, so waiting on it (ListAgents, Monitor, ScheduleWakeup) strands the job in needs_review with its work already done but uncommitted.',
|
|
160
|
-
'Call it synchronously instead — omit run_in_background (or set it to false) and read its result in this same turn, before the finish protocol.',
|
|
161
|
-
].join(' ');
|
|
162
|
-
return deny(reason);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return allow();
|
|
166
|
-
} catch (e) {
|
|
167
|
-
console.error(`[guard-self-schedule] internal error, failing open: ${e?.message}`);
|
|
168
|
-
return allow();
|
|
169
|
-
}
|
|
113
|
+
return emit(decide(parsePayload(raw)));
|
|
170
114
|
}
|
|
171
115
|
|
|
116
|
+
// Runs as a require-time side effect, NOT gated on `require.main === module`
|
|
117
|
+
// — see src/main/lib/guardShims.cjs's header: the installed shim invokes this
|
|
118
|
+
// script via a plain `require()`, so gating on `require.main` would silently
|
|
119
|
+
// stop the shim from ever running the guard.
|
|
172
120
|
main().catch((e) => {
|
|
173
121
|
console.error(`[guard-self-schedule] unhandled error, failing open: ${e?.message}`);
|
|
174
|
-
|
|
122
|
+
emit({ continue: true });
|
|
175
123
|
});
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* guard-destructive-git-policy.cjs — decision logic for
|
|
3
|
+
* scripts/hooks/guard-destructive-git.cjs, split out so it can be
|
|
4
|
+
* `require()`d in-process (by tests, notably) with no CLI-level side effects.
|
|
5
|
+
*
|
|
6
|
+
* guard-destructive-git.cjs itself runs its CLI as a require-time side
|
|
7
|
+
* effect (stdin -> decision -> process.exit, not gated on
|
|
8
|
+
* `require.main === module` — see src/main/lib/guardShims.cjs's header for
|
|
9
|
+
* why: the installed shim invokes the real script via a plain `require()`,
|
|
10
|
+
* so gating on `require.main` would silently stop the shim from ever running
|
|
11
|
+
* the guard). Requiring THIS module never reads stdin or calls
|
|
12
|
+
* `process.exit`, so it's safe to load from a test process — but `decide()`
|
|
13
|
+
* is not fully I/O-free: for a destructive verdict outside a
|
|
14
|
+
* path-recognized managed worktree, it shells out to a real `git
|
|
15
|
+
* rev-parse` (see `isInsideManagedWorktreeByBranch` below) before returning.
|
|
16
|
+
* Most `expectDenied(...)` cases in the test file hit this, so most test
|
|
17
|
+
* runs still spawn a `git` child process per denied case — fewer spawns
|
|
18
|
+
* than the old ~50-CLI-process-per-run shape, not zero.
|
|
19
|
+
*/
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const path = require('node:path');
|
|
23
|
+
const os = require('node:os');
|
|
24
|
+
const { execFileSync } = require('node:child_process');
|
|
25
|
+
|
|
26
|
+
// Duplicated from src/main/lib/gitWorktree.cjs's KIND_CONFIG — see
|
|
27
|
+
// guard-destructive-git.cjs's header for why this is a deliberate
|
|
28
|
+
// duplication, not a require. Same SM_WORKTREE_ROOT-else-tmpdir base as
|
|
29
|
+
// schedulerPaths.worktreeBase().
|
|
30
|
+
const WORKTREE_BASE = process.env.SM_WORKTREE_ROOT || os.tmpdir();
|
|
31
|
+
const WORKTREE_ROOTS = ['job', 'epic'].map((k) => path.join(WORKTREE_BASE, `session-manager-${k}-worktrees`));
|
|
32
|
+
const BRANCH_PREFIXES = ['sm-job/', 'sm-epic/'];
|
|
33
|
+
|
|
34
|
+
const MAX_SH_C_DEPTH = 5;
|
|
35
|
+
|
|
36
|
+
/** Quote-aware split on top-level `&&`, `||`, `;`, `|`, and newlines. */
|
|
37
|
+
function splitTopLevel(cmd) {
|
|
38
|
+
const parts = [];
|
|
39
|
+
let cur = '';
|
|
40
|
+
let inSingle = false;
|
|
41
|
+
let inDouble = false;
|
|
42
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
43
|
+
const c = cmd[i];
|
|
44
|
+
if (inSingle) {
|
|
45
|
+
cur += c;
|
|
46
|
+
if (c === "'") inSingle = false;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (inDouble) {
|
|
50
|
+
cur += c;
|
|
51
|
+
if (c === '"' && cmd[i - 1] !== '\\') inDouble = false;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (c === "'") { inSingle = true; cur += c; continue; }
|
|
55
|
+
if (c === '"') { inDouble = true; cur += c; continue; }
|
|
56
|
+
const two = cmd.slice(i, i + 2);
|
|
57
|
+
if (two === '&&' || two === '||') { parts.push(cur); cur = ''; i++; continue; }
|
|
58
|
+
if (c === ';' || c === '|' || c === '\n') { parts.push(cur); cur = ''; continue; }
|
|
59
|
+
cur += c;
|
|
60
|
+
}
|
|
61
|
+
parts.push(cur);
|
|
62
|
+
return { parts: parts.map((s) => s.trim()).filter(Boolean), unterminatedQuote: inSingle || inDouble };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Quote-aware whitespace tokenizer. Returns null on an unterminated quote. */
|
|
66
|
+
function tokenize(str) {
|
|
67
|
+
const tokens = [];
|
|
68
|
+
let cur = '';
|
|
69
|
+
let started = false;
|
|
70
|
+
let inSingle = false;
|
|
71
|
+
let inDouble = false;
|
|
72
|
+
for (let i = 0; i < str.length; i++) {
|
|
73
|
+
const c = str[i];
|
|
74
|
+
if (inSingle) {
|
|
75
|
+
if (c === "'") inSingle = false; else cur += c;
|
|
76
|
+
started = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (inDouble) {
|
|
80
|
+
if (c === '"') inDouble = false; else cur += c;
|
|
81
|
+
started = true;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (c === "'") { inSingle = true; started = true; continue; }
|
|
85
|
+
if (c === '"') { inDouble = true; started = true; continue; }
|
|
86
|
+
if (/\s/.test(c)) {
|
|
87
|
+
if (started) { tokens.push(cur); cur = ''; started = false; }
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
cur += c;
|
|
91
|
+
started = true;
|
|
92
|
+
}
|
|
93
|
+
if (started) tokens.push(cur);
|
|
94
|
+
if (inSingle || inDouble) return null;
|
|
95
|
+
return tokens;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isFlagWithChar(rest, longForms, shortChar) {
|
|
99
|
+
for (const t of rest) {
|
|
100
|
+
if (longForms.includes(t)) return true;
|
|
101
|
+
if (shortChar && t.startsWith('-') && !t.startsWith('--') && t.includes(shortChar)) return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Skip git's global options (`-C <dir>`, `-c <k>=<v>`, `--git-dir=...`, ...) to find the subcommand. */
|
|
107
|
+
function subcommandOf(args) {
|
|
108
|
+
const OPTS_WITH_SEPARATE_ARG = new Set(['-C', '-c', '--namespace', '--super-prefix']);
|
|
109
|
+
let i = 0;
|
|
110
|
+
while (i < args.length) {
|
|
111
|
+
const t = args[i];
|
|
112
|
+
if (OPTS_WITH_SEPARATE_ARG.has(t)) { i += 2; continue; }
|
|
113
|
+
if (t.startsWith('-')) { i += 1; continue; }
|
|
114
|
+
return { subcommand: t, rest: args.slice(i + 1) };
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Extract an explicit `git -C <dir>` argument (or `git -C=<dir>`), if present, from the tokens AFTER `git`. */
|
|
120
|
+
function gitDashCDirOf(args) {
|
|
121
|
+
for (let i = 0; i < args.length; i++) {
|
|
122
|
+
const t = args[i];
|
|
123
|
+
if (t === '-C' && args[i + 1] != null) return args[i + 1];
|
|
124
|
+
if (t.startsWith('-C') && t.length > 2) return t.slice(2);
|
|
125
|
+
if (!t.startsWith('-')) break; // reached the subcommand — no more global opts
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Text-level check for whether an UNPARSABLE segment mentions a verb this
|
|
132
|
+
* guard actually polices. Used only to narrow the unparsable-command fallback
|
|
133
|
+
* — an unparsable command that mentions none of these is allowed through
|
|
134
|
+
* rather than blanket-blocked, since blocking every command merely containing
|
|
135
|
+
* the substring "git" (e.g. a heredoc `git commit -m "$(cat <<'EOF' ...)"`)
|
|
136
|
+
* was itself a false-positive incident.
|
|
137
|
+
*/
|
|
138
|
+
function mentionsPolicedDestructiveToken(text) {
|
|
139
|
+
if (/\b(stash|reset|checkout|restore|clean)\b/.test(text)) return true;
|
|
140
|
+
if (/\badd\s+(-A\b|--all\b|\.(?:\s|$))/.test(text)) return true;
|
|
141
|
+
if (/\bcommit\b.*(-a\b|--all\b)/.test(text)) return true;
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Evaluate one `git <...>` invocation (args = tokens AFTER `git`). Returns
|
|
147
|
+
* `{ verb, alt }` when destructive, or null when this invocation is fine
|
|
148
|
+
* (read-only, or a form this guard doesn't restrict).
|
|
149
|
+
*/
|
|
150
|
+
function evaluateGit(args) {
|
|
151
|
+
const parsed = subcommandOf(args);
|
|
152
|
+
if (!parsed) return null;
|
|
153
|
+
const { subcommand, rest } = parsed;
|
|
154
|
+
|
|
155
|
+
switch (subcommand) {
|
|
156
|
+
case 'stash': {
|
|
157
|
+
const sub = rest.find((a) => !a.startsWith('-'));
|
|
158
|
+
if (sub === 'list' || sub === 'show') return null;
|
|
159
|
+
return {
|
|
160
|
+
verb: 'git stash',
|
|
161
|
+
alt: 'do not stash on a shared tree — a stash you never restore silently strands someone else\'s work; commit or discard only paths you created, or stop and report',
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
case 'reset': {
|
|
165
|
+
const hasHard = rest.includes('--hard');
|
|
166
|
+
const positional = rest.filter((a) => !a.startsWith('-'));
|
|
167
|
+
if (hasHard) {
|
|
168
|
+
return {
|
|
169
|
+
verb: 'git reset --hard',
|
|
170
|
+
alt: 'never hard-reset a shared tree — this discards a sibling job\'s or the human\'s uncommitted work outright; undo only your own change with `git checkout -- <your-path>` inside your own sm-job/ or sm-epic/ worktree, or stop and report',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (positional.length > 0) {
|
|
174
|
+
return {
|
|
175
|
+
verb: 'git reset <path>',
|
|
176
|
+
alt: 'do not `git reset` a path on a shared tree — that can unstage a sibling job\'s or the human\'s in-progress staging, not just your own',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
case 'checkout': {
|
|
182
|
+
const dashIdx = rest.indexOf('--');
|
|
183
|
+
if (dashIdx !== -1 && rest.slice(dashIdx + 1).length > 0) {
|
|
184
|
+
return {
|
|
185
|
+
verb: 'git checkout -- <path>',
|
|
186
|
+
alt: 'never discard tracked-file edits on a shared tree with `checkout --` — that can erase a sibling job\'s or the human\'s uncommitted work; scope changes to files you created, or stop and report',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// `.` is never a valid branch/commit name, so `git checkout .` (no
|
|
190
|
+
// `--` at all) is unambiguously the pathspec form — and, like
|
|
191
|
+
// `checkout -- .`, discards every dirty tracked file in the tree. Git
|
|
192
|
+
// accepts this bare form; requiring `--` before flagging it let this
|
|
193
|
+
// exact incident shape (real precedent: `starry-night-ships`, see
|
|
194
|
+
// guard-destructive-git.cjs's header) walk straight past the guard.
|
|
195
|
+
const positional = rest.filter((a) => a !== '--' && !a.startsWith('-'));
|
|
196
|
+
if (positional.includes('.')) {
|
|
197
|
+
return {
|
|
198
|
+
verb: 'git checkout .',
|
|
199
|
+
alt: 'never discard tracked-file edits on a shared tree with `checkout .` — that can erase a sibling job\'s or the human\'s uncommitted work; scope changes to files you created, or stop and report',
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
case 'restore': {
|
|
205
|
+
if (rest.includes('-n') || rest.includes('--dry-run')) return null;
|
|
206
|
+
const positional = rest.filter((a) => !a.startsWith('-'));
|
|
207
|
+
if (positional.length > 0) {
|
|
208
|
+
return {
|
|
209
|
+
verb: 'git restore',
|
|
210
|
+
alt: 'never `git restore` tracked paths on a shared tree — that discards whatever is currently dirty there, which may not be yours',
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
case 'clean': {
|
|
216
|
+
if (isFlagWithChar(rest, ['--dry-run'], 'n')) return null;
|
|
217
|
+
const hasF = isFlagWithChar(rest, ['--force'], 'f');
|
|
218
|
+
const hasD = isFlagWithChar(rest, [], 'd');
|
|
219
|
+
const hasX = isFlagWithChar(rest, [], 'x');
|
|
220
|
+
if (hasF || hasD || hasX) {
|
|
221
|
+
return {
|
|
222
|
+
verb: 'git clean -f/-d/-x',
|
|
223
|
+
alt: 'never force-clean a shared tree — that deletes files git doesn\'t track, which may be a sibling job\'s scratch output or the human\'s untracked WIP',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
case 'add': {
|
|
229
|
+
const hasBlanket = rest.some((a) => a === '-A' || a === '--all' || a === '.');
|
|
230
|
+
if (hasBlanket) {
|
|
231
|
+
return {
|
|
232
|
+
verb: 'git add -A/./--all',
|
|
233
|
+
alt: 'stage only the exact paths you created or modified — `git add <path> [<path>...]` — a blanket add on a shared tree captures a sibling job\'s or the human\'s in-progress edits and mis-attributes them to your commit',
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
case 'commit': {
|
|
239
|
+
const hasAll = isFlagWithChar(rest, ['--all'], 'a');
|
|
240
|
+
if (hasAll) {
|
|
241
|
+
return {
|
|
242
|
+
verb: 'git commit -a',
|
|
243
|
+
alt: 'commit only the paths you explicitly `git add`ed — `git commit -a`/`--all` on a shared tree sweeps up whatever a sibling job or the human currently has dirty',
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
default:
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const SH_WRAPPERS = new Set(['sh', 'bash', 'zsh', 'dash']);
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Returns the first destructive verdict found anywhere in `command`, or null.
|
|
257
|
+
* `baseCwd` is the shell's starting cwd; a `cd <dir>` segment updates the
|
|
258
|
+
* EFFECTIVE cwd for every subsequent segment in the same top-level command
|
|
259
|
+
* (segments share one shell), and an explicit `git -C <dir>` overrides it for
|
|
260
|
+
* that one invocation only. The returned verdict carries the effective cwd at
|
|
261
|
+
* the point the destructive call was found, so the caller checks exemption
|
|
262
|
+
* against where the command actually runs, not just `payload.cwd`.
|
|
263
|
+
*/
|
|
264
|
+
function findDestructiveVerdict(command, baseCwd, depth = 0) {
|
|
265
|
+
if (depth > MAX_SH_C_DEPTH) return null;
|
|
266
|
+
const { parts, unterminatedQuote } = splitTopLevel(command);
|
|
267
|
+
if (unterminatedQuote) {
|
|
268
|
+
if (mentionsPolicedDestructiveToken(command)) {
|
|
269
|
+
return { verb: 'an unparsable command mentioning git', alt: 'this hook could not confidently parse this command\'s quoting to confirm it is safe — split it into a plain, simply-quoted git invocation, or stop and report', cwd: baseCwd };
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let effectiveCwd = baseCwd;
|
|
275
|
+
|
|
276
|
+
for (const segment of parts) {
|
|
277
|
+
const tokens = tokenize(segment);
|
|
278
|
+
if (tokens === null) {
|
|
279
|
+
if (mentionsPolicedDestructiveToken(segment)) {
|
|
280
|
+
return { verb: 'an unparsable command mentioning git', alt: 'this hook could not confidently parse this command\'s quoting to confirm it is safe — split it into a plain, simply-quoted git invocation, or stop and report', cwd: effectiveCwd };
|
|
281
|
+
}
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (tokens.length === 0) continue;
|
|
285
|
+
|
|
286
|
+
// Strip leading env assignments (FOO=bar git ...).
|
|
287
|
+
let i = 0;
|
|
288
|
+
while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i])) i++;
|
|
289
|
+
const cmdTokens = tokens.slice(i);
|
|
290
|
+
if (cmdTokens.length === 0) continue;
|
|
291
|
+
|
|
292
|
+
const head = path.basename(cmdTokens[0]);
|
|
293
|
+
|
|
294
|
+
if (head === 'cd' && cmdTokens[1] != null) {
|
|
295
|
+
const target = cmdTokens[1];
|
|
296
|
+
effectiveCwd = path.isAbsolute(target) ? target : path.resolve(effectiveCwd || process.cwd(), target);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (SH_WRAPPERS.has(head)) {
|
|
301
|
+
const cIdx = cmdTokens.indexOf('-c');
|
|
302
|
+
if (cIdx !== -1 && cmdTokens[cIdx + 1] != null) {
|
|
303
|
+
const nested = findDestructiveVerdict(cmdTokens[cIdx + 1], effectiveCwd, depth + 1);
|
|
304
|
+
if (nested) return nested;
|
|
305
|
+
}
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (head === 'git') {
|
|
310
|
+
const gitArgs = cmdTokens.slice(1);
|
|
311
|
+
const verdict = evaluateGit(gitArgs);
|
|
312
|
+
if (verdict) {
|
|
313
|
+
const dashC = gitDashCDirOf(gitArgs);
|
|
314
|
+
const invocationCwd = dashC
|
|
315
|
+
? (path.isAbsolute(dashC) ? dashC : path.resolve(effectiveCwd || process.cwd(), dashC))
|
|
316
|
+
: effectiveCwd;
|
|
317
|
+
return { ...verdict, cwd: invocationCwd };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function isInsideManagedWorktreeByPath(cwd) {
|
|
325
|
+
if (!cwd) return false;
|
|
326
|
+
const resolved = path.resolve(cwd);
|
|
327
|
+
return WORKTREE_ROOTS.some((root) => resolved === root || resolved.startsWith(root + path.sep));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function isInsideManagedWorktreeByBranch(cwd) {
|
|
331
|
+
if (!cwd) return false;
|
|
332
|
+
try {
|
|
333
|
+
const branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
334
|
+
cwd,
|
|
335
|
+
timeout: 3_000,
|
|
336
|
+
encoding: 'utf8',
|
|
337
|
+
windowsHide: true,
|
|
338
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
339
|
+
}).trim();
|
|
340
|
+
return BRANCH_PREFIXES.some((p) => branch.startsWith(p));
|
|
341
|
+
} catch {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function isExemptSharedTree(cwd) {
|
|
347
|
+
return isInsideManagedWorktreeByPath(cwd) || isInsideManagedWorktreeByBranch(cwd);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function buildAllow() {
|
|
351
|
+
return { continue: true };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function buildDeny(reason) {
|
|
355
|
+
return {
|
|
356
|
+
continue: true,
|
|
357
|
+
decision: 'block',
|
|
358
|
+
reason,
|
|
359
|
+
hookSpecificOutput: {
|
|
360
|
+
hookEventName: 'PreToolUse',
|
|
361
|
+
permissionDecision: 'deny',
|
|
362
|
+
permissionDecisionReason: reason,
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Decision function: PreToolUse payload in, hook-response object out. No
|
|
369
|
+
* stdin/stdout and no `process.exit` — safe to call in-process (e.g. from
|
|
370
|
+
* tests) — but for a destructive verdict outside a path-recognized managed
|
|
371
|
+
* worktree it does shell out to `git rev-parse` (see
|
|
372
|
+
* `isInsideManagedWorktreeByBranch`); it is not I/O-free in that case.
|
|
373
|
+
*/
|
|
374
|
+
function decide(payload) {
|
|
375
|
+
try {
|
|
376
|
+
if (payload.tool_name !== 'Bash') return buildAllow();
|
|
377
|
+
|
|
378
|
+
const command = payload?.tool_input?.command;
|
|
379
|
+
if (!command || typeof command !== 'string') return buildAllow();
|
|
380
|
+
|
|
381
|
+
const cwd = typeof payload.cwd === 'string' && payload.cwd ? payload.cwd : process.cwd();
|
|
382
|
+
const verdict = findDestructiveVerdict(command, cwd);
|
|
383
|
+
if (!verdict) return buildAllow();
|
|
384
|
+
|
|
385
|
+
const effectiveCwd = verdict.cwd || cwd;
|
|
386
|
+
if (isExemptSharedTree(effectiveCwd)) return buildAllow();
|
|
387
|
+
|
|
388
|
+
const reason = [
|
|
389
|
+
`Blocked: \`${verdict.verb}\` in what this hook believes is a SHARED working tree (${effectiveCwd}).`,
|
|
390
|
+
verdict.alt,
|
|
391
|
+
'You are normally inside your own sm-job/<slug> or sm-epic/<epicId> worktree for this kind of operation — run `git rev-parse --git-common-dir` to check; this guard permits the same command there.',
|
|
392
|
+
].join(' ');
|
|
393
|
+
return buildDeny(reason);
|
|
394
|
+
} catch (e) {
|
|
395
|
+
console.error(`[guard-destructive-git] internal error, failing open: ${e?.message}`);
|
|
396
|
+
return buildAllow();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Mirrors the CLI's stdin-JSON handling so it can be exercised without a spawn. */
|
|
401
|
+
function parsePayload(raw) {
|
|
402
|
+
try {
|
|
403
|
+
return raw ? JSON.parse(raw) : {};
|
|
404
|
+
} catch (e) {
|
|
405
|
+
console.error(`[guard-destructive-git] malformed stdin JSON, failing open: ${e?.message}`);
|
|
406
|
+
return {};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
module.exports = { decide, parsePayload };
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* guard-inline-implementation-policy.cjs — decision logic for
|
|
3
|
+
* scripts/hooks/guard-inline-implementation.cjs, split out so it can be
|
|
4
|
+
* `require()`d in-process (by tests, notably) with no CLI-level side effects.
|
|
5
|
+
*
|
|
6
|
+
* guard-inline-implementation.cjs itself runs its CLI as a require-time side
|
|
7
|
+
* effect (stdin -> decision -> process.exit, not gated on
|
|
8
|
+
* `require.main === module` — see its header for why). Requiring THIS module
|
|
9
|
+
* never reads stdin or calls `process.exit`, so it's safe to load from a test
|
|
10
|
+
* process — but `decide()` is not I/O-free: it reads
|
|
11
|
+
* `<cwd>/session-manager-operations/prompt-sessions/active-index.json` off
|
|
12
|
+
* disk and, when the Epic runs in a linked worktree, shells out to a real
|
|
13
|
+
* `git rev-parse` to resolve the ops root (see `resolveOpsRootCwd` below).
|
|
14
|
+
* Calling it directly from a test still removes the outer CLI-process spawn
|
|
15
|
+
* per case, exercising real filesystem/git behavior instead of mocking it.
|
|
16
|
+
*/
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
const { execFileSync } = require('node:child_process');
|
|
22
|
+
// Pure fs helper only — deliberately NOT gitWorktree (keeps this guard light).
|
|
23
|
+
const { nearestGitEntry } = require('../../../src/main/lib/cwdClassify.cjs');
|
|
24
|
+
|
|
25
|
+
const SOURCE_DIRS = ['src', 'scripts', 'plugins', 'bin'];
|
|
26
|
+
const DENY_TAGS = new Set(['feature', 'bug']);
|
|
27
|
+
|
|
28
|
+
function targetPathFor(toolName, toolInput) {
|
|
29
|
+
if (!toolInput || typeof toolInput !== 'object') return null;
|
|
30
|
+
if (toolName === 'NotebookEdit') return toolInput.notebook_path ?? null;
|
|
31
|
+
return toolInput.file_path ?? null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isApplicationSource(cwd, absPath) {
|
|
35
|
+
const rel = path.relative(cwd, absPath);
|
|
36
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) return false; // outside cwd
|
|
37
|
+
const topLevel = rel.split(path.sep)[0];
|
|
38
|
+
return SOURCE_DIRS.includes(topLevel);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The PreToolUse payload's `cwd` is the CLI process's actual spawn cwd. For
|
|
43
|
+
* an Epic running in its default isolated `git worktree` (gitWorktree.cjs,
|
|
44
|
+
* epicSpawnCwd.cjs — "both the Terminal PTY and headless Chat spawn cwd
|
|
45
|
+
* resolve to it"), that is a throwaway checkout under os.tmpdir(), which
|
|
46
|
+
* never contains `session-manager-operations/` — the ops root always lives
|
|
47
|
+
* in the real project tree (see gitWorktree.cjs's "ops-root hazard" header:
|
|
48
|
+
* "spawn cwd = worktree dir, ops-root cwd = real project cwd, always").
|
|
49
|
+
* Resolve back to the real project cwd via git's shared `.git` dir before
|
|
50
|
+
* giving up, so this guard isn't silently inert for every Epic running in
|
|
51
|
+
* its default worktree.
|
|
52
|
+
*/
|
|
53
|
+
function resolveOpsRootCwd(rawCwd) {
|
|
54
|
+
if (fs.existsSync(path.join(rawCwd, 'session-manager-operations', 'prompt-sessions', 'active-index.json'))) {
|
|
55
|
+
return rawCwd;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
// Only a `.git` FILE (linked worktree) needs git to find the shared dir; a
|
|
59
|
+
// `.git` directory IS the project root, so skip the spawn.
|
|
60
|
+
const entry = nearestGitEntry(rawCwd);
|
|
61
|
+
if (!entry) return rawCwd;
|
|
62
|
+
if (!entry.isFile) return entry.dir;
|
|
63
|
+
const commonDir = execFileSync(
|
|
64
|
+
'git',
|
|
65
|
+
['rev-parse', '--path-format=absolute', '--git-common-dir'],
|
|
66
|
+
{ cwd: rawCwd, encoding: 'utf8', timeout: 5000 },
|
|
67
|
+
).trim();
|
|
68
|
+
if (!commonDir) return rawCwd;
|
|
69
|
+
return path.basename(commonDir) === '.git' ? path.dirname(commonDir) : commonDir;
|
|
70
|
+
} catch {
|
|
71
|
+
return rawCwd;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveEpicTag(cwd, sessionId) {
|
|
76
|
+
if (!sessionId) return null;
|
|
77
|
+
const indexPath = path.join(cwd, 'session-manager-operations', 'prompt-sessions', 'active-index.json');
|
|
78
|
+
if (!fs.existsSync(indexPath)) return null;
|
|
79
|
+
const raw = fs.readFileSync(indexPath, 'utf8');
|
|
80
|
+
const index = JSON.parse(raw);
|
|
81
|
+
const sessions = index && typeof index === 'object' ? index.sessions : null;
|
|
82
|
+
if (!sessions || typeof sessions !== 'object') return null;
|
|
83
|
+
for (const record of Object.values(sessions)) {
|
|
84
|
+
if (record && record.claudeSessionId === sessionId) return record;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildAllow() {
|
|
90
|
+
return { continue: true };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buildDeny(reason) {
|
|
94
|
+
return {
|
|
95
|
+
continue: true,
|
|
96
|
+
decision: 'block',
|
|
97
|
+
reason,
|
|
98
|
+
hookSpecificOutput: {
|
|
99
|
+
hookEventName: 'PreToolUse',
|
|
100
|
+
permissionDecision: 'deny',
|
|
101
|
+
permissionDecisionReason: reason,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Decision function: PreToolUse payload in, hook-response object out. No
|
|
108
|
+
* stdin/stdout and no `process.exit` — safe to call in-process (e.g. from
|
|
109
|
+
* tests) — but does real filesystem reads and, in the linked-worktree case, a
|
|
110
|
+
* real `git rev-parse` spawn (see `resolveOpsRootCwd`).
|
|
111
|
+
*/
|
|
112
|
+
function decide(payload) {
|
|
113
|
+
try {
|
|
114
|
+
if (process.env.SM_ALLOW_INLINE_IMPLEMENTATION === '1') return buildAllow();
|
|
115
|
+
|
|
116
|
+
const toolName = payload.tool_name;
|
|
117
|
+
if (toolName !== 'Write' && toolName !== 'Edit' && toolName !== 'NotebookEdit') {
|
|
118
|
+
return buildAllow();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const rawPath = targetPathFor(toolName, payload.tool_input);
|
|
122
|
+
if (!rawPath || typeof rawPath !== 'string') return buildAllow();
|
|
123
|
+
|
|
124
|
+
const cwd = typeof payload.cwd === 'string' && payload.cwd ? payload.cwd : process.cwd();
|
|
125
|
+
const absPath = path.isAbsolute(rawPath) ? rawPath : path.resolve(cwd, rawPath);
|
|
126
|
+
|
|
127
|
+
if (!isApplicationSource(cwd, absPath)) return buildAllow();
|
|
128
|
+
|
|
129
|
+
const sessionId = typeof payload.session_id === 'string' ? payload.session_id : null;
|
|
130
|
+
const opsRootCwd = resolveOpsRootCwd(cwd);
|
|
131
|
+
const epic = resolveEpicTag(opsRootCwd, sessionId);
|
|
132
|
+
if (!epic) return buildAllow(); // unresolvable session — allow by construction
|
|
133
|
+
|
|
134
|
+
if (epic.allowInlineImplementation) return buildAllow();
|
|
135
|
+
if (!DENY_TAGS.has(epic.tag)) return buildAllow();
|
|
136
|
+
|
|
137
|
+
const reason = [
|
|
138
|
+
`Direct ${toolName} to application source ("${path.relative(cwd, absPath)}") is discouraged inside a "${epic.tag}" Epic.`,
|
|
139
|
+
'Feature and bug work should be decomposed and queued via /develop rather than implemented inline, so it runs through the scheduler with acceptance criteria and review.',
|
|
140
|
+
'If this inline edit is deliberate, set SM_ALLOW_INLINE_IMPLEMENTATION=1 in the environment, or set allowInlineImplementation: true on this Epic\'s record in active-index.json, and retry.',
|
|
141
|
+
].join(' ');
|
|
142
|
+
return buildDeny(reason);
|
|
143
|
+
} catch (e) {
|
|
144
|
+
console.error(`[guard-inline-implementation] internal error, failing open: ${e?.message}`);
|
|
145
|
+
return buildAllow();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Mirrors the CLI's stdin-JSON handling so it can be exercised without a spawn. */
|
|
150
|
+
function parsePayload(raw) {
|
|
151
|
+
try {
|
|
152
|
+
return raw ? JSON.parse(raw) : {};
|
|
153
|
+
} catch (e) {
|
|
154
|
+
console.error(`[guard-inline-implementation] malformed stdin JSON, failing open: ${e?.message}`);
|
|
155
|
+
return {};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = { decide, parsePayload };
|