@inceptionstack/pi-hard-no 1.2.1 → 1.3.1

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/dismiss.test.ts CHANGED
@@ -1,15 +1,21 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { findingKey, numberFindings, parseDismissals, filterSuppressed, DismissTracker } from "./dismiss";
2
+ import {
3
+ findingKey,
4
+ numberFindings,
5
+ parseDismissals,
6
+ filterSuppressed,
7
+ DismissTracker,
8
+ } from "./dismiss";
3
9
 
4
10
  describe("findingKey", () => {
5
11
  it("extracts severity + location from finding line", () => {
6
- const line = '- **Medium:** src/gateway/model.ts:97 — chatId extraction uses wrong index';
12
+ const line = "- **Medium:** src/gateway/model.ts:97 — chatId extraction uses wrong index";
7
13
  const key = findingKey(line);
8
14
  expect(key).toContain("medium:src/gateway/model.ts:97");
9
15
  });
10
16
 
11
17
  it("handles numbered finding format (F# prefix)", () => {
12
- const line = '- **F1 Medium:** src/foo.ts:10 — something bad';
18
+ const line = "- **F1 Medium:** src/foo.ts:10 — something bad";
13
19
  const key = findingKey(line);
14
20
  expect(key).toBe("medium:src/foo.ts:10 — something bad");
15
21
  });
@@ -22,7 +28,7 @@ describe("findingKey", () => {
22
28
 
23
29
  describe("numberFindings", () => {
24
30
  it("numbers finding bullets sequentially", () => {
25
- const text = '- **High:** foo.ts:1 — bug\n- **Low:** bar.ts:2 — nit\nSome other text';
31
+ const text = "- **High:** foo.ts:1 — bug\n- **Low:** bar.ts:2 — nit\nSome other text";
26
32
  const { numbered, findings } = numberFindings(text);
27
33
  expect(numbered).toContain("**F1 High:**");
28
34
  expect(numbered).toContain("**F2 Low:**");
@@ -31,7 +37,7 @@ describe("numberFindings", () => {
31
37
  });
32
38
 
33
39
  it("preserves non-finding lines unchanged", () => {
34
- const text = 'Header\n\n- **Medium:** x.ts:5 — issue\n\nFooter';
40
+ const text = "Header\n\n- **Medium:** x.ts:5 — issue\n\nFooter";
35
41
  const { numbered } = numberFindings(text);
36
42
  expect(numbered).toContain("Header");
37
43
  expect(numbered).toContain("Footer");
@@ -41,7 +47,8 @@ describe("numberFindings", () => {
41
47
 
42
48
  describe("parseDismissals", () => {
43
49
  it("parses DISMISS F# with colon separator", () => {
44
- const text = "The chatId extraction is intentional.\nDISMISS F1: intentional design for telegram thread format";
50
+ const text =
51
+ "The chatId extraction is intentional.\nDISMISS F1: intentional design for telegram thread format";
45
52
  const dismissals = parseDismissals(text);
46
53
  expect(dismissals.size).toBe(1);
47
54
  expect(dismissals.get(1)).toBe("intentional design for telegram thread format");
@@ -69,21 +76,21 @@ describe("parseDismissals", () => {
69
76
 
70
77
  describe("filterSuppressed", () => {
71
78
  it("removes suppressed findings", () => {
72
- const text = '- **High:** foo.ts:1 — bug one\n- **Low:** bar.ts:2 — nit two';
73
- const suppressed = new Set([findingKey('- **High:** foo.ts:1 — bug one')]);
79
+ const text = "- **High:** foo.ts:1 — bug one\n- **Low:** bar.ts:2 — nit two";
80
+ const suppressed = new Set([findingKey("- **High:** foo.ts:1 — bug one")]);
74
81
  const result = filterSuppressed(text, suppressed);
75
82
  expect(result).not.toContain("bug one");
76
83
  expect(result).toContain("nit two");
77
84
  });
78
85
 
79
86
  it("returns null when all findings suppressed", () => {
80
- const text = '- **High:** foo.ts:1 — bug one';
81
- const suppressed = new Set([findingKey('- **High:** foo.ts:1 — bug one')]);
87
+ const text = "- **High:** foo.ts:1 — bug one";
88
+ const suppressed = new Set([findingKey("- **High:** foo.ts:1 — bug one")]);
82
89
  expect(filterSuppressed(text, suppressed)).toBeNull();
83
90
  });
84
91
 
85
92
  it("returns original when no suppressions", () => {
86
- const text = '- **Low:** x.ts:5 — something';
93
+ const text = "- **Low:** x.ts:5 — something";
87
94
  expect(filterSuppressed(text, new Set())).toBe(text);
88
95
  });
89
96
  });
@@ -91,7 +98,7 @@ describe("filterSuppressed", () => {
91
98
  describe("DismissTracker", () => {
92
99
  it("tracks dismissals and suppresses after threshold", () => {
93
100
  const tracker = new DismissTracker();
94
- const findings = ['- **Medium:** src/foo.ts:10 — bad pattern', '- **Low:** src/bar.ts:5 — nit'];
101
+ const findings = ["- **Medium:** src/foo.ts:10 — bad pattern", "- **Low:** src/bar.ts:5 — nit"];
95
102
  tracker.setLastFindings(findings);
96
103
 
97
104
  // First dismiss
@@ -106,7 +113,7 @@ describe("DismissTracker", () => {
106
113
 
107
114
  it("reset clears all state", () => {
108
115
  const tracker = new DismissTracker();
109
- tracker.setLastFindings(['- **High:** x.ts:1 — bug']);
116
+ tracker.setLastFindings(["- **High:** x.ts:1 — bug"]);
110
117
  tracker.processDismissals("DISMISS F1: nope");
111
118
  tracker.processDismissals("DISMISS F1: nope again");
112
119
  expect(tracker.getSuppressed().size).toBe(1);
package/dismiss.ts CHANGED
@@ -35,16 +35,18 @@ export function numberFindings(text: string): { numbered: string; findings: stri
35
35
  const findings: string[] = [];
36
36
  let counter = 0;
37
37
 
38
- const numbered = lines.map(line => {
39
- // Match finding bullets: - **Severity:** ...
40
- const match = line.match(/^(\s*-\s*)\*\*(\w+):\*\*(.*)$/);
41
- if (match) {
42
- counter++;
43
- findings.push(line);
44
- return `${match[1]}**F${counter} ${match[2]}:**${match[3]}`;
45
- }
46
- return line;
47
- }).join("\n");
38
+ const numbered = lines
39
+ .map((line) => {
40
+ // Match finding bullets: - **Severity:** ...
41
+ const match = line.match(/^(\s*-\s*)\*\*(\w+):\*\*(.*)$/);
42
+ if (match) {
43
+ counter++;
44
+ findings.push(line);
45
+ return `${match[1]}**F${counter} ${match[2]}:**${match[3]}`;
46
+ }
47
+ return line;
48
+ })
49
+ .join("\n");
48
50
 
49
51
  return { numbered, findings };
50
52
  }
@@ -53,7 +55,7 @@ export function numberFindings(text: string): { numbered: string; findings: stri
53
55
  export function parseDismissals(text: string): Map<number, string> {
54
56
  const dismissals = new Map<number, string>();
55
57
  // Match: DISMISS F1: reason or DISMISS F1 - reason or DISMISS F1 reason
56
- const pattern = /DISMISS\s+F(\d+)\s*[:–\-]\s*(.+)/gi;
58
+ const pattern = /DISMISS\s+F(\d+)\s*[:–-]\s*(.+)/gi;
57
59
  let match;
58
60
  while ((match = pattern.exec(text)) !== null) {
59
61
  dismissals.set(parseInt(match[1], 10), match[2].trim());
@@ -66,7 +68,7 @@ export function filterSuppressed(text: string, suppressed: Set<string>): string
66
68
  if (suppressed.size === 0) return text;
67
69
 
68
70
  const lines = text.split("\n");
69
- const filtered = lines.filter(line => {
71
+ const filtered = lines.filter((line) => {
70
72
  const match = line.match(/^\s*-\s*\*\*\w+:\*\*/);
71
73
  if (!match) return true; // keep non-finding lines
72
74
  const key = findingKey(line);
@@ -74,7 +76,7 @@ export function filterSuppressed(text: string, suppressed: Set<string>): string
74
76
  });
75
77
 
76
78
  // If all findings were suppressed, return null (should be LGTM)
77
- const remaining = filtered.filter(l => l.match(/^\s*-\s*\*\*/));
79
+ const remaining = filtered.filter((l) => l.match(/^\s*-\s*\*\*/));
78
80
  if (remaining.length === 0) return null;
79
81
 
80
82
  return filtered.join("\n");
@@ -112,7 +114,9 @@ export class DismissTracker {
112
114
  this.dismissed.set(key, { key, reason, count: 1 });
113
115
  }
114
116
  count++;
115
- log(`dismiss: F${fNum} dismissed (${key}) — "${reason}" [count=${this.dismissed.get(key)!.count}]`);
117
+ log(
118
+ `dismiss: F${fNum} dismissed (${key}) — "${reason}" [count=${this.dismissed.get(key)!.count}]`,
119
+ );
116
120
  }
117
121
  return count;
118
122
  }
package/index.ts CHANGED
@@ -706,7 +706,9 @@ export default function (pi: ExtensionAPI) {
706
706
 
707
707
  // Process DISMISS markers from agent's response (before running review)
708
708
  if (lastAssistant) {
709
- const textParts = (lastAssistant.content ?? []).filter((b: any) => b.type === "text").map((b: any) => b.text);
709
+ const textParts = (lastAssistant.content ?? [])
710
+ .filter((b: any) => b.type === "text")
711
+ .map((b: any) => b.text);
710
712
  const agentText = textParts.join("\n");
711
713
  if (agentText) {
712
714
  orchestrator.processDismissals(agentText);
package/judge.ts CHANGED
@@ -66,6 +66,7 @@ TAXONOMY (authoritative):
66
66
  - npm/pnpm/yarn/pip/cargo install, make, cargo build, npm run format, codegen scripts → modifying
67
67
  - kill/pkill/systemctl, docker run, docker compose up → modifying
68
68
  - sed -i, perl -pi → modifying (in-place edit)
69
+ - perl -e, python -c, node -e, ruby -e (one-liners) → unsure (static analysis cannot determine side effects; caught by detectSubprocessWrapper pre-check)
69
70
  - ./script.sh or npm run <unknown> → unsure unless clearly read-only
70
71
  - truncated command (e.g. "git commi") → unsure
71
72
  - Compound commands with &&, ;, ||, pipes, subshells: ANY modifying part → modifying; ANY unknown/truncated → unsure; otherwise the class of the safest-subset.
@@ -101,9 +102,47 @@ export function parseJudgeResponse(raw: string): BashClassification {
101
102
  return "unsure";
102
103
  }
103
104
 
105
+ /**
106
+ * Detect subprocess wrappers (perl -e, python -c, node -e, ruby -e) that cannot
107
+ * be statically analyzed. Returns 'unsure' for these patterns (conservative).
108
+ *
109
+ * Note: False-positives are acceptable here (fail-safe direction). Regexes match
110
+ * broadly to catch unquoted/variable forms (perl -e $code) and even literal strings
111
+ * in other commands (echo "perl -e foo"), since marking those 'unsure' is
112
+ * safer than missing an actual one-liner.
113
+ */
114
+ function detectSubprocessWrapper(command: string): BashClassification | null {
115
+ if (!command) return null;
116
+
117
+ // perl -e / -E with any argument (quoted, unquoted, or variable)
118
+ if (/\bperl\b.*\s-[eE](?:\s+\S|$)/.test(command)) {
119
+ return "unsure";
120
+ }
121
+
122
+ // python / python3 -c with any argument
123
+ if (/\bpython\d?\b.*\s-c(?:\s+\S|$)/.test(command)) {
124
+ return "unsure";
125
+ }
126
+
127
+ // node -e / -E / --eval with any argument
128
+ if (/\bnode\b.*(?:\s-[eE]|--eval)(?:\s+\S|$)/.test(command)) {
129
+ return "unsure";
130
+ }
131
+
132
+ // ruby -e with any argument
133
+ if (/\bruby\b.*\s-e(?:\s+\S|$)/.test(command)) {
134
+ return "unsure";
135
+ }
136
+
137
+ return null;
138
+ }
139
+
104
140
  /**
105
141
  * Run the judge on a single bash command. Always resolves (never rejects);
106
142
  * any failure collapses to `unsure` so the caller's skip logic stays safe.
143
+ *
144
+ * Pre-checks for subprocess wrappers (perl -e, python -c, etc.) before calling
145
+ * the LLM, since these patterns cannot be statically analyzed.
107
146
  */
108
147
  export async function classifyBashCommand(
109
148
  runner: JudgeRunner,
@@ -111,6 +150,14 @@ export async function classifyBashCommand(
111
150
  opts: JudgeOptions,
112
151
  ): Promise<BashClassification> {
113
152
  if (!command || typeof command !== "string") return "unsure";
153
+
154
+ // Deterministic pre-check: subprocess wrappers
155
+ const subprocessResult = detectSubprocessWrapper(command);
156
+ if (subprocessResult) {
157
+ log(`judge: detected subprocess wrapper (${command.slice(0, 40)}...) → unsure`);
158
+ return subprocessResult;
159
+ }
160
+
114
161
  try {
115
162
  const { text } = await runner(command, opts);
116
163
  return parseJudgeResponse(text);
package/orchestrator.ts CHANGED
@@ -166,7 +166,11 @@ export class ReviewOrchestrator {
166
166
  // All findings suppressed — treat as LGTM
167
167
  if (filtered === null) {
168
168
  log("dismiss: all findings suppressed — treating as LGTM");
169
- return { ...result, isLgtm: true, text: "No issues found (previously dismissed findings suppressed)." };
169
+ return {
170
+ ...result,
171
+ isLgtm: true,
172
+ text: "No issues found (previously dismissed findings suppressed).",
173
+ };
170
174
  }
171
175
 
172
176
  // Number remaining findings and track them
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inceptionstack/pi-hard-no",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "type": "module",
5
5
  "description": "Pi extension — automatic code review after every agent turn",
6
6
  "license": "MIT",