opencode-wait-a-minute 0.0.0-stage → 1.1.0-rc.10

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 (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +318 -2
  3. package/index.js +1836 -0
  4. package/package.json +70 -4
  5. package/skills/codebase-design/SKILL.md +114 -0
  6. package/skills/design-contract/SKILL.md +27 -0
  7. package/skills/design-critique/SKILL.md +19 -0
  8. package/skills/design-system/SKILL.md +17 -0
  9. package/skills/design-system/references/image-analysis.md +14 -0
  10. package/skills/design-system/references/tokens.md +32 -0
  11. package/skills/design-system/references/ui-rules.md +14 -0
  12. package/skills/diagnosing-bugs/SKILL.md +138 -0
  13. package/skills/frontend-design/SKILL.md +36 -0
  14. package/skills/handoff/SKILL.md +16 -0
  15. package/skills/improve-codebase-architecture/SKILL.md +71 -0
  16. package/skills/linter-negative/SKILL.md +74 -0
  17. package/skills/no-slop-ui/SKILL.md +26 -0
  18. package/skills/prototype/SKILL.md +26 -0
  19. package/skills/registry.json +44087 -0
  20. package/skills/tastemaker/SKILL.md +8 -0
  21. package/skills/tastemaker/references/tastemaker.md +14 -0
  22. package/skills/tdd/SKILL.md +38 -0
  23. package/skills/visual-review/SKILL.md +103 -0
  24. package/skills/visual-review/references/review-gates.md +42 -0
  25. package/skills/visual-review/references/surface-archetypes.md +37 -0
  26. package/skills/wizard/SKILL.md +44 -0
  27. package/skills/writing-for-agents/SKILL.md +81 -0
  28. package/src/cognition/cognition-store.js +251 -0
  29. package/src/cognition/cognitive-state.js +126 -0
  30. package/src/cognition/hypothesis-manager.js +113 -0
  31. package/src/cognition/observation-engine.js +112 -0
  32. package/src/cognition/observation-provenance.js +268 -0
  33. package/src/context/active-context-boundary.js +68 -0
  34. package/src/context/assembly.js +494 -0
  35. package/src/context/assessment-rules.js +55 -0
  36. package/src/context/capsule-staleness.js +267 -0
  37. package/src/context/capsules.js +2 -0
  38. package/src/context/compare-array.js +98 -0
  39. package/src/context/compare-object.js +89 -0
  40. package/src/context/compare-values.js +42 -0
  41. package/src/context/context-assembly-contract.js +71 -0
  42. package/src/context/context-budget-manager.js +232 -0
  43. package/src/context/context-builder.js +49 -0
  44. package/src/context/context-cache.js +55 -0
  45. package/src/context/context-capture.js +243 -0
  46. package/src/context/context-classification.js +199 -0
  47. package/src/context/context-compaction.js +179 -0
  48. package/src/context/context-decision-audit.js +267 -0
  49. package/src/context/context-demotion.js +173 -0
  50. package/src/context/context-dictionary.js +106 -0
  51. package/src/context/context-drift-detection.js +225 -0
  52. package/src/context/context-evaluation.js +374 -0
  53. package/src/context/context-event-ingress.js +100 -0
  54. package/src/context/context-event-normalization.js +224 -0
  55. package/src/context/context-feedback-loop.js +216 -0
  56. package/src/context/context-freshness.js +21 -0
  57. package/src/context/context-graph-builder.js +71 -0
  58. package/src/context/context-graph.js +451 -0
  59. package/src/context/context-history-relevance.js +321 -0
  60. package/src/context/context-interception.js +145 -0
  61. package/src/context/context-invalidation-events.js +154 -0
  62. package/src/context/context-lifecycle.js +170 -0
  63. package/src/context/context-loop-prevention.js +347 -0
  64. package/src/context/context-manager.js +340 -0
  65. package/src/context/context-memory-layers.js +265 -0
  66. package/src/context/context-minimality.js +166 -0
  67. package/src/context/context-normalization.js +130 -0
  68. package/src/context/context-optimization-metrics.js +434 -0
  69. package/src/context/context-output-policy.js +135 -0
  70. package/src/context/context-pack-authority.js +157 -0
  71. package/src/context/context-promotion.js +172 -0
  72. package/src/context/context-query-contract.js +101 -0
  73. package/src/context/context-ranking.js +164 -0
  74. package/src/context/context-rate-distortion.js +261 -0
  75. package/src/context/context-recovery.js +209 -0
  76. package/src/context/context-retrieval-integration.js +60 -0
  77. package/src/context/context-retrieval.js +93 -0
  78. package/src/context/context-router.js +457 -0
  79. package/src/context/context-routing-evaluation.js +331 -0
  80. package/src/context/context-routing-observability.js +296 -0
  81. package/src/context/context-routing-shadow.js +237 -0
  82. package/src/context/context-scope.js +181 -0
  83. package/src/context/context-snapshot.js +226 -0
  84. package/src/context/context-source-registry.js +95 -0
  85. package/src/context/context-sufficiency-gate.js +120 -0
  86. package/src/context/context-sufficiency-oracle.js +167 -0
  87. package/src/context/context-telemetry.js +176 -0
  88. package/src/context/context.js +633 -0
  89. package/src/context/dependency-closure.js +246 -0
  90. package/src/context/page-fault-tracker.js +275 -0
  91. package/src/context/prompt-engine.js +74 -0
  92. package/src/context/runtime-context-graph.js +260 -0
  93. package/src/context/selection.js +2 -0
  94. package/src/context/session.js +2 -0
  95. package/src/context/sufficiency-contract.js +300 -0
  96. package/src/context/tokenizer.js +17 -0
  97. package/src/context-benchmark-router.mjs +125 -0
  98. package/src/evidence/evidence-engine.js +161 -0
  99. package/src/evidence/evidence-factory.js +3 -0
  100. package/src/evidence/evidence-freshness.js +463 -0
  101. package/src/evidence/evidence-gap.js +76 -0
  102. package/src/evidence/evidence-lineage.js +525 -0
  103. package/src/evidence/evidence.js +136 -0
  104. package/src/evidence/freshness-validation.js +187 -0
  105. package/src/evidence/lineage.js +4 -0
  106. package/src/execution/action-evaluation.js +179 -0
  107. package/src/execution/circuit-breaker.js +70 -0
  108. package/src/execution/claim-interception.js +63 -0
  109. package/src/execution/execution-engine.js +258 -0
  110. package/src/execution/execution-intent.js +69 -0
  111. package/src/execution/execution-state.js +89 -0
  112. package/src/execution/formatting.js +60 -0
  113. package/src/execution/l1-sanitize.js +16 -0
  114. package/src/execution/l2-resource-guards.js +19 -0
  115. package/src/execution/l3-invariant-checker.js +5 -0
  116. package/src/execution/l4-containment.js +6 -0
  117. package/src/execution/runtime-guards.js +106 -0
  118. package/src/execution/strategy-identity.js +162 -0
  119. package/src/execution/tool-interceptor.js +7 -0
  120. package/src/execution/tool-result-classifier.js +63 -0
  121. package/src/integration/canary-deploy.js +143 -0
  122. package/src/integration/collector.js +59 -0
  123. package/src/integration/error-recovery.js +6 -0
  124. package/src/integration/event-bus.js +66 -0
  125. package/src/integration/health-checker.js +57 -0
  126. package/src/integration/logger.js +39 -0
  127. package/src/integration/message-handler.js +611 -0
  128. package/src/integration/opencode-db.js +153 -0
  129. package/src/integration/orchestration.js +419 -0
  130. package/src/integration/part-provenance.js +65 -0
  131. package/src/integration/rate-limiter.js +55 -0
  132. package/src/integration/release-gate.js +336 -0
  133. package/src/integration/rollback-manager.js +164 -0
  134. package/src/integration/router-adapter.js +269 -0
  135. package/src/integration/telemetry.js +45 -0
  136. package/src/integration/update-checker.js +10 -0
  137. package/src/persistence/event-sourcing.js +75 -0
  138. package/src/persistence/memory.js +501 -0
  139. package/src/persistence/migration-runner.js +16 -0
  140. package/src/persistence/persistence-manager.js +39 -0
  141. package/src/persistence/transaction-log.js +157 -0
  142. package/src/policy/action-capabilities.js +116 -0
  143. package/src/policy/action-policy.js +4 -0
  144. package/src/policy/assessment-engine.js +104 -0
  145. package/src/policy/assumption-tracking.js +66 -0
  146. package/src/policy/budget-enforcer.js +7 -0
  147. package/src/policy/config-validator.js +102 -0
  148. package/src/policy/execution-assessment.js +142 -0
  149. package/src/policy/failure-classification.js +5 -0
  150. package/src/policy/feature-flags.js +65 -0
  151. package/src/policy/governance-enforcement.js +118 -0
  152. package/src/policy/locale.js +15 -0
  153. package/src/policy/multi-signer.js +151 -0
  154. package/src/policy/policy-state-machine.js +97 -0
  155. package/src/policy/ponytail-constraint.js +38 -0
  156. package/src/policy/project-inspector.js +93 -0
  157. package/src/policy/query-builder.js +14 -0
  158. package/src/policy/request-classifier.js +79 -0
  159. package/src/policy/risk-engine.js +266 -0
  160. package/src/policy/scope-enforcement.js +140 -0
  161. package/src/policy/secret-scanner.js +68 -0
  162. package/src/policy/skill-routing.js +104 -0
  163. package/src/policy/strategy-capabilities.js +211 -0
  164. package/src/policy/strategy.js +6 -0
  165. package/src/policy/uncertainty.js +117 -0
  166. package/src/shared/container.js +44 -0
  167. package/src/shared/decision-types.js +24 -0
  168. package/src/shared/simple-test.js +160 -0
  169. package/src/shared/wam-events.js +52 -0
  170. package/src/skills/engine.js +1705 -0
  171. package/src/skills/skill-registry.js +19 -0
  172. package/src/skills/skill-routing.js +340 -0
  173. package/src/state/lifecycle-manager.js +113 -0
  174. package/src/state/operational-state.js +198 -0
  175. package/src/state/projections.js +47 -0
  176. package/src/state/replay-engine.js +5 -0
  177. package/src/state/requirement-ownership.js +20 -0
  178. package/src/state/requirement-state.js +152 -0
  179. package/src/state/run-event-identity.js +172 -0
  180. package/src/state/state-engine.js +19 -0
  181. package/src/state/state-machine.js +119 -0
  182. package/src/state/state-schema.js +318 -0
  183. package/src/state/state-store.js +335 -0
  184. package/src/state/state-transition.js +84 -0
  185. package/src/state/task-dependencies.js +287 -0
  186. package/src/state/task-execution.js +217 -0
  187. package/src/state/task-lifecycle.js +59 -0
  188. package/src/state/task-runs.js +361 -0
  189. package/src/state/task-state-file.js +86 -0
  190. package/src/state/task-state.js +41 -0
  191. package/src/state/task-status.js +54 -0
  192. package/src/state/task-store.js +48 -0
  193. package/src/state/wam-state.js +148 -0
  194. package/src/verification/completion-gate-simple.js +8 -0
  195. package/src/verification/completion-gate.js +183 -0
  196. package/src/verification/contract.js +9 -0
  197. package/src/verification/contradiction-resolution.js +255 -0
  198. package/src/verification/diagnostic-engine.js +659 -0
  199. package/src/verification/false-completion-prevention.js +100 -0
  200. package/src/verification/hook-validator.js +6 -0
  201. package/src/verification/requirements.js +6 -0
  202. package/src/verification/verification-context.js +162 -0
  203. package/src/verification/verification-lifecycle.js +66 -0
  204. package/src/verification/verification-model.js +89 -0
  205. package/src/verification/verification-persistence.js +129 -0
  206. package/src/verification/verification-policy.js +127 -0
  207. package/src/verification/verification.js +415 -0
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Action Risk Envelope — Runtime risk classification for agent actions.
3
+ *
4
+ * Implementa spec: action-risk-envelope.
5
+ *
6
+ * Categorías:
7
+ * - SAFE: Read-only o efectos reversibles mínimos. Autónoma.
8
+ * - GUARDED: Mutaciones bounded y reversibles. Autónoma si está dentro del scope.
9
+ * - BLOCKED: Destructivo, irreversible, o sensible. Requiere autorización explícita.
10
+ * - UNKNOWN: herramienta no catalogada. Se trata como BLOCKED (fail-closed).
11
+ *
12
+ * Las capabilities explícitas viven en `src/policy/action-capabilities.js`.
13
+ * Aquí solo se aplica la evaluación contextual (path, scope, contenido del comando).
14
+ */
15
+
16
+ import nodePath from "node:path";
17
+ import nodeFs from "node:fs";
18
+ import {
19
+ CAPABILITY_LEVELS,
20
+ getEffectiveCapability,
21
+ isMutatingCapability,
22
+ } from "./action-capabilities.js";
23
+
24
+ const PROTECTED_PATHS = [
25
+ "/etc/", "/root/", "/boot/", "/sys/", "/proc/",
26
+ "/var/lib/", "/usr/bin/", "/usr/sbin/",
27
+ "C:\\Windows", "C:\\System32",
28
+ ];
29
+
30
+ function isProtectedPath(path) {
31
+ if (!path || typeof path !== "string") return false;
32
+ return PROTECTED_PATHS.some((p) => path.startsWith(p) || path.includes(p));
33
+ }
34
+
35
+ /**
36
+ * Canonicalize and resolve a path against the task root.
37
+ *
38
+ * Estrategia:
39
+ * 1. Si la ruta es absoluta, se resuelve contra el cwd.
40
+ * 2. Si es relativa y hay taskRoot, se resuelve contra taskRoot.
41
+ * 3. Si el path resuelto existe en disco → se usa `fs.realpathSync` para
42
+ * resolver symlinks (un symlink que apunta fuera del scope se bloquea).
43
+ * 4. Si el path NO existe todavía (caso típico: write de archivo nuevo) →
44
+ * se usa el realpath del directorio padre. Esto evita que un symlink en
45
+ * un directorio padre redirija el archivo fuera del scope.
46
+ * 5. Si el padre tampoco existe → se devuelve la ruta absoluta resuelta
47
+ * (resolución lexical), suficiente para validar prefijo.
48
+ *
49
+ * Returns the absolute canonical path, or null if unsafe (path traversal).
50
+ */
51
+ function canonicalPath(p, taskRoot) {
52
+ if (!p || typeof p !== "string") return null;
53
+ try {
54
+ let resolved;
55
+ if (nodePath.isAbsolute(p)) {
56
+ resolved = nodePath.resolve(p);
57
+ } else if (taskRoot) {
58
+ resolved = nodePath.resolve(taskRoot, p);
59
+ } else {
60
+ return null;
61
+ }
62
+
63
+ // Resolver symlinks usando realpath. Rama depende de si el path existe.
64
+ if (nodeFs.existsSync(resolved)) {
65
+ try {
66
+ return nodeFs.realpathSync(resolved);
67
+ } catch {
68
+ return resolved;
69
+ }
70
+ }
71
+
72
+ // El path no existe: usamos realpath del padre (cuando el padre existe)
73
+ // para que un symlink en el directorio padre no redirija fuera del scope.
74
+ const parent = nodePath.dirname(resolved);
75
+ if (nodeFs.existsSync(parent)) {
76
+ try {
77
+ const realParent = nodeFs.realpathSync(parent);
78
+ return nodePath.join(realParent, nodePath.basename(resolved));
79
+ } catch {
80
+ return resolved;
81
+ }
82
+ }
83
+
84
+ // Padre tampoco existe: devolvemos la ruta absoluta resuelta (lexical).
85
+ return resolved;
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ function isWithinScope(absPath, taskRoot) {
92
+ if (!taskRoot) return true; // No scope constraint
93
+ try {
94
+ const resolvedRoot = nodePath.resolve(taskRoot);
95
+ // Si el root tiene symlinks, también los resolvemos para la comparación.
96
+ let canonicalRoot;
97
+ try {
98
+ canonicalRoot = nodeFs.realpathSync(resolvedRoot);
99
+ } catch {
100
+ canonicalRoot = resolvedRoot;
101
+ }
102
+ const rel = nodePath.relative(canonicalRoot, absPath);
103
+ return !rel.startsWith("..") && !nodePath.isAbsolute(rel);
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Structured error for policy blocks. MUST NOT be swallowed by catch.
111
+ */
112
+ export class WamPolicyBlock extends Error {
113
+ constructor(message, policy = {}) {
114
+ super(message);
115
+ this.name = "WamPolicyBlock";
116
+ this.wamPolicyBlock = true;
117
+ this.policy = policy;
118
+ }
119
+ }
120
+
121
+ function evaluateBashCommand(cmd) {
122
+ if (!cmd || typeof cmd !== "string") return { safe: true };
123
+
124
+ // NOTE: en este regex NO se usa `\b` al final del grupo porque alternativas
125
+ // como `dd if=` terminan en `=` (no-word), por lo que `\b` no aplica.
126
+ // Tampoco se envuelve todo el grupo en `\b...\b` por la misma razón.
127
+ const destructive = /(rm\s+-rf|rm\s+-fr|dd\s+if=|mkfs|format|:()\s*\{|\bdrop\s+database\b|\bdrop\s+table\b|\btruncate\s+table\b|delete\s+from\s+\S+\s+where|pkill\b|killall\b)/i;
128
+ const privileged = /\b(sudo|chmod\s+777|chown\s+root|iptables|ufw\s+disable)\b/i;
129
+ const network = /\b(curl[^|]*\|\s*sh|wget[^|]*\|\s*sh|nc\s+-|netcat)\b/i;
130
+ const gitDestructive = /\bgit\s+push\s+(-f|--force)|git\s+reset\s+--hard|git\s+clean\s+-fd|git\s+filter-branch/i;
131
+
132
+ if (destructive.test(cmd)) return { safe: false, reason: `Comando destructivo detectado: ${cmd.slice(0, 80)}` };
133
+ if (privileged.test(cmd)) return { safe: false, reason: `Comando privilegiado detectado: ${cmd.slice(0, 80)}` };
134
+ if (gitDestructive.test(cmd)) return { safe: false, reason: `Operación git destructiva: ${cmd.slice(0, 80)}` };
135
+ if (network.test(cmd)) return { safe: false, reason: `Pipe a shell desde red: ${cmd.slice(0, 80)}` };
136
+
137
+ return { safe: true };
138
+ }
139
+
140
+ /**
141
+ * Evalúa una acción propuesta por el agente y retorna su clasificación de riesgo.
142
+ *
143
+ * Política fail-closed (C01):
144
+ * - Herramientas catalogadas como SAFE/GUARDED/BLOCKED se evalúan normalmente.
145
+ * - Herramientas NO catalogadas → se clasifican como UNKNOWN y se tratan como
146
+ * BLOCKED con reason "fail-closed: herramienta no catalogada".
147
+ *
148
+ * El match contra el catálogo es EXACTO (no sub-string), para evitar colisiones
149
+ * como "bash-safe" resolviendo a "bash".
150
+ *
151
+ * Las validaciones de scope y rutas protegidas aplican a TODAS las
152
+ * herramientas (incluso SAFE): un `read` de `/etc/passwd` o de un symlink
153
+ * fuera del root sigue siendo un escape de scope.
154
+ *
155
+ * @param {string} tool — Nombre de la herramienta invocada.
156
+ * @param {object} args — Argumentos de la invocación.
157
+ * @param {string} taskRoot — Root de la tarea activa (para validación de scope).
158
+ * @returns {{level: string, reason?: string, requiresUser: boolean}}
159
+ */
160
+ export function evaluateAction(tool, args = {}, taskRoot = "") {
161
+ const lowerTool = (tool || "").toLowerCase();
162
+
163
+ // 0. Resolución de capability explícita (fail-closed para UNKNOWN).
164
+ const capability = getEffectiveCapability(lowerTool);
165
+
166
+ // Fail-closed: herramientas no catalogadas → BLOCKED inmediato.
167
+ if (capability === CAPABILITY_LEVELS.BLOCKED) {
168
+ // Si la herramienta tampoco está en el catálogo (UNKNOWN → BLOCKED),
169
+ // el reason debe indicar fail-closed para que el guard lo registre.
170
+ const knownBlocked = new Set([
171
+ "rm", "rmdir", "mv", "sudo", "chmod", "chown",
172
+ "docker", "ssh", "scp", "rsync",
173
+ "push", "force_push",
174
+ ]);
175
+ if (!knownBlocked.has(lowerTool)) {
176
+ return {
177
+ level: "BLOCKED",
178
+ reason: `fail-closed: herramienta no catalogada (${tool})`,
179
+ requiresUser: true,
180
+ };
181
+ }
182
+ return {
183
+ level: "BLOCKED",
184
+ reason: `Herramienta peligrosa por defecto: ${tool}`,
185
+ requiresUser: true,
186
+ };
187
+ }
188
+
189
+ // 1. Extracción de path crudo y validaciones universales (aplican a
190
+ // SAFE y GUARDED). Las rutas protegidas o fuera del scope elevan
191
+ // la acción a BLOCKED independientemente del capability original.
192
+ const rawPath = args?.path || args?.file || args?.file_path || args?.target || "";
193
+ if (isProtectedPath(rawPath)) {
194
+ return {
195
+ level: "BLOCKED",
196
+ reason: `Ruta protegida: ${rawPath}`,
197
+ requiresUser: true,
198
+ };
199
+ }
200
+
201
+ if (taskRoot && rawPath) {
202
+ const canonical = canonicalPath(rawPath, taskRoot);
203
+ if (canonical && !isWithinScope(canonical, taskRoot)) {
204
+ return {
205
+ level: "BLOCKED",
206
+ reason: `Acción fuera del scope de tarea (path traversal): ${rawPath} → ${canonical} (root: ${taskRoot})`,
207
+ requiresUser: true,
208
+ };
209
+ }
210
+ }
211
+
212
+ // 2. SAFE: autónoma (post validación de scope).
213
+ if (capability === CAPABILITY_LEVELS.SAFE) {
214
+ return { level: "SAFE", requiresUser: false };
215
+ }
216
+
217
+ // 3. GUARDED: requiere validación contextual adicional (bash commands).
218
+ if (capability === CAPABILITY_LEVELS.GUARDED) {
219
+ if (lowerTool === "bash" || lowerTool === "sh" || lowerTool === "pty_spawn") {
220
+ const cmd = args?.command || args?.cmd || args?.script || "";
221
+ const bashEval = evaluateBashCommand(cmd);
222
+ if (!bashEval.safe) {
223
+ return {
224
+ level: "BLOCKED",
225
+ reason: bashEval.reason,
226
+ requiresUser: true,
227
+ };
228
+ }
229
+ }
230
+
231
+ return { level: "GUARDED", requiresUser: false };
232
+ }
233
+
234
+ // Cualquier otro nivel no debería ocurrir (SAFE/GUARDED/BLOCKED cubren todo
235
+ // gracias a fail-closed), pero por seguridad devolvemos BLOCKED.
236
+ return {
237
+ level: "BLOCKED",
238
+ reason: `fail-closed: capacidad no resuelta (${tool})`,
239
+ requiresUser: true,
240
+ };
241
+ }
242
+
243
+ export const RISK_LEVELS = { SAFE: "SAFE", GUARDED: "GUARDED", BLOCKED: "BLOCKED" };
244
+
245
+ /**
246
+ * Conjunto de herramientas mutantes (GUARDED + BLOCKED explícitos).
247
+ * NOTA: las herramientas UNKNOWN NO aparecen aquí — la verificación
248
+ * de mutación se hace preferentemente con `isMutatingTool`, que aplica
249
+ * fail-closed para UNKNOWN. Este Set se conserva por compatibilidad.
250
+ */
251
+ export const MUTATING_TOOLS = new Set([
252
+ // GUARDED
253
+ "write", "edit", "apply_patch", "patch",
254
+ "todo_write", "todowrite",
255
+ "bash", "sh", "pty_spawn", "pty_write",
256
+ "task",
257
+ // BLOCKED
258
+ "rm", "rmdir", "mv", "sudo", "chmod", "chown",
259
+ "docker", "ssh", "scp", "rsync",
260
+ "push", "force_push",
261
+ ]);
262
+
263
+ export function isMutatingTool(tool) {
264
+ // Fail-closed: una herramienta desconocida también se considera mutante.
265
+ return isMutatingCapability(tool);
266
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Scope & Strategy Enforcement — scope drift detection.
3
+ * Implements spec: scope-and-strategy-enforcement.
4
+ *
5
+ * Strategy = objective, scope, allowedActions, prohibitedActions
6
+ * Current Execution = hypothesis, experiment, files touched, actions, evidence
7
+ * Drift = deviation between strategy and current execution.
8
+ *
9
+ * Policy:
10
+ * none / small_safe → allow + record
11
+ * material → require authorization
12
+ * prohibited → block
13
+ */
14
+
15
+ const DRIFT_LEVELS = {
16
+ NONE: "none",
17
+ SMALL_SAFE: "small_safe",
18
+ MATERIAL: "material",
19
+ PROHIBITED: "prohibited",
20
+ };
21
+
22
+ const SAFE_FAMILIES = new Set([
23
+ "read", "search", "inspect", "test", "lint", "typecheck", "list", "glob", "grep",
24
+ ]);
25
+
26
+ const MATERIAL_FAMILIES = new Set([
27
+ "refactor", "rename", "migrate", "schema", "schema-change", "restructure", "rewrite",
28
+ "scope expansion", "scope-expansion", "architectural change",
29
+ ]);
30
+
31
+ const PROHIBITED_FAMILIES = new Set([
32
+ "drop database", "drop schema", "production deploy", "delete production data",
33
+ "credential modification", "destructive operation", "rm -rf", "git reset --hard",
34
+ "wipe", "truncate", "shred",
35
+ ]);
36
+
37
+ function familyOf(action = "", command = "") {
38
+ const text = `${action} ${command}`.toLowerCase();
39
+ for (const p of PROHIBITED_FAMILIES) {
40
+ if (text.includes(p)) return { level: DRIFT_LEVELS.PROHIBITED, family: p };
41
+ }
42
+ for (const m of MATERIAL_FAMILIES) {
43
+ if (text.includes(m)) return { level: DRIFT_LEVELS.MATERIAL, family: m };
44
+ }
45
+ for (const s of SAFE_FAMILIES) {
46
+ if (text.includes(s)) return { level: DRIFT_LEVELS.SMALL_SAFE, family: s };
47
+ }
48
+ return { level: DRIFT_LEVELS.NONE, family: null };
49
+ }
50
+
51
+ function isInScope(action, strategy) {
52
+ if (!strategy) return false;
53
+ const allowed = strategy.allowedActions || [];
54
+ const prohibited = strategy.prohibitedActions || [];
55
+ const a = (action || "").toLowerCase();
56
+ for (const p of prohibited) {
57
+ if (a.includes(p.toLowerCase())) return { allowed: false, reason: `prohibited: ${p}` };
58
+ }
59
+ for (const allow of allowed) {
60
+ if (a.includes(allow.toLowerCase())) return { allowed: true, reason: `matched: ${allow}` };
61
+ }
62
+ return { allowed: false, reason: "no allowed action matched" };
63
+ }
64
+
65
+ export function detectScopeDrift(strategy, execution) {
66
+ const action = execution?.action || execution?.description || "";
67
+ const command = execution?.command || execution?.args?.command || "";
68
+
69
+ // Empty action = no drift
70
+ if (!action && !command) {
71
+ return {
72
+ level: DRIFT_LEVELS.NONE,
73
+ family: null,
74
+ allowed: true,
75
+ reason: "no action",
76
+ action,
77
+ command,
78
+ };
79
+ }
80
+
81
+ const result = familyOf(action, command);
82
+ const scopeCheck = isInScope(action, strategy);
83
+
84
+ let drift = result.level;
85
+
86
+ // If action is explicitly allowed by strategy, it's at most SMALL_SAFE
87
+ if (scopeCheck.allowed === true) {
88
+ // Allowed by strategy: if it's already a safe family, keep it; otherwise make it SMALL_SAFE
89
+ if (result.level === DRIFT_LEVELS.NONE) {
90
+ drift = DRIFT_LEVELS.SMALL_SAFE;
91
+ }
92
+ } else if (scopeCheck.allowed === false && result.level !== DRIFT_LEVELS.PROHIBITED) {
93
+ // Not allowed by strategy
94
+ if (result.level === DRIFT_LEVELS.SMALL_SAFE) {
95
+ drift = DRIFT_LEVELS.SMALL_SAFE;
96
+ } else if (result.level === DRIFT_LEVELS.NONE) {
97
+ drift = DRIFT_LEVELS.MATERIAL;
98
+ } else {
99
+ drift = result.level;
100
+ }
101
+ }
102
+
103
+ return {
104
+ level: drift,
105
+ family: result.family,
106
+ allowed: scopeCheck.allowed,
107
+ reason: scopeCheck.reason,
108
+ action,
109
+ command,
110
+ };
111
+ }
112
+
113
+ export function policyFor(drift) {
114
+ switch (drift.level) {
115
+ case DRIFT_LEVELS.NONE:
116
+ case DRIFT_LEVELS.SMALL_SAFE:
117
+ return "allow+record";
118
+ case DRIFT_LEVELS.MATERIAL:
119
+ return "require-authorization";
120
+ case DRIFT_LEVELS.PROHIBITED:
121
+ return "block";
122
+ default:
123
+ return "block";
124
+ }
125
+ }
126
+
127
+ export function recordDrift(taskState, drift) {
128
+ if (!taskState) return taskState;
129
+ if (!Array.isArray(taskState.driftLog)) taskState.driftLog = [];
130
+ taskState.driftLog.push({
131
+ at: Date.now(),
132
+ level: drift.level,
133
+ family: drift.family,
134
+ action: drift.action,
135
+ reason: drift.reason,
136
+ });
137
+ return taskState;
138
+ }
139
+
140
+ export const SCOPE_DRIFT = DRIFT_LEVELS;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Secret Scanner — regex-based detection of common secret patterns.
3
+ *
4
+ * Detects: AWS keys, GitHub tokens, generic API keys, JWTs, private
5
+ * keys, Slack tokens. Returns findings with line numbers. Production
6
+ * hardening (M5-56).
7
+ */
8
+
9
+ /**
10
+ * @typedef {Object} Finding
11
+ * @property {string} type
12
+ * @property {number} line
13
+ * @property {number} column
14
+ * @property {string} match
15
+ * @property {string} severity
16
+ */
17
+
18
+ /**
19
+ * @typedef {Object} ScanResult
20
+ * @property {Finding[]} findings
21
+ * @property {boolean} clean
22
+ * @property {number} count
23
+ */
24
+
25
+ /** @type {Array<{type: string, severity: string, regex: RegExp}>} */
26
+ const PATTERNS = [
27
+ { type: "aws_access_key", severity: "high", regex: /AKIA[0-9A-Z]{16}/g },
28
+ { type: "github_token", severity: "high", regex: /gh[pousr]_[A-Za-z0-9]{36,}/g },
29
+ { type: "slack_token", severity: "high", regex: /xox[abprs]-[A-Za-z0-9-]{10,}/g },
30
+ { type: "private_key", severity: "high", regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
31
+ { type: "jwt", severity: "medium", regex: /eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g },
32
+ { type: "generic_api_key", severity: "medium", regex: /(?:api[_-]?key|apikey|secret)\s*[:=]\s*['"]?([A-Za-z0-9_\-]{20,})['"]?/gi },
33
+ { type: "stripe_key", severity: "high", regex: /sk_(?:live|test)_[A-Za-z0-9]{24,}/g },
34
+ ];
35
+
36
+ /**
37
+ * Scan text for secret patterns.
38
+ * @param {string} text
39
+ * @returns {ScanResult}
40
+ */
41
+ export function scanSecrets(text) {
42
+ if (typeof text !== "string") {
43
+ throw new TypeError("text must be string");
44
+ }
45
+ const findings = [];
46
+ const lines = text.split(/\r?\n/);
47
+ for (let i = 0; i < lines.length; i++) {
48
+ const line = lines[i];
49
+ for (const p of PATTERNS) {
50
+ p.regex.lastIndex = 0;
51
+ let m;
52
+ while ((m = p.regex.exec(line)) !== null) {
53
+ findings.push({
54
+ type: p.type,
55
+ severity: p.severity,
56
+ line: i + 1,
57
+ column: m.index + 1,
58
+ match: m[0],
59
+ });
60
+ }
61
+ }
62
+ }
63
+ return {
64
+ findings,
65
+ clean: findings.length === 0,
66
+ count: findings.length,
67
+ };
68
+ }
@@ -0,0 +1,104 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ /**
6
+ * Portable skill discovery.
7
+ *
8
+ * Resolution order (deterministic precedence — first wins):
9
+ * 1. project-local: <root>/.opencode/skills
10
+ * 2. project-local: <root>/.opencode/.skills
11
+ * 3. user-global: <homedir>/.config/opencode/skills
12
+ * 4. user-global: <homedir>/.config/opencode/.skills
13
+ * 5. user-global: <homedir>/.claude/skills
14
+ * 6. user-global: <homedir>/.agents/skills
15
+ * 7. user-global: <homedir>/.opencode/skills
16
+ * 8. bundled: <thisFile>/../../skills
17
+ *
18
+ * The first occurrence of a skill name wins. All paths are computed
19
+ * dynamically from `os.homedir()` and the optional `root` argument so the
20
+ * module is portable across machines.
21
+ */
22
+
23
+ function fileExists(filePath) {
24
+ try {
25
+ fs.accessSync(filePath);
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Build the ordered list of skill directories to scan.
34
+ *
35
+ * @param {object} [opts]
36
+ * @param {string} [opts.root] - Project root. Defaults to process.cwd().
37
+ * @param {string} [opts.home] - Override homedir (used by tests).
38
+ * @returns {{ dir: string, tier: string }[]}
39
+ */
40
+ export function getSkillSearchPaths({ root, home } = {}) {
41
+ const effectiveRoot = root || process.cwd();
42
+ const effectiveHome = home || os.homedir();
43
+ const thisDir = path.dirname(new URL(import.meta.url).pathname);
44
+
45
+ return [
46
+ { dir: path.join(effectiveRoot, ".opencode", "skills"), tier: "project-local" },
47
+ { dir: path.join(effectiveRoot, ".opencode", ".skills"), tier: "project-local" },
48
+ { dir: path.join(effectiveHome, ".config", "opencode", "skills"), tier: "user-global" },
49
+ { dir: path.join(effectiveHome, ".config", "opencode", ".skills"), tier: "user-global" },
50
+ { dir: path.join(effectiveHome, ".claude", "skills"), tier: "user-global" },
51
+ { dir: path.join(effectiveHome, ".agents", "skills"), tier: "user-global" },
52
+ { dir: path.join(effectiveHome, ".opencode", "skills"), tier: "user-global" },
53
+ { dir: path.join(thisDir, "..", "..", "skills"), tier: "bundled" },
54
+ ];
55
+ }
56
+
57
+ /**
58
+ * Discover all available skills across the configured search paths.
59
+ * Earlier tiers override later ones (project-local > user-global > bundled).
60
+ *
61
+ * @param {object} [opts] - forwarded to getSkillSearchPaths
62
+ * @returns {Object<string, { name: string, path: string, dir: string, tier: string }>}
63
+ */
64
+ export function discoverSkills(opts = {}) {
65
+ const searchPaths = getSkillSearchPaths(opts);
66
+ const candidates = {};
67
+
68
+ for (const { dir: skillDir, tier } of searchPaths) {
69
+ if (!fileExists(skillDir)) continue;
70
+
71
+ let entries;
72
+ try {
73
+ entries = fs.readdirSync(skillDir, { withFileTypes: true });
74
+ } catch {
75
+ continue;
76
+ }
77
+
78
+ for (const entry of entries) {
79
+ if (!entry.isDirectory()) continue;
80
+ const skillPath = path.join(skillDir, entry.name, "SKILL.md");
81
+ if (fileExists(skillPath)) {
82
+ if (!candidates[entry.name]) {
83
+ candidates[entry.name] = {
84
+ name: entry.name,
85
+ path: skillPath,
86
+ dir: skillDir,
87
+ tier,
88
+ };
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ return candidates;
95
+ }
96
+
97
+ /**
98
+ * Return the list of configured search paths (useful for debugging and tests).
99
+ * @param {object} [opts]
100
+ * @returns {string[]}
101
+ */
102
+ export function getSkillSearchDirs(opts = {}) {
103
+ return getSkillSearchPaths(opts).map((p) => p.dir);
104
+ }