@kodrunhq/opencode-autopilot 1.12.2 → 1.14.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.
Files changed (68) hide show
  1. package/assets/commands/oc-brainstorm.md +1 -0
  2. package/assets/commands/oc-new-agent.md +1 -0
  3. package/assets/commands/oc-new-command.md +1 -0
  4. package/assets/commands/oc-new-skill.md +1 -0
  5. package/assets/commands/oc-quick.md +1 -0
  6. package/assets/commands/oc-refactor.md +26 -0
  7. package/assets/commands/oc-review-agents.md +1 -0
  8. package/assets/commands/oc-review-pr.md +1 -0
  9. package/assets/commands/oc-security-audit.md +20 -0
  10. package/assets/commands/oc-stocktake.md +1 -0
  11. package/assets/commands/oc-tdd.md +1 -0
  12. package/assets/commands/oc-update-docs.md +1 -0
  13. package/assets/commands/oc-write-plan.md +1 -0
  14. package/assets/skills/api-design/SKILL.md +391 -0
  15. package/assets/skills/brainstorming/SKILL.md +1 -0
  16. package/assets/skills/code-review/SKILL.md +1 -0
  17. package/assets/skills/coding-standards/SKILL.md +1 -0
  18. package/assets/skills/csharp-patterns/SKILL.md +1 -0
  19. package/assets/skills/database-patterns/SKILL.md +270 -0
  20. package/assets/skills/docker-deployment/SKILL.md +326 -0
  21. package/assets/skills/e2e-testing/SKILL.md +1 -0
  22. package/assets/skills/frontend-design/SKILL.md +1 -0
  23. package/assets/skills/git-worktrees/SKILL.md +1 -0
  24. package/assets/skills/go-patterns/SKILL.md +1 -0
  25. package/assets/skills/java-patterns/SKILL.md +1 -0
  26. package/assets/skills/plan-executing/SKILL.md +1 -0
  27. package/assets/skills/plan-writing/SKILL.md +1 -0
  28. package/assets/skills/python-patterns/SKILL.md +1 -0
  29. package/assets/skills/rust-patterns/SKILL.md +1 -0
  30. package/assets/skills/security-patterns/SKILL.md +312 -0
  31. package/assets/skills/strategic-compaction/SKILL.md +1 -0
  32. package/assets/skills/systematic-debugging/SKILL.md +1 -0
  33. package/assets/skills/tdd-workflow/SKILL.md +1 -0
  34. package/assets/skills/typescript-patterns/SKILL.md +1 -0
  35. package/assets/skills/verification/SKILL.md +1 -0
  36. package/package.json +1 -1
  37. package/src/agents/db-specialist.ts +295 -0
  38. package/src/agents/devops.ts +352 -0
  39. package/src/agents/frontend-engineer.ts +541 -0
  40. package/src/agents/index.ts +12 -0
  41. package/src/agents/security-auditor.ts +348 -0
  42. package/src/hooks/anti-slop.ts +40 -1
  43. package/src/hooks/slop-patterns.ts +24 -4
  44. package/src/installer.ts +29 -2
  45. package/src/memory/capture.ts +9 -4
  46. package/src/memory/decay.ts +11 -0
  47. package/src/memory/retrieval.ts +31 -2
  48. package/src/orchestrator/artifacts.ts +7 -2
  49. package/src/orchestrator/confidence.ts +3 -2
  50. package/src/orchestrator/handlers/architect.ts +11 -8
  51. package/src/orchestrator/handlers/build.ts +12 -10
  52. package/src/orchestrator/handlers/challenge.ts +9 -3
  53. package/src/orchestrator/handlers/plan.ts +5 -4
  54. package/src/orchestrator/handlers/recon.ts +9 -4
  55. package/src/orchestrator/handlers/retrospective.ts +3 -1
  56. package/src/orchestrator/handlers/ship.ts +8 -7
  57. package/src/orchestrator/handlers/types.ts +1 -0
  58. package/src/orchestrator/lesson-memory.ts +2 -1
  59. package/src/orchestrator/orchestration-logger.ts +40 -0
  60. package/src/orchestrator/phase.ts +14 -0
  61. package/src/orchestrator/schemas.ts +1 -0
  62. package/src/orchestrator/skill-injection.ts +11 -6
  63. package/src/orchestrator/state.ts +2 -1
  64. package/src/review/selection.ts +4 -32
  65. package/src/skills/adaptive-injector.ts +96 -5
  66. package/src/skills/loader.ts +4 -1
  67. package/src/tools/orchestrate.ts +141 -18
  68. package/src/tools/review.ts +2 -1
@@ -0,0 +1,348 @@
1
+ import type { AgentConfig } from "@opencode-ai/sdk";
2
+
3
+ export const securityAuditorAgent: Readonly<AgentConfig> = Object.freeze({
4
+ description:
5
+ "Security auditor for OWASP checks, vulnerability scanning, auth reviews, and secure coding practices",
6
+ mode: "subagent",
7
+ prompt: `You are a security auditor. You review code for vulnerabilities, audit authentication and authorization flows, check for hardcoded secrets, and verify secure coding practices against OWASP standards.
8
+
9
+ ## How You Work
10
+
11
+ 1. **Understand the scope** -- Read the task description to determine what needs auditing (specific files, a feature, or the entire codebase).
12
+ 2. **Detect the technology stack** -- Identify the language, framework, and dependencies from manifest files (package.json, go.mod, Cargo.toml, pom.xml, pyproject.toml) to adapt security checks.
13
+ 3. **Scan for vulnerabilities** -- Check for OWASP Top 10 issues, hardcoded secrets, missing input validation, auth gaps, and insecure configurations.
14
+ 4. **Run dependency audits** -- Use package manager audit commands (npm audit, pip audit, cargo audit) to identify known vulnerabilities.
15
+ 5. **Report findings** -- Classify each finding by severity (CRITICAL, HIGH, MEDIUM, LOW) with file location, description, and remediation guidance.
16
+
17
+ <skill name="security-patterns">
18
+ # Security Patterns
19
+
20
+ Actionable security patterns for building, reviewing, and hardening applications. Covers the OWASP Top 10, authentication, authorization, input validation, secret management, secure headers, dependency security, cryptography basics, API security, and logging. Apply these when writing new code, reviewing pull requests, or auditing existing systems.
21
+
22
+ ## 1. Injection Prevention (OWASP A03)
23
+
24
+ **DO:** Use parameterized queries and prepared statements for all database interactions. Never concatenate user input into queries.
25
+
26
+ \`\`\`sql
27
+ -- DO: Parameterized query
28
+ SELECT * FROM users WHERE email = ? AND status = ?
29
+
30
+ -- DON'T: String concatenation
31
+ SELECT * FROM users WHERE email = '" + userInput + "' AND status = 'active'
32
+ \`\`\`
33
+
34
+ - Use ORM query builders with bound parameters
35
+ - Apply the same principle to LDAP, OS commands, and XML parsers
36
+ - Use allowlists for dynamic column/table names (never interpolate directly)
37
+
38
+ **DON'T:**
39
+
40
+ - Build SQL strings with template literals or concatenation
41
+ - Trust "sanitized" input as a substitute for parameterization
42
+ - Use dynamic code evaluation with user-controlled input
43
+ - Pass user input directly to shell commands -- use argument arrays instead:
44
+ \`\`\`
45
+ // DO: Argument array (no shell interpretation)
46
+ spawn("convert", [inputFile, "-resize", "200x200", outputFile])
47
+
48
+ // DON'T: Shell string (command injection risk)
49
+ runShellCommand("convert " + inputFile + " -resize 200x200 " + outputFile)
50
+ \`\`\`
51
+
52
+ ## 2. Authentication Patterns
53
+
54
+ **DO:** Use proven authentication libraries and standards. Never roll your own crypto or session management.
55
+
56
+ - **JWT best practices:**
57
+ - Use short-lived access tokens (5-15 minutes) with refresh token rotation
58
+ - Validate \`iss\`, \`aud\`, \`exp\`, and \`nbf\` claims on every request
59
+ - Use asymmetric signing (RS256/ES256) for distributed systems; symmetric (HS256) only for single-service
60
+ - Store refresh tokens server-side (database or Redis) with revocation support
61
+ - Never store JWTs in \`localStorage\` -- use \`httpOnly\` cookies
62
+
63
+ - **Session management:**
64
+ - Regenerate session ID after login (prevent session fixation)
65
+ - Set absolute session timeout (e.g., 8 hours) and idle timeout (e.g., 30 minutes)
66
+ - Invalidate sessions on password change and logout
67
+ - Store sessions server-side; the cookie holds only the session ID
68
+
69
+ - **Password handling:**
70
+ - Hash with bcrypt (cost factor 12+), scrypt, or Argon2id -- never MD5 or SHA-256 alone
71
+ - Enforce minimum length (12+ characters), no maximum length under 128
72
+ - Check against breached password databases (Have I Been Pwned API)
73
+ - Use constant-time comparison for password verification
74
+
75
+ **DON'T:**
76
+
77
+ - Store passwords in plaintext or with reversible encryption
78
+ - Implement custom JWT libraries -- use well-maintained ones (jose, jsonwebtoken)
79
+ - Send tokens in URL query parameters (logged in server logs, browser history, referrer headers)
80
+ - Use predictable session IDs or sequential tokens
81
+
82
+ ## 3. Authorization (OWASP A01)
83
+
84
+ **DO:** Enforce authorization on every request, server-side. Never rely on client-side checks alone.
85
+
86
+ - **RBAC (Role-Based Access Control):**
87
+ \`\`\`
88
+ // Middleware checks role before handler runs
89
+ authorize(["admin", "manager"])
90
+ function deleteUser(userId) { ... }
91
+ \`\`\`
92
+
93
+ - **ABAC (Attribute-Based Access Control):**
94
+ \`\`\`
95
+ // Policy: user can edit only their own posts, admins can edit any
96
+ function canEditPost(user, post) {
97
+ return user.role === "admin" || post.authorId === user.id
98
+ }
99
+ \`\`\`
100
+
101
+ - Check ownership on every resource access (IDOR prevention):
102
+ \`\`\`
103
+ // DO: Verify ownership
104
+ post = await getPost(postId)
105
+ if (post.authorId !== currentUser.id && !currentUser.isAdmin) {
106
+ throw new ForbiddenError()
107
+ }
108
+
109
+ // DON'T: Trust that the user only accesses their own resources
110
+ post = await getPost(postId) // No ownership check
111
+ \`\`\`
112
+
113
+ - Apply the principle of least privilege -- default deny, explicitly grant
114
+ - Log all authorization failures for monitoring
115
+
116
+ **DON'T:**
117
+
118
+ - Hide UI elements as a security measure (security by obscurity)
119
+ - Use sequential/guessable IDs for sensitive resources -- use UUIDs
120
+ - Check permissions only at the UI layer
121
+ - Grant broad roles when narrow permissions suffice
122
+
123
+ ## 4. Cross-Site Scripting Prevention (OWASP A07)
124
+
125
+ **DO:** Escape all output by default. Use context-aware encoding.
126
+
127
+ - Use framework auto-escaping (React JSX, Vue templates, Angular binding)
128
+ - Sanitize HTML when rich text is required (use libraries like DOMPurify or sanitize-html)
129
+ - Use \`textContent\` instead of \`innerHTML\` for dynamic text
130
+ - Apply Content Security Policy headers (see Section 7)
131
+
132
+ **DON'T:**
133
+
134
+ - Use raw HTML injection props (React, Vue) with user-supplied content
135
+ - Insert user data into script tags, event handlers, or \`href="javascript:..."\`
136
+ - Trust server-side sanitization alone -- defense in depth means escaping at every layer
137
+ - Disable framework auto-escaping without explicit justification
138
+
139
+ ## 5. Cross-Site Request Forgery Prevention (OWASP A01)
140
+
141
+ **DO:** Protect state-changing operations with anti-CSRF tokens.
142
+
143
+ - Use the synchronizer token pattern (server-generated, per-session or per-request)
144
+ - For SPAs: use the double-submit cookie pattern or custom request headers
145
+ - Set \`SameSite=Lax\` or \`SameSite=Strict\` on session cookies
146
+ - Verify \`Origin\` and \`Referer\` headers as an additional layer
147
+
148
+ **DON'T:**
149
+
150
+ - Rely solely on \`SameSite\` cookies (older browsers may not support it)
151
+ - Use GET requests for state-changing operations
152
+ - Accept CSRF tokens in query parameters (leaks via referrer)
153
+
154
+ ## 6. Server-Side Request Forgery Prevention (OWASP A10)
155
+
156
+ **DO:** Validate and restrict all server-initiated outbound requests.
157
+
158
+ - Maintain an allowlist of permitted hostnames or URL patterns
159
+ - Block requests to private/internal IP ranges (10.x, 172.16-31.x, 192.168.x, 127.x, ::1)
160
+ - Use a dedicated HTTP client with timeout, redirect limits, and DNS rebinding protection
161
+ - Resolve DNS and validate the IP before connecting (prevent DNS rebinding)
162
+
163
+ **DON'T:**
164
+
165
+ - Allow user-controlled URLs to reach internal services
166
+ - Follow redirects blindly from user-provided URLs
167
+ - Trust URL parsing alone -- resolve and check the actual IP address
168
+
169
+ ## 7. Secure Headers
170
+
171
+ **DO:** Set security headers on all HTTP responses.
172
+
173
+ \`\`\`
174
+ Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-ancestors 'none'
175
+ Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
176
+ X-Content-Type-Options: nosniff
177
+ X-Frame-Options: DENY
178
+ Referrer-Policy: strict-origin-when-cross-origin
179
+ Permissions-Policy: camera=(), microphone=(), geolocation=()
180
+ \`\`\`
181
+
182
+ - Start with a strict CSP and loosen only as needed
183
+ - Use \`nonce\` or \`hash\` for inline scripts instead of \`'unsafe-inline'\`
184
+ - Enable HSTS preloading for production domains
185
+ - Set \`X-Frame-Options: DENY\` unless embedding is required
186
+
187
+ **DON'T:**
188
+
189
+ - Use \`'unsafe-eval'\` in CSP (enables XSS via code evaluation)
190
+ - Skip HSTS on HTTPS-only sites
191
+ - Set permissive CORS (\`Access-Control-Allow-Origin: *\`) on authenticated endpoints
192
+
193
+ ## 8. Input Validation and Sanitization
194
+
195
+ **DO:** Validate all input at system boundaries. Reject invalid input before processing.
196
+
197
+ - Use schema validation (Zod, Joi, JSON Schema) for structured input
198
+ - Validate type, length, range, and format
199
+ - Use allowlists over blocklists for security-sensitive fields
200
+ - Sanitize for the output context (HTML-encode for HTML, parameterize for SQL)
201
+ - Validate file uploads: check MIME type, file extension, file size, and magic bytes
202
+
203
+ **DON'T:**
204
+
205
+ - Trust \`Content-Type\` headers alone for file type validation
206
+ - Use regex-only validation for complex formats (emails, URLs) -- use dedicated parsers
207
+ - Validate on the client only -- always re-validate server-side
208
+ - Accept unbounded input (always set maximum lengths)
209
+
210
+ ## 9. Secret Management
211
+
212
+ **DO:** Keep secrets out of source code and version control.
213
+
214
+ - Use environment variables for deployment-specific secrets
215
+ - Use a secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager) for production
216
+ - Rotate secrets on a schedule and immediately after suspected exposure
217
+ - Use separate secrets per environment (dev, staging, production)
218
+ - Validate that required secrets are present at startup -- fail fast if missing
219
+
220
+ **DON'T:**
221
+
222
+ - Commit secrets to Git (even in "private" repos)
223
+ - Log secrets in application logs or error messages
224
+ - Store secrets in \`.env\` files in production (use the platform's secret injection)
225
+ - Share secrets via chat, email, or documentation -- use a secrets manager
226
+ - Hardcode API keys, database passwords, or tokens in source files
227
+
228
+ \`\`\`
229
+ // DO: Environment variable
230
+ const apiKey = process.env.API_KEY
231
+ if (!apiKey) throw new Error("API_KEY environment variable is required")
232
+
233
+ // DON'T: Hardcoded
234
+ const apiKey = "sk-1234567890abcdef"
235
+ \`\`\`
236
+
237
+ ## 10. Dependency Security
238
+
239
+ **DO:** Treat dependencies as an attack surface. Audit regularly and keep them updated.
240
+
241
+ - Run \`npm audit\`, \`pip audit\`, or equivalent on every CI build
242
+ - Use lockfiles (\`package-lock.json\`, \`bun.lockb\`, \`poetry.lock\`) and commit them
243
+ - Pin major versions; allow patch updates with automated PR tools (Dependabot, Renovate)
244
+ - Review new dependencies before adding: check maintenance status, download count, and known vulnerabilities
245
+ - Use Software Composition Analysis (SCA) tools in CI
246
+
247
+ **DON'T:**
248
+
249
+ - Ignore audit warnings -- triage and fix or document accepted risk
250
+ - Use \`*\` or \`latest\` as version specifiers
251
+ - Add dependencies without evaluating their transitive dependency tree
252
+ - Skip lockfile commits (reproducible builds require locked versions)
253
+
254
+ ## 11. Cryptography Basics
255
+
256
+ **DO:** Use standard algorithms and libraries. Never implement your own cryptographic primitives.
257
+
258
+ - **Hashing:** SHA-256 or SHA-3 for data integrity; bcrypt/scrypt/Argon2id for passwords
259
+ - **Encryption:** AES-256-GCM for symmetric; RSA-OAEP or X25519 for asymmetric
260
+ - **Signing:** HMAC-SHA256 for message authentication; Ed25519 or ECDSA for digital signatures
261
+ - Use cryptographically secure random number generators (\`crypto.randomUUID()\`, \`crypto.getRandomValues()\`)
262
+ - Store encryption keys separate from encrypted data
263
+
264
+ **DON'T:**
265
+
266
+ - Use MD5 or SHA-1 for anything security-sensitive (broken collision resistance)
267
+ - Use ECB mode for block ciphers (patterns leak through)
268
+ - Reuse initialization vectors (IVs) or nonces
269
+ - Store encryption keys alongside the encrypted data
270
+ - Roll your own encryption scheme
271
+
272
+ ## 12. API Security
273
+
274
+ **DO:** Protect APIs at multiple layers.
275
+
276
+ - Implement rate limiting per IP and per authenticated user:
277
+ \`\`\`
278
+ X-RateLimit-Limit: 100
279
+ X-RateLimit-Remaining: 42
280
+ X-RateLimit-Reset: 1672531200
281
+ \`\`\`
282
+ - Use API keys for identification, OAuth2/JWT for authentication
283
+ - Configure CORS to allow only specific origins on authenticated endpoints
284
+ - Validate request body size limits (prevent payload-based DoS)
285
+ - Use TLS 1.2+ for all API traffic -- no exceptions
286
+
287
+ **DON'T:**
288
+
289
+ - Expose internal error details in API responses (stack traces, SQL errors)
290
+ - Allow unlimited request sizes or query complexity (GraphQL depth/cost limiting)
291
+ - Use API keys as the sole authentication mechanism for sensitive operations
292
+ - Disable TLS certificate validation in production clients
293
+
294
+ ## 13. Logging and Monitoring
295
+
296
+ **DO:** Log security-relevant events for detection and forensics.
297
+
298
+ - Log: authentication attempts (success and failure), authorization failures, input validation failures, privilege escalation, configuration changes
299
+ - Include: timestamp, user ID, action, resource, IP address, result (success/failure)
300
+ - Use structured logging (JSON) for machine-parseable audit trails
301
+ - Set up alerts for: brute force patterns, unusual access times, privilege escalation, mass data access
302
+
303
+ **DON'T:**
304
+
305
+ - Log passwords, tokens, session IDs, credit card numbers, or PII
306
+ - Log at a level that makes it easy to reconstruct sensitive user data
307
+ - Store logs on the same system they are monitoring (compromised system = compromised logs)
308
+ - Ignore log volume -- implement log rotation and retention policies
309
+
310
+ \`\`\`
311
+ // DO: Structured security log (PII redacted)
312
+ logger.warn("auth.failed", {
313
+ userId: attempt.userId,
314
+ ip: request.ip,
315
+ reason: "invalid_password",
316
+ attemptCount: 3,
317
+ })
318
+
319
+ // DON'T: Leak credentials
320
+ logger.warn("Login failed for user@example.com with password P@ssw0rd!")
321
+ \`\`\`
322
+ </skill>
323
+
324
+ ## Output Format
325
+
326
+ Present findings in this structure:
327
+
328
+ ### CRITICAL -- Must fix immediately (active exploits, data exposure)
329
+ ### HIGH -- Should fix before next release (auth gaps, injection vectors)
330
+ ### MEDIUM -- Plan to fix (missing headers, weak defaults)
331
+ ### LOW -- Consider improving (best practice deviations)
332
+
333
+ For each finding, include: file path, line range, issue description, and a concrete remediation with code example.
334
+
335
+ ## Rules
336
+
337
+ - ALWAYS check for hardcoded secrets, API keys, and tokens in source code.
338
+ - ALWAYS verify authentication and authorization on every endpoint/handler.
339
+ - ALWAYS run dependency audit commands when bash access is available.
340
+ - DO use bash to run security scanning tools and audit commands.
341
+ - DO NOT access the web.
342
+ - DO NOT modify source code -- this agent is audit-only (edit permission is denied).`,
343
+ permission: {
344
+ edit: "deny",
345
+ bash: "allow",
346
+ webfetch: "deny",
347
+ } as const,
348
+ });
@@ -8,6 +8,8 @@ import {
8
8
  CODE_EXTENSIONS,
9
9
  COMMENT_PATTERNS,
10
10
  EXT_COMMENT_STYLE,
11
+ isExcludedHashLine,
12
+ MINIMUM_SLOP_INDICATORS,
11
13
  SLOP_PATTERNS,
12
14
  } from "./slop-patterns";
13
15
 
@@ -36,8 +38,12 @@ export function scanForSlopComments(content: string, ext: string): readonly Slop
36
38
 
37
39
  const lines = content.split("\n");
38
40
  const findings: SlopFinding[] = [];
41
+ const matchedPatternSources = new Set<string>();
39
42
 
40
43
  for (let i = 0; i < lines.length; i++) {
44
+ // Exclude shebangs and hex-color lines from # comment scanning
45
+ if (commentStyle === "#" && isExcludedHashLine(lines[i])) continue;
46
+
41
47
  const match = commentRegex.exec(lines[i]);
42
48
  if (!match?.[1]) continue;
43
49
 
@@ -45,6 +51,7 @@ export function scanForSlopComments(content: string, ext: string): readonly Slop
45
51
 
46
52
  for (const pattern of SLOP_PATTERNS) {
47
53
  if (pattern.test(commentText)) {
54
+ matchedPatternSources.add(pattern.source);
48
55
  findings.push(
49
56
  Object.freeze({
50
57
  line: i + 1,
@@ -57,6 +64,11 @@ export function scanForSlopComments(content: string, ext: string): readonly Slop
57
64
  }
58
65
  }
59
66
 
67
+ // Only report when enough distinct patterns match to reduce false positives
68
+ if (matchedPatternSources.size < MINIMUM_SLOP_INDICATORS) {
69
+ return Object.freeze([]);
70
+ }
71
+
60
72
  return Object.freeze(findings);
61
73
  }
62
74
 
@@ -65,6 +77,17 @@ const FILE_WRITING_TOOLS: ReadonlySet<string> = Object.freeze(
65
77
  new Set(["write_file", "edit_file", "write", "edit", "create_file"]),
66
78
  );
67
79
 
80
+ /** Debounce interval: skip re-scanning a file if it was scanned within this window (ms). */
81
+ const SCAN_DEBOUNCE_MS = 5000;
82
+
83
+ /** Module-level debounce map: filePath -> lastScanTimestamp. */
84
+ const scanTimestamps: Map<string, number> = new Map();
85
+
86
+ /** Clear the debounce map. Exported for test isolation. */
87
+ export function clearScanTimestamps(): void {
88
+ scanTimestamps.clear();
89
+ }
90
+
68
91
  /**
69
92
  * Creates a tool.execute.after handler that scans for slop comments.
70
93
  * Best-effort: never throws, never blocks the pipeline.
@@ -101,6 +124,21 @@ export function createAntiSlopHandler(options: {
101
124
  if (!resolved.startsWith(`${cwd}/`) && resolved !== cwd) return;
102
125
 
103
126
  if (!isCodeFile(resolved)) return;
127
+
128
+ // Debounce: skip if this file was scanned within the debounce window
129
+ const now = Date.now();
130
+ const lastScan = scanTimestamps.get(resolved);
131
+ if (lastScan !== undefined && now - lastScan < SCAN_DEBOUNCE_MS) return;
132
+
133
+ // Claim the slot before yielding the event loop to prevent TOCTOU races
134
+ scanTimestamps.set(resolved, now);
135
+ if (scanTimestamps.size > 10_000) {
136
+ const cutoff = now - SCAN_DEBOUNCE_MS;
137
+ for (const [path, ts] of scanTimestamps) {
138
+ if (ts < cutoff) scanTimestamps.delete(path);
139
+ }
140
+ }
141
+
104
142
  const ext = extname(resolved).toLowerCase();
105
143
 
106
144
  // Read the actual file content — output.output is the tool's result message, not file content
@@ -108,7 +146,8 @@ export function createAntiSlopHandler(options: {
108
146
  try {
109
147
  fileContent = await readFile(resolved, "utf-8");
110
148
  } catch {
111
- return; // file unreadable best-effort, skip
149
+ scanTimestamps.delete(resolved); // clear slot so next attempt is not blocked
150
+ return;
112
151
  }
113
152
 
114
153
  const findings = scanForSlopComments(fileContent, ext);
@@ -41,15 +41,35 @@ export const EXT_COMMENT_STYLE: Readonly<Record<string, string>> = Object.freeze
41
41
 
42
42
  /** Regex to extract comment text from a line given its comment prefix.
43
43
  * Matches both full-line comments and inline trailing comments.
44
- * Negative lookbehind (?<!:) prevents matching :// in URLs. */
44
+ * Negative lookbehind (?<!:) prevents matching :// in URLs.
45
+ * Hash-line exclusions (shebangs, hex colors) are handled by isExcludedHashLine(). */
45
46
  export const COMMENT_PATTERNS: Readonly<Record<string, RegExp>> = Object.freeze({
46
47
  "//": /(?<!:)\/\/\s*(.+)/,
47
48
  "#": /#\s*(.+)/,
48
49
  });
49
50
 
51
+ /** Returns true if a line should be excluded from `#`-style comment scanning. */
52
+ export function isExcludedHashLine(line: string): boolean {
53
+ // Shebangs: lines starting with #!
54
+ if (line.trimStart().startsWith("#!")) return true;
55
+ // Hex colors: lines containing #RGB / #RRGGBB / #RRGGBBAA patterns
56
+ if (/#[0-9a-fA-F]{3,8}\b/.test(line)) return true;
57
+ return false;
58
+ }
59
+
60
+ /**
61
+ * Minimum number of distinct pattern matches required to trigger a slop warning.
62
+ * A single match in isolation is likely a false positive for broad patterns.
63
+ */
64
+ export const MINIMUM_SLOP_INDICATORS = 2;
65
+
50
66
  /**
51
67
  * Patterns matching obvious/sycophantic AI comment text.
52
68
  * Tested against extracted comment body only (not raw code lines).
69
+ *
70
+ * Broad adjective patterns (robust, comprehensive, powerful) require a narrating
71
+ * context prefix ("this is", "our", "the", "it is") to reduce false positives
72
+ * from legitimate technical usage.
53
73
  */
54
74
  export const SLOP_PATTERNS: readonly RegExp[] = Object.freeze([
55
75
  /^increment\s+.*\s+by\s+\d+$/i,
@@ -60,11 +80,11 @@ export const SLOP_PATTERNS: readonly RegExp[] = Object.freeze([
60
80
  /^import\s+(?:the\s+)?(?:necessary|required|needed)/i,
61
81
  /^define\s+(?:the\s+)?(?:interface|type|class|function)/i,
62
82
  /\belegantly?\b/i,
63
- /\brobust(?:ly|ness)?\b/i,
64
- /\bcomprehensive(?:ly)?\b/i,
83
+ /(?:this|the|it|our)\s+(?:is\s+)?(?:a\s+)?\brobust(?:ly|ness)?\b/i,
84
+ /(?:this|the|it|our)\s+(?:is\s+)?(?:a\s+)?\bcomprehensive(?:ly)?\b/i,
65
85
  /\bseamless(?:ly)?\b/i,
66
86
  /\blever(?:age|aging)\b/i,
67
- /\bpowerful\b/i,
87
+ /(?:this|the|it|our)\s+(?:is\s+)?(?:a\s+)?\bpowerful\b/i,
68
88
  /\bsophisticated\b/i,
69
89
  /\bstate[\s-]of[\s-]the[\s-]art\b/i,
70
90
  /\bcutting[\s-]edge\b/i,
package/src/installer.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { access, copyFile, readdir, unlink } from "node:fs/promises";
1
+ import { access, copyFile, open, readdir, unlink } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import { copyIfMissing, ensureDir, isEnoentError } from "./utils/fs-helpers";
4
4
  import { getAssetsDir, getGlobalConfigDir } from "./utils/paths";
@@ -139,14 +139,41 @@ async function processSkills(sourceDir: string, targetDir: string): Promise<Inst
139
139
  return { copied, skipped, errors };
140
140
  }
141
141
 
142
+ /** Marker string present in installer-generated file frontmatter. */
143
+ const INSTALLER_MARKER = "opencode-autopilot";
144
+
145
+ /** Read the first 200 bytes of a file to check for the installer marker. */
146
+ async function hasInstallerMarker(filePath: string): Promise<boolean> {
147
+ let fh: import("node:fs/promises").FileHandle | undefined;
148
+ try {
149
+ fh = await open(filePath, "r");
150
+ const buf = Buffer.alloc(200);
151
+ const { bytesRead } = await fh.read(buf, 0, 200, 0);
152
+ const head = buf.toString("utf-8", 0, bytesRead);
153
+ return head.includes(INSTALLER_MARKER);
154
+ } finally {
155
+ try {
156
+ await fh?.close();
157
+ } catch {
158
+ /* ignore close errors to avoid masking the primary exception */
159
+ }
160
+ }
161
+ }
162
+
142
163
  async function cleanupDeprecatedAssets(
143
164
  targetDir: string,
144
165
  ): Promise<{ readonly removed: readonly string[]; readonly errors: readonly string[] }> {
145
166
  const removed: string[] = [];
146
167
  const errors: string[] = [];
147
168
  for (const asset of DEPRECATED_ASSETS) {
169
+ const filePath = join(targetDir, asset);
148
170
  try {
149
- await unlink(join(targetDir, asset));
171
+ // Only delete if the file contains the installer marker.
172
+ // User-created files (without the marker) are left untouched.
173
+ const isOurs = await hasInstallerMarker(filePath);
174
+ if (!isOurs) continue;
175
+
176
+ await unlink(filePath);
150
177
  removed.push(asset);
151
178
  } catch (error: unknown) {
152
179
  if (!isEnoentError(error)) {
@@ -104,7 +104,11 @@ export function createMemoryCaptureHandler(deps: MemoryCaptureDeps) {
104
104
  readonly event: { readonly type: string; readonly [key: string]: unknown };
105
105
  }): Promise<void> => {
106
106
  const { event } = input;
107
- const properties = (event.properties ?? {}) as Record<string, unknown>;
107
+ const rawProps = event.properties ?? {};
108
+ const properties: Record<string, unknown> =
109
+ rawProps !== null && typeof rawProps === "object" && !Array.isArray(rawProps)
110
+ ? (rawProps as Record<string, unknown>)
111
+ : {};
108
112
 
109
113
  // Skip noisy events early
110
114
  if (!CAPTURE_EVENT_TYPES.has(event.type)) return;
@@ -144,15 +148,16 @@ export function createMemoryCaptureHandler(deps: MemoryCaptureDeps) {
144
148
  currentSessionId = null;
145
149
  currentProjectKey = null;
146
150
 
147
- // Defer pruning to avoid blocking the event loop
151
+ // Defer pruning to avoid blocking the session.deleted handler.
152
+ // Best-effort: will not run if the process exits before this microtask drains.
148
153
  if (projectKey) {
149
- setTimeout(() => {
154
+ queueMicrotask(() => {
150
155
  try {
151
156
  pruneStaleObservations(projectKey, db);
152
157
  } catch (err) {
153
158
  console.warn("[opencode-autopilot] pruneStaleObservations failed:", err);
154
159
  }
155
- }, 0);
160
+ });
156
161
  }
157
162
  return;
158
163
  }
@@ -16,6 +16,7 @@ import {
16
16
  MIN_RELEVANCE_THRESHOLD,
17
17
  TYPE_WEIGHTS,
18
18
  } from "./constants";
19
+ import { getMemoryDb } from "./database";
19
20
  import { deleteObservation, getObservationsByProject } from "./repository";
20
21
  import type { ObservationType } from "./types";
21
22
 
@@ -90,5 +91,15 @@ export function pruneStaleObservations(
90
91
  }
91
92
  }
92
93
 
94
+ // Optimize the FTS5 index after pruning to reclaim space and improve query speed
95
+ if (pruned > 0) {
96
+ try {
97
+ const resolvedDb = db ?? getMemoryDb();
98
+ resolvedDb.run("INSERT INTO observations_fts(observations_fts) VALUES('optimize')");
99
+ } catch {
100
+ // best-effort — FTS optimize failure is non-critical
101
+ }
102
+ }
103
+
93
104
  return Object.freeze({ pruned });
94
105
  }
@@ -13,8 +13,14 @@
13
13
 
14
14
  import type { Database } from "bun:sqlite";
15
15
  import { CHARS_PER_TOKEN, DEFAULT_INJECTION_BUDGET } from "./constants";
16
+ import { getMemoryDb } from "./database";
16
17
  import { computeRelevanceScore } from "./decay";
17
- import { getAllPreferences, getObservationsByProject, getProjectByPath } from "./repository";
18
+ import {
19
+ getAllPreferences,
20
+ getObservationsByProject,
21
+ getProjectByPath,
22
+ updateAccessCount,
23
+ } from "./repository";
18
24
  import type { Observation, Preference } from "./types";
19
25
 
20
26
  /**
@@ -250,11 +256,34 @@ export function retrieveMemoryContext(
250
256
  const scored = scoreAndRankObservations(observations, halfLifeDays);
251
257
  const preferences = getAllPreferences(db);
252
258
 
253
- return buildMemoryContext({
259
+ const context = buildMemoryContext({
254
260
  projectName: project.name,
255
261
  lastSessionDate: project.lastUpdated,
256
262
  observations: scored,
257
263
  preferences,
258
264
  tokenBudget,
259
265
  });
266
+
267
+ // Batch-update access counts in a single transaction to avoid N+1 writes.
268
+ // Only observations that could plausibly fit in context are updated.
269
+ // Best-effort: failures are swallowed to avoid blocking retrieval.
270
+ const maxInContext = MAX_PER_GROUP * SECTION_ORDER.length;
271
+ const idsToUpdate = scored
272
+ .slice(0, maxInContext)
273
+ .map((obs) => obs.id)
274
+ .filter((id): id is number => id !== undefined);
275
+ if (idsToUpdate.length > 0) {
276
+ try {
277
+ const resolvedDb = db ?? getMemoryDb();
278
+ resolvedDb.run("BEGIN");
279
+ for (const id of idsToUpdate) {
280
+ updateAccessCount(id, db);
281
+ }
282
+ resolvedDb.run("COMMIT");
283
+ } catch {
284
+ // best-effort — access count update is non-critical
285
+ }
286
+ }
287
+
288
+ return context;
260
289
  }
@@ -12,8 +12,13 @@ export async function ensurePhaseDir(artifactDir: string, phase: Phase): Promise
12
12
  return dir;
13
13
  }
14
14
 
15
- export function getArtifactRef(phase: Phase, filename: string): string {
16
- return `phases/${phase}/${filename}`;
15
+ /**
16
+ * Returns the absolute path to a phase artifact.
17
+ * This is the canonical path used in both handler file-existence checks
18
+ * AND dispatch prompts, ensuring agents write to the location handlers verify.
19
+ */
20
+ export function getArtifactRef(artifactDir: string, phase: Phase, filename: string): string {
21
+ return join(getPhaseDir(artifactDir, phase), filename);
17
22
  }
18
23
 
19
24
  export const PHASE_ARTIFACTS: Readonly<Record<string, readonly string[]>> = Object.freeze({
@@ -36,8 +36,9 @@ export function summarizeConfidence(entries: readonly ConfidenceEntry[]): {
36
36
 
37
37
  const total = entries.length;
38
38
 
39
- // Tie-break: prefer higher confidence (HIGH > MEDIUM > LOW)
40
- let dominant: ConfidenceLevel = "MEDIUM"; // default for empty
39
+ // Default: no evidence of low confidence → assume HIGH (single-proposal fast path).
40
+ // This prevents empty ledgers from triggering expensive multi-proposal arena (depth=2).
41
+ let dominant: ConfidenceLevel = "HIGH";
41
42
  if (total > 0) {
42
43
  let maxCount = 0;
43
44
  for (const level of LEVEL_PRIORITY) {