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

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
package/index.js ADDED
@@ -0,0 +1,1836 @@
1
+ import { analyze, getTaskState, persistTaskState, routeSkillsV2, loadSkillOnDemand, cavemanify, estimateTokens, buildAssumptions, escalateAssumptions, formatBacklog, findDuplicateTask } from "./src/skills/engine.js";
2
+ import { startExperiment, noteSuccess, noteFailure } from "./src/execution/execution-engine.js";
3
+ import { migrateLegacyCognition } from "./src/cognition/cognition-store.js";
4
+ import { handleMessage } from "./src/integration/message-handler.js";
5
+ import { injectWamParts } from "./src/integration/part-provenance.js";
6
+
7
+ import { initMemory, updateProjectMemo, summarizeOperationalContext, updateContext, getOperationalContext, updateTaskMemory, addRecentChange, recordDecision, getDecision, updateLiveContext, compactDecisions } from "./src/persistence/memory.js";
8
+ import { getSessionId, listCapsules, getCapsule, promoteCapsule, selectContext, retrieveContext, closeSession, resolveWamRoot, migrateLegacyCapsules } from "./src/context/context.js";
9
+ import { assembleContext } from "./src/context/assembly.js";
10
+ import { evaluateRequirement as evaluateRequirementChecks, verifyRequirement } from "./src/verification/verification.js";
11
+ import { ContextDecisionTracer } from "./src/context/context-decision-audit.js";
12
+ import { guardAction } from "./src/execution/runtime-guards.js";
13
+ import { WamPolicyBlock, evaluateAction } from "./src/policy/risk-engine.js";
14
+ import { enforceGovernance } from "./src/policy/governance-enforcement.js";
15
+ import { classifyByCapabilities, buildCandidate } from "./src/policy/strategy-capabilities.js";
16
+ import { getStatusReport } from "./src/execution/execution-state.js";
17
+ import { createSnapshot, checkContinuation, rebuildScope } from "./src/context/context-snapshot.js";
18
+ import fs from "node:fs";
19
+ import path from "node:path";
20
+
21
+ const sessionExecutions = new Map();
22
+
23
+ const SAFE_READ_TOOLS = new Set(["read", "read_file", "list_directory", "list_files", "get_file"]);
24
+
25
+ function isSafeReadTool(tool) {
26
+ const normalized = String(tool || "").toLowerCase();
27
+ if (SAFE_READ_TOOLS instanceof Set) return SAFE_READ_TOOLS.has(normalized);
28
+ if (SAFE_READ_TOOLS?.length) return SAFE_READ_TOOLS.some((t) => t?.toLowerCase() === normalized);
29
+ return ["read", "read_file", "list_directory", "list_files", "get_file"].includes(normalized);
30
+ }
31
+
32
+ function inferExpectedObservation(tool, args, requirement) {
33
+ const operation = String(tool || "").toLowerCase();
34
+ const outcomeType = inferOutcomeType(operation, args);
35
+
36
+ if (requirement) {
37
+ return {
38
+ requirementId: requirement.id,
39
+ outcomeType,
40
+ expected: {
41
+ status: "success",
42
+ exitCode: 0,
43
+ },
44
+ };
45
+ }
46
+
47
+ if (operation.includes("read") || operation.includes("list") || operation.includes("inspect")) {
48
+ return { outcomeType, expected: null };
49
+ }
50
+
51
+ if (operation.includes("write") || operation.includes("edit") || operation.includes("apply") || operation.includes("run") || operation.includes("test")) {
52
+ return {
53
+ outcomeType,
54
+ expected: {
55
+ status: "success",
56
+ exitCode: 0,
57
+ },
58
+ };
59
+ }
60
+
61
+ return { outcomeType, expected: null };
62
+ }
63
+
64
+ function inferOutcomeType(operation, args) {
65
+ if (operation.includes("run") || operation.includes("exec") || operation.includes("bash")) return "command_execution";
66
+ if (operation.includes("test")) return "test_execution";
67
+ if (operation.includes("write") || operation.includes("edit") || operation.includes("apply")) return "file_modification";
68
+ if (operation.includes("read") || operation.includes("list") || operation.includes("inspect")) return "file_inspection";
69
+ if (operation.includes("search") || operation.includes("grep")) return "search";
70
+ return "unknown";
71
+ }
72
+
73
+ async function bridgeExecution({ taskId, taskRoot, state, tool, args, callID, sessionID }) {
74
+ if (!callID) {
75
+ callID = `${sessionID || getSessionId()}__exec_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
76
+ }
77
+ const resolvedTaskRoot = taskRoot;
78
+ const resolvedTaskId = taskId;
79
+ const resolvedTool = tool;
80
+ const resolvedArgs = args || {};
81
+ const resolvedState = state || {};
82
+ const requirements = Array.isArray(resolvedState.requirements) ? resolvedState.requirements : [];
83
+ const requirement = requirements.find((item) => item?.status !== "done" && item?.status !== "verified") || requirements[0] || null;
84
+ const contractApproved = resolvedState.contract?.status === "APPROVED";
85
+ const phase = resolvedState.phase;
86
+
87
+ if (!taskId || !taskRoot || !tool || (!contractApproved && !["IMPLEMENTING", "VERIFYING"].includes(phase)) || isSafeReadTool(tool)) {
88
+ return;
89
+ }
90
+
91
+ const requirementText = requirement?.title || requirement?.description || requirement?.text || null;
92
+ const hypothesisStatement = requirementText ? `${requirement.id || "req"}: ${requirementText}` : `${tool} ${JSON.stringify(args)}`;
93
+ try {
94
+ const result = await startExperiment(taskRoot, taskId, {
95
+ statement: hypothesisStatement,
96
+ tool,
97
+ args,
98
+ expectedObservation: inferExpectedObservation(tool, args, requirement),
99
+ confidence: 0.5,
100
+ });
101
+
102
+ if (!result?.ok || !result.hypothesis || !result.experiment) {
103
+ console.log(`[wait-a-minute] No se pudo iniciar el experimento para ${tool}: ${result?.guard?.reason || "sin resultado"}`);
104
+ return;
105
+ }
106
+
107
+ const key = callID || `${sessionID}:${tool}`;
108
+ const mapping = {
109
+ hypothesisId: result.hypothesis.id,
110
+ experimentId: result.experiment.id,
111
+ requirementId: requirement?.id || null,
112
+ };
113
+ sessionExecutions.set(key, mapping);
114
+ return {
115
+ ok: true,
116
+ callID: key,
117
+ hypothesisId: result.hypothesis.id,
118
+ experimentId: result.experiment.id,
119
+ requirementId: requirement?.id || null,
120
+ };
121
+ } catch (error) {
122
+ console.log(`[wait-a-minute] Falló el puente de ejecución para ${tool}: ${error.message}`);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Wait a Minute plugin for OpenCode.
128
+ *
129
+ * Adds deterministic control, task-state management, context enrichment, and
130
+ * token optimization to OpenCode agents by correlating tasks, skills, context,
131
+ * evidence, and verified state to determine what should happen next.
132
+ *
133
+ * Intercepts the prompt via chat.message hook before skill resolution and
134
+ * agent execution. Classifies the request, inspects the project, detects
135
+ * assumptions, and selects relevant skills.
136
+ *
137
+ * The analysis results are stored in the session and can be accessed by the
138
+ * main agent before proceeding with implementation.
139
+ */
140
+
141
+ const DEFAULT_CONFIG = {
142
+ tierCaps: { fast: 8, medium: 5, heavy: 3 },
143
+ activePreset: "omni",
144
+ silent: false,
145
+ budgetTokens: 32000,
146
+ contextBudget: 4000,
147
+ tierPrompts: {},
148
+ };
149
+
150
+ // opencode 1.18.25: chat.message output = { message, parts } (sin .system).
151
+ // Cada part debe incluir un id único (formato prt_...) o el mensaje falla al guardar
152
+ // (SchemaError: Missing key at ["part"]["id"]), bloqueando la sesión.
153
+ function genPartId() {
154
+ let s = "prt_";
155
+ while (s.length < 16) s += Math.floor(Math.random() * 16).toString(16);
156
+ return s;
157
+ }
158
+
159
+ // Inyecta texto visible al frente del mensaje de usuario; fallback a .system (API vieja).
160
+ function emitTextPart(output, text, meta = {}) {
161
+ if (!output) return;
162
+ const part = {
163
+ id: genPartId(),
164
+ type: "text",
165
+ text,
166
+ ...(meta.sessionID ? { sessionID: meta.sessionID } : {}),
167
+ ...(meta.messageID ? { messageID: meta.messageID } : {}),
168
+ };
169
+ injectWamParts(output, [part], { messageID: meta.messageID, phase: "emit", idFactory: genPartId, position: "prepend" });
170
+ }
171
+
172
+ // -- v1-enforcement: estado durable, contrato y progreso --------
173
+
174
+ function truncate(text, max = 80) {
175
+ if (!text || text.length <= max) return text || "";
176
+ return text.slice(0, max - 3) + "...";
177
+ }
178
+
179
+ function nextActionFrom(state) {
180
+ const pending = (state?.requirements || []).find((r) => !["done", "verified"].includes(r.status));
181
+ if (pending) return `Implementar ${truncate(pending.title)} (${pending.id} ${pending.status})`;
182
+ const unverified = (state?.requirements || []).find((r) => r.status === "done");
183
+ if (unverified) return `Verificar ${truncate(unverified.title)} — /wam progress ${unverified.id} verified <evidencia>`;
184
+ if (state?.requirements?.length) return "Verificar requisitos completos antes de DONE";
185
+ return "Continuar tarea";
186
+ }
187
+
188
+ const BLOCKED_TOOLS = new Set(["write", "edit", "bash", "task", "todowrite", "pty_spawn", "pty_write", "pty_kill"]);
189
+
190
+ // git de solo lectura (status/diff/log/...) es investigación read-only:
191
+ // nunca requiere run-loop ni delegación — permitido aun en ASKING.
192
+ const READONLY_GIT_RE = /^\s*git\s+(status|diff|log|show|ls-files|branch|stash\s+list|remote\s+-v|rev-parse|ls-remote)\b/;
193
+
194
+ function bashCommandOf(input) {
195
+ const c = input?.args?.command ?? input?.params?.command ?? input?.input?.command ?? "";
196
+ if (typeof c === "string" && c.trim()) return c;
197
+ try {
198
+ for (const v of Object.values(input?.args ?? {})) {
199
+ if (typeof v === "string" && /^\s*git\s+/m.test(v)) return v;
200
+ }
201
+ } catch {}
202
+ return "";
203
+ }
204
+
205
+ const ACTIVE_FILE = (root) => path.join(root || process.cwd(), ".wam", "active-task");
206
+
207
+ function readActiveTaskId(root) {
208
+ try {
209
+ const v = fs.readFileSync(ACTIVE_FILE(root), "utf-8").trim();
210
+ if (!v) return null;
211
+ try {
212
+ const j = JSON.parse(v);
213
+ if (j && typeof j === "object" && j.id) return j.id;
214
+ } catch {}
215
+ return v;
216
+ } catch {
217
+ return null;
218
+ }
219
+ }
220
+
221
+ function readActiveTaskRecord(root) {
222
+ try {
223
+ const raw = fs.readFileSync(ACTIVE_FILE(root), "utf-8").trim();
224
+ if (!raw) return null;
225
+ try {
226
+ const j = JSON.parse(raw);
227
+ if (j && j.id && typeof j.ts === "number") return j;
228
+ } catch {}
229
+ return { id: raw, ts: 0 };
230
+ } catch {
231
+ return null;
232
+ }
233
+ }
234
+
235
+
236
+
237
+ function writeActiveTaskId(id, root, options = {}) {
238
+ // Ownership: include sessionId + projectRoot + ts for session isolation.
239
+ // Legacy format {id,ts} remains readable; new format adds ownership fields.
240
+ const payload = {
241
+ id,
242
+ ts: Date.now(),
243
+ sessionId: options.sessionId || null,
244
+ projectRoot: options.projectRoot || root || process.cwd(),
245
+ };
246
+ try {
247
+ fs.mkdirSync(path.dirname(ACTIVE_FILE(root)), { recursive: true });
248
+ fs.writeFileSync(ACTIVE_FILE(root), JSON.stringify(payload));
249
+ } catch (err) {
250
+ // Critical write: surface error to caller for handling — never silent
251
+ console.error(`[WAM] writeActiveTaskId failed for ${id} at ${root}:`, err.message);
252
+ throw err;
253
+ }
254
+ }
255
+
256
+ function listTaskIds(root) {
257
+ const dir = path.join(root || process.cwd(), ".wam", "tasks");
258
+ try {
259
+ return fs
260
+ .readdirSync(dir, { withFileTypes: true })
261
+ .filter((d) => d.isDirectory())
262
+ .map((d) => d.name);
263
+ } catch {
264
+ return [];
265
+ }
266
+ }
267
+
268
+ // -- Persistencia por sesión: N sesiones pueden trabajar la MISMA carpeta sin
269
+ // colisionar en default-task / task-context.md / active-task. ----------------
270
+ const GENERIC_TASK = /^(default-task|task|general|)$/;
271
+
272
+ function readActiveTaskIdFresh(root, options = {}) {
273
+ const rec = readActiveTaskRecord(root);
274
+ if (!rec) return null;
275
+ const ttlMs = options.ttlMs || 60000;
276
+ if (!rec.ts) return null;
277
+ if (Date.now() - rec.ts > ttlMs) return null;
278
+ // Validate ownership: if record has sessionId, it must match caller session;
279
+ // if record has projectRoot, it must match caller's root. This prevents
280
+ // session A from silently adopting session B's active-task.
281
+ if (options.sessionId && rec.sessionId && rec.sessionId !== options.sessionId) return null;
282
+ if (options.projectRoot && rec.projectRoot && rec.projectRoot !== root) return null;
283
+ return rec.id;
284
+ }
285
+
286
+ /**
287
+ * taskId efectivo de WAM para un mensaje:
288
+ * - input.taskId explícito y específico (conversación de opencode) → se usa tal cual
289
+ * - taskId conocido en sessionTasks (cache de chat.message) → ese
290
+ * - active-task reciente (<=60s, escrito por /wam task switch o /wam resume) →
291
+ * ese (intención explícita reciente del usuario)
292
+ * - sin taskId ni sesión conocida → namespace por sesión: ses-<sessionID>
293
+ *
294
+ * active-task global NO se usa como fallback automático: solo /wam task switch
295
+ * o /wam resume lo consultan vía readActiveTaskIdFresh() con TTL de 60s. Esto
296
+ * evita que sesiones nuevas adopten estado de tareas previas en la misma
297
+ * carpeta; tras 60s sin actividad el active-task se considera obsoleto.
298
+ */
299
+ function effectiveTaskId(input, sessionTasks, wamRoot) {
300
+ if (input?.taskId && !GENERIC_TASK.test(input.taskId)) return input.taskId;
301
+ const cached = sessionTasks?.get?.(input?.sessionID);
302
+ if (cached) return cached;
303
+ const fresh = readActiveTaskIdFresh(wamRoot);
304
+ if (fresh) return fresh;
305
+ if (input?.sessionID) return `ses-${input.sessionID.slice(-10)}`;
306
+ return "default-task";
307
+ }
308
+
309
+ function taskDir(root, taskId) {
310
+ return path.join(root || process.cwd(), ".wam", "tasks", taskId);
311
+ }
312
+
313
+ function liveFileFor(root, taskId) {
314
+ return path.join(taskDir(root, taskId), "context.md");
315
+ }
316
+
317
+ function readLiveContext(root, taskId) {
318
+ const perSession = liveFileFor(root, taskId);
319
+ try {
320
+ if (fs.existsSync(perSession)) return fs.readFileSync(perSession, "utf-8").trim();
321
+ } catch {}
322
+ return "";
323
+ }
324
+
325
+ /**
326
+ * Live context por sesión: live snapshot en .wam/tasks/<taskId>/context.md.
327
+ * Aislado por task — dos sesiones sobre la misma carpeta NO se pisan el N2.
328
+ */
329
+ function persistLiveContext(taskId, state, root) {
330
+ updateLiveContext(taskId, state, root);
331
+ }
332
+
333
+ // -- operational memory (memory.js): contexto inicial acelerador (spec §13) --
334
+
335
+ function projectState(analysis) {
336
+ return {
337
+ contract: {
338
+ status: "PROPOSED",
339
+ rigor: analysis.completionContract?.rigor || "NORMAL",
340
+ requirements: analysis.completionContract?.requirements || [],
341
+ verification: analysis.completionContract?.verification || [],
342
+ constraints: analysis.completionContract?.constraints || [],
343
+ unknowns: analysis.completionContract?.unknowns || [],
344
+ assumptions: Array.isArray(analysis.assumptions)
345
+ ? analysis.assumptions
346
+ : buildAssumptions(analysis.assumed || []),
347
+ },
348
+ questions: [],
349
+ requirements: (analysis.completionContract?.requirements || []).map((title, i) => ({
350
+ id: `req-${i + 1}`,
351
+ title,
352
+ status: "pending",
353
+ evidence: [],
354
+ })),
355
+ persistentPolicies: (analysis.persistentPolicies || []).map((p) => p.policy),
356
+ activeGates: (analysis.persistentPolicies || []).flatMap((p) => p.gates || []),
357
+ };
358
+ }
359
+
360
+
361
+ function applyCompletionGate(state, promptText, taskId, waitAMinute, persistTaskState, nextActionFrom, root) {
362
+ const gate = waitAMinute.evaluateCompletionGate(state, promptText);
363
+ if (gate.blocked) {
364
+ state.phase = gate.verifying ? "VERIFYING" : "IMPLEMENTING";
365
+ state.nextAction = nextActionFrom(state);
366
+ persistTaskState(taskId, state, root);
367
+ } else if (gate.allDone) {
368
+ state.phase = "DONE";
369
+ state.nextAction = "Tarea completa — contrato verificado";
370
+ persistTaskState(taskId, state, root);
371
+ }
372
+ return gate;
373
+ }
374
+
375
+ /**
376
+ * Compresión automática de tarea (caveman-summary.md). Se genera en DONE
377
+ * (memoria comprimida para continuidad) y manualmente via /wam compress.
378
+ */
379
+ function writeCavemanSummary(taskId, state, root, extra = "") {
380
+ const reqs = state?.requirements || [];
381
+ const pend = reqs.filter((r) => r.status !== "done" && r.status !== "verified").length;
382
+ const base = [
383
+ `task ${taskId} — ${state?.phase || "?"} / ${state?.contract?.status || "?"}`,
384
+ `req: ${pend}/${reqs.length} pend | next: ${state?.nextAction || "—"}`,
385
+ ];
386
+ const cav = cavemanify([...base, extra].filter(Boolean).join("\n"));
387
+ try {
388
+ const dir = path.join(root || process.cwd(), ".wam", "tasks", taskId);
389
+ fs.mkdirSync(dir, { recursive: true });
390
+ fs.writeFileSync(path.join(dir, "caveman-summary.md"), cav + "\n");
391
+ } catch {}
392
+ return cav;
393
+ }
394
+
395
+
396
+ function domainHint(text = "") {
397
+ const t = text.toLowerCase();
398
+ const hints = [];
399
+ if (/\bfrontend\b|\bfe\b|front-?end|\bui\b|button|componente|vista|pantalla|react|web\b/.test(t)) hints.push("frontend");
400
+ if (/backend|back-?end|\bapi\b|server|endpoint|base de datos|\bdb\b|database|nest|express|postgres|mongo/.test(t)) hints.push("backend");
401
+ if (/scrap|crawl|parser|puppeteer|playwright/.test(t)) hints.push("scraper");
402
+ if (/test|e2e|spec|coverage|unitario/.test(t)) hints.push("tests");
403
+ if (/security|seguridad|auth|token|jwt|credencial/.test(t)) hints.push("security");
404
+ return hints.length ? hints.join("+") : "";
405
+ }
406
+
407
+ /**
408
+ * Directiva de delegación para reqs pendientes de un contrato APPROVED:
409
+ * cada req → Task en paralelo con agente libre (el que el entorno tenga).
410
+ */
411
+ function delegationLines(state) {
412
+ const reqs = (state?.requirements || []).filter((r) => r.status !== "done" && r.status !== "verified");
413
+ if (!reqs.length || state?.contract?.status !== "APPROVED") return [];
414
+ const lines = [
415
+ "[wam delegation] Reqs pendientes → delegar en PARALELO via Task (agente: el apropiado según config del entorno). Mutación directa de archivos desde la sesión principal está BLOQUEADA — solo un subagente ejecuta write/edit.",
416
+ ];
417
+ const visible = reqs.slice(0, MAX_VISIBLE_REQS);
418
+ const overflow = reqs.length - MAX_VISIBLE_REQS;
419
+ for (const r of visible) {
420
+ const hint = domainHint(r.title);
421
+ lines.push(` ${r.id} → Task(parallel) "${truncate(r.title, 120)}"${hint ? ` [contexto: ${hint}]` : ""}`);
422
+ }
423
+ if (overflow > 0) {
424
+ lines.push(` ...(+${overflow} requisitos adicionales consolidados)`);
425
+ }
426
+ return lines;
427
+ }
428
+
429
+ const MAX_VISIBLE_REQS = 8;
430
+
431
+ /**
432
+ * Strategy Continuity: classify whether an action is covered by an active
433
+ * approved strategy. Used by tool.execute.before to avoid asking for
434
+ * per-step approval for actions that are ordinary execution of an
435
+ * already-approved strategy.
436
+ *
437
+ * Returns:
438
+ * { covered: true, reason } → action proceeds without re-approval
439
+ * { covered: false, reason } → action requires explicit user decision
440
+ * { covered: null, reason } → no active strategy, fall through to default logic
441
+ */
442
+ function classifyActionAgainstStrategy(action, tool, args, approvedStrategy) {
443
+ if (!approvedStrategy || approvedStrategy.status !== "ACTIVE") return { covered: null, reason: "No active approved strategy" }; // Self-contained capability matching (extracted by autonomy-behavior-suite.test.mjs via `new Function(...)`, so this body must not reference external symbols and must avoid inner `\n}` that would prematurely terminate the regex match).
444
+ const actionStr = String(action || "").trim();
445
+ const toolStr = String(tool || "").trim();
446
+ const argv = (args && Array.isArray(args.argv)) ? args.argv : (args && typeof args.command === "string" ? args.command.split(/\s+/) : []);
447
+ const executable = toolStr === "bash" ? String(args && args.command || "").split(/\s+/)[0] : toolStr;
448
+ const candidate = { action: actionStr, tool: toolStr, executable: args };
449
+ const toCapability = (s) => { const str = String(s || "").trim(); const parts = str.split(/\s+/).filter(Boolean); if (parts.length === 0) return null; if (parts.length === 1) return { capability: "action.execute", action: parts[0] }; return { capability: "command.execute", executable: parts[0], argsPattern: parts.slice(1) }; };
450
+ const allowedCaps = (approvedStrategy.allowedActions || []).map(toCapability).filter(Boolean);
451
+ const prohibitedCaps = (approvedStrategy.prohibitedActions || []).map(toCapability).filter(Boolean);
452
+ // Canonical structured classifier (./src/policy/strategy-capabilities.js). In normal
453
+ // module scope these are imported; when this function is extracted via `new Function`
454
+ // (autonomy-behavior-suite.test.mjs) they are absent, so the `typeof` guards fall
455
+ // through to the equivalent inline matcher below (kept self-contained on purpose).
456
+ if (typeof classifyByCapabilities === "function" && typeof buildCandidate === "function") {
457
+ const builtCandidate = buildCandidate({ action: actionStr, tool: toolStr, args: args || {} });
458
+ const verdict = classifyByCapabilities(builtCandidate, { allowed: allowedCaps, prohibited: prohibitedCaps });
459
+ if (verdict && verdict.allowed === true) return { covered: true, reason: "Acción cubierta por strategy activa" };
460
+ if (allowedCaps.length === 0) return { covered: false, reason: "Acción no cubierta por capabilities estructuradas de la strategy" };
461
+ return { covered: false, reason: (verdict && verdict.reason) || "Acción no cubierta por strategy activa" };
462
+ }
463
+ const matchCapability = (cap, cand) => { if (!cap) return false; if (cap.capability === "action.execute") return cap.action === cand.action || cap.action === cand.tool; if (cap.capability === "command.execute") { if (cap.executable && cap.executable !== cand.executable) return false; if (Array.isArray(cap.argsPattern)) { for (let i = 0; i < cap.argsPattern.length; i++) { if (cap.argsPattern[i] !== cand.args[i]) return false; } } return true; } return false; };
464
+ const isProhibited = prohibitedCaps.some((cap) => matchCapability(cap, candidate));
465
+ const isAllowed = allowedCaps.some((cap) => matchCapability(cap, candidate));
466
+ if (isProhibited) return { covered: false, reason: "Acción prohibida explícitamente en strategy" };
467
+ if (allowedCaps.length === 0) return { covered: false, reason: "Acción no cubierta por capabilities estructuradas de la strategy" };
468
+ if (isAllowed) return { covered: true, reason: "Acción cubierta por strategy activa" };
469
+ return { covered: false, reason: "Acción no cubierta por strategy activa" };
470
+ }
471
+
472
+ // (Legacy substring-matching path removed. Strategy continuity is now evaluated
473
+ // against structured capabilities in ./src/policy/strategy-capabilities.js so that
474
+ // "npm test" cannot accidentally cover "npm publish" or "npm install <pkg>".)
475
+
476
+ /**
477
+ * Evidence-Guided Recovery: classify a failure so the agent must diagnose
478
+ * before proposing a strategic change.
479
+ *
480
+ * Returns a category hint. Provisional, MUST NOT be treated as fact.
481
+ */
482
+ function classifyFailure(result, expected, observed) {
483
+ const obs = String(observed || "").toLowerCase();
484
+ const res = String(result || "").toLowerCase();
485
+
486
+ if (/timeout|timed out/.test(obs)) return { category: "TIMING", priority: 2 };
487
+ if (/403|401|307|blocked|captcha|perimeterx|big-ip/.test(obs)) return { category: "ANTI_BOT", priority: 8 };
488
+ if (/enotfound|econnrefused|network/.test(obs)) return { category: "NETWORK", priority: 1 };
489
+ if (/0 products|empty|no results|not found/.test(obs)) return { category: "ZERO_RESULT", priority: 3 };
490
+ if (/selector|element not found|null/.test(obs)) return { category: "SELECTOR", priority: 4 };
491
+ if (/chromium|browser|executable/.test(obs)) return { category: "BROWSER_RUNTIME", priority: 5 };
492
+ if (/dependency|module not found|cannot find/.test(obs)) return { category: "DEPENDENCY", priority: 1 };
493
+ if (/permission denied|eacces/.test(obs)) return { category: "ENVIRONMENT", priority: 2 };
494
+ if (/parse|json|syntax/.test(obs)) return { category: "PARSER", priority: 4 };
495
+
496
+ return { category: "UNKNOWN", priority: 10 };
497
+ }
498
+
499
+ /**
500
+ * Reference Evidence Priority: load known-good references for the current
501
+ * task domain. Used in context assembly to anchor the agent to proven
502
+ * implementations instead of letting it invent alternatives.
503
+ */
504
+ function loadReferenceEvidence(taskRoot, intent) {
505
+ const refsFile = path.join(taskRoot || "", ".wam", "references.json");
506
+ if (!fs.existsSync(refsFile)) return [];
507
+ try {
508
+ const refs = JSON.parse(fs.readFileSync(refsFile, "utf-8"));
509
+ const goal = String(intent?.goal || intent?.classification || "").toLowerCase();
510
+ return (refs.references || []).filter((r) => {
511
+ const targets = (r.targets || []).map((t) => t.toLowerCase());
512
+ return targets.some((t) => goal.includes(t)) || goal.length === 0;
513
+ });
514
+ } catch {
515
+ return [];
516
+ }
517
+ }
518
+
519
+ function prepareSystemInject(analysis, state, cfg, projectDirectory, waitAMinute, taskId) {
520
+ // WAM injects objective + remaining requirements as advisory state.
521
+ // The agent implements EXACTLY what the user asked — no strategy deviation,
522
+ // no "decide your own approach". Only ambiguity triggers clarification.
523
+ const inject = [];
524
+ const reqs = state.requirements || [];
525
+ const pend = reqs.filter((r) => r.status !== "done");
526
+ const objective = state.contract?.objective || analysis.intent?.goal || null;
527
+
528
+ // Silence for trivial/fast: no inject noise.
529
+ if (analysis.intent?.classification === "trivial" || analysis.strategy === "FAST") {
530
+ return inject;
531
+ }
532
+
533
+ // Once: contract display, then contractDisplayed flips. After that the agent
534
+ // already has the objective in working memory — no need to repeat.
535
+ if (state.phase === "PROPOSED" && !state.contractDisplayed) {
536
+ const visible = reqs.slice(0, MAX_VISIBLE_REQS);
537
+ const overflow = reqs.length - MAX_VISIBLE_REQS;
538
+ inject.push(
539
+ `Objective: ${objective || "pending"}`,
540
+ `Remaining (${pend.length}/${reqs.length}):`,
541
+ ...visible.map((r) => ` - ${truncate(r.title, 120)}`),
542
+ ...(overflow > 0 ? [` ...(+${overflow} more)`] : [])
543
+ );
544
+ state.contractDisplayed = true;
545
+ }
546
+
547
+ return inject;
548
+ }
549
+
550
+ /**
551
+ * Plugin factory — loaded by OpenCode when registered in opencode.jsonc.
552
+ * Autocontained: no external deps beyond ./engine.js.
553
+ */
554
+ const WaitAMinutePlugin = async (pluginInput) => {
555
+ let cfg = { ...DEFAULT_CONFIG };
556
+
557
+ // Per-plugin-instance session store
558
+ const sessionStore = new Map();
559
+
560
+ // Project directory (opencode 1.18.25: plugin input, not ctx)
561
+ const projectDirectory = pluginInput?.directory || process.cwd();
562
+
563
+ // Track if plugin is bypassed
564
+ let bypassed = false;
565
+
566
+ // Multi-repo: root real de cada sesión. La factory del plugin V1 es GLOBAL
567
+ // al server (pluginInput.directory = cwd donde se lanzó opencode), pero cada
568
+ // sesión tiene su propio directorio de trabajo (location.directory) — lo
569
+ // consultamos vía client.session.get para no depender del cwd del proceso.
570
+ const sessionRoots = new Map();
571
+ const sessionParents = new Map(); // sessionID → parentID (subagentes Task tienen parent)
572
+ const sessionResolved = new Set(); // sessionIDs cuyo parentID se resolvió (distingue main de subagente)
573
+ const processedMessages = new Map(); // messageID+kind → processed (chat.message idempotency)
574
+ const sessionTasks = new Map(); // sessionID → taskId activo visto en chat.message
575
+ const client = pluginInput?.client;
576
+
577
+ async function resolveSessionBase(sessionID) {
578
+ if (sessionRoots.has(sessionID)) return sessionRoots.get(sessionID);
579
+ let base = projectDirectory;
580
+ try {
581
+ const info = client?.session?.get && sessionID ? await client.session.get({ sessionID }) : null;
582
+ if (info?.location?.directory) base = info.location.directory;
583
+ const parent = info?.parentID || info?.parent_id || info?.parent?.id;
584
+ if (parent) sessionParents.set(sessionID, parent);
585
+ if (info) sessionResolved.add(sessionID);
586
+ } catch {
587
+ // fallback: cwd del server (sesión no resuelta → fail-open, no se gatea)
588
+ }
589
+ sessionRoots.set(sessionID, base);
590
+ return base;
591
+ }
592
+
593
+ // .wam root para esta sesión+mensaje: repo git objetivo de la sesión real.
594
+ const wamRootFor = async (sessionID, promptText) => {
595
+ const base = await resolveSessionBase(sessionID);
596
+ return resolveWamRoot(promptText, base);
597
+ };
598
+
599
+ // Al iniciar/retomar cualquier sesión la memoria .wam debe existir.
600
+ // initMemory es idempotente: crea .wam/context si falta, no fabrica nada.
601
+ const ensureWamMemory = async (sessionID, promptText) => {
602
+ const root = await wamRootFor(sessionID, promptText);
603
+ try {
604
+ initMemory(root);
605
+ } catch {}
606
+ return root;
607
+ };
608
+
609
+ // -------------------------------------------------------------------------
610
+ // opencode 1.18.25 plugin API: factory RETURNS the hooks object
611
+ // -------------------------------------------------------------------------
612
+ return {
613
+ // /wam commands are registered via opencode.jsonc "command" entries on startup,
614
+ // NOT via a plugin config hook (removed: plugin config hook mutated config and
615
+ // is incompatible with opencode 1.18.26+, causing `N.config` TypeError).
616
+
617
+ // Chat message hook — Persistence & Progress Gate.
618
+ // Slim adapter: delegates message processing to src/integration/message-handler.js.
619
+ "chat.message": (input, output) =>
620
+ handleMessage(input, output, {
621
+ bypassed,
622
+ sessionTasks,
623
+ sessionStore,
624
+ cfg,
625
+ resolveSessionBase,
626
+ wamRootFor,
627
+ ensureWamMemory,
628
+ effectiveTaskId,
629
+ genPartId,
630
+ emitTextPart,
631
+ processedMessages,
632
+ readActiveTaskIdFresh,
633
+ writeActiveTaskId,
634
+ waitAMinute,
635
+ migrateLegacyCognition,
636
+ noteSuccess,
637
+ noteFailure,
638
+ getTaskState,
639
+ persistTaskState,
640
+ findDuplicateTask,
641
+ escalateAssumptions,
642
+ buildAssumptions,
643
+ initMemory,
644
+ updateProjectMemo,
645
+ updateLiveContext,
646
+ readLiveContext,
647
+ persistLiveContext,
648
+ assembleContext,
649
+ createSnapshot,
650
+ checkContinuation,
651
+ rebuildScope,
652
+ getDecision,
653
+ recordDecision,
654
+ compactDecisions,
655
+ writeCavemanSummary,
656
+ truncate,
657
+ delegationLines,
658
+ updateTaskMemory,
659
+ addRecentChange,
660
+ closeSession,
661
+ getSessionId,
662
+ classifyAskingMessage,
663
+ }),
664
+
665
+ // Handle /wam CLI (opencode 1.18.25: commands arrive via command.execute.before)
666
+ "command.execute.before": async (input, output) => {
667
+ if (input.command !== "wam") return;
668
+ output.parts = output.parts || [];
669
+ const sid = input.sessionID;
670
+ const root = await resolveSessionBase(sid);
671
+ const taskKey = sessionTasks.get(sid) || readActiveTaskIdFresh(root) || (sid ? `ses-${sid.slice(-10)}` : "default-task");
672
+ const argsArr = Array.isArray(input.arguments)
673
+ ? input.arguments
674
+ : (typeof input.arguments === "string" ? input.arguments.split(/\s+/) : []);
675
+ output.parts.push({
676
+ id: genPartId(),
677
+ type: "text",
678
+ text: await wamCli(argsArr, cfg, root, taskKey),
679
+ });
680
+ },
681
+
682
+ // Enforce Clarification Gate (spec change 4): en ASKING se bloquean las
683
+ // herramientas mutantes — la investigación read-only sigue permitida.
684
+ // Además captura el root real de la sesión desde los args de los tools
685
+ // (openmode web multi-proyecto: el cwd de la sesión llega por aquí, no
686
+ // por pluginInput.directory que es el cwd del proceso server).
687
+ "tool.execute.before": async (input, output) => {
688
+ let sid;
689
+ let taskRoot;
690
+ let taskId;
691
+ let st;
692
+ try {
693
+ if (bypassed) return;
694
+ sid = input?.sessionID;
695
+ taskRoot = await resolveSessionBase(sid);
696
+ taskId = sessionTasks.get(sid) || readActiveTaskIdFresh(taskRoot) || (sid ? `ses-${sid.slice(-10)}` : "default-task");
697
+ st = getTaskState(taskId, taskRoot);
698
+ // Fallback: try the active task from disk if first lookup failed
699
+ if (!st && !sid) {
700
+ const activeId = readActiveTaskIdFresh(taskRoot);
701
+ if (activeId && activeId !== taskId) {
702
+ taskId = activeId;
703
+ st = getTaskState(taskId, taskRoot);
704
+ }
705
+ }
706
+ const tool = input?.tool || "";
707
+ if (process.env.WAM_DEBUG_TE) {
708
+ console.log(`[WAM-DEBUG-TE] sid=${sid} taskId=${taskId} phase=${st?.phase} tool=${tool}`);
709
+ }
710
+ if (process.env.WAM_DEBUG_TE) {
711
+ console.log(`[WAM-DEBUG-TE-CHECK] phase=${st?.phase} tool=${tool} inBlocked=${BLOCKED_TOOLS.has(tool)} riskCheckWillPass`);
712
+ }
713
+
714
+
715
+ // Strategy Continuity: si hay estrategia aprobada, verificar si la acción
716
+ // está cubierta antes de proceder con la lógica de ASKING.
717
+ if (st?.approvedStrategy && st.approvedStrategy.status === "ACTIVE") {
718
+ const stratCheck = classifyActionAgainstStrategy(
719
+ input?.description || tool,
720
+ tool,
721
+ output?.args || input?.args || input?.parameters || {},
722
+ st.approvedStrategy
723
+ );
724
+ if (stratCheck.covered === true) {
725
+ // La acción está explícitamente cubierta por la estrategia aprobada.
726
+ // Loggear para observabilidad pero NO bloquear.
727
+ try {
728
+ sessionStore.set(`wam-strategy-hit-${tool}`, { reason: stratCheck.reason, at: Date.now() });
729
+ } catch {}
730
+ } else if (stratCheck.covered === false) {
731
+ // Acción prohibida explícitamente por la estrategia → bloquear.
732
+ const directive = `[wait-a-minute] STRATEGY VIOLATION: ${stratCheck.reason}. La estrategia aprobada "${st.approvedStrategy.strategy}" no autoriza esta acción.`;
733
+ input.output = directive;
734
+ throw new WamPolicyBlock(directive, {
735
+ tool,
736
+ reason: stratCheck.reason,
737
+ level: "BLOCKED",
738
+ source: "approved-strategy-continuity",
739
+ });
740
+ }
741
+ // covered === null → no decidir aquí; fall through to risk/asking logic
742
+ }
743
+
744
+ // Action Risk Envelope: evaluar riesgo de la herramienta antes de cualquier
745
+ // otra lógica. Bloquea BLOCKED con WamPolicyBlock (no genérico Error) para
746
+ // evitar que catch genérico lo trague.
747
+ const risk = evaluateAction(tool, input.args || input.parameters || {}, taskRoot);
748
+ if (risk.level === "BLOCKED") {
749
+ const directive = `[wait-a-minute] RISK BLOCK (${tool}): ${risk.reason || "acción fuera del envelope de riesgo"}. Requiere autorización explícita del usuario.`;
750
+ input.output = directive;
751
+ throw new WamPolicyBlock(directive, { tool, reason: risk.reason, level: risk.level });
752
+ }
753
+
754
+ // Governance Enforcement: block mutating tools when contract is not APPROVED.
755
+ // This prevents user-explicit-execute bypass without proper contract approval.
756
+ // Defer to the Clarification Gate (ENFORCED BLOCK) when in ASKING so the
757
+ // user is directed to answer the blocking unknown, not approve a contract.
758
+ // Sub-sessions (delegated workers, identified by parentID) are NEVER blocked
759
+ // here: they execute the main session's approved plan via Task delegation,
760
+ // not their own contracts. Blocking them breaks flujo sin fricción.
761
+ // Governance Enforcement centralizado (src/policy/governance-enforcement.js).
762
+ // Exento: subagentes (parentID), sesiones no resueltas (fail-open),
763
+ // override explícito (WAM_GOVERNANCE=off), fase terminal, ASKING, y
764
+ // cambios triviales declarados. Solo se gatea la sesión main con un
765
+ // contrato tracked y no aprobado.
766
+ try {
767
+ enforceGovernance(tool, st, {
768
+ isSubagent: sessionParents.has(sid) || !sessionResolved.has(sid),
769
+ declaredFiles: st?.contract?.files || st?.contract?.declaredFiles || [],
770
+ });
771
+ } catch (err) {
772
+ if (err?.wamPolicyBlock) input.output = err.message;
773
+ throw err;
774
+ }
775
+
776
+ if (st?.phase !== "ASKING") return;
777
+ // git read-only (status/diff/log) es investigación: permitido aun en ASKING.
778
+ // git mutante (commit/push) sigue bloqueado hasta responder la pregunta.
779
+ if (tool === "bash" && READONLY_GIT_RE.test(bashCommandOf(input))) return;
780
+ if (BLOCKED_TOOLS.has(tool)) {
781
+ if (process.env.WAM_DEBUG_TE) console.log(`[WAM-DEBUG-TE] BLOCKING ${tool} in ASKING`);
782
+ const u = (st.contract?.unknowns || []).find((x) => x.status === "blocking");
783
+ const question = u ? `${u.id}: ${u.question}` : "pregunta bloqueante pendiente";
784
+ const directive = `[wait-a-minute] ENFORCED BLOCK — tarea en ASKING (${question}). Herramienta ${tool} bloqueada. Responder: /wam answer ${u?.id || "U1"} <respuesta>`;
785
+ input.output = directive;
786
+ throw new Error(directive);
787
+ }
788
+ } catch (err) {
789
+ // WamPolicyBlock MUST be re-thrown — never swallowed by generic catch.
790
+ if (err?.wamPolicyBlock === true) throw err;
791
+ if (typeof err?.message === "string" && err.message.includes("ENFORCED BLOCK")) throw err;
792
+ }
793
+
794
+ try {
795
+ const resolvedCallID = input?.callID || `${sid || getSessionId()}__exec_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
796
+ const bridgeResult = await bridgeExecution({
797
+ taskId,
798
+ taskRoot,
799
+ state: st,
800
+ tool: input?.tool || "",
801
+ args: input?.args || input?.parameters || {},
802
+ callID: resolvedCallID,
803
+ sessionID: sid,
804
+ });
805
+ if (bridgeResult?.ok) {
806
+ input._wamCallID = bridgeResult.callID;
807
+ input._wamHypothesisId = bridgeResult.hypothesisId;
808
+ input._wamExperimentId = bridgeResult.experimentId;
809
+ input._wamRequirementId = bridgeResult.requirementId;
810
+ }
811
+ } catch (bridgeError) {
812
+ console.log("[wait-a-minute] Bridge execution non-blocking error:", bridgeError.message);
813
+ }
814
+ },
815
+
816
+ "tool.execute.after": async (input, output) => {
817
+ try {
818
+ await postToolExecution(input, output);
819
+ } catch (e) {
820
+ console.log(`[wait-a-minute] tool.execute.after non-blocking error:`, e.message);
821
+ }
822
+ },
823
+ };
824
+ };
825
+
826
+ /**
827
+ * Runs after tool.execute.before — persists success/failure to the
828
+ * cognition store via noteSuccess/noteFailure, reading experiment IDs
829
+ * from the sessionExecutions map (or input._wam* overrides) instead of
830
+ * input.hypothesisId which is never set by tool.execute.before.
831
+ */
832
+ async function postToolExecution(input, output) {
833
+ try {
834
+ if (bypassed) return;
835
+ const sid = input?.sessionID;
836
+ const taskRoot = await resolveSessionBase(sid);
837
+ const taskId = sessionTasks.get(sid) || readActiveTaskIdFresh(taskRoot) || "default-task";
838
+ const wamRoot = taskRoot;
839
+ const toolName = input?.tool || "";
840
+ const key = input._wamCallID || input?.callID || `${sid}:${toolName}`;
841
+ const mapping = sessionExecutions.get(key) || {};
842
+
843
+ if (input?.tool && output?.error) {
844
+ try {
845
+ await noteFailure(wamRoot, taskId, {
846
+ hypothesisId: input._wamHypothesisId || mapping.hypothesisId || input.hypothesisId,
847
+ experimentId: input._wamExperimentId || mapping.experimentId || input.experimentId,
848
+ requirementId: input._wamRequirementId || mapping.requirementId || input.requirementId,
849
+ reason: output.error || "tool execution failed",
850
+ actual: output.actual,
851
+ unexpected: output.unexpected,
852
+ provenance: `agent-tool-${toolName}-failure`,
853
+ });
854
+ } catch (e) {
855
+ console.log(`[wait-a-minute] postToolExecution noteFailure error:`, e.message);
856
+ }
857
+ } else if (input?.tool) {
858
+ try {
859
+ await noteSuccess(wamRoot, taskId, {
860
+ hypothesisId: input._wamHypothesisId || mapping.hypothesisId || input.hypothesisId,
861
+ experimentId: input._wamExperimentId || mapping.experimentId || input.experimentId,
862
+ requirementId: input._wamRequirementId || mapping.requirementId || input.requirementId,
863
+ result: output.result,
864
+ actual: output.actual,
865
+ unexpected: output.unexpected,
866
+ provenance: `agent-tool-${toolName}-success`,
867
+ });
868
+ } catch (e) {
869
+ console.log(`[wait-a-minute] postToolExecution noteSuccess error:`, e.message);
870
+ }
871
+ }
872
+ } catch (err) {
873
+ console.error("[wait-a-minute] postToolExecution error:", err);
874
+ } finally {
875
+ const cleanupKey = input._wamCallID || input?.callID || `${input?.sessionID}:${toolName}`;
876
+ if (cleanupKey && cleanupKey !== `${input?.sessionID}:${toolName}`) {
877
+ sessionExecutions.delete(cleanupKey);
878
+ }
879
+ }
880
+ }
881
+
882
+
883
+ /**
884
+ * Clasifica un mensaje recibido durante ASKING (spec clarification-gate):
885
+ * "answer" (respuesta natural), "implementation" (intento de implementar →
886
+ * interceptar, no consumir) o "new-intent" (cambio de tarea → nuevo pre-flight).
887
+ */
888
+ function classifyAskingMessage(text = "") {
889
+ const lower = (text || "").toLowerCase().trim();
890
+ const doneClaims =
891
+ /(^|\s)(done|finish|finished|complete|completed|terminate|terminated|listo|termin[eé]|complet[ao]|finalizad[oa])\b|(task|tarea)\s+(complete|complet(a|ada|o)|terminad(a|o))|declare.*done/i;
892
+ if (doneClaims.test(lower)) return "blocked-message";
893
+ if (/\b(olvida|olvídate|no quiero|mejor no|en realidad|nada que ver|cambia.*idea|descartar)\b/.test(lower)) return "new-intent";
894
+ if (/\b(implementa|implementar|agrega|agregar|haz|hacer|refactoriza|refactorizar|migra|migrar|crea|crear|elimina|eliminar|arregla|arreglar|fix|configura|configurar|escribe|instala|instalar|construye|build)\b/.test(lower)) return "blocked-message";
895
+ return "answer";
896
+ }
897
+
898
+ /**
899
+ * /wam CLI — opencode 1.18.25 entrega comandos vía command.execute.before,
900
+ * no vía ctx.command. Lógica extraída del handler antiguo.
901
+ */
902
+ async function wamCli(args, cfg = {}, root = process.cwd(), taskId = readActiveTaskId(root) || "default-task") {
903
+ const [sub, action, ...rest] = args || [];
904
+ // taskId de la sesión del comando (namespaced ses-<id> si genérico)
905
+
906
+ if (sub === "skills") {
907
+ const skills = waitAMinute.getRegistry();
908
+ const list = Object.values(skills);
909
+ if (action === "list") {
910
+ return list.map(s => `${s.id} [${s.status}]`).join("\n");
911
+ }
912
+ if (action === "search") {
913
+ const q = rest.join(" ").toLowerCase();
914
+ return list
915
+ .filter(s => (s.name || "").toLowerCase().includes(q) || (s.description || "").toLowerCase().includes(q))
916
+ .slice(0, 20)
917
+ .map(s => s.id)
918
+ .join("\n");
919
+ }
920
+ if (action === "inspect") {
921
+ const id = rest.join(" ");
922
+ const s = skills[id];
923
+ if (!s) return `Skill ${id} no encontrada`;
924
+ return [
925
+ `${s.id} [${s.status}]`,
926
+ `name: ${s.name}`,
927
+ `description: ${s.description}`,
928
+ `risk: ${s.risk}`,
929
+ `source: ${s.source?.repository || s.source?.kind || "local"} (${s.source?.path || "—"})`,
930
+ `--- content (max 1500 chars) ---`,
931
+ `${(s.content || "(sin contenido)").slice(0, 1500)}`,
932
+ ].join("\n");
933
+ }
934
+ if (action === "explain") {
935
+ const prompt = rest.join(" ");
936
+ if (!prompt) return "Uso: /wam skills explain <prompt>";
937
+ const sel = routeSkillsV2(prompt, {}, waitAMinute.getRegistry(), "STANDARD");
938
+ return sel.selected.length
939
+ ? sel.selected.map(s => `${s.name}: ${s.reason}`).join("\n")
940
+ : "Ninguna skill seleccionada para ese prompt";
941
+ }
942
+ return "Uso: /wam skills <list|search|inspect|explain> [args]";
943
+ }
944
+
945
+ if (sub === "contract") {
946
+ if (action === "approve") return JSON.stringify(waitAMinute.approveContract(taskId, root));
947
+ if (action === "reject") return JSON.stringify(waitAMinute.rejectContract(taskId, root));
948
+ if (action === "edit") {
949
+ try {
950
+ return JSON.stringify(waitAMinute.editContract(taskId, JSON.parse(rest.join(" ")), root));
951
+ } catch {
952
+ return "Error: JSON inválido para /wam contract edit";
953
+ }
954
+ }
955
+ return "Uso: /wam contract <approve|reject|edit <json>>";
956
+ }
957
+
958
+ if (sub === "resume") {
959
+ return JSON.stringify(waitAMinute.resumeTask(action || taskId, root));
960
+ }
961
+
962
+ if (sub === "strategy") {
963
+ if (action === "set") {
964
+ const scope = rest.join(" ").trim();
965
+ if (!scope) return "Uso: /wam strategy set <scope...>";
966
+ const st = getTaskState(taskId, root);
967
+ if (!st) return "Sin tarea activa";
968
+ if (st.contract?.status !== "APPROVED") {
969
+ return `Contrato no aprobado (status=${st.contract?.status || "?"})`;
970
+ }
971
+ if (!st.approvedStrategy || st.approvedStrategy.status !== "ACTIVE") {
972
+ return "Sin estrategia aprobada activa";
973
+ }
974
+ st.approvedStrategy = {
975
+ ...st.approvedStrategy,
976
+ strategy: scope.slice(0, 100),
977
+ scope: scope.slice(0, 100),
978
+ };
979
+ persistTaskState(taskId, st, root);
980
+ return `Estrategia actualizada: "${st.approvedStrategy.strategy}"`;
981
+ }
982
+ return "Uso: /wam strategy set <scope...>";
983
+ }
984
+
985
+ if (sub === "progress") {
986
+ const [reqId, op, ...evidence] = [action, ...rest];
987
+ if (!reqId) {
988
+ const st = getTaskState(taskId, root);
989
+ if (!st) return "Sin estado de tarea (persiste tras el primer mensaje)";
990
+ return st.requirements
991
+ .map(r => `${r.id} [${r.status}] ${r.title}${r.evidence?.length ? " | evidence: " + r.evidence.join("; ") : ""}`)
992
+ .join("\n");
993
+ }
994
+ if (op === "done") return JSON.stringify(await waitAMinute.markRequirement(taskId, reqId, "done", evidence.join(" "), root));
995
+ if (op === "verified") return JSON.stringify(await waitAMinute.markRequirement(taskId, reqId, "verified", evidence.join(" "), root));
996
+ if (op === "pending") return JSON.stringify(await waitAMinute.markRequirement(taskId, reqId, "pending", "", root));
997
+ return "Uso: /wam progress | /wam progress <id> done <evidencia> | /wam progress <id> verified <evidencia> | /wam progress <id> pending";
998
+ }
999
+
1000
+ if (sub === "answer") {
1001
+ const qid = action;
1002
+ const answer = rest.join(" ").trim();
1003
+ if (!qid || !answer) return "Uso: /wam answer <questionId> <respuesta>";
1004
+ return JSON.stringify(waitAMinute.answerQuestion(taskId, qid, answer, root));
1005
+ }
1006
+
1007
+ if (sub === "assumptions") {
1008
+ const st = getTaskState(taskId, root);
1009
+ if (!st) return "Sin estado de tarea";
1010
+ const list = st.contract?.assumptions || [];
1011
+ if (!list.length) return "Sin asunciones registradas";
1012
+ return list
1013
+ .map((a) => `${a.id} [${a.classification}/${a.status}] ${a.statement}${a.resolvedBy ? ` (resuelta: ${a.resolvedBy})` : ""}`)
1014
+ .join("\n");
1015
+ }
1016
+
1017
+ if (sub === "resolve") {
1018
+ const aid = action;
1019
+ const evidence = rest.join(" ").trim();
1020
+ if (!aid || !evidence) return "Uso: /wam resolve <assumptionId> <evidencia>";
1021
+ return JSON.stringify(waitAMinute.resolveAssumption(taskId, aid, evidence, root));
1022
+ }
1023
+
1024
+ if (sub === "status") {
1025
+ const st = getTaskState(taskId, root);
1026
+ if (!st) return "Sin estado de tarea";
1027
+ const experiments = st.experiments || [];
1028
+ const failures = experiments.filter(e => e.status === "failed");
1029
+ const pendingUnknowns = (st.contract?.unknowns || []).filter(u => u.status === "blocking");
1030
+ const pendingAuth = pendingUnknowns.length > 0 ? pendingUnknowns[0] : null;
1031
+ const verifiedReqs = (st.requirements || []).filter(r => r.status === "verified");
1032
+ const totalReqs = (st.requirements || []).length;
1033
+ const verification = { status: `${verifiedReqs.length}/${totalReqs} verified` };
1034
+ const completionEvidence = (st.requirements || [])
1035
+ .filter(r => r.evidence && r.evidence.length > 0)
1036
+ .map(r => ({ req: r.id, evidence: r.evidence.join("; ") }));
1037
+ return getStatusReport(st, { experiments, failures, pendingAuth, verification, completionEvidence });
1038
+ }
1039
+
1040
+ if (sub === "backlog") {
1041
+ const st = getTaskState(taskId, root);
1042
+ if (!st) return "Sin estado de tarea";
1043
+ const formatted = formatBacklog(st);
1044
+ if (!formatted) return "Sin requerimientos en backlog (todos están en el contract activo)";
1045
+ return formatted;
1046
+ }
1047
+
1048
+ if (sub === "compress") {
1049
+ const id = rest.join(" ") || action || taskId;
1050
+ const st = getTaskState(id, root);
1051
+ if (!st) return "Sin estado de tarea";
1052
+ const budget = cfg.budgetTokens || DEFAULT_CONFIG.budgetTokens;
1053
+ const cav = writeCavemanSummary(id, st, root);
1054
+ const tokens = estimateTokens(cav);
1055
+ const headroom = Math.max(0, budget - tokens);
1056
+ return `${cav}\n[tokens ${tokens} | headroom ${headroom}/${budget}]`;
1057
+ }
1058
+
1059
+ if (sub === "task") {
1060
+ if (action === "list") {
1061
+ const ids = listTaskIds(root);
1062
+ if (!ids.length) return "Sin tareas persistidas (.wam/tasks)";
1063
+ const active = readActiveTaskId(root);
1064
+ return ids
1065
+ .map((id) => {
1066
+ const st = getTaskState(id, root);
1067
+ return `${id}${id === active ? " *activa" : ""} [${st?.phase || "?"}] ${st?.contract?.status || ""}`;
1068
+ })
1069
+ .join("\n");
1070
+ }
1071
+ if (action === "switch") {
1072
+ const id = rest.join(" ");
1073
+ if (!id) return "Uso: /wam task switch <taskId>";
1074
+ writeActiveTaskId(id);
1075
+ const st = getTaskState(id);
1076
+ return st
1077
+ ? `Tarea activa: ${id} [${st.phase}] — ${st.nextAction || ""}`
1078
+ : `Tarea activa: ${id} (sin estado persistido — enviar primer mensaje)`;
1079
+ }
1080
+ return "Uso: /wam task <list|switch <id>>";
1081
+ }
1082
+
1083
+ if (sub === "ctx") {
1084
+ if (action === "list") {
1085
+ const caps = listCapsules(root);
1086
+ if (!caps.length) return "Sin cápsulas (usa /wam ctx add o extracción en DONE)";
1087
+ return caps.map((c) => `${c.context_id} [${c.level} ${c.lifecycle}] ${c.provenance} ${(c.purpose || "").slice(0, 60)}`).join("\n");
1088
+ }
1089
+ if (action === "get") {
1090
+ const q = rest.join(" ");
1091
+ if (!q) return "Uso: /wam ctx get <query>";
1092
+ const r = retrieveContext(q, { root });
1093
+ if (!r.ok) return r.message;
1094
+ return r.capsules.map((h) => `[${h.capsule.level}] ${h.capsule.context_id} (rel ${h.relevance.toFixed(2)}) ${(h.capsule.purpose || "").slice(0, 80)}`).join("\n");
1095
+ }
1096
+ if (action === "show") {
1097
+ const id = rest.join(" ");
1098
+ const c = getCapsule(id, root);
1099
+ if (!c) return `Cápsula ${id} no existe`;
1100
+ return [
1101
+ `${c.context_id} [${c.level} ${c.lifecycle}]`,
1102
+ `provenance: ${c.provenance} | importance: ${c.importance}/10 | confidence: ${c.confidence}`,
1103
+ `purpose: ${c.purpose}`,
1104
+ `scope: ${c.scope}`,
1105
+ `deps: ${(c.dependencies || []).join(", ") || "—"} | supersedes: ${c.supersedes || "—"}`,
1106
+ `--- content (max 600 chars) ---`,
1107
+ `${(c.content || "(sin contenido)").slice(0, 600)}`,
1108
+ ].join("\n");
1109
+ }
1110
+ if (action === "promote") {
1111
+ const [id, target, approved] = rest;
1112
+ if (!id || !target) return "Uso: /wam ctx promote <id> <L2|L1> [approved]";
1113
+ const r = promoteCapsule(id, target, { approvedBy: approved === "approved" ? "user" : "", root });
1114
+ if (!r.ok) return `Promoción rechazada: ${r.reason}`;
1115
+ return `Promovida ${id} → ${target} [${r.capsule.provenance}]`;
1116
+ }
1117
+ if (action === "session") {
1118
+ const sid = getSessionId(root);
1119
+ const caps = listCapsules(process.cwd(), { sessionId: sid });
1120
+ return `session: ${sid}\ncapsules de esta sesión: ${caps.length}\nL1 base: ${listCapsules(process.cwd(), { level: "L1", lifecycle: "active" }).length} cápsula(s)`;
1121
+ }
1122
+ if (action === "migrate") {
1123
+ const dryRun = rest.includes("--dry-run");
1124
+ const r = migrateLegacyCapsules(root, { dryRun });
1125
+ if (!r.migrated.length) return "Sin cápsulas legacy (todas tienen session_id)";
1126
+ const tag = r.dryRun ? "[DRY-RUN] " : "";
1127
+ return `${tag}migradas ${r.migrated.length} cápsula(s) a session_id="legacy":\n${r.migrated.map((m) => ` ${m.context_id || m.file}`).join("\n")}`;
1128
+ }
1129
+ if (sub === "decision") {
1130
+ const decisionPoint = action;
1131
+ if (!decisionPoint) {
1132
+ return "Error: decision point required. Usage: /wam decision <point> rationale=\"...\" alternatives=\"...\" evidence=\"...\"";
1133
+ }
1134
+
1135
+ // Parse key-value pairs from rest
1136
+ const params = {};
1137
+ for (const item of rest) {
1138
+ const match = item.match(/^([^=]+)=(.*)$/);
1139
+ if (match) {
1140
+ let key = match[1].trim();
1141
+ let value = match[2].trim();
1142
+
1143
+ // Remove surrounding quotes if present
1144
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
1145
+ value = value.slice(1, -1);
1146
+ } else if (value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
1147
+ value = value.slice(1, -1);
1148
+ }
1149
+
1150
+ params[key] = value;
1151
+ }
1152
+ }
1153
+
1154
+ const rationale = params.rationale;
1155
+ const alternativesStr = params.alternatives;
1156
+ const evidenceStr = params.evidence;
1157
+
1158
+ if (!rationale || !alternativesStr || !evidenceStr) {
1159
+ return "Error: rationale, alternatives, and evidence are required. Usage: /wam decision <point> rationale=\"...\" alternatives=\"...\" evidence=\"...\"";
1160
+ }
1161
+
1162
+ let alternatives, evidence;
1163
+ try {
1164
+ alternatives = JSON.parse(alternativesStr);
1165
+ evidence = JSON.parse(evidenceStr);
1166
+
1167
+ // Validate that they are arrays
1168
+ if (!Array.isArray(alternatives) || !Array.isArray(evidence)) {
1169
+ return "Error: alternatives and evidence must be JSON arrays.";
1170
+ }
1171
+ } catch (e) {
1172
+ return `Error: Invalid JSON in alternatives or evidence: ${e.message}`;
1173
+ }
1174
+
1175
+ // Create tracer and log the decision
1176
+ const tracer = new ContextDecisionTracer(`decision-${Date.now()}`, root);
1177
+ tracer.logTechnicalDecision(decisionPoint, rationale, alternatives, evidence);
1178
+
1179
+ return `Decision "${decisionPoint}" logged successfully.`;
1180
+ }
1181
+ return "Uso: /wam ctx <list|get <q>|show <id>|promote <id> <L2|L1> [approved]|session|migrate [--dry-run]>";
1182
+ }
1183
+
1184
+ return "Uso: /wam <skills|contract|progress|task|compress|ctx|answer|assumptions|resolve|decision>";
1185
+ }
1186
+
1187
+ /**
1188
+ * Wait a Minute — deterministic task-state management and context reconstruction API.
1189
+ */
1190
+ const waitAMinute = {
1191
+ name: "wait-a-minute",
1192
+
1193
+ /**
1194
+ * Obtiene el registry actual.
1195
+ */
1196
+ getRegistry: function() {
1197
+ return this.loadBundledRegistry();
1198
+ },
1199
+
1200
+ /**
1201
+ * Analiza el prompt del usuario y retorna el contexto de pre-flight.
1202
+ *
1203
+ * @param {Object} options
1204
+ * @param {string} options.prompt - El prompt del usuario
1205
+ * @param {string} [options.projectPath] - Ruta del proyecto (usa cwd por defecto)
1206
+ * @param {Object} [options.config] - Configuración
1207
+ * @param {Object} [options.tierCaps] - Caps por tier
1208
+ * @returns {Object} - Resultado del análisis de pre-flight
1209
+ */
1210
+ analyze: async function({
1211
+ prompt,
1212
+ projectPath,
1213
+ config,
1214
+ tierCaps,
1215
+ activePreset,
1216
+ activeMode,
1217
+ } = {}) {
1218
+ if (!prompt || typeof prompt !== "string") {
1219
+ return {
1220
+ intent: { classification: "trivial", ambiguity: "low", confidence: 100 },
1221
+ project: { detected_stack: "unknown", architecture: "unknown", relevant_files: [] },
1222
+ known: [],
1223
+ inferred: [],
1224
+ assumed: [],
1225
+ unknown: ["No hay prompt para analizar"],
1226
+ skills: { candidates: [], selected: [], rejected: [] },
1227
+ risk: "low",
1228
+ complexity: "trivial",
1229
+ ambiguity: "low",
1230
+ strategy: "FAST",
1231
+ ready: true,
1232
+ advice: "Sin prompt - sin análisis necesario",
1233
+ };
1234
+ }
1235
+
1236
+ const trimmedPrompt = prompt.trim();
1237
+ if (!trimmedPrompt) {
1238
+ return {
1239
+ intent: { classification: "trivial", ambiguity: "low", confidence: 100 },
1240
+ project: { detected_stack: "unknown", architecture: "unknown", relevant_files: [] },
1241
+ known: ["Prompt vacío o nulo"],
1242
+ inferred: [],
1243
+ assumed: [],
1244
+ unknown: ["No hay prompt para analizar"],
1245
+ skills: { candidates: [], selected: [], rejected: [] },
1246
+ risk: "low",
1247
+ complexity: "trivial",
1248
+ ambiguity: "low",
1249
+ strategy: "FAST",
1250
+ ready: true,
1251
+ advice: "Sin prompt - sin análisis necesario",
1252
+ };
1253
+ }
1254
+
1255
+ const result = await analyze({
1256
+ prompt: trimmedPrompt,
1257
+ projectPath: projectPath || process.cwd(),
1258
+ config,
1259
+ tierCaps: tierCaps || DEFAULT_CONFIG.tierCaps,
1260
+ activePreset: activePreset || DEFAULT_CONFIG.activePreset,
1261
+ activeMode: activeMode || DEFAULT_CONFIG.activeMode,
1262
+ });
1263
+
1264
+ const summary = this.generateSummary(result);
1265
+
1266
+ return {
1267
+ ...result,
1268
+ _summary: summary,
1269
+ };
1270
+ },
1271
+
1272
+ /**
1273
+ * Genera un resumen humano-readable del análisis de pre-flight.
1274
+ */
1275
+ generateSummary: function(result) {
1276
+ const lines = [];
1277
+
1278
+ lines.push(
1279
+ `Intención: ${result.intent.classification} (confianza: ${result.intent.confidence}%)`
1280
+ );
1281
+ lines.push(`Stack: ${result.project.detected_stack}`);
1282
+ if (result.project.architecture !== "unknown") {
1283
+ lines.push(`Arquitectura: ${result.project.architecture}`);
1284
+ }
1285
+
1286
+ if (result.known.length > 0) {
1287
+ lines.push(`\nConocido(s): ${result.known.slice(0, 5).join("; ")}`);
1288
+ }
1289
+ if (result.inferred.length > 0) {
1290
+ lines.push(`Inferido(s): ${result.inferred.slice(0, 5).join("; ")}`);
1291
+ }
1292
+ if (result.assumed.length > 0) {
1293
+ lines.push(`Asumido(s): ${result.assumed.slice(0, 3).join("; ")}`);
1294
+ }
1295
+ if (result.unknown.length > 0) {
1296
+ lines.push(`Desconocido(s): ${result.unknown.slice(0, 3).join("; ")}`);
1297
+ }
1298
+
1299
+ lines.push(
1300
+ `\nSkills candidatas: ${result.skills.candidates
1301
+ .slice(0, 5)
1302
+ .map((c) => c.name)
1303
+ .join(", ")}`
1304
+ );
1305
+ lines.push(`Skills seleccionadas: ${result.skills.selected.join(", ")}`);
1306
+ if (result.skills.rejected.length > 0) {
1307
+ lines.push(`Skills rechazadas: ${result.skills.rejected.join(", ")}`);
1308
+ }
1309
+
1310
+ lines.push(`\nRiesgo: ${result.risk}`);
1311
+ lines.push(`Complejidad: ${result.complexity}`);
1312
+ lines.push(`Ambigüedad: ${result.ambiguity}`);
1313
+ lines.push(`\nEstrategia: ${result.strategy}`);
1314
+ lines.push(`\n¿Listo para proceder?: ${result.ready ? "SÍ" : "NO"}`);
1315
+ lines.push(`\nConsejo: ${result.advice || ""}`);
1316
+
1317
+ return lines.join("\n");
1318
+ },
1319
+
1320
+ /**
1321
+ * Carga el catálogo embebido (skills/registry.json) del plugin.
1322
+ * El corpus es distribuido con WAM — sin red en runtime.
1323
+ */
1324
+ loadBundledRegistry: function() {
1325
+ try {
1326
+ const registryPath = path.join(import.meta.dirname, "skills", "registry.json");
1327
+ if (!fs.existsSync(registryPath)) return {};
1328
+ const entries = JSON.parse(fs.readFileSync(registryPath, "utf-8"));
1329
+ const reg = {};
1330
+ for (const s of entries) reg[s.id] = s;
1331
+ return reg;
1332
+ } catch {
1333
+ return {};
1334
+ }
1335
+ },
1336
+
1337
+ /**
1338
+ * Estado durable de tarea: contrato + requisitos por evidencia.
1339
+ * PROPOSED la primera vez; preserva contrato APPROVED y progreso existente.
1340
+ */
1341
+ buildPersistedState: function(taskId, analysis, root) {
1342
+ const existing = getTaskState(taskId, root);
1343
+ if (existing && existing.requirements && existing.requirements.length) {
1344
+ // Preservar contrato/requisitos existentes (PROPOSED/APPROVED/REJECTED):
1345
+ // el contrato se sintetiza UNA vez (primer mensaje); los mensajes
1346
+ // siguientes no re-sintetizan del texto actual (claims incluidos).
1347
+ const merged = {
1348
+ ...existing,
1349
+ lastAction: existing.lastAction,
1350
+ nextAction: nextActionFrom(existing),
1351
+ };
1352
+ persistTaskState(taskId, merged, root);
1353
+ return merged;
1354
+ }
1355
+ const fresh = {
1356
+ ...projectState(analysis),
1357
+ phase: "PROPOSED",
1358
+ nextAction: "Revisar contrato — /wam contract approve o edit",
1359
+ lastAction: "",
1360
+ projectPath: root,
1361
+ };
1362
+ // Safety: consolidar si synthesizeContract generó demasiados requisitos
1363
+ const MAX = 15;
1364
+ if (fresh.requirements && fresh.requirements.length > MAX) {
1365
+ const kept = fresh.requirements.slice(0, MAX);
1366
+ const overflow = fresh.requirements.length - MAX;
1367
+ kept.push({ id: `req-overflow`, title: `(+${overflow} requisitos consolidados de la especificación)`, status: "pending", evidence: [] });
1368
+ fresh.requirements = kept;
1369
+ if (fresh.contract) fresh.contract.requirements = kept.map((r) => r.title);
1370
+ }
1371
+ persistTaskState(taskId, fresh, root);
1372
+ return fresh;
1373
+ },
1374
+
1375
+ /**
1376
+ * Completion Gate: detecta claims de fin de tarea y bloquea si hay requisitos pendientes.
1377
+ */
1378
+ evaluateCompletionGate: function(state, prompt) {
1379
+ const lower = (prompt || "").toLowerCase().trim();
1380
+ if (lower.includes("aprobar contrato") || lower.includes("continuar")) {
1381
+ return { blocked: false, allDone: false, autoApprove: true };
1382
+ }
1383
+ const doneClaims =
1384
+ /(^|\s)(done|finish|finished|complete|completed|terminate|terminated|listo|termin[eé]|complet[ao]|finalizad[oa])\b|(task|tarea)\s+(complete|complet(a|ada|o)|terminad(a|o))|declare.*done/i;
1385
+ if (!doneClaims.test(lower)) return { blocked: false };
1386
+ const blockingUnknowns = (state?.contract?.unknowns || []).filter((u) => u.status === "blocking");
1387
+ if (blockingUnknowns.length > 0) {
1388
+ return {
1389
+ blocked: true,
1390
+ pending: blockingUnknowns.map((u) => `${u.id} — ${u.question} (DECISION_CRITICAL sin responder)`),
1391
+ };
1392
+ }
1393
+ const blockingAssumptions = (state?.contract?.assumptions || []).filter(
1394
+ (a) => a.classification === "DECISION_CRITICAL" && a.status !== "resolved"
1395
+ );
1396
+ if (blockingAssumptions.length > 0) {
1397
+ return {
1398
+ blocked: true,
1399
+ pending: blockingAssumptions.map((a) => `${a.id} — ${a.statement} (DECISION_CRITICAL sin resolver)`),
1400
+ };
1401
+ }
1402
+ const pending = (state?.requirements || []).filter(
1403
+ (r) =>
1404
+ (r.status !== "done" && r.status !== "verified") ||
1405
+ (r.status === "done" && !(r.evidence || []).length)
1406
+ );
1407
+ if (pending.length > 0) {
1408
+ return {
1409
+ blocked: true,
1410
+ pending: pending.map((r) => {
1411
+ const missingEvidence = r.status === "done" && !(r.evidence || []).length;
1412
+ return `${r.id} — ${r.title}${missingEvidence ? " (sin evidencia)" : ""}`;
1413
+ }),
1414
+ };
1415
+ }
1416
+
1417
+ const failingCriteria = (state?.completionContract?.criteria || []).filter(
1418
+ (c) => c.status === "FAIL" || c.status === "UNKNOWN"
1419
+ );
1420
+ if (failingCriteria.length > 0) {
1421
+ return {
1422
+ blocked: true,
1423
+ pending: failingCriteria.map((c) => `${c.id} — ${c.criterion} (${c.status})`),
1424
+ };
1425
+ }
1426
+
1427
+ if (state?.contract?.status !== "APPROVED") {
1428
+ return {
1429
+ blocked: true,
1430
+ pending: [`contrato ${state.contract?.status || "PROPOSED"} — aprobar con /wam contract approve antes de DONE`],
1431
+ };
1432
+ }
1433
+ const unverified = (state?.requirements || []).filter((r) => r.status !== "verified");
1434
+ if (unverified.length > 0) {
1435
+ return {
1436
+ blocked: true,
1437
+ verifying: true,
1438
+ pending: unverified.map((r) => `${r.id} — ${r.title} (verificar: /wam progress ${r.id} verified <evidencia>)`),
1439
+ };
1440
+ }
1441
+ const checks = state?.contract?.checks || [];
1442
+ if (checks.length > 0) {
1443
+ const byReq = new Map();
1444
+ for (const c of checks) {
1445
+ if (!byReq.has(c.reqId)) byReq.set(c.reqId, []);
1446
+ byReq.get(c.reqId).push(c);
1447
+ }
1448
+ const checkFailures = [];
1449
+ for (const [reqId, list] of byReq) {
1450
+ const verdict = evaluateRequirementChecks(list, list);
1451
+ if (verdict.status !== "VERIFIED") checkFailures.push(`${reqId}: ${verdict.reason || "checks pendientes"}`);
1452
+ }
1453
+ if (checkFailures.length > 0) {
1454
+ return { blocked: true, verifying: true, pending: checkFailures };
1455
+ }
1456
+ }
1457
+ return { blocked: false, allDone: true };
1458
+ },
1459
+
1460
+ /** Transición de fase basada en el resultado del Completion Gate. */
1461
+ applyPhaseTransition: function(state, gate) {
1462
+ let phase;
1463
+ if (gate?.allDone) {
1464
+ phase = "DONE";
1465
+ } else if (gate?.blocked && gate?.verifying) {
1466
+ phase = "VERIFYING";
1467
+ } else {
1468
+ phase = state?.phase || "IMPLEMENTING";
1469
+ }
1470
+ let nextAction;
1471
+ if (gate?.allDone) {
1472
+ nextAction = "Tarea completa — contrato verificado";
1473
+ } else if (gate?.blocked) {
1474
+ nextAction = "Continuar con requisitos pendientes";
1475
+ } else {
1476
+ nextAction = state?.nextAction;
1477
+ }
1478
+ return { phase, nextAction };
1479
+ },
1480
+
1481
+ /** Aprueba el contrato: PROPOSED → APPROVED, fase → IMPLEMENTING. */
1482
+ resumeTask: function(taskId, root) {
1483
+ const state = getTaskState(taskId, root);
1484
+ if (!state) return { ok: false, reason: "Sin estado de tarea para: " + taskId };
1485
+ if (state.phase === "DONE") return { ok: false, reason: "Tarea DONE — no se reabre automáticamente. Crea una nueva." };
1486
+ if (!String(taskId).startsWith("ses-")) writeActiveTaskId(taskId, root);
1487
+ try {
1488
+ updateLiveContext(taskId, state, root);
1489
+ } catch {}
1490
+ return { ok: true, taskId, phase: state.phase, contract: state.contract?.status || "?" };
1491
+ },
1492
+
1493
+ approveContract: function(taskId, root) {
1494
+ const state = getTaskState(taskId, root);
1495
+ if (!state) return { ok: false, reason: "Sin estado de tarea" };
1496
+ const blocking = (state.contract?.unknowns || []).filter((u) => u.status === "blocking");
1497
+ if (blocking.length > 0) {
1498
+ return { ok: false, reason: `${blocking[0].id} DECISION_CRITICAL sin responder (bloquea aprobación): ${blocking[0].question}` };
1499
+ }
1500
+ const blockingAssumptions = (state.contract?.assumptions || []).filter(
1501
+ (a) => a.classification === "DECISION_CRITICAL" && a.status !== "resolved"
1502
+ );
1503
+ if (blockingAssumptions.length > 0) {
1504
+ return {
1505
+ ok: false,
1506
+ reason: `${blockingAssumptions[0].id} DECISION_CRITICAL sin resolver (bloquea aprobación): ${blockingAssumptions[0].statement} — /wam resolve ${blockingAssumptions[0].id} <evidencia>`,
1507
+ };
1508
+ }
1509
+ state.contract = { ...(state.contract || {}), status: "APPROVED" };
1510
+ if (state.phase !== "DONE") state.phase = "IMPLEMENTING";
1511
+ state.nextAction = nextActionFrom(state);
1512
+ // Strategy Continuity: persist approved strategy so tool.execute.before
1513
+ // knows which actions are covered without requiring per-step approval.
1514
+ // Approved scope = the strategy itself + all safe execution steps required
1515
+ // to execute it (install deps, run tests, configure browser, retry).
1516
+ const inferredStrategy = (
1517
+ state.contract?.objective ||
1518
+ state.intent?.goal ||
1519
+ state.requirements?.[0]?.title ||
1520
+ state.lastAction?.split("\n")[0] ||
1521
+ "task execution"
1522
+ ).slice(0, 100);
1523
+ state.approvedStrategy = {
1524
+ strategy: inferredStrategy,
1525
+ approvedAt: Date.now(),
1526
+ scope: inferredStrategy,
1527
+ allowedActions: [
1528
+ "read", "search", "inspect", "test", "lint", "typecheck",
1529
+ "install dependency", "install browser binary", "run validation",
1530
+ "modify source", "create fixture", "diagnose", "retry",
1531
+ "write", "edit", "bash", "sh",
1532
+ "task", "todowrite", "pty_spawn",
1533
+ "openspec", "openspec new change", "openspec instructions",
1534
+ "openspec validate", "openspec archive",
1535
+ "git commit", "npm test", "pnpm test", "pnpm install",
1536
+ "crear change OpenSpec", "ejecutar test suite",
1537
+ "instalar dependencia", "delegar via Task",
1538
+ ],
1539
+ prohibitedActions: [
1540
+ "delete production data", "drop database", "production deploy",
1541
+ "credential modification", "scope expansion", "destructive operation",
1542
+ ],
1543
+ invalidationConditions: [
1544
+ "environment cannot satisfy required runtime",
1545
+ "required dependency unavailable and unfixable",
1546
+ "explicit user retraction",
1547
+ "verified evidence contradicts strategy at architectural level",
1548
+ ],
1549
+ status: "ACTIVE",
1550
+ };
1551
+ persistTaskState(taskId, state, root);
1552
+ try {
1553
+ updateLiveContext(taskId, state, root);
1554
+ } catch {}
1555
+ return { ok: true, status: "APPROVED", phase: state.phase };
1556
+ },
1557
+
1558
+ /** Rechaza el contrato: REJECTED, fase WAITING. */
1559
+ rejectContract: function(taskId, root) {
1560
+ const state = getTaskState(taskId, root);
1561
+ if (!state) return { ok: false, reason: "Sin estado de tarea" };
1562
+ state.contract = { ...(state.contract || {}), status: "REJECTED" };
1563
+ state.phase = "WAITING";
1564
+ state.nextAction = "Revisar contrato con el usuario";
1565
+ persistTaskState(taskId, state, root);
1566
+ return { ok: true, status: "REJECTED", phase: state.phase };
1567
+ },
1568
+
1569
+ /** Edita el contrato (JSON patch), vuelve a PROPOSED. */
1570
+ editContract: function(taskId, patch, root) {
1571
+ const state = getTaskState(taskId, root);
1572
+ if (!state) return { ok: false, reason: "Sin estado de tarea" };
1573
+ const contract = state.contract || { status: "PROPOSED", requirements: [], verification: [], constraints: [] };
1574
+ if (Array.isArray(patch?.requirements)) {
1575
+ contract.requirements = patch.requirements;
1576
+ state.requirements = patch.requirements.map((title, i) => ({
1577
+ id: `req-${i + 1}`,
1578
+ title,
1579
+ status: "pending",
1580
+ evidence: [],
1581
+ }));
1582
+ }
1583
+ if (Array.isArray(patch?.verification)) contract.verification = patch.verification;
1584
+ if (Array.isArray(patch?.constraints)) contract.constraints = patch.constraints;
1585
+ contract.status = "PROPOSED";
1586
+ state.contract = contract;
1587
+ state.phase = "PROPOSED";
1588
+ state.nextAction = "Revisar contrato — /wam contract approve o edit";
1589
+ persistTaskState(taskId, state, root);
1590
+ return { ok: true, status: "PROPOSED", requirements: contract.requirements.length };
1591
+ },
1592
+
1593
+ /** Marca requisito done/pending con evidencia. DONE exige evidencia (no "parece funcionar"). */
1594
+ markRequirement: async function(taskId, reqId, status, evidence, root) {
1595
+ const state = getTaskState(taskId, root);
1596
+ if (!state) return { ok: false, reason: "Sin estado de tarea" };
1597
+ const req = (state.requirements || []).find((r) => r.id === reqId);
1598
+ if (!req) return { ok: false, reason: `Requisito ${reqId} no existe` };
1599
+ if ((status === "done" || status === "verified") && !(evidence && evidence.trim())) {
1600
+ return { ok: false, reason: `Requisito ${reqId}: evidencia requerida para marcar ${status}` };
1601
+ }
1602
+ if (status === "verified" && req.status !== "done") {
1603
+ return { ok: false, reason: `Requisito ${reqId}: marcar done antes de verified` };
1604
+ }
1605
+ const checksForReq = (state.contract?.checks || []).filter((c) => c.reqId === reqId);
1606
+ if (status === "verified" && checksForReq.length > 0) {
1607
+ const verdictResult = await verifyRequirement(checksForReq, { timeout_ms: 60000 });
1608
+ if (verdictResult.status !== "VERIFIED") {
1609
+ const failedChecks = verdictResult.results?.filter(r => r.status !== "PASS").map(r => `${r.check_id}: ${r.status}`).join(", ") || "unknown";
1610
+ return { ok: false, reason: `Requisito ${reqId}: verificación falló (${failedChecks}).` };
1611
+ }
1612
+ req.evidence.push(...verdictResult.evidence.map(e => `${e.check_type}:${e.check_id}`));
1613
+ }
1614
+ if (status === "done" && req.status === "verified" && checksForReq.length > 0) {
1615
+ return { ok: false, reason: `Requisito ${reqId}: ya está verificado, no se puede retroceder a done.` };
1616
+ }
1617
+ req.status = status;
1618
+ if (status === "done" || status === "verified") req.evidence.push(evidence.trim());
1619
+ if (status === "pending") req.evidence = [];
1620
+ state.nextAction = nextActionFrom(state);
1621
+ persistTaskState(taskId, state, root);
1622
+ return { ok: true, phase: state.phase, nextAction: state.nextAction };
1623
+ },
1624
+
1625
+ /** Responde una pregunta bloqueante: unknown → answered, fase ASKING → ANSWERED → PROPOSED. */
1626
+ answerQuestion: function(taskId, qid, answer, root) {
1627
+ const state = getTaskState(taskId, root);
1628
+ if (!state) return { ok: false, reason: "Sin estado de tarea" };
1629
+ const u = (state.contract?.unknowns || []).find((x) => x.id === qid);
1630
+ if (!u) return { ok: false, reason: `Pregunta ${qid} no existe` };
1631
+ if (!(answer && answer.trim())) return { ok: false, reason: `Respuesta requerida para ${qid}` };
1632
+ u.status = "answered";
1633
+ u.answer = answer.trim();
1634
+ if (u.assumptionId) {
1635
+ const a = (state.contract?.assumptions || []).find((x) => x.id === u.assumptionId);
1636
+ if (a) {
1637
+ a.status = "resolved";
1638
+ a.classification = "RESOLVED";
1639
+ a.resolvedBy = "answer";
1640
+ }
1641
+ }
1642
+ state.phase = "ANSWERED";
1643
+ persistTaskState(taskId, state, root);
1644
+ state.phase = "PROPOSED";
1645
+ state.nextAction = "Revisar contrato — /wam contract approve";
1646
+ persistTaskState(taskId, state, root);
1647
+ return { ok: true, phase: "PROPOSED", unknown: u };
1648
+ },
1649
+
1650
+ /** Reconocimiento de respuesta natural (spec enforce-clarification-gate): en ASKING,
1651
+ * un mensaje normal resuelve TODAS las preguntas bloqueantes con ese texto y
1652
+ * re-evalúa el estado (assumption asociada → resolved). */
1653
+ answerFromMessage: function(taskId, text, root) {
1654
+ const state = getTaskState(taskId, root);
1655
+ if (!state) return { ok: false, reason: "Sin estado de tarea" };
1656
+ const blocking = (state.contract?.unknowns || []).filter((u) => u.status === "blocking");
1657
+ if (!blocking.length) return { ok: false, reason: "Sin preguntas bloqueantes" };
1658
+ const answer = (text || "").trim();
1659
+ if (!answer) return { ok: false, reason: "Respuesta vacía" };
1660
+ let first = null;
1661
+ for (const u of blocking) {
1662
+ u.status = "answered";
1663
+ u.answer = answer;
1664
+ if (u.assumptionId) {
1665
+ const a = (state.contract?.assumptions || []).find((x) => x.id === u.assumptionId);
1666
+ if (a) {
1667
+ a.status = "resolved";
1668
+ a.classification = "RESOLVED";
1669
+ a.resolvedBy = "answer";
1670
+ }
1671
+ }
1672
+ first = first || u;
1673
+ }
1674
+ state.phase = "PROPOSED";
1675
+ state.nextAction = "Revisar contrato — /wam contract approve";
1676
+ persistTaskState(taskId, state, root);
1677
+ return { ok: true, u: first, phase: state.phase, nextAction: state.nextAction };
1678
+ },
1679
+ /** Resuelve una asunción con evidencia del repo (spec change 3 R4): → RESOLVED sin preguntar al usuario. */
1680
+ resolveAssumption: function(taskId, aid, evidence, root) {
1681
+ const state = getTaskState(taskId, root);
1682
+ if (!state) return { ok: false, reason: "Sin estado de tarea" };
1683
+ const a = (state.contract?.assumptions || []).find((x) => x.id === aid);
1684
+ if (!a) return { ok: false, reason: `Asunción ${aid} no existe` };
1685
+ if (!(evidence && evidence.trim())) return { ok: false, reason: `Evidencia requerida para resolver ${aid}` };
1686
+ a.status = "resolved";
1687
+ a.classification = "RESOLVED";
1688
+ a.resolvedBy = "evidence";
1689
+ a.evidence = evidence.trim();
1690
+ const u = (state.contract?.unknowns || []).find((x) => x.assumptionId === aid && x.status === "blocking");
1691
+ if (u) {
1692
+ u.status = "answered";
1693
+ u.answer = evidence.trim();
1694
+ }
1695
+ const stillBlocking = (state.contract?.unknowns || []).some((x) => x.status === "blocking");
1696
+ if (state.phase === "ASKING" && !stillBlocking) {
1697
+ state.phase = "PROPOSED";
1698
+ state.nextAction = "Revisar contrato — /wam contract approve";
1699
+ }
1700
+ persistTaskState(taskId, state, root);
1701
+ return { ok: true, assumption: a };
1702
+ },
1703
+
1704
+ /** Carga contenido real de una skill del catálogo bajo demanda. */
1705
+ loadSkillOnDemand: function(skillId, registry, baseDir) {
1706
+ return loadSkillOnDemand(skillId, registry, baseDir);
1707
+ },
1708
+
1709
+ /**
1710
+ * Verifica si el prompt puede ser procesado en modo FAST (trivial).
1711
+ * Basado en el CONTENIDO del prompt, no en el origen (usuario/agente).
1712
+ */
1713
+ isTrivial: function(prompt) {
1714
+ const lower = prompt.toLowerCase().trim();
1715
+ const fastPatterns = [
1716
+ /^\s*rename\s+/i,
1717
+ /^\s*change\s+\w+/i,
1718
+ /^\s*what(is|are)\s+/i,
1719
+ /^\s*explain\s+/i,
1720
+ /^\s*how\s+to\s+/i,
1721
+ /^\s*list\s+/i,
1722
+ /^\s*show\s+\w+/i,
1723
+ /^\s*get\s+\w+/i,
1724
+ /^\s*error\s+line/i,
1725
+ /^\s*fix\s+this/i,
1726
+ ];
1727
+
1728
+ for (const pattern of fastPatterns) {
1729
+ if (pattern.test(lower)) return true;
1730
+ }
1731
+ return false;
1732
+ },
1733
+
1734
+ /**
1735
+ * Verifica si el prompt requiere modo STRICT (arquitectura, seguridad, etc.)
1736
+ */
1737
+ requiresStrict: function(prompt) {
1738
+ const lower = prompt.toLowerCase();
1739
+ const strictPatterns = [
1740
+ /migra|migrate/i,
1741
+ /seguridad|security/i,
1742
+ /arquitectura|architecture/i,
1743
+ /alto impacto|high impact/i,
1744
+ /producción|production/i,
1745
+ /destructivo|destructive/i,
1746
+ ];
1747
+
1748
+ return strictPatterns.some((p) => p.test(lower));
1749
+ },
1750
+
1751
+ /**
1752
+ * Presentar validación de pre-flight al agente/usuario.
1753
+ *
1754
+ * Checkpoint estratégico: muestra el resumen y solicita confirmación
1755
+ * sobre la estrategia a seguir. Se muestra para todo mensaje, incluido
1756
+ * modo FAST — el análisis nunca se omite.
1757
+ *
1758
+ * @param {Object} opts
1759
+ * @param {Object} opts.analysis - Resultado de analyze()
1760
+ * @returns {Object} - { mode: 'continue'|'validation-pending', ... }
1761
+ */
1762
+ presentValidation: async function({ analysis, ctx, meta = {} } = {}) {
1763
+ if (!analysis) {
1764
+ return { mode: "continue", advice: "No hay análisis previo" };
1765
+ }
1766
+
1767
+ const summary = this.generateSummary(analysis);
1768
+ const mode = analysis.strategy || "NORMAL";
1769
+
1770
+ const contractStatus = analysis.contractStatus || analysis.completionContract?.status || "PROPOSED";
1771
+
1772
+ const skillsLine = analysis.skills?.selected?.length
1773
+ ? analysis.skills.selected.map((s) => s.name).join(",")
1774
+ : "ninguna";
1775
+ const blockingUnknowns = (analysis.completionContract?.unknowns || []).filter((u) => u.status === "blocking");
1776
+ const blockingAssumptions = (analysis.completionContract?.assumptions || []).filter(
1777
+ (a) => a.classification === "DECISION_CRITICAL" && a.status !== "resolved"
1778
+ );
1779
+ const validationLines = [
1780
+ `wait-a-minute: contrato ${contractStatus}${analysis.phase ? ` (fase ${analysis.phase})` : ""}`,
1781
+ `req: ${(analysis.completionContract?.requirements || []).join("; ") || "—"}`,
1782
+ ...(blockingUnknowns.length
1783
+ ? [`blocking: ${blockingUnknowns.map((u) => `${u.id} ${u.question}`).join(" | ")}`]
1784
+ : []),
1785
+ ...(blockingAssumptions.length
1786
+ ? [`assumptions blocking: ${blockingAssumptions.map((a) => `${a.id} ${a.statement}`).join(" | ")}`]
1787
+ : []),
1788
+ "",
1789
+ "Ejecutar lo pedido. Si la instrucción es ambigua, preguntar.",
1790
+ "/wam contract approve → aprobar | /wam compress → resumen terse",
1791
+ ];
1792
+
1793
+ if (ctx) {
1794
+ const text = validationLines.filter((l) => l !== false).join("\n") + "\n";
1795
+ if (Array.isArray(ctx.parts)) {
1796
+ ctx.parts.unshift({
1797
+ id: genPartId(),
1798
+ type: "text",
1799
+ text,
1800
+ synthetic: true,
1801
+ ...(meta.sessionID ? { sessionID: meta.sessionID } : {}),
1802
+ ...(meta.messageID ? { messageID: meta.messageID } : {}),
1803
+ });
1804
+
1805
+ // Agregar directiva clara de ejecución
1806
+ ctx.parts.push({
1807
+ id: genPartId(),
1808
+ type: "text",
1809
+ text: `\n[wam: fase ${analysis.phase || "IMPLEMENTING"} → ${analysis.nextAction || "continuar"}]`,
1810
+ synthetic: true,
1811
+ ...(meta.sessionID ? { sessionID: meta.sessionID } : {}),
1812
+ ...(meta.messageID ? { messageID: meta.messageID } : {}),
1813
+ });
1814
+ } else if (ctx.system) {
1815
+ ctx.system.unshift({ id: genPartId(), type: "text", text, synthetic: true });
1816
+ }
1817
+ }
1818
+
1819
+ return {
1820
+ mode: mode === "FAST" ? "continue" : "validation-pending",
1821
+ summary,
1822
+ validationLines,
1823
+ advice:
1824
+ mode === "FAST"
1825
+ ? "Tarea trivial - análisis mostrado, proceder directamente"
1826
+ : "Proceder con la implementación",
1827
+ };
1828
+ },
1829
+ };
1830
+
1831
+ Object.keys(waitAMinute).forEach((k) => {
1832
+ if (k === "name") return;
1833
+ WaitAMinutePlugin[k] = waitAMinute[k];
1834
+ });
1835
+
1836
+ export default WaitAMinutePlugin;