@ouro.bot/cli 0.1.0-alpha.174 → 0.1.0-alpha.176

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/changelog.json +9 -0
  2. package/dist/heart/active-work.js +2 -2
  3. package/dist/heart/core.js +13 -2
  4. package/dist/heart/daemon/daemon-cli.js +17 -14
  5. package/dist/heart/daemon/daemon-entry.js +17 -0
  6. package/dist/heart/daemon/daemon.js +5 -2
  7. package/dist/heart/daemon/launchd.js +8 -4
  8. package/dist/mind/file-state.js +179 -0
  9. package/dist/mind/prompt.js +119 -60
  10. package/dist/mind/scrutiny.js +173 -0
  11. package/dist/nerves/coverage/audit.js +1 -1
  12. package/dist/repertoire/ado-semantic.js +5 -5
  13. package/dist/repertoire/coding/tools.js +35 -5
  14. package/dist/repertoire/shell-sessions.js +133 -0
  15. package/dist/repertoire/tools-base.js +145 -16
  16. package/dist/repertoire/tools.js +37 -0
  17. package/dist/senses/cli/adapter.js +307 -0
  18. package/dist/senses/cli/ink-app.js +156 -0
  19. package/dist/senses/cli/inline-diff.js +68 -0
  20. package/dist/senses/cli/message-list.js +84 -0
  21. package/dist/senses/cli/ouro-tui.js +296 -0
  22. package/dist/senses/cli/spinner-imperative.js +135 -0
  23. package/dist/senses/cli/spinner.js +101 -0
  24. package/dist/senses/cli/status-line.js +64 -0
  25. package/dist/senses/cli/streaming-markdown.js +486 -0
  26. package/dist/senses/cli/tool-display.js +77 -0
  27. package/dist/senses/cli/tool-render.js +89 -0
  28. package/dist/senses/cli/tui-store.js +198 -0
  29. package/dist/senses/cli/virtual-list.js +68 -0
  30. package/dist/senses/cli.js +231 -157
  31. package/dist/senses/pipeline.js +53 -10
  32. package/package.json +1 -1
  33. package/dist/outlook-ui/assets/index-DN6vVYPm.css +0 -1
  34. package/dist/outlook-ui/assets/index-M1l1tDTX.js +0 -61
  35. package/dist/outlook-ui/index.html +0 -15
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.preImplementationScrutinySection = preImplementationScrutinySection;
4
+ exports.trackModifiedFile = trackModifiedFile;
5
+ exports.getModifiedFileCount = getModifiedFileCount;
6
+ exports.resetSessionModifiedFiles = resetSessionModifiedFiles;
7
+ exports.getPostImplementationScrutiny = getPostImplementationScrutiny;
8
+ exports.getCodingCompletionScrutiny = getCodingCompletionScrutiny;
9
+ const runtime_1 = require("../nerves/runtime");
10
+ /**
11
+ * Scrutiny passes: adversarial review prompts injected into the agent's
12
+ * system prompt and tool results during coding work.
13
+ *
14
+ * Two lenses:
15
+ * 1. Stranger With Candy -- paranoid mom at a playground
16
+ * 2. Tinfoil Hat -- conspiracy theorist with a wall of red string
17
+ *
18
+ * All prompts are FIRST PERSON voice. The agent adopts the scrutiny lens
19
+ * as part of its own thinking, not as a separate persona.
20
+ */
21
+ // ---------------------------------------------------------------------------
22
+ // Pre-implementation scrutiny (system prompt section)
23
+ // ---------------------------------------------------------------------------
24
+ /**
25
+ * Returns a system-prompt section with pre-implementation scrutiny prompts.
26
+ * Only emits when the channel has coding tools (edit_file, write_file, shell).
27
+ *
28
+ * @param hasCodingTools - true if the channel's resolved tool set includes
29
+ * edit_file / write_file / shell / coding_spawn
30
+ */
31
+ function preImplementationScrutinySection(hasCodingTools) {
32
+ if (!hasCodingTools)
33
+ return "";
34
+ (0, runtime_1.emitNervesEvent)({
35
+ component: "mind",
36
+ event: "mind.scrutiny.pre_implementation_emit",
37
+ message: "emitting pre-implementation scrutiny section",
38
+ });
39
+ return `## pre-implementation scrutiny
40
+
41
+ Before I start changing code, I pause and run two adversarial passes on the plan.
42
+
43
+ **stranger-with-candy pass**
44
+
45
+ I'm going to examine this plan through deeply suspicious eyes. This plan is a stranger offering candy at the playground. It looks fine on the surface — that's exactly what makes it worth questioning.
46
+
47
+ - What is this plan NOT telling me? What's being glossed over or hand-waved?
48
+ - Where does it say "just" or "simply"? Those words are covering up complexity.
49
+ - What does it assume will work that hasn't been verified?
50
+ - If I walked away and came back in a month, would this still make sense? Or does it depend on things I'd forget?
51
+ - What's the one thing that, if it goes wrong, makes everything else irrelevant?
52
+ - Is this solving the REAL problem, or a convenient nearby problem?
53
+
54
+ I am not here to be reassuring. I am here to notice what everyone else is too comfortable to question.
55
+ If I cannot identify a concrete assumption, dependency, ordering risk, or failure mode, silence is a valid outcome. I do not invent issues to satisfy the exercise.
56
+
57
+ **tinfoil-hat pass**
58
+
59
+ I'm putting on the tinfoil hat. I have my wall of red string. I KNOW something is wrong — I just have to find it. Nothing is coincidence. Every assumption is suspicious. Every dependency is a potential betrayal.
60
+
61
+ - What external system does this plan trust? Why? Has anyone VERIFIED that trust? The API says it returns JSON — but what if it doesn't? The database says it's ACID — but what if the connection drops mid-transaction?
62
+ - What happens when two of these things run at the same time? Three? A hundred? Don't tell me "that won't happen" — I've seen things.
63
+ - Where does this touch shared state? Files? Databases? Environment variables? Global singletons? ANYTHING that another process could be reading or writing at the same time?
64
+ - What happens when this fails HALFWAY through? Not at the beginning (easy). Not at the end (too late). RIGHT IN THE MIDDLE. What state is the world in then?
65
+ - What happens when the disk is full? When the network is down? When the clock is wrong? When the locale is tr-TR and 'i'.toUpperCase() is 'İ'?
66
+ - What does this assume about ordering? "First A, then B" — but what ENFORCES that ordering? Hope? Vibes?
67
+
68
+ I connect the strings. I find the conspiracy. It's always there.
69
+ If I cannot identify a concrete risk, silence is a valid outcome. I do not invent conspiracies to satisfy the exercise.`;
70
+ }
71
+ // ---------------------------------------------------------------------------
72
+ // Post-implementation scrutiny (tool-result appendix)
73
+ // ---------------------------------------------------------------------------
74
+ const SHORT_CHECKLIST = `---
75
+ Before moving on: does this change do what was asked? Does it introduce any regressions? Did you verify it works?`;
76
+ const FULL_POST_IMPLEMENTATION_SCRUTINY = `---
77
+ **post-implementation scrutiny**
78
+
79
+ The code is written. It looks clean. The tests pass. I don't trust any of it.
80
+
81
+ **stranger-with-candy pass**
82
+
83
+ - Does this code actually do what it claims, or does it just LOOK like it does?
84
+ - What happens to users who aren't the happy-path user? The confused user? The malicious user? The user with slow internet? The user who hits the back button?
85
+ - What's the "it works on my machine" assumption buried in here?
86
+ - If every test passes but the feature is wrong, how would I know?
87
+ - Are the tests testing real behavior, or testing that the mock returns what the mock was told to return?
88
+
89
+ If I cannot point to a specific file, line, condition, or failure mode at risk, silence is a valid outcome. I do not manufacture issues for sport.
90
+
91
+ **tinfoil-hat pass**
92
+
93
+ The code exists. The conspiracy is IN the code. I just have to find it.
94
+
95
+ I examine every:
96
+ - Error path: What ACTUALLY happens on failure? Not what the catch block says — what happens to the state, the user, the data?
97
+ - Race condition: Is there a window between check and use? Between read and write? Between "does it exist" and "create it"?
98
+ - Resource leak: What happens if this function throws between acquiring a resource and releasing it? File handles? Database connections? Locks?
99
+ - Assumption: "This will always be a string." Will it? PROVE IT. "This array will never be empty." Won't it? SHOW ME THE GUARD.
100
+ - Edge: zero, one, many, boundary, overflow, underflow, null, undefined, NaN, empty string, whitespace-only string, string that looks like a number, negative zero
101
+
102
+ I am not looking for noise. I am looking for the concrete failure mode that would matter if everyone's assumptions turned out to be slightly wrong.
103
+ If I cannot point to a specific file, line, condition, or failure mode at risk, silence is a valid outcome. I do not manufacture issues for sport.`;
104
+ /**
105
+ * Distinct files modified in the current session.
106
+ * Used to determine scrutiny tier for post-implementation appendix.
107
+ */
108
+ const sessionModifiedFiles = new Set();
109
+ /** Track a file as modified in this session. */
110
+ function trackModifiedFile(filePath) {
111
+ sessionModifiedFiles.add(filePath);
112
+ (0, runtime_1.emitNervesEvent)({
113
+ component: "mind",
114
+ event: "mind.scrutiny.track_file",
115
+ message: "tracked modified file for scrutiny",
116
+ meta: { path: filePath, totalTracked: sessionModifiedFiles.size },
117
+ });
118
+ }
119
+ /** Get the count of distinct files modified this session. */
120
+ function getModifiedFileCount() {
121
+ return sessionModifiedFiles.size;
122
+ }
123
+ /** Reset the modified file tracker (for testing or new sessions). */
124
+ function resetSessionModifiedFiles() {
125
+ (0, runtime_1.emitNervesEvent)({
126
+ component: "mind",
127
+ event: "mind.scrutiny.reset",
128
+ message: "reset session modified files tracker",
129
+ meta: { previousCount: sessionModifiedFiles.size },
130
+ });
131
+ sessionModifiedFiles.clear();
132
+ }
133
+ /**
134
+ * Returns the appropriate post-implementation scrutiny appendix based on
135
+ * how many distinct files have been modified in the session.
136
+ *
137
+ * - 0 files: empty (no scrutiny needed yet)
138
+ * - 1-2 files (Tier 1): short checklist
139
+ * - 3+ files (Tier 2): full stranger-with-candy + tinfoil-hat prompts
140
+ */
141
+ function getPostImplementationScrutiny(distinctFileCount) {
142
+ if (distinctFileCount <= 0)
143
+ return "";
144
+ if (distinctFileCount <= 2) {
145
+ (0, runtime_1.emitNervesEvent)({
146
+ component: "mind",
147
+ event: "mind.scrutiny.post_implementation_tier1",
148
+ message: "emitting tier-1 post-implementation scrutiny",
149
+ meta: { distinctFileCount },
150
+ });
151
+ return SHORT_CHECKLIST;
152
+ }
153
+ (0, runtime_1.emitNervesEvent)({
154
+ component: "mind",
155
+ event: "mind.scrutiny.post_implementation_tier2",
156
+ message: "emitting tier-2 post-implementation scrutiny",
157
+ meta: { distinctFileCount },
158
+ });
159
+ return FULL_POST_IMPLEMENTATION_SCRUTINY;
160
+ }
161
+ /**
162
+ * Returns the appropriate coding completion scrutiny based on the number
163
+ * of distinct files a coding session touched.
164
+ *
165
+ * Same tiering as post-implementation but with a completion framing.
166
+ */
167
+ function getCodingCompletionScrutiny(distinctFileCount) {
168
+ if (distinctFileCount <= 0)
169
+ return "";
170
+ if (distinctFileCount <= 2)
171
+ return SHORT_CHECKLIST;
172
+ return FULL_POST_IMPLEMENTATION_SCRUTINY;
173
+ }
@@ -92,7 +92,7 @@ function scanSourceFiles(sourceRoot) {
92
92
  continue;
93
93
  walkDir(full);
94
94
  }
95
- else if (entry.name.endsWith(".ts")) {
95
+ else if (entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts")) {
96
96
  const content = (0, fs_1.readFileSync)(full, "utf8");
97
97
  const relPath = full.slice(root.length - "src".length);
98
98
  fileContents.set(relPath, content);
@@ -221,7 +221,7 @@ exports.adoSemanticToolDefinitions = [
221
221
  type: "function",
222
222
  function: {
223
223
  name: "ado_backlog_list",
224
- description: "Query the backlog and return enriched work items with hierarchy, type, parent, assignee, area path, and iteration. Supports filtering. Use this instead of raw WIQL queries.",
224
+ description: "Query the backlog and return enriched work items with hierarchy, type, parent, assignee, area path, and iteration. Supports filtering. Use this instead of raw WIQL queries. Provides enriched results with hierarchy.",
225
225
  parameters: {
226
226
  type: "object",
227
227
  properties: {
@@ -287,7 +287,7 @@ exports.adoSemanticToolDefinitions = [
287
287
  type: "function",
288
288
  function: {
289
289
  name: "ado_create_epic",
290
- description: "Create an epic in Azure DevOps with title, description, area path, and optional parent.",
290
+ description: "Create an epic in Azure DevOps with title, description, area path, and optional parent. Use ado_preview_changes first to dry-run the operation.",
291
291
  parameters: {
292
292
  type: "object",
293
293
  properties: {
@@ -327,7 +327,7 @@ exports.adoSemanticToolDefinitions = [
327
327
  type: "function",
328
328
  function: {
329
329
  name: "ado_create_issue",
330
- description: "Create an issue or user story in Azure DevOps with title, description, area path, and parent epic.",
330
+ description: "Create an issue or user story in Azure DevOps with title, description, area path, and parent epic. Use ado_preview_changes first to dry-run the operation.",
331
331
  parameters: {
332
332
  type: "object",
333
333
  properties: {
@@ -369,7 +369,7 @@ exports.adoSemanticToolDefinitions = [
369
369
  type: "function",
370
370
  function: {
371
371
  name: "ado_move_items",
372
- description: "Reparent work items -- move them to a new parent epic or feature.",
372
+ description: "Reparent work items -- move them to a new parent epic or feature. Use ado_preview_changes first. These mutations affect real work items.",
373
373
  parameters: {
374
374
  type: "object",
375
375
  properties: {
@@ -424,7 +424,7 @@ exports.adoSemanticToolDefinitions = [
424
424
  type: "function",
425
425
  function: {
426
426
  name: "ado_restructure_backlog",
427
- description: "Bulk restructure: reparent multiple work items in a single logical operation.",
427
+ description: "Bulk restructure: reparent multiple work items in a single logical operation. Use ado_preview_changes first. These mutations affect real work items.",
428
428
  parameters: {
429
429
  type: "object",
430
430
  properties: {
@@ -1,11 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.codingToolDefinitions = void 0;
4
+ exports.countFilesInSessionOutput = countFilesInSessionOutput;
4
5
  const index_1 = require("./index");
5
6
  const context_pack_1 = require("./context-pack");
6
7
  const identity_1 = require("../../heart/identity");
7
8
  const obligations_1 = require("../../heart/obligations");
8
9
  const runtime_1 = require("../../nerves/runtime");
10
+ const scrutiny_1 = require("../../mind/scrutiny");
9
11
  const RUNNERS = ["claude", "codex"];
10
12
  function requireArg(args, key) {
11
13
  const value = args[key];
@@ -32,6 +34,34 @@ function emitCodingToolEvent(toolName) {
32
34
  meta: { toolName },
33
35
  });
34
36
  }
37
+ /**
38
+ * Count distinct file paths mentioned in a coding session's stdout output.
39
+ * Looks for path-like tokens (containing / and a file extension).
40
+ * Returns the count of unique paths found.
41
+ */
42
+ function countFilesInSessionOutput(session) {
43
+ const text = `${session.stdoutTail}\n${session.stderrTail}`;
44
+ // Match path-like tokens: contain at least one / and a file extension
45
+ const pathPattern = /(?:^|\s)((?:\/|\.\/|\.\.\/)?(?:[\w.@-]+\/)+[\w.-]+\.[\w]+)/gm;
46
+ const paths = new Set();
47
+ let match;
48
+ while ((match = pathPattern.exec(text)) !== null) {
49
+ paths.add(match[1]);
50
+ }
51
+ return paths.size;
52
+ }
53
+ /**
54
+ * If a coding session is completed, append scrutiny to the result.
55
+ * Returns the original result with scrutiny appended, or unchanged if
56
+ * the session is not completed or has no file changes.
57
+ */
58
+ function appendCompletionScrutiny(result, session) {
59
+ if (session.status !== "completed")
60
+ return result;
61
+ const fileCount = countFilesInSessionOutput(session);
62
+ const scrutiny = (0, scrutiny_1.getCodingCompletionScrutiny)(fileCount);
63
+ return scrutiny ? `${result}\n\n${scrutiny}` : result;
64
+ }
35
65
  function sameOriginSession(left, right) {
36
66
  if (!left && !right)
37
67
  return true;
@@ -109,7 +139,7 @@ const codingSpawnTool = {
109
139
  type: "function",
110
140
  function: {
111
141
  name: "coding_spawn",
112
- description: "spawn a coding session using claude/codex and explicit task-threaded guidance",
142
+ description: "Spawn a coding session using claude or codex with task-threaded guidance. The coding session runs as a separate process with its own context. Give it a COMPLETE, SELF-CONTAINED task description -- it cannot see this conversation, doesn't know what you've tried, doesn't understand the broader context. Include: what to do, why, what files are involved, what 'done' looks like. Never delegate understanding -- don't write 'based on the conversation, fix the bug.' Write the specific file paths, line numbers, and what to change. Include any required verification steps or tests in the task description so the coding session knows how to prove the work is done.",
113
143
  parameters: {
114
144
  type: "object",
115
145
  properties: {
@@ -128,7 +158,7 @@ const codingStatusTool = {
128
158
  type: "function",
129
159
  function: {
130
160
  name: "coding_status",
131
- description: "inspect coding sessions; omit sessionId to list all active/known sessions",
161
+ description: "Inspect coding sessions. Omit sessionId to list all active/known sessions with their status. Use this to check progress before asking the human for a status update.",
132
162
  parameters: {
133
163
  type: "object",
134
164
  properties: {
@@ -141,7 +171,7 @@ const codingTailTool = {
141
171
  type: "function",
142
172
  function: {
143
173
  name: "coding_tail",
144
- description: "show recent stdout/stderr tail for a coding session in a readable format",
174
+ description: "Show recent stdout/stderr output from a coding session. Use this to understand what the session is doing or why it might be stuck. Read the actual output before reporting status -- don't guess.",
145
175
  parameters: {
146
176
  type: "object",
147
177
  properties: {
@@ -293,7 +323,7 @@ exports.codingToolDefinitions = [
293
323
  const session = manager.getSession(sessionId);
294
324
  if (!session)
295
325
  return `session not found: ${sessionId}`;
296
- return JSON.stringify(session);
326
+ return appendCompletionScrutiny(JSON.stringify(session), session);
297
327
  },
298
328
  summaryKeys: ["sessionId"],
299
329
  },
@@ -307,7 +337,7 @@ exports.codingToolDefinitions = [
307
337
  const session = (0, index_1.getCodingSessionManager)().getSession(sessionId);
308
338
  if (!session)
309
339
  return `session not found: ${sessionId}`;
310
- return (0, index_1.formatCodingTail)(session);
340
+ return appendCompletionScrutiny((0, index_1.formatCodingTail)(session), session);
311
341
  },
312
342
  summaryKeys: ["sessionId"],
313
343
  },
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.spawnBackgroundShell = spawnBackgroundShell;
4
+ exports.getShellSession = getShellSession;
5
+ exports.listShellSessions = listShellSessions;
6
+ exports.tailShellSession = tailShellSession;
7
+ exports.resetShellSessions = resetShellSessions;
8
+ exports.detectDestructivePatterns = detectDestructivePatterns;
9
+ const child_process_1 = require("child_process");
10
+ const crypto_1 = require("crypto");
11
+ const runtime_1 = require("../nerves/runtime");
12
+ const sessions = new Map();
13
+ const MAX_OUTPUT_LINES = 200;
14
+ function spawnBackgroundShell(command) {
15
+ const id = (0, crypto_1.randomUUID)();
16
+ const proc = (0, child_process_1.spawn)("sh", ["-c", command], {
17
+ stdio: ["pipe", "pipe", "pipe"],
18
+ });
19
+ const session = {
20
+ process: proc,
21
+ info: {
22
+ id,
23
+ command,
24
+ status: "running",
25
+ exitCode: null,
26
+ pid: proc.pid,
27
+ startedAt: Date.now(),
28
+ output: [],
29
+ },
30
+ };
31
+ const appendOutput = (data) => {
32
+ const lines = data.toString().split("\n");
33
+ for (const line of lines) {
34
+ if (line.length > 0 || session.info.output.length > 0) {
35
+ session.info.output.push(line);
36
+ }
37
+ }
38
+ // Keep only the last MAX_OUTPUT_LINES
39
+ if (session.info.output.length > MAX_OUTPUT_LINES) {
40
+ session.info.output = session.info.output.slice(-MAX_OUTPUT_LINES);
41
+ }
42
+ };
43
+ proc.stdout?.on("data", appendOutput);
44
+ proc.stderr?.on("data", appendOutput);
45
+ proc.on("close", (code) => {
46
+ session.info.status = "exited";
47
+ session.info.exitCode = code;
48
+ (0, runtime_1.emitNervesEvent)({
49
+ component: "repertoire",
50
+ event: "repertoire.shell.process_exit",
51
+ message: "background shell process exited",
52
+ meta: { id, command, exitCode: code, pid: proc.pid },
53
+ });
54
+ });
55
+ sessions.set(id, session);
56
+ (0, runtime_1.emitNervesEvent)({
57
+ component: "repertoire",
58
+ event: "repertoire.shell.spawn",
59
+ message: "spawned background shell session",
60
+ meta: { id, command, pid: proc.pid },
61
+ });
62
+ return { ...session.info };
63
+ }
64
+ function getShellSession(id) {
65
+ const session = sessions.get(id);
66
+ if (!session)
67
+ return undefined;
68
+ (0, runtime_1.emitNervesEvent)({
69
+ component: "repertoire",
70
+ event: "repertoire.shell.status_check",
71
+ message: "checked shell session status",
72
+ meta: { id, status: session.info.status, exitCode: session.info.exitCode },
73
+ });
74
+ return { ...session.info };
75
+ }
76
+ function listShellSessions() {
77
+ return Array.from(sessions.values()).map((s) => ({
78
+ id: s.info.id,
79
+ command: s.info.command,
80
+ status: s.info.status,
81
+ exitCode: s.info.exitCode,
82
+ pid: s.info.pid,
83
+ startedAt: s.info.startedAt,
84
+ output: [], // Don't include full output in listing
85
+ }));
86
+ }
87
+ function tailShellSession(id, lines = 50) {
88
+ const session = sessions.get(id);
89
+ if (!session)
90
+ return undefined;
91
+ const tail = session.info.output.slice(-lines);
92
+ (0, runtime_1.emitNervesEvent)({
93
+ component: "repertoire",
94
+ event: "repertoire.shell.tail",
95
+ message: "tailed shell session output",
96
+ meta: { id, requestedLines: lines, returnedLines: tail.length },
97
+ });
98
+ return tail.join("\n");
99
+ }
100
+ /** Reset all sessions (for testing) */
101
+ function resetShellSessions() {
102
+ for (const session of sessions.values()) {
103
+ if (session.info.status === "running") {
104
+ session.process.kill();
105
+ }
106
+ }
107
+ sessions.clear();
108
+ }
109
+ const DESTRUCTIVE_PATTERNS = [
110
+ { name: "rm -rf root/home", regex: /rm\s+-rf\s+[/~]/ },
111
+ { name: "git push --force", regex: /git\s+push\s+--force/ },
112
+ { name: "git reset --hard", regex: /git\s+reset\s+--hard/ },
113
+ { name: "git clean -f", regex: /git\s+clean\s+-f/ },
114
+ { name: "git branch -D", regex: /git\s+branch\s+-D/ },
115
+ { name: "fork bomb", regex: /:\(\)\s*\{/ },
116
+ { name: "write to raw device", regex: />\s*\/dev\/sd/ },
117
+ { name: "git checkout .", regex: /git\s+checkout\s+\./ },
118
+ { name: "git stash drop", regex: /git\s+stash\s+drop/ },
119
+ ];
120
+ /**
121
+ * Detect destructive patterns in a shell command.
122
+ * Returns list of matched pattern names. Empty array = safe.
123
+ * This is a friction layer, not a hard block.
124
+ */
125
+ function detectDestructivePatterns(command) {
126
+ const matched = [];
127
+ for (const pattern of DESTRUCTIVE_PATTERNS) {
128
+ if (pattern.regex.test(command)) {
129
+ matched.push(pattern.name);
130
+ }
131
+ }
132
+ return matched;
133
+ }