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,1705 @@
1
+ /**
2
+ * Wait a Minute — Pre-Flight Cognitive Analysis Engine
3
+ *
4
+ * Analiza peticiones de usuario antes de la resolución de skills y la ejecución del agente.
5
+ * Clasifica la tarea, inspecciona el proyecto, detecta supuestos y selecciona skills.
6
+ */
7
+ // Debug removed — no hardcoded paths in runtime package.
8
+
9
+ const fs = await import("node:fs");
10
+ const path = await import("node:path");
11
+ const os = await import("node:os");
12
+ import { logger } from "../integration/logger.js";
13
+ const tmpdir = os.tmpdir();
14
+
15
+ // Pre-flight modules (facade)
16
+ import { classifyRequest } from "../policy/request-classifier.js";
17
+ import { detectStack } from "../policy/project-inspector.js";
18
+ import { classifyUncertainty, buildUncertainties, buildAssumptions } from "../policy/uncertainty.js";
19
+ import { discoverSkills } from "../policy/skill-routing.js";
20
+
21
+ // Re-export for backward compatibility
22
+ export { classifyRequest, detectStack, classifyUncertainty, buildUncertainties, buildAssumptions, discoverSkills };
23
+
24
+
25
+ // -- Caveman compression (terse, para headroom de contexto) ----------------
26
+
27
+ const FILLERS = /\b(a|an|the|just|really|basically|actually|simply|please|por favor)\b/gi;
28
+
29
+ /**
30
+ * Comprime texto a estilo caveman: sin artículos, sin relleno, frases cortas.
31
+ * Sirve para inyecciones y resúmenes que deben ocupar el menor contexto posible.
32
+ */
33
+ export function cavemanify(text = "") {
34
+ return (text || "")
35
+ .replace(FILLERS, "")
36
+ .replace(/[ \t]{2,}/g, " ")
37
+ .replace(/\n{3,}/g, "\n\n")
38
+ .trim();
39
+ }
40
+
41
+ /** Estimación de tokens (chars/4 — proxy barato, sin tokenizer). */
42
+ export function estimateTokens(text = "") {
43
+ return Math.ceil((text || "").length / 4);
44
+ }
45
+
46
+
47
+ // -- Task Deduplication --
48
+
49
+ /**
50
+ * Finds an existing active task with the same summary to prevent infinite loops.
51
+ * Returns the taskId if found, null otherwise.
52
+ *
53
+ * @param {string} summary - The task summary to search for
54
+ * @param {string} root - The workspace root
55
+ * @returns {string|null} - Existing taskId or null
56
+ */
57
+ export function findDuplicateTask(summary, root) {
58
+ if (!summary || summary.length < 10) return null;
59
+ const tasksDir = path.join(root || process.cwd(), ".wam", "tasks");
60
+ try {
61
+ const entries = fs.readdirSync(tasksDir, { withFileTypes: true });
62
+ for (const entry of entries) {
63
+ if (!entry.isDirectory()) continue;
64
+ const stateFile = path.join(tasksDir, entry.name, "state.yaml");
65
+ try {
66
+ const raw = fs.readFileSync(stateFile, "utf-8");
67
+ const state = JSON.parse(raw);
68
+ if (state?.phase === "DONE" || state?.phase === "VERIFIED") continue;
69
+ const taskSummary = state?.lastAction || state?.contract?.objective || "";
70
+ if (taskSummary && normalizeText(taskSummary) === normalizeText(summary)) {
71
+ return entry.name;
72
+ }
73
+ } catch {}
74
+ }
75
+ } catch {}
76
+ return null;
77
+ }
78
+
79
+ function normalizeText(text = "") {
80
+ return (text || "").toLowerCase().replace(/[^a-z0-9áéíóúñü\s]/g, "").replace(/\s+/g, " ").trim();
81
+ }
82
+
83
+ // -- Task State Management --
84
+
85
+ import { ContextDecisionTracer } from "../context/context-decision-audit.js";
86
+
87
+ /**
88
+ * Archives a completed task session by moving its directory to history.
89
+ *
90
+ * @param {string} taskId - The ID of the task to archive.
91
+ * @param {string} root - The workspace root.
92
+ */
93
+ export async function archiveTaskSession(taskId, root) {
94
+ const fs = await import("node:fs/promises");
95
+ const path = await import("node:path");
96
+
97
+ const taskDir = path.join(root, ".wam", "tasks", taskId);
98
+ const date = new Date().toISOString().split("T")[0];
99
+ const historyDir = path.join(root, ".wam", "history", date);
100
+ const destDir = path.join(historyDir, taskId);
101
+
102
+ try {
103
+ await fs.mkdir(historyDir, { recursive: true });
104
+ await fs.rename(taskDir, destDir);
105
+ } catch (err) {
106
+ if (err.code !== "ENOENT") {
107
+ console.error(`[WAM] Failed to archive task ${taskId}:`, err);
108
+ }
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Decision Gate — helper para subagentes.
114
+ *
115
+ * Invocado cuando una bifurcación arquitectónica (nuevo servicio, modelo de datos,
116
+ * cambio de arquitectura) es detectada. Delegado obligatorio: documentar alternativas
117
+ * y evidencia técnica ANTES de implementar.
118
+ *
119
+ * @param {Object} args
120
+ * @param {string} args.taskId - ID de tarea (default: "default").
121
+ * @param {string} args.projectRoot - Raíz del proyecto (default: cwd).
122
+ * @param {string} args.decisionPoint - Qué se está decidiendo.
123
+ * @param {string} args.rationale - Por qué esta ruta.
124
+ * @param {Array} args.alternatives - Alternativas descartadas [{name, pros, cons, whyRejected}].
125
+ * @param {Array} args.evidence - Evidencia técnica (links, benchmarks, docs, file:line).
126
+ * @param {Object} [args.technicalMetrics] - Métricas opcionales (latencia, costo, complejidad).
127
+ * @returns {{ok: boolean, entry: object|null, errors: string[]}}
128
+ */
129
+ export function logArchitecturalDecision({
130
+ taskId = "default",
131
+ projectRoot,
132
+ decisionPoint,
133
+ rationale,
134
+ alternatives = [],
135
+ evidence = [],
136
+ technicalMetrics = {},
137
+ } = {}) {
138
+ const errors = [];
139
+ if (!decisionPoint || typeof decisionPoint !== "string") errors.push("decisionPoint requerido");
140
+ if (!rationale || typeof rationale !== "string") errors.push("rationale requerido");
141
+ if (!Array.isArray(alternatives) || alternatives.length === 0) {
142
+ errors.push("alternatives[] requerido (≥1 alternativa descartada)");
143
+ }
144
+ if (!Array.isArray(evidence) || evidence.length === 0) {
145
+ errors.push("evidence[] requerido (≥1 evidencia técnica)");
146
+ }
147
+ if (errors.length > 0) {
148
+ logger.warn("DecisionGate", `Bloqueado: ${errors.join("; ")}`);
149
+ return { ok: false, entry: null, errors };
150
+ }
151
+ const tracer = new ContextDecisionTracer(taskId, projectRoot || process.cwd());
152
+ tracer.logTechnicalDecision(decisionPoint, rationale, alternatives, evidence, technicalMetrics);
153
+ logger.info("DecisionGate", `Registrada bifurcación: ${decisionPoint}`);
154
+ return { ok: true, entry: { decisionPoint, rationale }, errors: [] };
155
+ }
156
+
157
+ export function persistTaskState(taskId, state, root) {
158
+ // Migrate legacy progress to requirements if needed
159
+ if (state.progress && !state.requirements) {
160
+ const total = state.progress.total || 0;
161
+ state.requirements = Array.from({length: total}, (_, i) => ({
162
+ id: `req-${i + 1}`,
163
+ title: `Requisito ${i + 1}`,
164
+ status: 'pending',
165
+ evidence: []
166
+ }));
167
+ delete state.progress;
168
+ }
169
+
170
+ const dir = path.join(root || process.cwd(), ".wam", "tasks", taskId);
171
+ if (!fileExists(dir)) fs.mkdirSync(dir, { recursive: true });
172
+ fs.writeFileSync(path.join(dir, "state.yaml"), JSON.stringify(state, null, 2));
173
+ }
174
+
175
+ /**
176
+ * Recupera estado de tarea
177
+ */
178
+ export function getTaskState(taskId, root) {
179
+ const file = path.join(root || process.cwd(), ".wam", "tasks", taskId, "state.yaml");
180
+ if (!fileExists(file)) return null;
181
+ try {
182
+ return JSON.parse(readFileSafely(file));
183
+ } catch {
184
+ return null;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Lee un archivo si existe, retorna vacío en caso contrario
190
+ */
191
+ function readFileSafely(filePath) {
192
+ try {
193
+ return fs.readFileSync(filePath, "utf-8");
194
+ } catch {
195
+ return "";
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Verifica si un archivo existe
201
+ */
202
+ function fileExists(filePath) {
203
+ try {
204
+ fs.accessSync(filePath);
205
+ return true;
206
+ } catch {
207
+ return false;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Obtiene el contenido de AGENTS.md si existe
213
+ */
214
+ function getAgentsMd(projectPath) {
215
+ const agentsPath = path.join(projectPath, "AGENTS.md");
216
+ return readFileSafely(agentsPath);
217
+ }
218
+
219
+ /**
220
+ * Obtiene el package.json si existe
221
+ */
222
+ function getPackageJson(projectPath) {
223
+ const pkgPath = path.join(projectPath, "package.json");
224
+ if (!fileExists(pkgPath)) return null;
225
+ try {
226
+ return JSON.parse(readFileSafely(pkgPath));
227
+ } catch {
228
+ return null;
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Obtiene dependencias relevantes del package.json
234
+ */
235
+ function getDependencies(pkgJson) {
236
+ if (!pkgJson) return {};
237
+ return {
238
+ ...(pkgJson.dependencies || {}),
239
+ ...(pkgJson.devDependencies || {}),
240
+ };
241
+ }
242
+
243
+
244
+
245
+ /**
246
+ * Obtiene la configuración de OpenCode (agents.md equivalents)
247
+ */
248
+ function getOpenCodeConfig(projectPath) {
249
+ // Check for .opencode directory
250
+ const opencodeDir = path.join(projectPath, ".opencode");
251
+ if (!fileExists(opencodeDir)) return null;
252
+
253
+ // Check for AGENTS.md
254
+ const agentsMd = readFileSafely(path.join(opencodeDir, "AGENTS.md"));
255
+ if (agentsMd.trim()) return { agentsMd };
256
+
257
+ // Check for opencode.jsonc
258
+ const configPath = path.join(opencodeDir, "opencode.jsonc");
259
+ if (fileExists(configPath)) {
260
+ try {
261
+ return { opencodeJsonc: readFileSafely(configPath) };
262
+ } catch {}
263
+ }
264
+
265
+ return null;
266
+ }
267
+
268
+ /**
269
+ * Busca skills relevantes en las directories conocidas
270
+ */
271
+ // ---------------------------------------------------------------------------
272
+ // PERSISTENT POLICIES — reglas transversales del Policy Engine.
273
+ // Siempre aplicables. NO son skills; no requieren carga bajo demanda.
274
+ // ---------------------------------------------------------------------------
275
+
276
+ const PERSISTENT_POLICIES = {
277
+ scope: {
278
+ name: "Scope",
279
+ kind: "persistent",
280
+ rules: [
281
+ "Entregar únicamente lo pedido — sin expansión de alcance",
282
+ "No agregar features no solicitadas",
283
+ "No refactorizar código ajeno a la tarea",
284
+ "Cambiar la mínima superficie posible",
285
+ ],
286
+ gates: ["scope-creep", "surface-change"],
287
+ },
288
+ verify: {
289
+ name: "Verify",
290
+ kind: "persistent",
291
+ rules: [
292
+ "Toda implementación debe tener resultado verificable",
293
+ "Ninguna tarea se marca DONE sin evidencia satisfecha",
294
+ "Ejecutar tests/lint/typecheck de los artefactos tocados",
295
+ "Verificar consumidores de los cambios, no solo el cambio",
296
+ ],
297
+ gates: ["completion-gate", "evidence-required"],
298
+ },
299
+ simplify: {
300
+ name: "Simplify",
301
+ kind: "persistent",
302
+ rules: [
303
+ "YAGNI — no construir para necesidades futuras especulativas",
304
+ "Reusar código y abstracciones existentes",
305
+ "Preferir mecanismos nativos y stdlib cuando aplique",
306
+ "Implementación mínima y superficie de cambio mínima",
307
+ "Evitar dependencias innecesarias y generalización prematura",
308
+ "Abstracción solo cuando el requerimiento la exige ahora",
309
+ ],
310
+ gates: ["speculative-abstraction", "unnecessary-deps"],
311
+ },
312
+ };
313
+
314
+ // Excepciones de Simplify: nunca eliminar restricciones esenciales.
315
+ const SIMPLIFY_PROTECTED = [
316
+ "security",
317
+ "seguridad",
318
+ "data integrity",
319
+ "integridad",
320
+ "explicit requirements",
321
+ "requisito",
322
+ "error handling",
323
+ "manejo de errores",
324
+ "trust-boundary",
325
+ "observability",
326
+ "observabilidad",
327
+ "required tests",
328
+ "tests requeridos",
329
+ ];
330
+
331
+ /**
332
+ * Evalúa las políticas persistentes contra la tarea.
333
+ * Aplica transversalmente en todo ciclo; devuelve qué políticas aplican y las gates activas.
334
+ */
335
+ function evaluatePersistentPolicies(prompt, projectInfo, classification, mode) {
336
+ const lower = prompt.toLowerCase();
337
+ const applied = [];
338
+
339
+ // -- Decision Gate: detectar bifurcaciones arquitectónicas / modelo de datos --
340
+ // Bifurcaciones que exigen documentación obligatoria de alternativas + evidencia
341
+ // antes de proceder. Si hay match, se loguea vía tracer (si está disponible) y se
342
+ // marca como POLICY ACTIVE con gates adicionales para forzar cumplimiento.
343
+ const DECISION_GATES = [
344
+ {
345
+ id: "new-service",
346
+ label: "Nuevo servicio / componente",
347
+ pattern: /\b(nuevo|nueva|create|new)\s+(servicio|service|microservicio|módulo|module|servicio\s+externo)\b/,
348
+ gates: ["service-design-evidence", "alternatives-documented"],
349
+ },
350
+ {
351
+ id: "data-model",
352
+ label: "Cambio de modelo de datos / schema",
353
+ pattern: /\b(schema|migraci(ó|o)n|migration|modelo\s+de\s+datos|data\s+model|entidad\s+nueva|new\s+entity|nueva\s+tabla|new\s+table)\b/,
354
+ gates: ["schema-evolution-evidence", "backward-compat-check"],
355
+ },
356
+ {
357
+ id: "architecture",
358
+ label: "Cambio de arquitectura",
359
+ pattern: /\b(arquitectura|architecture|refactor\s+arquitect(ó|o)nico|migrar\s+a|migrate\s+to|reemplazar\s+stack|replace\s+stack|monorepo|monolith\s+to\s+microservices|event\s+driven|ddd|hexagonal)\b/,
360
+ gates: ["adr-required", "trade-off-analysis"],
361
+ },
362
+ ];
363
+
364
+ const bifurcations = DECISION_GATES.filter((g) => g.pattern.test(lower));
365
+ if (bifurcations.length > 0) {
366
+ applied.push({
367
+ policy: "decision-gate",
368
+ status: "ACTIVE",
369
+ gates: bifurcations.flatMap((b) => b.gates),
370
+ restrictions: bifurcations.map(
371
+ (b) => `Bifurcación detectada (${b.label}): subagente DEBE invocar tracer.logTechnicalDecision(decisionPoint, rationale, alternatives[], evidence[], technicalMetrics{}) antes de implementar.`
372
+ ),
373
+ bifurcations: bifurcations.map((b) => b.id),
374
+ });
375
+ }
376
+
377
+
378
+ // Scope: siempre aplica salvo tareas triviales puras
379
+ if (classification.type !== "trivial") {
380
+ applied.push({
381
+ policy: "scope",
382
+ status: "ACTIVE",
383
+ gates: ["scope-creep", "surface-change"],
384
+ restrictions: PERSISTENT_POLICIES.scope.rules.slice(0, 2),
385
+ });
386
+ }
387
+
388
+ // Verify: aplica a toda tarea con resultado material; gate de completitud
389
+ if (classification.type !== "trivial") {
390
+ applied.push({
391
+ policy: "verify",
392
+ status: "ACTIVE",
393
+ gates: ["completion-gate", "evidence-required"],
394
+ restrictions: PERSISTENT_POLICIES.verify.rules,
395
+ });
396
+ }
397
+
398
+ // Simplify: aplica transversalmente (YAGNI/reuso/stdlib)
399
+ const simplifyRestrictions = [...PERSISTENT_POLICIES.simplify.rules];
400
+
401
+ // Detectar si la tarea toca zonas protegidas (no simplificar restricciones esenciales)
402
+ const protectedHits = SIMPLIFY_PROTECTED.filter((w) => lower.includes(w));
403
+ if (protectedHits.length > 0) {
404
+ simplifyRestrictions.unshift(
405
+ `Protegido por Simplify: NO eliminar ${protectedHits.join(", ")} — simplificar implementación, no la restricción`
406
+ );
407
+ }
408
+
409
+ applied.push({
410
+ policy: "simplify",
411
+ status: "ACTIVE",
412
+ gates: ["speculative-abstraction", "unnecessary-deps", ...(protectedHits.length > 0 ? ["protected-restriction"] : [])],
413
+ restrictions: simplifyRestrictions.slice(0, 6),
414
+ });
415
+
416
+ return { applied, templates: applied, priority: ["scope", "verify", "simplify"] };
417
+ }
418
+
419
+ // ---------------------------------------------------------------------------
420
+ // SKILL REGISTRY — catálogo metadata-first de skills autocontenido.
421
+ // ---------------------------------------------------------------------------
422
+
423
+ const SKILL_LIFECYCLE = ["DISCOVERED", "EVALUATING", "APPROVED", "AVAILABLE", "LOADED", "USED", "REJECTED", "DEPRECATED", "DISABLED"];
424
+ const APPROVED_STATUSES = ["APPROVED", "AVAILABLE", "LOADED"];
425
+
426
+ // Pesos de scoring configurables (spec §7)
427
+ const DEFAULT_SCORING_WEIGHTS = {
428
+ name: 5,
429
+ capability: 4,
430
+ keyword: 3,
431
+ description: 2,
432
+ domain: 1,
433
+ };
434
+
435
+ // Base skills (loadStrategy: "base") bypass scoring and are always selected
436
+ // First level of guarantee: they appear even if the prompt doesn’t match anything.
437
+ const BASE_SKILL_DEFAULTS = {
438
+ writing_for_agents: { "loadStrategy": "base" },
439
+ codebase_design: { "loadStrategy": "base" },
440
+ };
441
+
442
+ /**
443
+ * Asegura la estructura del corpus local .wam/skills/
444
+ */
445
+ function ensureSkillCorpus(baseDir) {
446
+ const root = path.join(baseDir || process.cwd(), ".wam", "skills");
447
+ try {
448
+ for (const sub of ["sources", "registry", "cache", "policies"]) {
449
+ const dir = path.join(root, sub);
450
+ if (!fileExists(dir)) fs.mkdirSync(dir, { recursive: true });
451
+ }
452
+ return root;
453
+ } catch {
454
+ return null; // corpus no disponible (permisos/ruta inválida) — continuar sin persistencia
455
+ }
456
+ }
457
+
458
+ /**
459
+ * Parseo de frontmatter YAML mínimo (name, description, keywords, compatibility).
460
+ * v1: regex simple, sin dependencias. No carga contenido post-frontmatter.
461
+ */
462
+ function parseFrontmatter(skillMdContent) {
463
+ const meta = { name: null, description: "", keywords: [], compatibility: [] };
464
+ const match = skillMdContent.match(/^---\s*\n([\s\S]*?)\n---/);
465
+ if (!match) return meta;
466
+
467
+ const block = match[1];
468
+ const nameMatch = block.match(/^name\s*:\s*(.+)$/im);
469
+ if (nameMatch) meta.name = nameMatch[1].trim();
470
+
471
+ const descMatch = block.match(/^description\s*:\s*(.+)$/im);
472
+ if (descMatch) meta.description = descMatch[1].trim();
473
+
474
+ const kwMatch = block.match(/keywords\s*:\s*([\s\S]*?)(?:\n\w|\n---|$)/i);
475
+ if (kwMatch) {
476
+ const kwLines = kwMatch[1]
477
+ .split("\n")
478
+ .map((l) => l.trim().replace(/^-\s*/, "").replace(/^["']|["']$/g, ""))
479
+ .filter(Boolean);
480
+ meta.keywords = kwLines;
481
+ }
482
+
483
+ // Khasky usa metadata.tags — soportar ambos formatos
484
+ if (meta.keywords.length === 0) {
485
+ const tagsMatch = block.match(/tags\s*:\s*(\[[^\]]*\]|[\s\S]*?)(?:\n\w|\n---|$)/i);
486
+ if (tagsMatch) {
487
+ const tags = tagsMatch[1].trim();
488
+ if (tags.startsWith("[")) {
489
+ meta.keywords = tags
490
+ .replace(/^\[|\]$/g, "")
491
+ .split(",")
492
+ .map((t) => t.trim().replace(/^["']|["']$/g, ""))
493
+ .filter(Boolean);
494
+ } else {
495
+ meta.keywords = tags
496
+ .split("\n")
497
+ .map((l) => l.trim().replace(/^-\s*/, "").replace(/^["']|["']$/g, ""))
498
+ .filter(Boolean);
499
+ }
500
+ }
501
+ }
502
+
503
+ const compatMatch = block.match(/compatibility\s*:\s*(.+)$/im);
504
+ if (compatMatch) {
505
+ meta.compatibility = compatMatch[1]
506
+ .split(/[,\s]+/)
507
+ .map((s) => s.trim().toLowerCase())
508
+ .filter(Boolean);
509
+ }
510
+ return meta;
511
+ }
512
+
513
+ /**
514
+ * Genera keywords deterministas desde name + description.
515
+ * Las keywords derivadas quedan marcadas (spec §9).
516
+ */
517
+ function deriveKeywords(name, description) {
518
+ const words = new Set();
519
+ const source = `${name || ""} ${description || ""}`.toLowerCase();
520
+ for (const w of source.split(/[^a-z0-9]+/)) {
521
+ if (w.length >= 3 && !["the", "and", "for", "with", "skill", "skills", "this"].includes(w)) {
522
+ words.add(w);
523
+ }
524
+ }
525
+ return [...words].slice(0, 12);
526
+ }
527
+
528
+ /**
529
+ * Lee el commit actual de un repo local.
530
+ */
531
+ function getRepoCommit(repoDir) {
532
+ const head = path.join(repoDir, ".git", "HEAD");
533
+ if (!fileExists(head)) return null;
534
+ const ref = readFileSafely(head).trim().replace(/^ref:\s*/, "");
535
+ const refPath = path.join(repoDir, ".git", ref);
536
+ const sha = fileExists(refPath) ? readFileSafely(refPath).trim() : null;
537
+ return sha || ref;
538
+ }
539
+
540
+ /**
541
+ * Escanea un source clonado buscando SKILL.md, normaliza metadata, status DISCOVERED.
542
+ * No carga contenido post-frontmatter (spec §5).
543
+ */
544
+ /**
545
+ * Escanea un source existente en el corpus local buscando SKILL.md.
546
+ * NO realiza operaciones de red.
547
+ */
548
+ function scanSourceSkills(source, sourceDir) {
549
+ if (!fileExists(sourceDir)) return [];
550
+ const found = [];
551
+ const walk = (dir) => {
552
+ try {
553
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
554
+ for (const entry of entries) {
555
+ const full = path.join(dir, entry.name);
556
+ if (entry.isDirectory()) {
557
+ if (![".git", "node_modules"].includes(entry.name)) walk(full);
558
+ } else if (entry.name === "SKILL.md") {
559
+ const content = readFileSafely(full);
560
+ const fm = parseFrontmatter(content);
561
+ const relPath = path.relative(sourceDir, full).replace(/\\/g, "/");
562
+ const skillName =
563
+ fm.name?.toLowerCase().replace(/[^a-z0-9]+/g, "-") ||
564
+ path.basename(path.dirname(full)).toLowerCase();
565
+ const keywords = fm.keywords.length > 0 ? fm.keywords : deriveKeywords(fm.name, fm.description);
566
+ found.push({
567
+ id: `${source.id}-${skillName}`,
568
+ name: fm.name || skillName,
569
+ description: fm.description || "",
570
+ keywords,
571
+ keywordsDerived: fm.keywords.length === 0,
572
+ capabilities: [],
573
+ domain: [],
574
+ compatibility: fm.compatibility.length > 0 ? fm.compatibility : ["opencode"],
575
+ risk: "low",
576
+ source: {
577
+ id: source.id,
578
+ repository: source.repository,
579
+ path: relPath,
580
+ },
581
+ status: "DISCOVERED",
582
+ trust: source.trust,
583
+ });
584
+ }
585
+ }
586
+ } catch {}
587
+ };
588
+ walk(sourceDir);
589
+ return found;
590
+ }
591
+
592
+ /**
593
+ * Carga el catálogo embebido del plugin (skills/registry.json).
594
+ * El corpus es distribuido con WAM — sin red en runtime.
595
+ */
596
+ function loadBundledRegistry() {
597
+ try {
598
+ const regFile = path.join(import.meta.dirname, "..", "..", "skills", "registry.json");
599
+ if (!fileExists(regFile)) return {};
600
+ const entries = JSON.parse(readFileSafely(regFile));
601
+ const reg = {};
602
+ for (const s of Array.isArray(entries) ? entries : Object.values(entries)) {
603
+ if (!s?.id) continue;
604
+ if (!s.name || s.name.trim().length < 2) continue; // filtrar ruido (nombres de 1 char)
605
+ const desc = (s.description || "").replace(/^["']|["']$/g, "");
606
+ if (desc.length < 20) continue; // filtrar skills sin contenido significativo
607
+ reg[s.id] = { ...s, description: desc };
608
+ }
609
+ return reg;
610
+ } catch {
611
+ return {};
612
+ }
613
+ }
614
+
615
+ /**
616
+ * Construye registry unificado: skills locales instaladas (APPROVED)
617
+ * + catálogo embebido distribuido con el plugin (skills/registry.json).
618
+ * No realiza operaciones de red.
619
+ */
620
+ export function buildRegistry(availableSkills, baseDir) {
621
+ const registry = buildSkillRegistry(availableSkills);
622
+ const bundled = loadBundledRegistry();
623
+
624
+ for (const [id, skill] of Object.entries(bundled)) {
625
+ if (registry[id]) continue; // skills locales ganan
626
+ registry[id] = { ...skill, status: "APPROVED" };
627
+ }
628
+
629
+ const sources = [];
630
+ for (const [, s] of Object.entries(bundled)) {
631
+ const repo = s.source?.repository || "bundled-local";
632
+ if (!sources.includes(repo)) sources.push(repo);
633
+ }
634
+
635
+ return { registry, corpusRoot: null, sources, registryFile: null };
636
+ }
637
+
638
+ /**
639
+ * Persiste el registry en skills.json (spec §2). No incluye contenido, solo metadata.
640
+ */
641
+ function persistRegistry(registryFile, registry) {
642
+ fs.writeFileSync(registryFile, JSON.stringify(registry, null, 2));
643
+ }
644
+
645
+ /**
646
+ * Score de relevancia ponderado (spec §7) — pesos configurables.
647
+ */
648
+ function scoreSkill(skill, promptTokens, weights) {
649
+ const w = { ...DEFAULT_SCORING_WEIGHTS, ...(weights || {}) };
650
+ const lowerPrompt = promptTokens.toLowerCase();
651
+ const name = (skill.name || "").toLowerCase();
652
+ const desc = (skill.description || "").toLowerCase();
653
+ let score = 0;
654
+
655
+ if (name && lowerPrompt.includes(name)) score += w.name;
656
+ for (const c of skill.capabilities || []) {
657
+ if (lowerPrompt.includes(c.toLowerCase())) score += w.capability;
658
+ }
659
+ for (const k of skill.keywords || []) {
660
+ if (lowerPrompt.includes(k.toLowerCase())) score += w.keyword;
661
+ }
662
+ if (desc && lowerPrompt.includes(desc.slice(0, 20))) score += w.description;
663
+ for (const d of skill.domain || []) {
664
+ if (lowerPrompt.includes(d.toLowerCase())) score += w.domain;
665
+ }
666
+ return score;
667
+ }
668
+
669
+ /**
670
+ * Detección de duplicados por similitud de nombre (spec §19).
671
+ * Clúster: canonical + alternatives. Nunca elimina automáticamente.
672
+ */
673
+ function dedupRegistry(registry) {
674
+ const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-");
675
+ const clusters = [];
676
+ const seen = new Set();
677
+
678
+ for (const [id, skill] of Object.entries(registry)) {
679
+ const base = norm(skill.name || id).replace(/-(skill|skills)$/, "");
680
+ const baseWords = base.split("-");
681
+ const key = baseWords.slice(0, 2).join("-");
682
+ if (!seen.has(key)) {
683
+ seen.add(key);
684
+ clusters.push({ canonical: id, alternatives: [] });
685
+ } else {
686
+ const cluster = clusters.find((c) => norm(registry[c.canonical].name).startsWith(key) || key.startsWith(norm(registry[c.canonical].name).slice(0, 6)));
687
+ if (cluster) cluster.alternatives.push(id);
688
+ }
689
+ }
690
+ return clusters.filter((c) => c.alternatives.length > 0);
691
+ }
692
+
693
+ /**
694
+ * Registra uso de skill para auditabilidad (spec §24).
695
+ */
696
+ function recordSkillUsage(usage, baseDir) {
697
+ const logFile = path.join(baseDir || process.cwd(), ".wam", "skills", "registry", "usage.log");
698
+ const entry = { at: new Date().toISOString(), ...usage };
699
+ let log = [];
700
+ if (fileExists(logFile)) {
701
+ try {
702
+ log = JSON.parse(readFileSafely(logFile));
703
+ } catch {}
704
+ }
705
+ log.push(entry);
706
+ if (log.length > 200) log = log.slice(-200);
707
+ fs.mkdirSync(path.dirname(logFile), { recursive: true });
708
+ fs.writeFileSync(logFile, JSON.stringify(log, null, 2));
709
+ return entry;
710
+ }
711
+
712
+ /**
713
+ * Routing con scoring ponderado + persistencia. Reemplaza routeSkills() en analyze.
714
+ */
715
+ export function routeSkillsV2(prompt, projectInfo, registry, mode, options = {}) {
716
+ prompt = String(prompt ?? "");
717
+ logger.info("pipeline-routing", `Starting routeSkillsV2. Mode: ${mode}, Prompt: ${prompt.substring(0, 50)}...`);
718
+ const tracer = new ContextDecisionTracer(options.taskId || "default", options.projectRoot || process.cwd());
719
+
720
+ // Log concept extraction
721
+ tracer.logConceptExtraction(prompt, { source: "user" });
722
+
723
+ // Log candidate retrieval
724
+ tracer.logCandidateRetrieval(prompt, Object.keys(registry).length, { source: "registry" });
725
+
726
+ // Continue with existing routing logic...
727
+ const rigor = options.rigor || (mode === "FAST" ? "MINIMAL" : mode === "STRICT" ? "RIGOROUS" : "STANDARD");
728
+ const lower = prompt.toLowerCase();
729
+ const candidates = [];
730
+ const rejected = [];
731
+ const baseSkills = []; // always-selected skills (loadStrategy: "base")
732
+
733
+ for (const [id, skill] of Object.entries(registry)) {
734
+ if (!APPROVED_STATUSES.includes(skill.status)) {
735
+ rejected.push({ name: skill.name || id, reason: `estado ${skill.status} — requiere approval` });
736
+ continue;
737
+ }
738
+
739
+ // Base skills bypass scoring: always included, even if no prompt match
740
+ if (skill.loadStrategy === "base") {
741
+ baseSkills.push({
742
+ id,
743
+ name: skill.name || id,
744
+ score: 0,
745
+ matchingKeywords: [],
746
+ matchingCapabilities: [],
747
+ risk: skill.risk,
748
+ source: skill.source?.id || "local",
749
+ base: true,
750
+ });
751
+ tracer.logSelectionDecision(id, 0, {
752
+ reason: `Skill base (loadStrategy: "base") — siempre seleccionada`
753
+ });
754
+ continue;
755
+ }
756
+
757
+ const score = scoreSkill(skill, lower, options.weights);
758
+ if (score > 0) {
759
+ candidates.push({
760
+ id,
761
+ name: skill.name || id,
762
+ score,
763
+ matchingKeywords: (skill.keywords || []).filter((k) => lower.includes(k.toLowerCase())).slice(0, 4),
764
+ matchingCapabilities: (skill.capabilities || []).filter((c) => lower.includes(c.toLowerCase())),
765
+ risk: skill.risk,
766
+ source: skill.source?.id || "local",
767
+ });
768
+ }
769
+ }
770
+
771
+ candidates.sort((a, b) => b.score - a.score);
772
+ const limit = rigor === "MINIMAL" ? 0 : rigor === "RIGOROUS" ? 5 : 3;
773
+ // Base skills always selected; on-demand candidates fill remaining slots
774
+ const selected = [...baseSkills, ...candidates.slice(0, limit)];
775
+ const overflow = candidates.slice(Math.max(0, limit - baseSkills.length));
776
+
777
+ // Log selection decisions
778
+ selected.forEach(skill => {
779
+ tracer.logSelectionDecision(skill.id, skill.score, {
780
+ reason: skill.base
781
+ ? `Skill base (loadStrategy: "base") — siempre seleccionada`
782
+ : `Seleccionado por score ${skill.score} y keywords: ${skill.matchingKeywords.join(', ')}`
783
+ });
784
+ });
785
+
786
+ overflow.forEach(skill => {
787
+ tracer.logSelectionDecision(skill.id, skill.score, {
788
+ reason: `Rechazado por score bajo (${skill.score})`
789
+ });
790
+ });
791
+
792
+ rejected.forEach(skill => {
793
+ tracer.logSelectionDecision(skill.id, 0, {
794
+ reason: skill.reason
795
+ });
796
+ });
797
+
798
+ return {
799
+ candidates,
800
+ selected: selected.map((c) => ({
801
+ id: c.id,
802
+ name: c.name,
803
+ relevance: c.score,
804
+ reason: c.base
805
+ ? `Skill base (loadStrategy: "base")`
806
+ : `score ${c.score} (${c.matchingCapabilities.join(",") || c.matchingKeywords.join(",")})`,
807
+ capabilities: c.matchingCapabilities,
808
+ keywords: c.matchingKeywords,
809
+ risk: c.risk,
810
+ source: c.source,
811
+ hasContent: !!(registry[c.id]?.content || "").trim(),
812
+ loaded: !!(registry[c.id]?.content || "").trim(),
813
+ base: !!c.base,
814
+ })),
815
+ rejected: rejected.map((r) => r.name),
816
+ exceeded: overflow.map((c) => c.name),
817
+ counts: { selected: selected.length, limit, total: candidates.length, base: baseSkills.length },
818
+ explain: () =>
819
+ selected.map((c) => ` ${c.name}: score ${c.score} | caps: ${c.matchingCapabilities.join(",") || "-"} | kw: ${c.matchingKeywords.join(",") || "-"} | risk: ${c.risk} | src: ${c.source}${c.base ? " | BASE" : ""}`).join("\n"),
820
+ };
821
+ }
822
+
823
+ /**
824
+ * Descubre skills en el registro (dirs locales), extrae metadata sin cargar contenido.
825
+ * Registry: id, name, source, capabilities, triggers, risk, compatibility, status.
826
+ */
827
+ /**
828
+ * Lee el contenido de una skill local desde su SKILL.md y remueve el
829
+ * frontmatter YAML. Se embebe en el registry en build-time para que la
830
+ * inyeccion N3 (assembleContext) y la carga on-demand operen igual que con
831
+ * el catalogo bundleado (que ya trae content embebido).
832
+ */
833
+ function readSkillContent(skillPath) {
834
+ if (!skillPath) return "";
835
+ try {
836
+ const raw = fs.readFileSync(skillPath, "utf-8");
837
+ return raw.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "").trim();
838
+ } catch {
839
+ return "";
840
+ }
841
+ }
842
+
843
+ function buildSkillRegistry(availableSkills) {
844
+ const registry = {};
845
+ const builtinCapabilities = {
846
+ "nestjs-developer": { capabilities: ["backend", "nestjs", "api"], triggers: ["nestjs", "nest", "api", "endpoint", "guard", "module"], risk: "medium" },
847
+ "agent-context-engineering": { capabilities: ["context", "prompt", "agent"], triggers: ["contexto", "context", "prompt", "system prompt"], risk: "low" },
848
+ "angular-developer": { capabilities: ["frontend", "angular", "component"], triggers: ["angular", "componente", "frontend"], risk: "medium" },
849
+ "angular-new-app": { capabilities: ["frontend", "angular", "scaffold"], triggers: ["angular", "app", "nuevo proyecto"], risk: "low" },
850
+ "anti-reimplementation": { capabilities: ["design", "reuse"], triggers: ["reimplementar", "reescribir", "desde cero"], risk: "medium" },
851
+ "architectural-governance": { capabilities: ["architecture", "coherence"], triggers: ["arquitectura", "architecture", "drift"], risk: "high" },
852
+ "backend-integrity": { capabilities: ["backend", "transactions", "data"], triggers: ["base de datos", "transaccion", "db", "source of truth"], risk: "high" },
853
+ "frontend-boundary-protection": { capabilities: ["frontend", "contract"], triggers: ["frontend", "componente", "interfaz"], risk: "medium" },
854
+ "efficient-coding": { capabilities: ["implementation", "efficiency"], triggers: ["implementar", "codigo", "refactor"], risk: "low" },
855
+ "graphify": { capabilities: ["analysis", "knowledge-graph"], triggers: ["graph", "grafo", "arquitectura", "analisis"], risk: "low" },
856
+ "repository-semantic-map": { capabilities: ["analysis", "onboarding"], triggers: ["onboarding", "mapear", "explorar"], risk: "low" },
857
+ "senior-refactor-reviewer": { capabilities: ["refactor", "review"], triggers: ["refactor", "review", "mantenibilidad"], risk: "medium" },
858
+ "shadcn": { capabilities: ["frontend", "ui"], triggers: ["shadcn", "ui", "component"], risk: "low" },
859
+ "ui-ux-design-pro": { capabilities: ["frontend", "ui", "design"], triggers: ["ui", "ux", "diseno", "design", "dashboard"], risk: "low" },
860
+ "fixing-accessibility": { capabilities: ["frontend", "a11y", "accessibility"], triggers: ["accessibilidad", "a11y", "aria"], risk: "low" },
861
+ "fixing-metadata": { capabilities: ["frontend", "seo", "meta"], triggers: ["seo", "meta", "og", "cannonical"], risk: "low" },
862
+ "fixing-motion-performance": { capabilities: ["frontend", "performance", "animation"], triggers: ["animacion", "animation", "rendimiento"], risk: "low" },
863
+ "baseline-ui": { capabilities: ["frontend", "ui"], triggers: ["ui", "css", "tailwind"], risk: "low" },
864
+ "bailian-cli": { capabilities: ["ai", "bailian", "alibaba"], triggers: ["bailian", "百炼", "dashscope"], risk: "low" },
865
+ "bailian-finetune": { capabilities: ["ai", "bailian", "finetune"], triggers: ["finetune", "fine-tune", "bailian"], risk: "low" },
866
+ "bailian-gen": { capabilities: ["ai", "bailian", "generation"], triggers: ["generar", "imagen", "video", "tts", "bailian"], risk: "low" },
867
+ "bailian-managed-agent": { capabilities: ["ai", "bailian", "agent"], triggers: ["agent", "bailian", "managed-agent"], risk: "low" },
868
+ "bailian-protocol": { capabilities: ["ai", "bailian", "protocol"], triggers: ["bailian", "protocol"], risk: "low" },
869
+ // mattpocock/skills — MIT licensed, vendored into skills/
870
+ "writing-for-agents": { capabilities: ["context", "prompt", "agent"], triggers: ["escribir skill", "editar skill", "AGENTS.md", "CLAUDE.md", "writing for agents"], risk: "low", loadStrategy: "base" },
871
+ "codebase-design": { capabilities: ["architecture", "deep-module"], triggers: ["deep module", "arquitectura de modulo", "seam", "testable", "interface"], risk: "low", loadStrategy: "base" },
872
+ "diagnosing-bugs": { capabilities: ["debug", "diagnosis"], triggers: ["debug", "diagnose", "broken", "throwing", "failing", "slow", "regression"], risk: "high" },
873
+ "tdd": { capabilities: ["test", "tdd"], triggers: ["test", "TDD", "test-first", "red-green-refactor", "integration test"], risk: "low" },
874
+ "handoff": { capabilities: ["context", "handoff"], triggers: ["handoff", "hand-off", "continuar despues", "next agent", "pasar la pelota"], risk: "low" },
875
+ "wizard": { capabilities: ["setup", "infra", "credentials"], triggers: ["setup", "configurar", "credenciales", "one-shot", "migracion", "cutover"], risk: "medium" },
876
+ "prototype": { capabilities: ["design", "ui", "state"], triggers: ["prototype", "sanity-check", "state model", "validar disenio"], risk: "medium" },
877
+ "improve-codebase-architecture": { capabilities: ["architecture", "deepening"], triggers: ["deepening", "arquitectura", "scan", "friction", "AI-navigable"], risk: "medium" },
878
+ };
879
+
880
+ for (const [name, info] of Object.entries(availableSkills || {})) {
881
+ const base = builtinCapabilities[name] || {
882
+ capabilities: [],
883
+ triggers: [],
884
+ risk: "low",
885
+ };
886
+ // Extension contract: an entry MAY carry explicit routing metadata under
887
+ // `metadata`. Injected metadata wins over builtins so a new capability can
888
+ // be registered without editing this module. Entries without `metadata`
889
+ // behave exactly as before.
890
+ const ext = info?.metadata || {};
891
+ const baseLoad = base.loadStrategy ?? "ondemand";
892
+ const meta = {
893
+ capabilities: ext.capabilities ?? base.capabilities,
894
+ triggers: ext.triggers ?? base.triggers,
895
+ risk: ext.risk ?? base.risk,
896
+ loadStrategy: ext.loadStrategy ?? baseLoad,
897
+ };
898
+ // Skills locales ya presentes -> marcadas APPROVED (confianza local)
899
+ registry[name] = {
900
+ id: name,
901
+ name,
902
+ source: { kind: "local", path: info?.path, ref: "installed" },
903
+ capabilities: meta.capabilities,
904
+ triggers: meta.triggers,
905
+ risk: meta.risk,
906
+ loadStrategy: meta.loadStrategy,
907
+ compatibility: { opencode: true },
908
+ status: "APPROVED",
909
+ cache: false,
910
+ content: readSkillContent(info?.path),
911
+ ...(ext.keywords ? { keywords: ext.keywords } : {}),
912
+ ...(ext.domain ? { domain: ext.domain } : {}),
913
+ };
914
+ }
915
+
916
+ return registry;
917
+ }
918
+
919
+ /**
920
+ * Evalúa una skill bajo demanda (heurística para v1).
921
+ * Determinístico: status es APPROVED/REJECTED según criterios de calidad.
922
+ */
923
+ function evaluateSkill(skillId, registry) {
924
+ const skill = registry[skillId];
925
+ if (!skill) return { skillId, status: "REJECTED", reason: "No registrada" };
926
+ if (!skill.compatibility?.opencode && skill.source?.kind !== "local") {
927
+ return { skillId, status: "REJECTED", reason: "Incompatible con OpenCode" };
928
+ }
929
+ if (skill.risk === "high" && skill.status !== "APPROVED") {
930
+ return { skillId, status: "REJECTED", reason: "Skill de alto riesgo no aprobada" };
931
+ }
932
+ return { skillId, status: "APPROVED", reason: "Aprobada por registry" };
933
+ }
934
+
935
+ /**
936
+ * Carga una skill bajo demanda (content-load, sólo tras selección).
937
+ * El contenido real viaja embebido en el catálogo (build-time); sin red en runtime.
938
+ */
939
+ export function loadSkillOnDemand(skillId, registry, baseDir) {
940
+ logger.info("plugin-load", `Starting loadSkillOnDemand for skill: ${skillId}`);
941
+ const tracer = new ContextDecisionTracer(`load-${skillId}`, baseDir || process.cwd());
942
+ tracer.logConceptExtraction(`Carga de skill ${skillId}`, { source: "demand" });
943
+
944
+ const skill = registry[skillId];
945
+ if (!skill) {
946
+ tracer.logSelectionDecision(skillId, 0, { reason: "No registrada" });
947
+ return { loaded: false, reason: "No registrada" };
948
+ }
949
+ if (!APPROVED_STATUSES.includes(skill.status)) {
950
+ tracer.logSelectionDecision(skillId, 0, { reason: `No aprobada (${skill.status})` });
951
+ return { loaded: false, reason: `No aprobada (${skill.status})` };
952
+ }
953
+ if (!skill.content || !skill.content.trim()) {
954
+ tracer.logSelectionDecision(skillId, 0, { reason: `Sin contenido embebido (${skillId}) — catálogo metadata-only` });
955
+ return { loaded: false, skillId, reason: `Sin contenido embebido (${skillId}) — catálogo metadata-only` };
956
+ }
957
+ const root = baseDir || process.cwd();
958
+ const dir = path.join(root, ".wam", "skills", "bundled", skillId);
959
+ const skillFile = path.join(dir, "SKILL.md");
960
+ if (!fileExists(skillFile)) {
961
+ fs.mkdirSync(dir, { recursive: true });
962
+ fs.writeFileSync(skillFile, skill.content);
963
+ }
964
+ tracer.logSelectionDecision(skillId, 1, { reason: "Cargada exitosamente" });
965
+ return {
966
+ loaded: true,
967
+ skillId,
968
+ contentPath: skillFile,
969
+ instructions: "Skill materializada para el runtime nativo de OpenCode. Cargar SKILL.md desde contentPath si el agente ejecuta esta skill.",
970
+ };
971
+ }
972
+
973
+
974
+
975
+ /**
976
+ * Inspecciona el proyecto para Known/Inferred/Assumed/Unknown
977
+ */
978
+ async function inspectProject(projectPath) {
979
+ const known = [];
980
+ const inferred = [];
981
+ const assumed = [];
982
+ const unknown = [];
983
+
984
+ // Read AGENTS.md
985
+ const agentsMd = getAgentsMd(projectPath);
986
+ if (agentsMd.trim()) {
987
+ known.push("AGENTS.md presente en el proyecto");
988
+ }
989
+
990
+ // Read package.json
991
+ const pkgJson = getPackageJson(projectPath);
992
+ if (pkgJson) {
993
+ known.push("package.json detectado");
994
+ known.push(`Dependencias: ${Object.keys(pkgJson.dependencies || {}).join(", ") || "ninguna"}`);
995
+
996
+ // Detect framework
997
+ const deps = { ...(pkgJson.dependencies || {}), ...(pkgJson.devDependencies || {}) };
998
+ if (deps["@nestjs/*"] || deps.nestjs) inferred.push("NestJS framework detectado");
999
+ if (deps["@angular/core"] || deps.angular) inferred.push("Angular framework detectado");
1000
+ if (deps.express) inferred.push("Express.js detectado");
1001
+ if (deps["@nestjs/core"]) inferred.push("NestJS core detectado");
1002
+ }
1003
+
1004
+ // Detect OpenSpec specs
1005
+ const openspecDir = path.join(projectPath, "openspec");
1006
+ if (fileExists(openspecDir)) {
1007
+ known.push("Directorio openspec presente");
1008
+ // List specs
1009
+ try {
1010
+ const specFiles = fs.readdirSync(openspecDir).filter(f => f.endsWith(".md"));
1011
+ if (specFiles.length > 0) {
1012
+ known.push(`Specs OpenSpec: ${specFiles.join(", ")}`);
1013
+ }
1014
+ } catch {}
1015
+ }
1016
+
1017
+ // Detect skills in project
1018
+ const skillCandidates = discoverSkills();
1019
+ if (Object.keys(skillCandidates).length > 0) {
1020
+ known.push(`Skills disponibles: ${Object.keys(skillCandidates).join(", ")}`);
1021
+ }
1022
+
1023
+ // Detect stack
1024
+ const stackInfo = detectStack(projectPath);
1025
+ if (stackInfo.stack !== "unknown") {
1026
+ known.push(`Stack tecnológico: ${stackInfo.stack}`);
1027
+ stackInfo.languages.forEach(lang => inferred.push(`Idioma: ${lang}`));
1028
+ }
1029
+
1030
+ // If we have minimal info, add generic unknowns
1031
+ if (known.length < 3) {
1032
+ unknown.push("Contexto del proyecto limitado - inspección recomendada");
1033
+ }
1034
+
1035
+ return { known, inferred, assumed, unknown };
1036
+ }
1037
+
1038
+ /**
1039
+ * Audita supuestos detectados en la petición
1040
+ */
1041
+ function auditAssumptions(prompt, projectInfo) {
1042
+ const assumptions = [];
1043
+
1044
+ // Common assumptions based on prompt language
1045
+ const lower = prompt.toLowerCase();
1046
+
1047
+ // If user mentions "add X" without context
1048
+ if (/agregar|add/.test(lower) && !/ya tiene|already has/.test(lower)) {
1049
+ assumptions.push("El usuario asume que se necesita agregar nueva funcionalidad");
1050
+ }
1051
+
1052
+ // If mentions performance
1053
+ if (/mejorar.*rendimiento|performance/.test(lower)) {
1054
+ assumptions.push("El usuario asume que la mejora de rendimiento es la prioridad");
1055
+ }
1056
+
1057
+ // If mentions authentication
1058
+ if (/autentic(?:ión|ar|o|a)|oauth|jwt/.test(lower)) {
1059
+ assumptions.push("El usuario asume que el cambio es sobre autenticación");
1060
+ }
1061
+
1062
+ // If mentions cache/redis
1063
+ if (/(cache|redis)/.test(lower)) {
1064
+ assumptions.push("El usuario asume que se necesita cache/redis");
1065
+ }
1066
+
1067
+ // Project-context assumptions
1068
+ if (projectInfo) {
1069
+ const { known, inferred } = projectInfo;
1070
+
1071
+ // If NestJS detected but user says "add auth"
1072
+ if (inferred.some(i => i.includes("NestJS")) && /auth/.test(lower)) {
1073
+ assumptions.push("NestJS auth module existente - ¿reemplazar o extender?");
1074
+ }
1075
+
1076
+ // If Angular detected
1077
+ if (inferred.some(i => i.includes("Angular")) && /service/.test(lower)) {
1078
+ assumptions.push("Angular service - ¿nueva o reemplazo?");
1079
+ }
1080
+ }
1081
+
1082
+ return assumptions;
1083
+ }
1084
+
1085
+ /**
1086
+ * Descubre skills relevantes para la tarea
1087
+ */
1088
+ function discoverSkillsForTask(prompt, projectInfo, availableSkills) {
1089
+ const lower = prompt.toLowerCase();
1090
+ const candidates = [];
1091
+ const selected = [];
1092
+ const rejected = [];
1093
+
1094
+ // Evaluate each available skill
1095
+ for (const [name, skillInfo] of Object.entries(availableSkills)) {
1096
+ const description = (skillInfo.description || "").toLowerCase();
1097
+ const triggers = (skillInfo.triggers || []).map(t => t.toLowerCase());
1098
+
1099
+ let relevanceScore = 0;
1100
+ let isRedundant = false;
1101
+
1102
+ // Check triggers match
1103
+ const triggerMatch = triggers.some(t => lower.includes(t));
1104
+ if (triggerMatch) relevanceScore += 20;
1105
+
1106
+ // Check description keywords
1107
+ const keywordCounts = {
1108
+ implementation: ["implement", "crear", "agregar", "add", "nuevo"],
1109
+ refactoring: ["refactor", "mejorar", "restructur"],
1110
+ debugging: ["bug", "error", "fallo", "debug"],
1111
+ security: ["seguridad", "security", "vulnerabil", "auth", "oauth"],
1112
+ testing: ["test", "pruebas", "jest", "mocha"],
1113
+ architecture: ["arquitectura", "architecture", "diseño", "design"],
1114
+ performance: ["rendimiento", "performance", "optimizar", "cache"],
1115
+ migration: ["migra", "migrate", "upgrade"],
1116
+ documentation: ["doc", "documenta", "readme"],
1117
+ };
1118
+
1119
+ for (const [category, keywords] of Object.entries(keywordCounts)) {
1120
+ const matches = keywords.filter(k => description.includes(k) || lower.includes(k));
1121
+ if (matches.length > 0) {
1122
+ relevanceScore += matches.length * 10;
1123
+ // Check if this category is already selected
1124
+ if (selected.some(s => s.category === category)) {
1125
+ isRedundant = true;
1126
+ }
1127
+ }
1128
+ }
1129
+
1130
+ // Check project-awareness
1131
+ if (projectInfo) {
1132
+ const { inferred } = projectInfo;
1133
+ // Project-specific skill selection
1134
+ if (inferred.some(i => i.includes("NestJS")) && name === "nestjs-best-practices") {
1135
+ relevanceScore += 30;
1136
+ }
1137
+ if (inferred.some(i => i.includes("Angular")) && name === "angular-developer") {
1138
+ relevanceScore += 30;
1139
+ }
1140
+ }
1141
+
1142
+ // Deduplication: reject if too redundant
1143
+ if (isRedundant && relevanceScore < 40) {
1144
+ relevanceScore -= 20;
1145
+ }
1146
+
1147
+ // Only include skills with meaningful relevance
1148
+ if (relevanceScore >= 30) {
1149
+ candidates.push({
1150
+ name,
1151
+ relevance: relevanceScore,
1152
+ description: skillInfo.description,
1153
+ });
1154
+ }
1155
+ }
1156
+
1157
+ // Sort by relevance
1158
+ candidates.sort((a, b) => b.relevance - a.relevance);
1159
+
1160
+ // Select top 3-5, reject the rest
1161
+ const maxSelect = 5;
1162
+ for (let i = 0; i < Math.min(candidates.length, maxSelect); i++) {
1163
+ selected.push(candidates[i].name);
1164
+ }
1165
+ for (let i = maxSelect; i < candidates.length; i++) {
1166
+ rejected.push(candidates[i].name);
1167
+ }
1168
+
1169
+ // If no candidates have enough relevance, select based on task type
1170
+ if (selected.length === 0) {
1171
+ const type = classifyRequest(prompt);
1172
+ const fallbackSkills = {
1173
+ question: ["frontend-boundary-protection", "efficient-coding"],
1174
+ research: ["graphify", "nestjs-best-practices"],
1175
+ debugging: ["nestjs-best-practices", "efficient-coding"],
1176
+ "bug-fix": ["nestjs-best-practices", "efficient-coding"],
1177
+ implementation: ["nestjs-best-practices", "efficient-coding"],
1178
+ refactoring: ["nestjs-best-practices", "efficient-coding"],
1179
+ architecture: ["architectural-governance", "nestjs-best-practices"],
1180
+ security: ["nestjs-best-practices", "efficient-coding"],
1181
+ performance: ["nestjs-best-practices", "efficient-coding"],
1182
+ testing: ["nestjs-best-practices", "efficient-coding"],
1183
+ migration: ["nestjs-best-practices", "efficient-coding"],
1184
+ infrastructure: ["nestjs-best-practices", "efficient-coding"],
1185
+ documentation: ["nestjs-best-practices", "efficient-coding"],
1186
+ investigation: ["graphify", "nestjs-best-practices"],
1187
+ planning: ["nestjs-best-practices", "efficient-coding"],
1188
+ "destructive-operation": ["nestjs-best-practices", "efficient-coding"],
1189
+ }[type.type] || ["efficient-coding"];
1190
+
1191
+ for (const skill of fallbackSkills) {
1192
+ if (availableSkills[skill]) selected.push(skill);
1193
+ else rejected.push(skill);
1194
+ }
1195
+ }
1196
+
1197
+ return { candidates, selected, rejected };
1198
+ }
1199
+
1200
+ /**
1201
+ * Determina el modo de operación (FAST/NORMAL/STRICT)
1202
+ */
1203
+ function determineMode(classification, projectInfo, riskLevel) {
1204
+ // FAST: trivial tasks
1205
+ if (classification.type === "trivial") return { mode: "FAST", reason: "Tarea trivial y no ambigua" };
1206
+
1207
+ // STRICT: high risk or architectural
1208
+ if (riskLevel === "high" || classification.type === "architectural") return { mode: "STRICT", reason: "Alto riesgo o decisión arquitectónica" };
1209
+
1210
+ // Check for STRICT triggers in prompt
1211
+ const lower = (classification.prompt || "").toLowerCase();
1212
+ const strictTriggers = ["migra", "security", "arquitectura", "producción", "destructivo"];
1213
+ if (strictTriggers.some(t => lower.includes(t))) {
1214
+ return { mode: "STRICT", reason: "Triggers STRICT detectados en la petición" };
1215
+ }
1216
+
1217
+ // Default: NORMAL
1218
+ return { mode: "NORMAL", reason: "Modo por defecto - análisis ligero" };
1219
+ }
1220
+
1221
+ /**
1222
+ * Main analysis function - entry point for the plugin
1223
+ */
1224
+
1225
+ /**
1226
+ * Detecta incertidumbres DECISION_CRITICAL expresadas en el prompt mismo,
1227
+ * SOLO cuando hay una decisión explícita de material (migrar-o-eliminar,
1228
+ * alcance de eliminación). Una migración normal es la tarea misma, no una
1229
+ * incertidumbre bloqueante. Rule-based, determinista.
1230
+ */
1231
+ export function extractPromptCriticalUncertainties(prompt = "") {
1232
+ const text = (prompt || "").trim();
1233
+ if (!text) return [];
1234
+ const found = [];
1235
+ const lower = text.toLowerCase();
1236
+
1237
+ if (/(migra|migrate|migr)[\s\S]*(o|or)[\s\S]*(elim|borr|dele|descart|discard)/i.test(lower)) {
1238
+ found.push({
1239
+ question: "¿Deben migrarse o eliminarse los datos existentes?",
1240
+ source: text.slice(0, 140),
1241
+ options: ["Migrar", "Eliminar"],
1242
+ });
1243
+ } else if (/elim|borr[ao]|delet/i.test(lower)) {
1244
+ found.push({
1245
+ question: "¿Alcance de la eliminación (hard delete / soft delete / anonymizar)?",
1246
+ source: text.slice(0, 140),
1247
+ options: ["hard delete", "soft delete", "anonymizar"],
1248
+ });
1249
+ }
1250
+ return found;
1251
+ }
1252
+
1253
+ const ASSUMPTION_IMPACT =
1254
+ /elim|delet|borr|actualiz|update|schema|esquema|migraci|endpoint|api|arquitectur|estructura|m[oó]dulo|seguridad|auth|token|compatib|breaking|alcance|scope|destructiv|data.?loss|aceptaci[oó]n|criterios/i;
1255
+
1256
+ /**
1257
+ * Clasifica una asunción (rule-based): DECISION_CRITICAL si toca impacto
1258
+ * material (data mutation, API, arquitectura, seguridad, compat, alcance,
1259
+ * destructivo, aceptación); si no, NON_BLOCKING.
1260
+ */
1261
+ export function classifyAssumption(statement = "") {
1262
+ return ASSUMPTION_IMPACT.test(statement) ? "DECISION_CRITICAL" : "NON_BLOCKING";
1263
+ }/**
1264
+ * Convierte asunciones textuales del análisis en objetos de estado
1265
+ * {id, statement, classification, status} (spec assumption-gate R1).
1266
+ */
1267
+ /**
1268
+ * DECISION_CRITICAL/blocking, y las mirroriza a contract.unknowns (blocking,
1269
+ * con assumptionId) para reutilizar la fase ASKING (spec R2/R3).
1270
+ */
1271
+ export function escalateAssumptions(state, taskText = "") {
1272
+ const contract = state?.contract || {};
1273
+ const escalated = [];
1274
+ const assumptions = contract.assumptions || [];
1275
+ let unknowns = contract.unknowns || [];
1276
+ let changed = false;
1277
+
1278
+ for (const a of assumptions) {
1279
+ if (a.status === "active" && a.classification === "NON_BLOCKING") {
1280
+ const taskImpact = !!taskText && ASSUMPTION_IMPACT.test(taskText);
1281
+ const genericAssumption = /funcionalidad|agregar nueva|nueva funcionalidad|add support|add.*support/i.test(a.statement);
1282
+ const cls =
1283
+ classifyAssumption(a.statement) === "DECISION_CRITICAL" ||
1284
+ (taskImpact && genericAssumption)
1285
+ ? "DECISION_CRITICAL"
1286
+ : "NON_BLOCKING";
1287
+ if (cls === "DECISION_CRITICAL") {
1288
+ a.classification = cls;
1289
+ a.status = "blocking";
1290
+ const existing = unknowns.find((u) => u.status === "blocking" && !u.assumptionId);
1291
+ if (existing) {
1292
+ existing.assumptionId = a.id; // reuso: la pregunta existente cubre esta asunción (sin duplicar)
1293
+ } else if (!unknowns.some((u) => u.assumptionId === a.id)) {
1294
+ unknowns = [
1295
+ ...unknowns,
1296
+ {
1297
+ id: `U${unknowns.length + 1}`,
1298
+ kind: "ASSUMED",
1299
+ question: `¿${a.statement}?`,
1300
+ status: "blocking",
1301
+ assumptionId: a.id,
1302
+ },
1303
+ ];
1304
+ }
1305
+ escalated.push(a);
1306
+ changed = true;
1307
+ }
1308
+ }
1309
+ }
1310
+
1311
+ if (changed) {
1312
+ contract.assumptions = assumptions;
1313
+ contract.unknowns = unknowns;
1314
+ state.contract = contract;
1315
+ }
1316
+ return { escalated, changed };
1317
+ }
1318
+
1319
+ // ---------------------------------------------------------------------------
1320
+ // Topic Scope Classifier — Determinista, sin LLM
1321
+ // ---------------------------------------------------------------------------
1322
+ // Clasifica la relación entre un nuevo mensaje y el contract activo en 3 estados:
1323
+ // "continuation" — mismo tema, el agente debe continuar la tarea actual
1324
+ // "extension" — tema relacionado, se puede agregar al contract existente
1325
+ // "new-topic" — tema diferente, requiere nuevo contract / backlog
1326
+
1327
+ function tokenize(text = "") {
1328
+ return new Set(
1329
+ (text || "")
1330
+ .toLowerCase()
1331
+ .replace(/[^a-z0-9áéíóúñü\s]/g, " ")
1332
+ .split(/\s+/)
1333
+ .filter((w) => w.length > 2)
1334
+ );
1335
+ }
1336
+
1337
+ function tokenOverlap(a, b) {
1338
+ const aSet = typeof a === "Set" ? a : tokenize(a);
1339
+ const bSet = typeof b === "Set" ? b : tokenize(b);
1340
+ if (!aSet.size || !bSet.size) return 0;
1341
+ let hit = 0;
1342
+ for (const t of bSet) if (aSet.has(t)) hit++;
1343
+ return hit / Math.max(aSet.size, 1);
1344
+ }
1345
+
1346
+ const DOMAIN_KEYWORDS = {
1347
+ frontend: /\b(frontend|fe|front-?end|ui|button|componente|vista|pantalla|react|angular|css|tailwind|html|form|input|modal)\b/i,
1348
+ backend: /\b(backend|back-?end|api|server|endpoint|base de datos|db|database|nest|express|postgres|mongo|route|middleware)\b/i,
1349
+ scraper: /\b(scrap|crawl|parser|puppeteer|playwright|selenium|fetch|http client)\b/i,
1350
+ tests: /\b(test|e2e|spec|coverage|unitario|prueba|mock|stub|fixture)\b/i,
1351
+ security: /\b(security|seguridad|auth|oauth|token|jwt|credencial|encrypt|hash)\b/i,
1352
+ infra: /\b(infra|deploy|docker|ci|cd|pipeline|aws|gcp|azure|kubernetes|k8s)\b/i,
1353
+ data: /\b(data|datos|migration|migración|schema|modelo|query|sql|orm)\b/i,
1354
+ };
1355
+
1356
+ function detectDomain(text = "") {
1357
+ const domains = [];
1358
+ for (const [domain, pattern] of Object.entries(DOMAIN_KEYWORDS)) {
1359
+ if (pattern.test(text)) domains.push(domain);
1360
+ }
1361
+ return domains;
1362
+ }
1363
+
1364
+ /**
1365
+ * Clasifica la relación entre un nuevo prompt y el contract activo.
1366
+ *
1367
+ * @param {string} prompt - El mensaje nuevo del usuario
1368
+ * @param {object} contract - El contract actual (requirements, status, etc.)
1369
+ * @returns {"continuation"|"extension"|"new-topic"}
1370
+ */
1371
+ export function topicScope(prompt, contract) {
1372
+ if (!contract?.requirements?.length) return "new-topic";
1373
+
1374
+ const promptTokens = tokenize(prompt);
1375
+ const contractText = contract.requirements.map((r) => r.title || r).join(" ");
1376
+ const contractTokens = tokenize(contractText);
1377
+
1378
+ // 1. Token overlap — qué tan relacionado está el prompt con los reqs actuales
1379
+ const overlap = tokenOverlap(promptTokens, contractTokens);
1380
+
1381
+ // 2. Domain overlap — comparten mismo dominio técnico?
1382
+ const promptDomains = detectDomain(prompt);
1383
+ const contractDomains = detectDomain(contractText);
1384
+ const domainOverlap = promptDomains.some((d) => contractDomains.includes(d));
1385
+
1386
+ // 3. Continuation signals — el usuario dice explícitamente que continúa
1387
+ const CONTINUATION_RE = /\b(continuar|sigue|siguiente|proximo|próximo|seguir|avanzar|completar|terminar|finalizar|resuelto|listo|hecho|done)\b/i;
1388
+ const isContinuationClaim = CONTINUATION_RE.test(prompt);
1389
+
1390
+ // 4. Decision: continuation > extension > new-topic
1391
+ if (isContinuationClaim && overlap > 0.1) return "continuation";
1392
+ if (overlap > 0.3 || (domainOverlap && overlap > 0.15)) return "extension";
1393
+ return "new-topic";
1394
+ }
1395
+
1396
+ /**
1397
+ * Agrega un requerimiento al backlog (fuera de alcance del contract activo).
1398
+ * El backlog se persiste en state.backlog y NUNCA se inyecta en contexto activo.
1399
+ */
1400
+ export function addToBacklog(state, title, source = "user") {
1401
+ if (!state.backlog) state.backlog = [];
1402
+ const id = `backlog-${Date.now()}`;
1403
+ state.backlog.push({
1404
+ id,
1405
+ title: (title || "").slice(0, 200),
1406
+ addedAt: new Date().toISOString(),
1407
+ source,
1408
+ status: "pending",
1409
+ });
1410
+ return id;
1411
+ }
1412
+
1413
+ /**
1414
+ * Obtiene el backlog formateado para inyección en contexto.
1415
+ * Solo se muestra bajo demanda (/wam backlog), nunca en el pack N0-N3.
1416
+ */
1417
+ export function formatBacklog(state) {
1418
+ const pending = (state.backlog || []).filter((b) => b.status === "pending");
1419
+ if (!pending.length) return null;
1420
+ const lines = [`[wam backlog] ${pending.length} requerimiento(s) fuera de alcance:`];
1421
+ for (const b of pending.slice(0, 10)) {
1422
+ lines.push(` ${b.id}: ${b.title}`);
1423
+ }
1424
+ if (pending.length > 10) lines.push(` ...(+${pending.length - 10} más)`);
1425
+ return lines.join("\n");
1426
+ }
1427
+
1428
+ /**
1429
+ * Synthesiza un Completion Contract específico a partir del prompt (rule-based,
1430
+ * sin LLM): descompone cláusulas accionables y deriva requisitos/verificación.
1431
+ * Fallback: genéricos si no se puede extraer nada.
1432
+ */
1433
+ /**
1434
+ * Máximo de requisitos individuales que synthesizeContract generará antes de
1435
+ * consolidar. Más allá de este umbral, los excedentes se fusionan en un
1436
+ * req de alto nivel para evitar contratos ingobernables (54 reqs, 12M tokens).
1437
+ */
1438
+ const MAX_CONTRACT_REQUIREMENTS = 10;
1439
+
1440
+ /**
1441
+ * Patrones de cláusulas NO accionables — preguntas, URLs, narrativa pura,
1442
+ * fragmentos de spec que no representan una tarea concreta.
1443
+ */
1444
+ const NON_ACTIONABLE_PATTERNS = [
1445
+ /^\?/, // empieza con ?
1446
+ /\?$/, // termina con ?
1447
+ /^https?:\/\//, // URLs
1448
+ /^https?:/,
1449
+ /^\//, // paths /rutas
1450
+ /^\{/, // objetos JSON
1451
+ /^\[/, // arrays JSON
1452
+ /^".*":\s/, // líneas de spec JSON ("field": value)
1453
+ /^\|/, // tablas markdown
1454
+ /^---/, // separadores
1455
+ /^#+/, // headers markdown
1456
+ /^```/, // code fences
1457
+ /^ejemplo/i, // "Ejemplo: ..."
1458
+ /^ejemplo:/i,
1459
+ /^ver tamb/i, // "Ver también..."
1460
+ /^nota:/i, // "Nota: ..."
1461
+ /^nota /i,
1462
+ /^decisión/i, // "Decisión arquitectónica..."
1463
+ /^decisión/i,
1464
+ /^nombre que/i, // "Nombre que usaría..."
1465
+ /^se propondría/i, // "Yo separaría..."
1466
+ /^yo separaría/i,
1467
+ /^mantendría/i,
1468
+ /^independiente de/i,
1469
+ ];
1470
+
1471
+ export function synthesizeContract(prompt = "", mode = "NORMAL", uncertainties = []) {
1472
+ const requirements = [];
1473
+ const verification = [];
1474
+ const lower = (prompt || "").toLowerCase();
1475
+
1476
+ // --- Gate 1: prompts tipo documento/spec largo (>2000 chars) → alto nivel ---
1477
+ // Evita explosión de cláusulas por comas. Un spec de 50 párrafos no son
1478
+ // 50 requisitos — es 1-3 bloques de trabajo accionables.
1479
+ if ((prompt || "").length > 2000) {
1480
+ const isSpec = /\b(spec|specification|propuesta|propuesto|objetivo|alcance|requisitos?|criterios?|edge cases?|api|endpoint|flujo principal)\b/i.test(lower);
1481
+ if (isSpec) {
1482
+ requirements.push("Implementar funcionalidad según la especificación");
1483
+ if (/test|prueba|suite/i.test(lower)) {
1484
+ requirements.push("Agregar tests para el cambio");
1485
+ verification.push("Ejecutar suite de tests relevante");
1486
+ }
1487
+ if (/security|seguridad|auth|oauth|token/i.test(lower)) {
1488
+ requirements.push("Auditar seguridad del cambio");
1489
+ verification.push("Revisar superficie de autenticación/secretos");
1490
+ }
1491
+ if (!verification.length) verification.push("Evidencia de satisfacción del contrato");
1492
+ const unknowns = (uncertainties || [])
1493
+ .filter((u) => u.classification === "DECISION_CRITICAL")
1494
+ .map((u) => ({ id: u.id, question: u.question, classification: u.classification, status: "blocking", options: u.options || [] }));
1495
+ return { requirements, constraints: [], verification, unknowns, status: "PROPOSED", rigor: mode };
1496
+ }
1497
+ }
1498
+
1499
+ // --- Gate 2: splitting tradicional con filtros ---
1500
+ const clauses = (prompt || "")
1501
+ .split(/[;,]|\by\b|\b\+\b|\band\b|\bplus\b/i)
1502
+ .map((c) => c.trim())
1503
+ .filter((c) => c.length > 4);
1504
+ const clean = (c) =>
1505
+ c
1506
+ .replace(/^(agrega|agregar|implementa|implementar|crea|crear|haz|hacer|refactoriza|refactorizar|migra|migrar|configura|configurar|habilita|habilitar|elimina|eliminar|arregla|arreglar|fix|fixear)\s+/i, "")
1507
+ .replace(/[.!¿?]$/, "")
1508
+ .trim();
1509
+ const verbFor = (clause) =>
1510
+ /^(elimina|eliminar|quita|quitar)/i.test(clause)
1511
+ ? "Eliminar"
1512
+ : /^(configura|configurar|habilita|habilitar)/i.test(clause)
1513
+ ? "Configurar"
1514
+ : /^(migra|migrar)/i.test(clause)
1515
+ ? "Migrar"
1516
+ : "Implementar";
1517
+
1518
+ for (const clause of clauses) {
1519
+ if (requirements.length >= MAX_CONTRACT_REQUIREMENTS) break;
1520
+ const cleaned = clean(clause);
1521
+ if (!cleaned || cleaned.length < 3) continue;
1522
+ // Filtrar cláusulas no accionables
1523
+ if (NON_ACTIONABLE_PATTERNS.some((p) => p.test(cleaned))) continue;
1524
+ // Filtrar cláusulas que son predominantemente URLs
1525
+ if (/https?:\/\/\S+/.test(cleaned) && cleaned.replace(/https?:\/\/\S+/g, "").trim().length < 10) continue;
1526
+ const cap = cleaned[0].toUpperCase() + cleaned.slice(1);
1527
+ requirements.push(`${verbFor(clause)}: ${cap}`);
1528
+ }
1529
+
1530
+ // Keyword-derived requirements (con cap)
1531
+ if (/test|prueba|suite/i.test(lower) && requirements.length < MAX_CONTRACT_REQUIREMENTS) {
1532
+ requirements.push("Agregar tests para el cambio");
1533
+ verification.push("Ejecutar suite de tests relevante");
1534
+ }
1535
+ if (/migra|migrate|migración|refresh.?token/i.test(lower) && requirements.length < MAX_CONTRACT_REQUIREMENTS) {
1536
+ requirements.push("Invalidar/rotar credenciales anteriores");
1537
+ verification.push("Verificar integridad de datos post-migración");
1538
+ }
1539
+ if (/security|seguridad|auth|oauth|token/i.test(lower) && requirements.length < MAX_CONTRACT_REQUIREMENTS) {
1540
+ requirements.push("Auditar seguridad del cambio");
1541
+ verification.push("Revisar superficie de autenticación/secretos");
1542
+ }
1543
+ if (/cache|redis|rendimiento|performance/i.test(lower)) {
1544
+ verification.push("Medir impacto de rendimiento");
1545
+ }
1546
+ if (!verification.length) verification.push("Evidencia de satisfacción del contrato");
1547
+
1548
+ if (!requirements.length) {
1549
+ requirements.push("Tarea completada según intención", "Resultados verificables");
1550
+ }
1551
+ const unknowns = (uncertainties || [])
1552
+ .filter((u) => u.classification === "DECISION_CRITICAL")
1553
+ .map((u) => ({ id: u.id, question: u.question, classification: u.classification, status: "blocking", options: u.options || [] }));
1554
+ return { requirements, constraints: [], verification, unknowns, status: "PROPOSED", rigor: mode };
1555
+ }
1556
+
1557
+ export async function analyze(options = {}) {
1558
+ const {
1559
+ prompt = "",
1560
+ projectPath,
1561
+ config,
1562
+ tierCaps,
1563
+ activePreset,
1564
+ activeMode,
1565
+ } = options;
1566
+
1567
+ // Step 1: Classify the request
1568
+ const classification = classifyRequest(prompt);
1569
+
1570
+ // Step 2: Inspect the project
1571
+ const projectInfo = await inspectProject(projectPath || process.cwd());
1572
+
1573
+ // Step 3: Audit assumptions + clasificar incertidumbre (decision-critical)
1574
+ const assumptions = auditAssumptions(prompt, projectInfo);
1575
+ const uncertainties = buildUncertainties(assumptions, projectInfo.unknown);
1576
+ const promptCriticals = extractPromptCriticalUncertainties(prompt);
1577
+ for (const pc of promptCriticals) {
1578
+ if (!uncertainties.some((u) => u.question === pc.question)) {
1579
+ uncertainties.push({
1580
+ id: `U${uncertainties.length + 1}`,
1581
+ question: pc.question,
1582
+ kind: "UNKNOWN",
1583
+ classification: "DECISION_CRITICAL",
1584
+ status: "active",
1585
+ options: pc.options || [],
1586
+ });
1587
+ }
1588
+ }
1589
+
1590
+ // Step 5: Select skills (registry unificado: local APPROVED + catálogo embebido)
1591
+ const availableSkills = discoverSkills();
1592
+ const baseDir = projectPath || process.cwd();
1593
+ const { registry: skillRegistry, corpusRoot, sources: usedSources, registryFile } =
1594
+ buildRegistry(availableSkills, baseDir);
1595
+
1596
+ // Step 6: Determine mode and contract
1597
+ const riskLevel = assumptions.some(a => /alto riesgo|high risk|peligroso|destructivo/.test(a)) ? "high" : "medium";
1598
+ const modeInfo = determineMode(classification, projectInfo, riskLevel);
1599
+
1600
+ // Step 6.5: Persistent policies — transversal, siempre evaluadas
1601
+ const persistentPolicies = evaluatePersistentPolicies(prompt, projectInfo, classification, modeInfo.mode);
1602
+
1603
+ // Step 5.5: Skill routing v2 — scoring ponderado + persistencia registry
1604
+ const rigor = modeInfo.mode === "FAST" ? "MINIMAL" : modeInfo.mode === "STRICT" ? "RIGOROUS" : "STANDARD";
1605
+ const skillSelection = routeSkillsV2(prompt, projectInfo, skillRegistry, rigor, { rigor, weights: config?.scoringWeights, projectRoot: baseDir });
1606
+ try { persistRegistry(registryFile, skillRegistry); } catch {}
1607
+
1608
+ // Explicalidad: qué skills seleccionadas y por qué (spec §23)
1609
+ const selectionExplain = skillSelection.explain();
1610
+ const auditEntries = [];
1611
+ try {
1612
+ for (const s of skillSelection.selected) {
1613
+ auditEntries.push(
1614
+ recordSkillUsage(
1615
+ { skill_id: s.name, source: s.source, selected_because: s.reason, loaded: s.loaded ?? false },
1616
+ baseDir
1617
+ )
1618
+ );
1619
+ }
1620
+ } catch {}
1621
+
1622
+ // Completion Contract proposal — específico del prompt (rule-based)
1623
+ const completionContract = synthesizeContract(prompt, modeInfo.mode, uncertainties);
1624
+
1625
+ // Step 7: Build the output
1626
+ const result = {
1627
+ intent: {
1628
+ classification: classification.type,
1629
+ ambiguity: classification.ambiguity,
1630
+ confidence: classification.confidence,
1631
+ },
1632
+ completionContract,
1633
+ project: {
1634
+ detected_stack: projectInfo.detected_stack || "unknown",
1635
+ architecture: projectInfo.known?.includes("openspec") ? "openspec project" : "unknown",
1636
+ relevant_files: [
1637
+ ...(projectInfo.known?.includes("AGENTS.md") ? ["AGENTS.md"] : []),
1638
+ ...(projectInfo.known?.includes("package.json") ? ["package.json"] : []),
1639
+ ],
1640
+ },
1641
+ known: projectInfo.known,
1642
+ inferred: projectInfo.inferred,
1643
+ assumed: assumptions,
1644
+ assumptions: buildAssumptions(assumptions),
1645
+ unknown: projectInfo.unknown,
1646
+ uncertainties,
1647
+
1648
+ questions: [],
1649
+
1650
+ // Persistent policies: siempre aplicadas (transversal)
1651
+ persistentPolicies: persistentPolicies.applied.map((p) => ({
1652
+ policy: p.policy,
1653
+ status: p.status,
1654
+ gates: p.gates,
1655
+ restrictions: p.restrictions.slice(0, 3),
1656
+ })),
1657
+
1658
+ // Skill Registry: metadata-first routing, on-demand
1659
+ skillRegistry: {
1660
+ total: Object.keys(skillRegistry).length,
1661
+ approved: Object.values(skillRegistry).filter((s) => APPROVED_STATUSES.includes(s.status)).length,
1662
+ discovered: Object.values(skillRegistry).filter((s) => s.status === "DISCOVERED").length,
1663
+ rejected: Object.values(skillRegistry).filter((s) => !APPROVED_STATUSES.includes(s.status) && s.status !== "DISCOVERED").length,
1664
+ sources: usedSources.map((s) => (typeof s === "string" ? s : s.id)),
1665
+ corpus: corpusRoot,
1666
+ },
1667
+ skills: {
1668
+ candidates: skillSelection.candidates.map((c) => ({
1669
+ name: c.name,
1670
+ relevance: c.score,
1671
+ capabilities: c.matchingCapabilities,
1672
+ keywords: c.matchingKeywords,
1673
+ risk: c.risk,
1674
+ source: c.source,
1675
+ })),
1676
+ selected: skillSelection.selected,
1677
+ rejected: skillSelection.rejected,
1678
+ exceeded: skillSelection.exceeded,
1679
+ limit: skillSelection.counts.limit,
1680
+ explanation: selectionExplain,
1681
+ },
1682
+
1683
+ risk: riskLevel,
1684
+ complexity: classification.type === "trivial" ? "trivial" : "medium",
1685
+ ambiguity: classification.ambiguity,
1686
+ strategy: modeInfo.mode,
1687
+
1688
+ ready: modeInfo.mode === "FAST" || (modeInfo.mode === "NORMAL" && classification.type !== "architectural" && riskLevel !== "high"),
1689
+ };
1690
+
1691
+ // Add mode-specific info
1692
+ if (modeInfo.mode === "STRICT") {
1693
+ result.strict = true;
1694
+ result.advice = "Análisis profundo requerido antes de continuar. Verificar supuestos y decisiones arquitectónicas.";
1695
+ } else if (modeInfo.mode === "FAST") {
1696
+ result.fast = true;
1697
+ result.advice = "Tarea trivial - proceder directamente sin entrevista";
1698
+ } else {
1699
+ result.strict = false;
1700
+ result.fast = false;
1701
+ result.advice = "Análisis normal - inspección contextual y preguntas solo si necesarias";
1702
+ }
1703
+
1704
+ return result;
1705
+ }