karajan-code 1.36.1 → 1.38.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.
@@ -131,6 +131,61 @@ export function normalizeAcceptanceCriteria(criteria) {
131
131
  return criteria.map(detectAcFormat);
132
132
  }
133
133
 
134
+ /**
135
+ * Build a prompt that asks the AI to decompose a complex task into 2-5 formal HUs.
136
+ * Each HU follows the role/goal/benefit format with acceptance criteria and optional dependencies.
137
+ * @param {string} task - The complex task description to decompose.
138
+ * @returns {string} The assembled decomposition prompt.
139
+ */
140
+ export function buildDecompositionPrompt(task) {
141
+ const sections = [SUBAGENT_PREAMBLE];
142
+
143
+ sections.push("## Task Decomposition");
144
+ sections.push(
145
+ "You are a senior product owner. Decompose the following complex task into 2-5 independent, implementable user stories (HUs).",
146
+ "Each HU must be small enough to implement in a single iteration and must follow the role/goal/benefit format."
147
+ );
148
+
149
+ sections.push(`## Task to Decompose\n${task}`);
150
+
151
+ sections.push(
152
+ "Return a single valid JSON object and nothing else.",
153
+ `JSON schema: {"stories":[{"id":string,"title":string,"role":string,"goal":string,"benefit":string,"acceptanceCriteria":[string],"dependsOn":[string]}]}`,
154
+ "Rules:",
155
+ "- Generate between 2 and 5 HUs",
156
+ "- Each id must be unique, formatted as HU-DECOMP-NNN (e.g. HU-DECOMP-001)",
157
+ "- dependsOn is an array of other HU ids from this decomposition that must be completed first (empty array if no dependencies)",
158
+ "- acceptanceCriteria should be concrete, testable statements",
159
+ "- Order stories so that dependencies are respected (a story should only depend on stories with lower ids)"
160
+ );
161
+
162
+ return sections.join("\n\n");
163
+ }
164
+
165
+ /**
166
+ * Parse the output from a decomposition prompt.
167
+ * @param {string} raw - Raw text output from the agent.
168
+ * @returns {Array<{id: string, title: string, role: string, goal: string, benefit: string, acceptanceCriteria: string[], dependsOn: string[]}>|null}
169
+ */
170
+ export function parseDecompositionOutput(raw) {
171
+ const parsed = extractFirstJson(raw);
172
+ if (!parsed || !Array.isArray(parsed.stories)) return null;
173
+
174
+ const stories = parsed.stories
175
+ .filter(s => s && s.id && s.title)
176
+ .map(s => ({
177
+ id: s.id,
178
+ title: s.title,
179
+ role: s.role || "developer",
180
+ goal: s.goal || s.title,
181
+ benefit: s.benefit || "",
182
+ acceptanceCriteria: Array.isArray(s.acceptanceCriteria) ? s.acceptanceCriteria : [],
183
+ dependsOn: Array.isArray(s.dependsOn) ? s.dependsOn : []
184
+ }));
185
+
186
+ return stories.length > 0 ? stories : null;
187
+ }
188
+
134
189
  /**
135
190
  * Parse the raw output from the HU reviewer agent.
136
191
  * @param {string} raw - Raw text output from the agent.
@@ -11,7 +11,8 @@ const ROLE_DESCRIPTIONS = [
11
11
  { role: "security", description: "Audits code for security vulnerabilities. Checks auth, input validation, injection risks." },
12
12
  { role: "refactorer", description: "Cleans up and refactors code after the main implementation." },
13
13
  { role: "reviewer", description: "Reviews the code diff for quality issues. Standard quality gate." },
14
- { role: "architect", description: "Designs solution architecture — layers, patterns, data model, API contracts, tradeoffs. Activate when task creates new module/app, affects data model/APIs, complexity is medium/complex, or design is ambiguous." }
14
+ { role: "architect", description: "Designs solution architecture — layers, patterns, data model, API contracts, tradeoffs. Activate when task creates new module/app, affects data model/APIs, complexity is medium/complex, or design is ambiguous." },
15
+ { role: "hu-reviewer", description: "Certifies user stories before coding. Activate for medium/complex tasks that need story decomposition." }
15
16
  ];
16
17
 
17
18
  export function buildTriagePrompt({ task, instructions, availableRoles }) {
@@ -37,13 +38,14 @@ export function buildTriagePrompt({ task, instructions, availableRoles }) {
37
38
  "- **refactorer**: Enable only when explicitly requested or when the task is a refactoring task.",
38
39
  "- **reviewer**: Enable for most tasks as a quality gate. Disable only for trivial, single-line changes.",
39
40
  "- **architect**: Enable when creating new modules/apps, changing data models or APIs, medium/complex tasks, or when design approach is ambiguous. Disable for simple fixes, doc-only, or CSS-only changes.",
41
+ "- **hu-reviewer**: Enable for medium/complex tasks that benefit from story decomposition and certification. Disable for trivial/simple tasks, doc-only, or CSS-only changes.",
40
42
  "",
41
43
  "Note: coder is ALWAYS active — you don't need to decide on it."
42
44
  ].join("\n"),
43
45
  "Classify the task complexity, determine its taskType, recommend only the necessary pipeline roles, and assess whether the task should be decomposed into smaller subtasks.",
44
46
  "Keep the reasoning short and practical.",
45
47
  "Return a single valid JSON object and nothing else.",
46
- 'JSON schema: {"level":"trivial|simple|medium|complex","roles":["planner|researcher|refactorer|reviewer|tester|security|architect"],"taskType":"sw|infra|doc|add-tests|refactor","reasoning":string,"shouldDecompose":boolean,"subtasks":string[]}',
48
+ 'JSON schema: {"level":"trivial|simple|medium|complex","roles":["planner|researcher|refactorer|reviewer|tester|security|architect|hu-reviewer"],"taskType":"sw|infra|doc|add-tests|refactor","reasoning":string,"shouldDecompose":boolean,"subtasks":string[]}',
47
49
  `## Task\n${task}`
48
50
  );
49
51
 
@@ -47,6 +47,20 @@ const ICONS = {
47
47
  "budget:update": "\ud83d\udcb8",
48
48
  "iteration:end": "\u23f1\ufe0f",
49
49
  "session:end": "\ud83c\udfc1",
50
+ "discover:start": "\ud83d\udd0e",
51
+ "discover:end": "\ud83d\udd0e",
52
+ "architect:start": "\ud83c\udfdb\ufe0f",
53
+ "architect:end": "\ud83c\udfdb\ufe0f",
54
+ "hu-reviewer:start": "\ud83d\udcdd",
55
+ "hu-reviewer:end": "\ud83d\udcdd",
56
+ "impeccable:start": "\ud83c\udfa8",
57
+ "impeccable:end": "\ud83c\udfa8",
58
+ "audit:start": "\ud83d\udccb",
59
+ "audit:end": "\ud83d\udccb",
60
+ "sonarcloud:start": "\u2601\ufe0f",
61
+ "sonarcloud:end": "\u2601\ufe0f",
62
+ "preflight:start": "\ud83d\udee1\ufe0f",
63
+ "preflight:end": "\ud83d\udee1\ufe0f",
50
64
  question: "\u2753"
51
65
  };
52
66
 
@@ -95,24 +109,37 @@ export function printHeader({ task, config }) {
95
109
  console.log();
96
110
  }
97
111
 
112
+ /* ── Helper: executor/provider tag ───────────────────────────── */
113
+
114
+ function formatExecutor(detail) {
115
+ if (!detail) return '';
116
+ const provider = detail.provider || '';
117
+ const type = detail.executorType || '';
118
+ if (provider) return ` ${ANSI.dim}${provider}${ANSI.reset}`;
119
+ if (type === 'local') return ` ${ANSI.dim}local${ANSI.reset}`;
120
+ if (type === 'system') return ` ${ANSI.dim}system${ANSI.reset}`;
121
+ return '';
122
+ }
123
+
98
124
  /* ── Helper: role start/end one-liners ───────────────────────── */
99
125
 
100
126
  function roleStart(icon, label, provider) {
101
127
  console.log(` \u251c\u2500 ${icon} ${label} (${provider || "?"}) running...`);
102
128
  }
103
129
 
104
- function roleEnd(status, label, elapsed) {
105
- console.log(` \u251c\u2500 ${status} ${label} completed ${elapsed}`);
130
+ function roleEnd(status, label, elapsed, executor) {
131
+ console.log(` \u251c\u2500 ${status} ${label} completed${executor || ''} ${elapsed}`);
106
132
  }
107
133
 
108
134
  /* ── Helper: pass/fail stage result ─────────────────────────── */
109
135
 
110
136
  function passFailStage(detail, label, failDefault, elapsed) {
137
+ const executor = formatExecutor(detail);
111
138
  if (detail?.ok === false) {
112
139
  const summary = detail?.summary || failDefault;
113
- console.log(` \u251c\u2500 ${ANSI.red}\u274c ${label}: ${summary}${ANSI.reset} ${elapsed}`);
140
+ console.log(` \u251c\u2500 ${ANSI.red}\u274c ${label}: ${summary}${ANSI.reset}${executor} ${elapsed}`);
114
141
  } else {
115
- console.log(` \u251c\u2500 ${ANSI.green}\u2705 ${label}: passed${ANSI.reset} ${elapsed}`);
142
+ console.log(` \u251c\u2500 ${ANSI.green}\u2705 ${label}: passed${ANSI.reset}${executor} ${elapsed}`);
116
143
  }
117
144
  }
118
145
 
@@ -270,49 +297,52 @@ const EVENT_HANDLERS = {
270
297
  roleStart(icon, "Planner", event.detail?.planner);
271
298
  },
272
299
 
273
- "planner:end": (_event, _icon, elapsed, status) => {
274
- roleEnd(status, "Planner", elapsed);
300
+ "planner:end": (event, _icon, elapsed, status) => {
301
+ roleEnd(status, "Planner", elapsed, formatExecutor(event.detail));
275
302
  },
276
303
 
277
304
  "coder:start": (event, icon) => {
278
305
  roleStart(icon, "Coder", event.detail?.coder);
279
306
  },
280
307
 
281
- "coder:end": (_event, _icon, elapsed, status) => {
282
- roleEnd(status, "Coder", elapsed);
308
+ "coder:end": (event, _icon, elapsed, status) => {
309
+ roleEnd(status, "Coder", elapsed, formatExecutor(event.detail));
283
310
  },
284
311
 
285
312
  "refactorer:start": (event, icon) => {
286
313
  roleStart(icon, "Refactorer", event.detail?.refactorer);
287
314
  },
288
315
 
289
- "refactorer:end": (_event, _icon, elapsed, status) => {
290
- roleEnd(status, "Refactorer", elapsed);
316
+ "refactorer:end": (event, _icon, elapsed, status) => {
317
+ roleEnd(status, "Refactorer", elapsed, formatExecutor(event.detail));
291
318
  },
292
319
 
293
320
  "tdd:result": (event, icon) => {
294
321
  const tdd = event.detail || {};
295
322
  const label = tdd.ok ? `${ANSI.green}PASS${ANSI.reset}` : `${ANSI.red}FAIL${ANSI.reset}`;
296
323
  const files = tdd.sourceFiles === undefined ? "" : ` (${tdd.sourceFiles} src, ${tdd.testFiles} test)`;
297
- console.log(` \u251c\u2500 ${icon} TDD policy: ${label}${files}`);
324
+ const executor = formatExecutor(event.detail);
325
+ console.log(` \u251c\u2500 ${icon} TDD policy: ${label}${files}${executor}`);
298
326
  },
299
327
 
300
328
  "researcher:start": (event, icon) => {
301
329
  console.log(` \u251c\u2500 ${icon} Researcher (${event.detail?.researcher || "?"}) investigating...`);
302
330
  },
303
331
 
304
- "researcher:end": (_event, _icon, elapsed, status) => {
305
- roleEnd(status, "Researcher", elapsed);
332
+ "researcher:end": (event, _icon, elapsed, status) => {
333
+ roleEnd(status, "Researcher", elapsed, formatExecutor(event.detail));
306
334
  },
307
335
 
308
- "sonar:start": (_event, icon) => {
309
- console.log(` \u251c\u2500 ${icon} SonarQube scanning...`);
336
+ "sonar:start": (event, icon) => {
337
+ const executor = formatExecutor(event.detail);
338
+ console.log(` \u251c\u2500 ${icon} SonarQube scanning...${executor}`);
310
339
  },
311
340
 
312
341
  "sonar:end": (event, _icon, elapsed, status) => {
313
342
  const gate = event.detail?.gateStatus || "?";
314
343
  const gateColor = gate === "OK" ? ANSI.green : ANSI.red;
315
- console.log(` \u251c\u2500 ${status} Quality gate: ${gateColor}${gate}${ANSI.reset} ${elapsed}`);
344
+ const executor = formatExecutor(event.detail);
345
+ console.log(` \u251c\u2500 ${status} Quality gate: ${gateColor}${gate}${ANSI.reset}${executor} ${elapsed}`);
316
346
  },
317
347
 
318
348
  "reviewer:start": (event, icon) => {
@@ -321,11 +351,12 @@ const EVENT_HANDLERS = {
321
351
 
322
352
  "reviewer:end": (event, _icon, elapsed) => {
323
353
  const review = event.detail || {};
354
+ const executor = formatExecutor(event.detail);
324
355
  if (review.approved) {
325
- console.log(` \u251c\u2500 ${ANSI.green}\u2705 Review: APPROVED${ANSI.reset} ${elapsed}`);
356
+ console.log(` \u251c\u2500 ${ANSI.green}\u2705 Review: APPROVED${ANSI.reset}${executor} ${elapsed}`);
326
357
  } else {
327
358
  const count = review.blockingCount || 0;
328
- console.log(` \u251c\u2500 ${ANSI.red}\u274c Review: REJECTED (${count} blocking)${ANSI.reset}`);
359
+ console.log(` \u251c\u2500 ${ANSI.red}\u274c Review: REJECTED (${count} blocking)${ANSI.reset}${executor}`);
329
360
  if (review.issues) {
330
361
  for (const issue of review.issues) {
331
362
  console.log(` \u2502 ${ANSI.dim}${issue}${ANSI.reset}`);
@@ -334,16 +365,18 @@ const EVENT_HANDLERS = {
334
365
  }
335
366
  },
336
367
 
337
- "tester:start": (_event, icon) => {
338
- console.log(` \u251c\u2500 ${icon} Tester evaluating...`);
368
+ "tester:start": (event, icon) => {
369
+ const executor = formatExecutor(event.detail);
370
+ console.log(` \u251c\u2500 ${icon} Tester evaluating...${executor}`);
339
371
  },
340
372
 
341
373
  "tester:end": (event, _icon, elapsed) => {
342
374
  passFailStage(event.detail, "Tester", "issues found", elapsed);
343
375
  },
344
376
 
345
- "security:start": (_event, icon) => {
346
- console.log(` \u251c\u2500 ${icon} Security auditing...`);
377
+ "security:start": (event, icon) => {
378
+ const executor = formatExecutor(event.detail);
379
+ console.log(` \u251c\u2500 ${icon} Security auditing...${executor}`);
347
380
  },
348
381
 
349
382
  "security:end": (event, _icon, elapsed) => {
@@ -351,7 +384,8 @@ const EVENT_HANDLERS = {
351
384
  },
352
385
 
353
386
  "solomon:start": (event, icon) => {
354
- console.log(` \u251c\u2500 ${icon} Solomon arbitrating ${event.detail?.conflictStage || "?"} conflict...`);
387
+ const executor = formatExecutor(event.detail);
388
+ console.log(` \u251c\u2500 ${icon} Solomon arbitrating ${event.detail?.conflictStage || "?"} conflict...${executor}`);
355
389
  },
356
390
 
357
391
  "solomon:end": (event, _icon, elapsed) => {
@@ -422,6 +456,82 @@ const EVENT_HANDLERS = {
422
456
  console.log(`${ANSI.dim}Resume with: kj resume ${event.sessionId} --answer "<response>"${ANSI.reset}`);
423
457
  },
424
458
 
459
+ "preflight:start": (event) => {
460
+ const executor = formatExecutor(event.detail);
461
+ console.log(` \u251c\u2500 \ud83d\udee1\ufe0f Preflight checks running...${executor}`);
462
+ },
463
+
464
+ "preflight:end": (event, _icon, elapsed) => {
465
+ const executor = formatExecutor(event.detail);
466
+ const ok = event.status === "ok";
467
+ const statusIcon = ok ? `${ANSI.green}\u2705` : `${ANSI.red}\u274c`;
468
+ console.log(` \u251c\u2500 ${statusIcon} Preflight: ${event.message}${ANSI.reset}${executor} ${elapsed}`);
469
+ },
470
+
471
+ "discover:start": (event, icon) => {
472
+ const provider = event.detail?.provider || event.detail?.discover || "?";
473
+ console.log(` \u251c\u2500 ${icon || "\ud83d\udd0e"} Discover (${provider}) analyzing...`);
474
+ },
475
+
476
+ "discover:end": (event, _icon, elapsed, status) => {
477
+ roleEnd(status, "Discover", elapsed, formatExecutor(event.detail));
478
+ },
479
+
480
+ "architect:start": (event) => {
481
+ const provider = event.detail?.provider || event.detail?.architect || "?";
482
+ console.log(` \u251c\u2500 \ud83c\udfdb\ufe0f Architect (${provider}) designing...`);
483
+ },
484
+
485
+ "architect:end": (event, _icon, elapsed, status) => {
486
+ roleEnd(status, "Architect", elapsed, formatExecutor(event.detail));
487
+ },
488
+
489
+ "hu-reviewer:start": (event) => {
490
+ const executor = formatExecutor(event.detail);
491
+ console.log(` \u251c\u2500 \ud83d\udcdd HU Reviewer certifying...${executor}`);
492
+ },
493
+
494
+ "hu-reviewer:end": (event, _icon, elapsed, status) => {
495
+ roleEnd(status, "HU Reviewer", elapsed, formatExecutor(event.detail));
496
+ },
497
+
498
+ "impeccable:start": (event) => {
499
+ const executor = formatExecutor(event.detail);
500
+ console.log(` \u251c\u2500 \ud83c\udfa8 Impeccable auditing design...${executor}`);
501
+ },
502
+
503
+ "impeccable:end": (event, _icon, elapsed, status) => {
504
+ roleEnd(status, "Impeccable", elapsed, formatExecutor(event.detail));
505
+ },
506
+
507
+ "audit:start": (event) => {
508
+ const executor = formatExecutor(event.detail);
509
+ console.log(` \u251c\u2500 \ud83d\udccb Final audit running...${executor}`);
510
+ },
511
+
512
+ "audit:end": (event, _icon, elapsed, status) => {
513
+ roleEnd(status, "Audit", elapsed, formatExecutor(event.detail));
514
+ },
515
+
516
+ "sonarcloud:start": (event) => {
517
+ const executor = formatExecutor(event.detail);
518
+ console.log(` \u251c\u2500 \u2601\ufe0f SonarCloud scanning...${executor}`);
519
+ },
520
+
521
+ "sonarcloud:end": (event, _icon, elapsed, status) => {
522
+ roleEnd(status, "SonarCloud", elapsed, formatExecutor(event.detail));
523
+ },
524
+
525
+ "guard:output": (event) => {
526
+ const executor = formatExecutor(event.detail);
527
+ console.log(` \u251c\u2500 \ud83d\udee1\ufe0f ${event.message}${executor}`);
528
+ },
529
+
530
+ "guard:perf": (event) => {
531
+ const executor = formatExecutor(event.detail);
532
+ console.log(` \u251c\u2500 \u26a1 ${event.message}${executor}`);
533
+ },
534
+
425
535
  "pipeline:tracker": (event) => {
426
536
  const trackerStages = event.detail?.stages || [];
427
537
  console.log(` ${ANSI.dim}\u250c Pipeline${ANSI.reset}`);
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Prompt-injection guard for AI-reviewed diffs and PR content.
3
+ *
4
+ * Scans text for patterns commonly used to hijack LLM instructions:
5
+ * directive overrides, role reassignment, invisible Unicode, and
6
+ * suspiciously large comment blocks that could hide payloads.
7
+ *
8
+ * @module utils/injection-guard
9
+ */
10
+
11
+ /** Phrases that attempt to override system/reviewer instructions. */
12
+ const DIRECTIVE_PATTERNS = [
13
+ /ignore\s+(all\s+)?(previous|prior|above|earlier)\s+(instructions?|rules?|prompts?|guidelines?)/i,
14
+ /disregard\s+(all\s+)?(previous|prior|above|earlier)\s+(instructions?|rules?|prompts?)/i,
15
+ /forget\s+(all\s+)?(previous|prior|your)\s+(instructions?|rules?|context)/i,
16
+ /override\s+(the\s+)?(system|reviewer|review)\s+(prompt|instructions?|rules?)/i,
17
+ /do\s+not\s+follow\s+(the\s+)?(above|previous|system)\s+(instructions?|rules?)/i,
18
+ /new\s+instructions?\s*:/i,
19
+ /system\s*:\s*you\s+are/i,
20
+ /\bact\s+as\s+(a\s+)?(different|new|my|an?\s+)/i,
21
+ /you\s+are\s+now\s+(a\s+)?/i,
22
+ /from\s+now\s+on\s*(,|\s)?\s*you\s+(will|should|must|are)/i,
23
+ /pretend\s+(you\s+are|to\s+be)/i,
24
+ /respond\s+(only\s+)?with\s+["']?approved["']?/i,
25
+ /always\s+(return|respond|output)\s+.*approved/i,
26
+ /set\s+approved\s*[=:]\s*true/i,
27
+ /\bapproved["']?\s*:\s*true\b.*ignore/i,
28
+ /output\s+the\s+following\s+json/i,
29
+ /return\s+this\s+exact\s+(json|response|output)/i,
30
+ ];
31
+
32
+ /** Unicode categories that can hide or disguise content. */
33
+ const UNICODE_PATTERNS = [
34
+ /[\u200B-\u200F]/, // zero-width spaces, LTR/RTL marks
35
+ /[\u202A-\u202E]/, // bidi embedding/override
36
+ /[\u2066-\u2069]/, // bidi isolate
37
+ /[\uFEFF]/, // BOM mid-text
38
+ /[\u00AD]{3,}/, // repeated soft hyphens
39
+ /[\u2060-\u2064]/, // invisible operators
40
+ /[\u{E0000}-\u{E007F}]/u, // tag characters
41
+ ];
42
+
43
+ /** Maximum comment block size (chars) before flagging. */
44
+ const MAX_COMMENT_BLOCK = 2000;
45
+
46
+ /**
47
+ * Comment block patterns for common languages.
48
+ * Each regex captures the full block content.
49
+ */
50
+ const COMMENT_BLOCK_RE = [
51
+ /\/\*[\s\S]{0,20000}?\*\//g, // C-style /* ... */
52
+ /<!--[\s\S]{0,20000}?-->/g, // HTML <!-- ... -->
53
+ /"""\s*[\s\S]{0,20000}?\s*"""/g, // Python docstrings
54
+ /'''\s*[\s\S]{0,20000}?\s*'''/g, // Python docstrings (single)
55
+ /=begin[\s\S]{0,20000}?=end/g, // Ruby block comments
56
+ ];
57
+
58
+ /**
59
+ * @typedef {Object} InjectionFinding
60
+ * @property {"directive"|"unicode"|"comment_block"} type
61
+ * @property {string} pattern - short label of what matched
62
+ * @property {string} snippet - excerpt of the offending text (max 120 chars)
63
+ * @property {number} [line] - approximate line number (1-based)
64
+ */
65
+
66
+ /**
67
+ * @typedef {Object} GuardResult
68
+ * @property {boolean} clean - true if no threats detected
69
+ * @property {InjectionFinding[]} findings
70
+ * @property {string} summary - human-readable one-liner
71
+ */
72
+
73
+ /**
74
+ * Scan a text (diff, PR description, comment) for prompt-injection signals.
75
+ *
76
+ * @param {string} text - content to scan
77
+ * @param {Object} [opts]
78
+ * @param {number} [opts.maxCommentBlock=2000] - flag comment blocks larger than this
79
+ * @returns {GuardResult}
80
+ */
81
+ export function scanForInjection(text, opts = {}) {
82
+ if (!text || typeof text !== "string") {
83
+ return { clean: true, findings: [], summary: "Nothing to scan" };
84
+ }
85
+
86
+ const maxBlock = opts.maxCommentBlock ?? MAX_COMMENT_BLOCK;
87
+ /** @type {InjectionFinding[]} */
88
+ const findings = [];
89
+
90
+ // 1. Directive patterns
91
+ for (const re of DIRECTIVE_PATTERNS) {
92
+ const match = re.exec(text);
93
+ if (match) {
94
+ findings.push({
95
+ type: "directive",
96
+ pattern: re.source.slice(0, 60),
97
+ snippet: excerpt(text, match.index),
98
+ line: lineAt(text, match.index),
99
+ });
100
+ }
101
+ }
102
+
103
+ // 2. Invisible Unicode
104
+ for (const re of UNICODE_PATTERNS) {
105
+ const match = re.exec(text);
106
+ if (match) {
107
+ const charCode = match[0].codePointAt(0).toString(16).toUpperCase();
108
+ findings.push({
109
+ type: "unicode",
110
+ pattern: `U+${charCode}`,
111
+ snippet: excerpt(text, match.index),
112
+ line: lineAt(text, match.index),
113
+ });
114
+ }
115
+ }
116
+
117
+ // 3. Oversized comment blocks
118
+ for (const re of COMMENT_BLOCK_RE) {
119
+ let m;
120
+ while ((m = re.exec(text)) !== null) {
121
+ if (m[0].length > maxBlock) {
122
+ findings.push({
123
+ type: "comment_block",
124
+ pattern: `block ${m[0].length} chars (max ${maxBlock})`,
125
+ snippet: excerpt(text, m.index),
126
+ line: lineAt(text, m.index),
127
+ });
128
+ }
129
+ }
130
+ }
131
+
132
+ const clean = findings.length === 0;
133
+ const summary = clean
134
+ ? "No injection patterns detected"
135
+ : `${findings.length} potential injection(s): ${[...new Set(findings.map((f) => f.type))].join(", ")}`;
136
+
137
+ return { clean, findings, summary };
138
+ }
139
+
140
+ /**
141
+ * Scan a git diff, focusing only on added lines (lines starting with +).
142
+ * Removed lines cannot inject into the AI prompt.
143
+ *
144
+ * @param {string} diff - unified diff content
145
+ * @param {Object} [opts]
146
+ * @returns {GuardResult}
147
+ */
148
+ export function scanDiff(diff, opts = {}) {
149
+ if (!diff) return { clean: true, findings: [], summary: "Empty diff" };
150
+
151
+ // Extract only added lines (skip diff headers like +++ b/file)
152
+ const addedLines = diff
153
+ .split("\n")
154
+ .filter((l) => l.startsWith("+") && !l.startsWith("+++"))
155
+ .map((l) => l.slice(1))
156
+ .join("\n");
157
+
158
+ return scanForInjection(addedLines, opts);
159
+ }
160
+
161
+ // --- Helpers ---
162
+
163
+ function excerpt(text, index) {
164
+ const start = Math.max(0, index - 20);
165
+ const end = Math.min(text.length, index + 100);
166
+ return text.slice(start, end).replace(/\n/g, "\\n").slice(0, 120);
167
+ }
168
+
169
+ function lineAt(text, index) {
170
+ return text.slice(0, index).split("\n").length;
171
+ }
@@ -5,37 +5,46 @@ import os from "node:os";
5
5
  */
6
6
  export function getPlatform() {
7
7
  const platform = os.platform();
8
- return platform === "darwin" ? "macos" : "linux";
8
+ if (platform === "win32") return "windows";
9
+ if (platform === "darwin") return "macos";
10
+ return "linux";
9
11
  }
10
12
 
11
13
  const INSTALL_COMMANDS = {
12
14
  rtk: {
13
15
  macos: "brew install rtk && rtk init --global",
14
- linux: "curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh && rtk init --global"
16
+ linux: "curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh && rtk init --global",
17
+ windows: "Download from https://github.com/rtk-ai/rtk/releases and add to PATH"
15
18
  },
16
19
  claude: {
17
20
  macos: "npm install -g @anthropic-ai/claude-code",
18
- linux: "npm install -g @anthropic-ai/claude-code"
21
+ linux: "npm install -g @anthropic-ai/claude-code",
22
+ windows: "npm install -g @anthropic-ai/claude-code"
19
23
  },
20
24
  codex: {
21
25
  macos: "npm install -g @openai/codex",
22
- linux: "npm install -g @openai/codex"
26
+ linux: "npm install -g @openai/codex",
27
+ windows: "npm install -g @openai/codex"
23
28
  },
24
29
  gemini: {
25
30
  macos: "npm install -g @google/gemini-cli",
26
- linux: "npm install -g @google/gemini-cli"
31
+ linux: "npm install -g @google/gemini-cli",
32
+ windows: "npm install -g @google/gemini-cli"
27
33
  },
28
34
  aider: {
29
35
  macos: "pipx install aider-chat",
30
- linux: "pipx install aider-chat || pip3 install aider-chat"
36
+ linux: "pipx install aider-chat || pip3 install aider-chat",
37
+ windows: "pip install aider-chat"
31
38
  },
32
39
  opencode: {
33
40
  macos: "curl -fsSL https://opencode.ai/install | bash",
34
- linux: "curl -fsSL https://opencode.ai/install | bash"
41
+ linux: "curl -fsSL https://opencode.ai/install | bash",
42
+ windows: "See https://github.com/nicepkg/opencode for Windows install"
35
43
  },
36
44
  docker: {
37
45
  macos: "brew install --cask docker",
38
- linux: "sudo apt install docker.io docker-compose-v2 (or see https://docs.docker.com/engine/install/)"
46
+ linux: "sudo apt install docker.io docker-compose-v2 (or see https://docs.docker.com/engine/install/)",
47
+ windows: "Download Docker Desktop from https://www.docker.com/products/docker-desktop/"
39
48
  }
40
49
  };
41
50
 
@@ -1,12 +1,15 @@
1
1
  import { execa } from "execa";
2
2
 
3
+ const isWin = process.platform === "win32";
4
+ const KILL_SIGNAL = isWin ? "SIGTERM" : "SIGKILL";
5
+
3
6
  function buildSilenceKilledResult(silenceTimeoutMs, stdout, signal) {
4
7
  return {
5
8
  exitCode: 143,
6
9
  stdout,
7
10
  stderr: `Command killed after ${Number(silenceTimeoutMs)}ms without output`,
8
11
  timedOut: true,
9
- signal: signal || "SIGKILL"
12
+ signal: signal || KILL_SIGNAL
10
13
  };
11
14
  }
12
15
 
@@ -52,7 +55,7 @@ async function awaitWithTimeout(subprocess, timeout, getStdout) {
52
55
  const timeoutResult = new Promise((resolve) => {
53
56
  timer = setTimeout(() => {
54
57
  try {
55
- subprocess.kill("SIGKILL", { forceKillAfterDelay: 1000 });
58
+ subprocess.kill(KILL_SIGNAL, { forceKillAfterDelay: 1000 });
56
59
  } catch {
57
60
  // no-op
58
61
  }
@@ -61,7 +64,7 @@ async function awaitWithTimeout(subprocess, timeout, getStdout) {
61
64
  stdout: getStdout(),
62
65
  stderr: `Command timed out after ${timeout}ms`,
63
66
  timedOut: true,
64
- signal: "SIGKILL"
67
+ signal: KILL_SIGNAL
65
68
  });
66
69
  }, timeout);
67
70
  });
@@ -94,7 +97,7 @@ export async function runCommand(command, args = [], options = {}) {
94
97
  outputSilenceTimer = setTimeout(() => {
95
98
  silenceTimedOut = true;
96
99
  try {
97
- subprocess.kill("SIGKILL", { forceKillAfterDelay: 1000 });
100
+ subprocess.kill(KILL_SIGNAL, { forceKillAfterDelay: 1000 });
98
101
  } catch {
99
102
  // no-op
100
103
  }