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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +318 -2
  3. package/index.js +1836 -0
  4. package/package.json +70 -4
  5. package/skills/codebase-design/SKILL.md +114 -0
  6. package/skills/design-contract/SKILL.md +27 -0
  7. package/skills/design-critique/SKILL.md +19 -0
  8. package/skills/design-system/SKILL.md +17 -0
  9. package/skills/design-system/references/image-analysis.md +14 -0
  10. package/skills/design-system/references/tokens.md +32 -0
  11. package/skills/design-system/references/ui-rules.md +14 -0
  12. package/skills/diagnosing-bugs/SKILL.md +138 -0
  13. package/skills/frontend-design/SKILL.md +36 -0
  14. package/skills/handoff/SKILL.md +16 -0
  15. package/skills/improve-codebase-architecture/SKILL.md +71 -0
  16. package/skills/linter-negative/SKILL.md +74 -0
  17. package/skills/no-slop-ui/SKILL.md +26 -0
  18. package/skills/prototype/SKILL.md +26 -0
  19. package/skills/registry.json +44087 -0
  20. package/skills/tastemaker/SKILL.md +8 -0
  21. package/skills/tastemaker/references/tastemaker.md +14 -0
  22. package/skills/tdd/SKILL.md +38 -0
  23. package/skills/visual-review/SKILL.md +103 -0
  24. package/skills/visual-review/references/review-gates.md +42 -0
  25. package/skills/visual-review/references/surface-archetypes.md +37 -0
  26. package/skills/wizard/SKILL.md +44 -0
  27. package/skills/writing-for-agents/SKILL.md +81 -0
  28. package/src/cognition/cognition-store.js +251 -0
  29. package/src/cognition/cognitive-state.js +126 -0
  30. package/src/cognition/hypothesis-manager.js +113 -0
  31. package/src/cognition/observation-engine.js +112 -0
  32. package/src/cognition/observation-provenance.js +268 -0
  33. package/src/context/active-context-boundary.js +68 -0
  34. package/src/context/assembly.js +494 -0
  35. package/src/context/assessment-rules.js +55 -0
  36. package/src/context/capsule-staleness.js +267 -0
  37. package/src/context/capsules.js +2 -0
  38. package/src/context/compare-array.js +98 -0
  39. package/src/context/compare-object.js +89 -0
  40. package/src/context/compare-values.js +42 -0
  41. package/src/context/context-assembly-contract.js +71 -0
  42. package/src/context/context-budget-manager.js +232 -0
  43. package/src/context/context-builder.js +49 -0
  44. package/src/context/context-cache.js +55 -0
  45. package/src/context/context-capture.js +243 -0
  46. package/src/context/context-classification.js +199 -0
  47. package/src/context/context-compaction.js +179 -0
  48. package/src/context/context-decision-audit.js +267 -0
  49. package/src/context/context-demotion.js +173 -0
  50. package/src/context/context-dictionary.js +106 -0
  51. package/src/context/context-drift-detection.js +225 -0
  52. package/src/context/context-evaluation.js +374 -0
  53. package/src/context/context-event-ingress.js +100 -0
  54. package/src/context/context-event-normalization.js +224 -0
  55. package/src/context/context-feedback-loop.js +216 -0
  56. package/src/context/context-freshness.js +21 -0
  57. package/src/context/context-graph-builder.js +71 -0
  58. package/src/context/context-graph.js +451 -0
  59. package/src/context/context-history-relevance.js +321 -0
  60. package/src/context/context-interception.js +145 -0
  61. package/src/context/context-invalidation-events.js +154 -0
  62. package/src/context/context-lifecycle.js +170 -0
  63. package/src/context/context-loop-prevention.js +347 -0
  64. package/src/context/context-manager.js +340 -0
  65. package/src/context/context-memory-layers.js +265 -0
  66. package/src/context/context-minimality.js +166 -0
  67. package/src/context/context-normalization.js +130 -0
  68. package/src/context/context-optimization-metrics.js +434 -0
  69. package/src/context/context-output-policy.js +135 -0
  70. package/src/context/context-pack-authority.js +157 -0
  71. package/src/context/context-promotion.js +172 -0
  72. package/src/context/context-query-contract.js +101 -0
  73. package/src/context/context-ranking.js +164 -0
  74. package/src/context/context-rate-distortion.js +261 -0
  75. package/src/context/context-recovery.js +209 -0
  76. package/src/context/context-retrieval-integration.js +60 -0
  77. package/src/context/context-retrieval.js +93 -0
  78. package/src/context/context-router.js +457 -0
  79. package/src/context/context-routing-evaluation.js +331 -0
  80. package/src/context/context-routing-observability.js +296 -0
  81. package/src/context/context-routing-shadow.js +237 -0
  82. package/src/context/context-scope.js +181 -0
  83. package/src/context/context-snapshot.js +226 -0
  84. package/src/context/context-source-registry.js +95 -0
  85. package/src/context/context-sufficiency-gate.js +120 -0
  86. package/src/context/context-sufficiency-oracle.js +167 -0
  87. package/src/context/context-telemetry.js +176 -0
  88. package/src/context/context.js +633 -0
  89. package/src/context/dependency-closure.js +246 -0
  90. package/src/context/page-fault-tracker.js +275 -0
  91. package/src/context/prompt-engine.js +74 -0
  92. package/src/context/runtime-context-graph.js +260 -0
  93. package/src/context/selection.js +2 -0
  94. package/src/context/session.js +2 -0
  95. package/src/context/sufficiency-contract.js +300 -0
  96. package/src/context/tokenizer.js +17 -0
  97. package/src/context-benchmark-router.mjs +125 -0
  98. package/src/evidence/evidence-engine.js +161 -0
  99. package/src/evidence/evidence-factory.js +3 -0
  100. package/src/evidence/evidence-freshness.js +463 -0
  101. package/src/evidence/evidence-gap.js +76 -0
  102. package/src/evidence/evidence-lineage.js +525 -0
  103. package/src/evidence/evidence.js +136 -0
  104. package/src/evidence/freshness-validation.js +187 -0
  105. package/src/evidence/lineage.js +4 -0
  106. package/src/execution/action-evaluation.js +179 -0
  107. package/src/execution/circuit-breaker.js +70 -0
  108. package/src/execution/claim-interception.js +63 -0
  109. package/src/execution/execution-engine.js +258 -0
  110. package/src/execution/execution-intent.js +69 -0
  111. package/src/execution/execution-state.js +89 -0
  112. package/src/execution/formatting.js +60 -0
  113. package/src/execution/l1-sanitize.js +16 -0
  114. package/src/execution/l2-resource-guards.js +19 -0
  115. package/src/execution/l3-invariant-checker.js +5 -0
  116. package/src/execution/l4-containment.js +6 -0
  117. package/src/execution/runtime-guards.js +106 -0
  118. package/src/execution/strategy-identity.js +162 -0
  119. package/src/execution/tool-interceptor.js +7 -0
  120. package/src/execution/tool-result-classifier.js +63 -0
  121. package/src/integration/canary-deploy.js +143 -0
  122. package/src/integration/collector.js +59 -0
  123. package/src/integration/error-recovery.js +6 -0
  124. package/src/integration/event-bus.js +66 -0
  125. package/src/integration/health-checker.js +57 -0
  126. package/src/integration/logger.js +39 -0
  127. package/src/integration/message-handler.js +611 -0
  128. package/src/integration/opencode-db.js +153 -0
  129. package/src/integration/orchestration.js +419 -0
  130. package/src/integration/part-provenance.js +65 -0
  131. package/src/integration/rate-limiter.js +55 -0
  132. package/src/integration/release-gate.js +336 -0
  133. package/src/integration/rollback-manager.js +164 -0
  134. package/src/integration/router-adapter.js +269 -0
  135. package/src/integration/telemetry.js +45 -0
  136. package/src/integration/update-checker.js +10 -0
  137. package/src/persistence/event-sourcing.js +75 -0
  138. package/src/persistence/memory.js +501 -0
  139. package/src/persistence/migration-runner.js +16 -0
  140. package/src/persistence/persistence-manager.js +39 -0
  141. package/src/persistence/transaction-log.js +157 -0
  142. package/src/policy/action-capabilities.js +116 -0
  143. package/src/policy/action-policy.js +4 -0
  144. package/src/policy/assessment-engine.js +104 -0
  145. package/src/policy/assumption-tracking.js +66 -0
  146. package/src/policy/budget-enforcer.js +7 -0
  147. package/src/policy/config-validator.js +102 -0
  148. package/src/policy/execution-assessment.js +142 -0
  149. package/src/policy/failure-classification.js +5 -0
  150. package/src/policy/feature-flags.js +65 -0
  151. package/src/policy/governance-enforcement.js +118 -0
  152. package/src/policy/locale.js +15 -0
  153. package/src/policy/multi-signer.js +151 -0
  154. package/src/policy/policy-state-machine.js +97 -0
  155. package/src/policy/ponytail-constraint.js +38 -0
  156. package/src/policy/project-inspector.js +93 -0
  157. package/src/policy/query-builder.js +14 -0
  158. package/src/policy/request-classifier.js +79 -0
  159. package/src/policy/risk-engine.js +266 -0
  160. package/src/policy/scope-enforcement.js +140 -0
  161. package/src/policy/secret-scanner.js +68 -0
  162. package/src/policy/skill-routing.js +104 -0
  163. package/src/policy/strategy-capabilities.js +211 -0
  164. package/src/policy/strategy.js +6 -0
  165. package/src/policy/uncertainty.js +117 -0
  166. package/src/shared/container.js +44 -0
  167. package/src/shared/decision-types.js +24 -0
  168. package/src/shared/simple-test.js +160 -0
  169. package/src/shared/wam-events.js +52 -0
  170. package/src/skills/engine.js +1705 -0
  171. package/src/skills/skill-registry.js +19 -0
  172. package/src/skills/skill-routing.js +340 -0
  173. package/src/state/lifecycle-manager.js +113 -0
  174. package/src/state/operational-state.js +198 -0
  175. package/src/state/projections.js +47 -0
  176. package/src/state/replay-engine.js +5 -0
  177. package/src/state/requirement-ownership.js +20 -0
  178. package/src/state/requirement-state.js +152 -0
  179. package/src/state/run-event-identity.js +172 -0
  180. package/src/state/state-engine.js +19 -0
  181. package/src/state/state-machine.js +119 -0
  182. package/src/state/state-schema.js +318 -0
  183. package/src/state/state-store.js +335 -0
  184. package/src/state/state-transition.js +84 -0
  185. package/src/state/task-dependencies.js +287 -0
  186. package/src/state/task-execution.js +217 -0
  187. package/src/state/task-lifecycle.js +59 -0
  188. package/src/state/task-runs.js +361 -0
  189. package/src/state/task-state-file.js +86 -0
  190. package/src/state/task-state.js +41 -0
  191. package/src/state/task-status.js +54 -0
  192. package/src/state/task-store.js +48 -0
  193. package/src/state/wam-state.js +148 -0
  194. package/src/verification/completion-gate-simple.js +8 -0
  195. package/src/verification/completion-gate.js +183 -0
  196. package/src/verification/contract.js +9 -0
  197. package/src/verification/contradiction-resolution.js +255 -0
  198. package/src/verification/diagnostic-engine.js +659 -0
  199. package/src/verification/false-completion-prevention.js +100 -0
  200. package/src/verification/hook-validator.js +6 -0
  201. package/src/verification/requirements.js +6 -0
  202. package/src/verification/verification-context.js +162 -0
  203. package/src/verification/verification-lifecycle.js +66 -0
  204. package/src/verification/verification-model.js +89 -0
  205. package/src/verification/verification-persistence.js +129 -0
  206. package/src/verification/verification-policy.js +127 -0
  207. package/src/verification/verification.js +415 -0
@@ -0,0 +1,611 @@
1
+ /**
2
+ * Runtime message handler — chat.message hook logic.
3
+ *
4
+ * Extracted from index.js to modularize the message processing pipeline.
5
+ * Receives all dependencies via a `deps` object (inversion of control),
6
+ * making it independently testable.
7
+ *
8
+ * Usage from index.js:
9
+ * "chat.message": (input, output) => handleMessage(input, output, deps),
10
+ *
11
+ * where `deps` contains everything the handler needs (see DEPENDENCIES
12
+ * section in the typedef below for the full list).
13
+ */
14
+
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+
18
+ import { hasWamMarkerFor, isWamSynthetic } from "./part-provenance.js";
19
+
20
+ // Idempotency guard: a chat.message invocation must be processed at most once
21
+ // per message id + kind. The store is normally owned by the plugin instance
22
+ // (deps.processedMessages); the module-level fallback keeps direct callers and
23
+ // tests working. Bounded so a long session cannot grow unbounded.
24
+ const PROCESSED_MESSAGE_LIMIT = 1000;
25
+ const fallbackProcessedMessages = new Map();
26
+
27
+ function markProcessed(key, store = fallbackProcessedMessages) {
28
+ store.set(key, true);
29
+ if (store.size > PROCESSED_MESSAGE_LIMIT) {
30
+ const oldest = store.keys().next().value;
31
+ if (oldest !== undefined) store.delete(oldest);
32
+ }
33
+ }
34
+
35
+ export function _resetProcessedMessages(store = fallbackProcessedMessages) {
36
+ store.clear();
37
+ }
38
+
39
+ /**
40
+ * Pure predicate: true when this message was already handled by WAM.
41
+ * Serves as the runtime guard and as a testable unit.
42
+ *
43
+ * @param {Object} input
44
+ * @param {Object} output
45
+ * @param {Map} [store]
46
+ * @returns {boolean}
47
+ */
48
+ export function isMessageAlreadyProcessed(input, output, store = fallbackProcessedMessages) {
49
+ const messageId = input?.messageID || output?.message?.id || input?.message?.id || null;
50
+ if (!messageId) return false;
51
+ const key = `${messageId}:${input?.tool ? "tool" : "prompt"}`;
52
+ return (
53
+ store.has(key) ||
54
+ hasWamMarkerFor(output?.parts, messageId) ||
55
+ hasWamMarkerFor(input?.message?.parts, messageId)
56
+ );
57
+ }
58
+
59
+ // Generic/new task ids must never be treated as explicit task identity.
60
+ // Kept in sync with index.js GENERIC_TASK.
61
+ const GENERIC_TASK = /^(default-task|task|general|)$/;
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // CONSTANTS
65
+ // ---------------------------------------------------------------------------
66
+
67
+ const RESUME_RE = /\b(en qué estábamos|en que estabamos|dónde íbamos|donde íbamos|sigamos|continuemos|retomar la tarea)\b/i;
68
+ const MAX_VISIBLE_REQS = 5;
69
+ const ASKING_CMD_RE = /^(answer|resolve|contract|progress|task|skills|assumptions|compress)\b/;
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // DEPENDENCIES (provided by caller via deps parameter)
73
+ // ---------------------------------------------------------------------------
74
+
75
+ /**
76
+ * @typedef {Object} MessageHandlerDeps
77
+ * @property {boolean} bypassed
78
+ * @property {Map} sessionTasks
79
+ * @property {Map} sessionStore
80
+ * @property {Object} cfg
81
+ * @property {Function} resolveSessionBase
82
+ * @property {Function} wamRootFor
83
+ * @property {Function} ensureWamMemory
84
+ * @property {Function} effectiveTaskId
85
+ * @property {Function} genPartId
86
+ * @property {Map} [processedMessages]
87
+ * @property {Function} emitTextPart
88
+ * @property {Function} readActiveTaskIdFresh
89
+ * @property {Function} writeActiveTaskId
90
+ * @property {Object} waitAMinute
91
+ * @property {Function} migrateLegacyCognition
92
+ * @property {Function} noteSuccess
93
+ * @property {Function} noteFailure
94
+ * @property {Function} getTaskState
95
+ * @property {Function} persistTaskState
96
+ * @property {Function} findDuplicateTask
97
+ * @property {Function} escalateAssumptions
98
+ * @property {Function} buildAssumptions
99
+ * @property {Function} initMemory
100
+ * @property {Function} updateProjectMemo
101
+ * @property {Function} updateLiveContext
102
+ * @property {Function} readLiveContext
103
+ * @property {Function} persistLiveContext
104
+ * @property {Function} assembleContext
105
+ * @property {Function} createSnapshot
106
+ * @property {Function} checkContinuation
107
+ * @property {Function} rebuildScope
108
+ * @property {Function} getDecision
109
+ * @property {Function} recordDecision
110
+ * @property {Function} compactDecisions
111
+ * @property {Function} writeCavemanSummary
112
+ * @property {Function} truncate
113
+ * @property {Function} delegationLines
114
+ * @property {Function} updateTaskMemory
115
+ * @property {Function} addRecentChange
116
+ * @property {Function} closeSession
117
+ * @property {Function} getSessionId
118
+ * @property {Function} classifyAskingMessage
119
+ */
120
+
121
+ /**
122
+ * Handles the chat.message hook: deterministic task-state and context analysis of user messages.
123
+ *
124
+ * @param {Object} input - OpenCode chat.message input
125
+ * @param {Object} output - OpenCode chat.message output (mutated in place)
126
+ * @param {MessageHandlerDeps} deps
127
+ * @returns {Promise<void>}
128
+ */
129
+ export async function handleMessage(input, output, deps) {
130
+ const {
131
+ collector = null,
132
+ bypassed,
133
+ sessionTasks,
134
+ sessionStore,
135
+ cfg,
136
+ resolveSessionBase,
137
+ wamRootFor,
138
+ ensureWamMemory,
139
+ effectiveTaskId,
140
+ genPartId,
141
+ emitTextPart,
142
+ readActiveTaskIdFresh,
143
+ writeActiveTaskId,
144
+ waitAMinute,
145
+ migrateLegacyCognition,
146
+ noteSuccess,
147
+ noteFailure,
148
+ getTaskState,
149
+ persistTaskState,
150
+ findDuplicateTask,
151
+ escalateAssumptions,
152
+ buildAssumptions,
153
+ initMemory,
154
+ updateProjectMemo,
155
+ updateLiveContext,
156
+ readLiveContext,
157
+ persistLiveContext,
158
+ assembleContext,
159
+ createSnapshot,
160
+ checkContinuation,
161
+ rebuildScope,
162
+ getDecision,
163
+ recordDecision,
164
+ compactDecisions,
165
+ writeCavemanSummary,
166
+ truncate,
167
+ delegationLines,
168
+ updateTaskMemory,
169
+ addRecentChange,
170
+ closeSession,
171
+ getSessionId,
172
+ classifyAskingMessage,
173
+ } = deps;
174
+
175
+ try {
176
+ if (bypassed) return;
177
+
178
+ // Idempotency: never process the same message twice. WAM injects synthetic
179
+ // parts into `output`; without this guard a re-invocation (retry/replay)
180
+ // would read its own injection as a fresh prompt and loop indefinitely.
181
+ const processedStore = deps.processedMessages || fallbackProcessedMessages;
182
+ if (isMessageAlreadyProcessed(input, output, processedStore)) return;
183
+ {
184
+ const messageId = input?.messageID || output?.message?.id || input?.message?.id || null;
185
+ if (messageId) markProcessed(`${messageId}:${input?.tool ? "tool" : "prompt"}`, processedStore);
186
+ }
187
+
188
+ const promptText = extractPrompt(input, output);
189
+ if (!promptText.trim()) return;
190
+
191
+ // --- Tool execution integration (passive state preservation) ---
192
+ if (input?.tool && output?.result !== undefined) {
193
+ const toolName = input.tool;
194
+ const wamRoot = await ensureWamMemory(input.sessionID, promptText);
195
+ const taskId = effectiveTaskId(input, sessionTasks, wamRoot);
196
+
197
+ try { migrateLegacyCognition(wamRoot, taskId); } catch (e) { console.log(`[wait-a-minute] migration error:`, e.message); }
198
+
199
+ try {
200
+ await noteSuccess(wamRoot, taskId, {
201
+ hypothesisId: input._wamHypothesisId || input.hypothesisId,
202
+ experimentId: input._wamExperimentId || input.experimentId,
203
+ result: output.result,
204
+ actual: output.actual,
205
+ unexpected: output.unexpected,
206
+ provenance: `agent-tool-${toolName}-success`,
207
+ requirementId: input._wamRequirementId || input.requirementId,
208
+ });
209
+ } catch (e) {
210
+ console.log(`[wait-a-minute] noteSuccess integration error:`, e.message);
211
+ }
212
+ }
213
+
214
+ if (input?.tool && output?.error) {
215
+ const toolName = input.tool;
216
+ const wamRoot = await ensureWamMemory(input.sessionID, promptText);
217
+ const taskId = effectiveTaskId(input, sessionTasks, wamRoot);
218
+
219
+ try { migrateLegacyCognition(wamRoot, taskId); } catch (e) { console.log(`[wait-a-minute] migration error:`, e.message); }
220
+
221
+ try {
222
+ await noteFailure(wamRoot, taskId, {
223
+ hypothesisId: input._wamHypothesisId || input.hypothesisId,
224
+ experimentId: input._wamExperimentId || input.experimentId,
225
+ reason: output.error || "tool execution failed",
226
+ actual: output.actual,
227
+ unexpected: output.unexpected,
228
+ provenance: `agent-tool-${toolName}-failure`,
229
+ });
230
+ } catch (e) {
231
+ console.log(`[wait-a-minute] noteFailure integration error:`, e.message);
232
+ }
233
+ }
234
+
235
+ if (!promptText.trim()) return;
236
+
237
+ // Ensure WAM memory for this session
238
+ const wamRoot = await ensureWamMemory(input.sessionID, promptText);
239
+
240
+ // No-task-assumption: resume intent without active task → ask, don't assume
241
+ if (!input.taskId && RESUME_RE.test(promptText)) {
242
+ const active = sessionTasks.get(input.sessionID) || readActiveTaskIdFresh(wamRoot);
243
+ const st = active ? getTaskState(active, wamRoot) : null;
244
+ if (st && st.phase !== "DONE") {
245
+ emitTextPart(output, `[wait-a-minute] Hay una tarea pendiente: ${active} (fase ${st.phase}). ¿Quieres continuarla? Responde /wam resume ${active} o define una tarea nueva — no asumo intención.`, { sessionID: input.sessionID, messageID: output.message?.id || input.messageID });
246
+ } else {
247
+ emitTextPart(output, "[wait-a-minute] No hay tarea activa. Dime qué tarea nueva quieres — no asumo intención previa.", { sessionID: input.sessionID, messageID: output.message?.id || input.messageID });
248
+ }
249
+ return;
250
+ }
251
+
252
+ // Effective taskId with session persistence
253
+ let taskId = effectiveTaskId(input, sessionTasks, wamRoot);
254
+ if (input.sessionID) sessionTasks.set(input.sessionID, taskId);
255
+
256
+ // Clarification Gate: in ASKING classify the message
257
+ const askingState = getTaskState(taskId, wamRoot);
258
+ if (askingState?.phase === "ASKING") {
259
+ const trimmed = promptText.trim();
260
+ if (ASKING_CMD_RE.test(trimmed)) return; // handled by command.execute.before
261
+ const kind = classifyAskingMessage(trimmed);
262
+ if (kind === "blocked-message") {
263
+ const u = (askingState.contract?.unknowns || []).find((x) => x.status === "blocking");
264
+ const directive = u
265
+ ? `⛔ [wait-a-minute] BLOQUEADO: Pregunta pendiente — ${u.id}: ${u.question}\nResponder: /wam answer ${u.id} <respuesta>`
266
+ : "⛔ [wait-a-minute] BLOQUEADO: Tienes preguntas sin responder.";
267
+ emitTextPart(output, directive, { sessionID: input.sessionID, messageID: output.message?.id || input.messageID });
268
+ // Rewrite the original prompt so the implementation attempt is NOT
269
+ // consumed downstream as an instruction (AC5).
270
+ const rewrite = `[wait-a-minute] BLOQUEADO — prompt de implementación no consumido como respuesta. ${directive}`;
271
+ if (input?.message?.parts && input.message.parts.length > 0) {
272
+ const tp = input.message.parts.find((p) => p.type === "text" && typeof p.text === "string");
273
+ if (tp) tp.text = rewrite;
274
+ }
275
+ if (input?.parts && input.parts.length > 0) {
276
+ const tp = input.parts.find((p) => p.type === "text" && typeof p.text === "string");
277
+ if (tp) tp.text = rewrite;
278
+ }
279
+ return;
280
+ }
281
+ if (kind === "new-intent") {
282
+ emitTextPart(output, "[wait-a-minute] Nueva intención detectada. Abriendo tarea nueva.", { sessionID: input.sessionID, messageID: output.message?.id || input.messageID });
283
+ // Override the explicit taskId so subsequent analysis creates a fresh
284
+ // task (task-<ts>) without contaminating unknowns from the previous task.
285
+ const newTaskId = `task-${Date.now()}`;
286
+ if (input) input.taskId = newTaskId;
287
+ try {
288
+ if (input?.sessionID) sessionTasks.set(input.sessionID, newTaskId);
289
+ writeActiveTaskId(newTaskId, wamRoot);
290
+ } catch {}
291
+ // Update local taskId so the rest of handleMessage uses the new task.
292
+ taskId = newTaskId;
293
+ // Fall through to normal analysis: a new task-<ts> task gets created.
294
+ }
295
+ // answer: in ASKING a natural-language message resolves the blocking question.
296
+ const ansRes = waitAMinute.answerFromMessage(taskId, trimmed, wamRoot);
297
+ if (ansRes && ansRes.ok) {
298
+ const u = ansRes.u;
299
+ emitTextPart(
300
+ output,
301
+ `[wait-a-minute] question answered — ${u?.id || "U1"}: ${u?.answer || trimmed}. Ready → Proceed (${u?.question || "pregunta resuelta"})`,
302
+ { sessionID: input.sessionID, messageID: output.message?.id || input.messageID },
303
+ );
304
+ // After resolving, fall through to re-analysis so contract is rebuilt.
305
+ }
306
+ }
307
+
308
+ // Project scope change → backlog + new task
309
+ const existingState = getTaskState(taskId, wamRoot);
310
+ if (existingState?.projectPath && existingState.projectPath !== wamRoot) {
311
+ waitAMinute.addToBacklog(
312
+ existingState,
313
+ `Task ${taskId}: ${existingState.contract?.requirements?.map((r) => r.title).join("; ") || "sin reqs"}`,
314
+ "project-switch",
315
+ );
316
+ persistTaskState(taskId, existingState, wamRoot);
317
+ try { fs.rmSync(path.join(wamRoot, ".wam", "active-task"), { force: true }); } catch {}
318
+ taskId = `task-${Date.now()}`;
319
+ input.taskId = taskId;
320
+ }
321
+
322
+ // Continuation fast-path: approved contract + no DONE claim
323
+ if (existingState?.contract?.status === "APPROVED") {
324
+ const claim = waitAMinute.evaluateCompletionGate(existingState, promptText);
325
+ if (!claim.blocked && !claim.allDone) {
326
+ const snapshotCheck = checkContinuation(taskId, existingState, wamRoot);
327
+
328
+ if (snapshotCheck.status === "VALID") {
329
+ if (collector) collector.record("Context_fast_path");
330
+ input.waitAnalysis = sessionStore.get("waitAnalysis") || null;
331
+ try { updateProjectMemo({}, wamRoot); } catch {}
332
+ try {
333
+ persistLiveContext(taskId, existingState, wamRoot);
334
+ const live = readLiveContext(wamRoot, taskId);
335
+ if (live) {
336
+ const parts = [`[wam N2 task]\n${live}\n`];
337
+ parts.push(...delegationLines(existingState).map((l) => l + "\n"));
338
+ emitTextPart(output, parts.join("\n"), { sessionID: input.sessionID, messageID: output.message?.id || input.messageID });
339
+ }
340
+ } catch {}
341
+ return;
342
+ }
343
+
344
+ const scope = rebuildScope(snapshotCheck.changedSignals, collector);
345
+ if (scope.rebuildN1 || scope.rebuildN3) {
346
+ input.waitAnalysis = sessionStore.get("waitAnalysis") || null;
347
+ try { updateProjectMemo({}, wamRoot); } catch {}
348
+ try {
349
+ persistLiveContext(taskId, existingState, wamRoot);
350
+ const live = readLiveContext(wamRoot, taskId);
351
+ if (live) {
352
+ const parts = [
353
+ `[wam continuation] Contexto reconstruido (${snapshotCheck.changedSignals.join(", ")})`,
354
+ `[wam N2 task]\n${live}\n`,
355
+ ];
356
+ parts.push(...delegationLines(existingState).map((l) => l + "\n"));
357
+ emitTextPart(output, parts.join("\n"), { sessionID: input.sessionID, messageID: output.message?.id || input.messageID });
358
+ }
359
+ } catch {}
360
+ return;
361
+ }
362
+ // INVALID: full rebuild → fall through to normal analyze()
363
+ }
364
+ }
365
+
366
+ // ---- Normal analysis flow ----
367
+ const analysis = await waitAMinute.analyze({
368
+ prompt: promptText,
369
+ projectPath: wamRoot,
370
+ config: cfg,
371
+ tierCaps: cfg.tierCaps,
372
+ activePreset: cfg.activePreset,
373
+ activeMode: cfg.activeMode,
374
+ });
375
+
376
+ const state = waitAMinute.buildPersistedState(taskId, analysis, wamRoot);
377
+ state.lastAction = promptText;
378
+
379
+ // Task Dedup - CANDIDATE ONLY: never overrides taskId implicitly.
380
+ // - If input.taskId is explicit → NEVER override; do NOT even consult findDuplicateTask
381
+ // - If taskId is generic/new → findDuplicateTask may return a candidate
382
+ // but is reported, not assigned; caller decides via /wam task switch
383
+ const isExplicit = input?.taskId && !GENERIC_TASK.test(input.taskId);
384
+ if (!isExplicit) {
385
+ const existingTaskId = findDuplicateTask(promptText, wamRoot);
386
+ if (existingTaskId && existingTaskId !== taskId) {
387
+ // Candidate-only: report, do not reassign. The session continues with
388
+ // the freshly-initialized taskId; the user can reuse via /wam task switch.
389
+ emitTextPart(output, `[wait-a-minute] Tarea similar detectada: ${existingTaskId} (usa /wam task switch ${existingTaskId} para retomar)`, { sessionID: input.sessionID, messageID: output.message?.id || input.messageID });
390
+ }
391
+ }
392
+
393
+ // Assumption Gate
394
+ try {
395
+ const { changed } = escalateAssumptions(state, promptText);
396
+ if (changed) persistTaskState(taskId, state, wamRoot);
397
+ } catch (err) {
398
+ console.error("[wait-a-minute] escalate assumptions failed:", err);
399
+ }
400
+
401
+ // Store analysis in session
402
+ sessionStore.set("waitAnalysis", analysis);
403
+ sessionStore.set("completionContract", state.contract);
404
+ sessionStore.set("persistentPolicies", analysis.persistentPolicies || []);
405
+ sessionStore.set("skillRegistry", analysis.skillRegistry || {});
406
+
407
+ // Blocking questions → ASKING
408
+ const blockingUnknowns = (state.contract?.unknowns || []).filter((u) => u.status === "blocking");
409
+ const finalQuestions = blockingUnknowns.filter((u) => {
410
+ const decision = getDecision(u.id, wamRoot);
411
+ if (decision) { console.log(`[wait-a-minute] Found existing decision for ${u.id}: ${decision.decision}`); return false; }
412
+ return true;
413
+ });
414
+
415
+ if (finalQuestions.length > 0 && state.phase !== "ANSWERED") {
416
+ state.phase = "ASKING";
417
+ state.nextAction = "Responder pregunta bloqueante antes de ejecutar";
418
+ persistTaskState(taskId, state, wamRoot);
419
+ const questions = [];
420
+ for (const u of finalQuestions) {
421
+ questions.push(`⛔ [wait-a-minute] ASKING — ${u.id}: ${u.question}\nNo implementar hasta responder. Responder: /wam answer ${u.id} <respuesta>`);
422
+ }
423
+ emitTextPart(output, questions.join("\n\n"), { sessionID: input.sessionID, messageID: output.message?.id || input.messageID });
424
+ return;
425
+ }
426
+
427
+ // Approval gate
428
+ const intentConf = Number(analysis.intent?.confidence ?? analysis.intent?.conf ?? 100);
429
+ const openUnknowns = (state.contract?.unknowns || []).filter((u) => u.status !== "answered" && u.status !== "blocking");
430
+ const highUncertainty =
431
+ (analysis.ambiguity || "low") === "high" ||
432
+ intentConf < 60 ||
433
+ openUnknowns.length > 0;
434
+ const trimmed = promptText.trim();
435
+ const userConfirms =
436
+ !highUncertainty ||
437
+ /^(s[ií]|ok|okey|dale|hazlo|adelante|continuar|continua|aprobar|confirmo|correcto|perfecto|bueno|va|listo|sigue)\b/i.test(trimmed) ||
438
+ trimmed.includes("aprobar contrato");
439
+
440
+ if (state.contract?.status === "PROPOSED" && state.phase === "PROPOSED" && userConfirms) {
441
+ waitAMinute.approveContract(taskId, wamRoot);
442
+ try {
443
+ recordDecision({
444
+ id: `strategy-${taskId}-${Date.now()}`,
445
+ decision: `Aprobar estrategia ${analysis.strategy || "NORMAL"} para ${taskId}`,
446
+ reason: promptText.slice(0, 120),
447
+ source: highUncertainty ? "user-decided" : "observed",
448
+ confidence: "high",
449
+ }, wamRoot);
450
+ try { compactDecisions(wamRoot); } catch {}
451
+ } catch {}
452
+ const fresh = getTaskState(taskId, wamRoot);
453
+ if (fresh) {
454
+ state.contract = fresh.contract;
455
+ state.phase = fresh.phase;
456
+ state.nextAction = fresh.nextAction;
457
+ }
458
+ }
459
+
460
+ const gate = waitAMinute.evaluateCompletionGate(state, promptText);
461
+ const { phase, nextAction } = waitAMinute.applyPhaseTransition(state, gate);
462
+ if (gate.blocked || gate.allDone) {
463
+ state.phase = phase;
464
+ state.nextAction = nextAction;
465
+ persistTaskState(taskId, state, wamRoot);
466
+ }
467
+
468
+ const updatedState = getTaskState(taskId, wamRoot);
469
+
470
+ // Live context snapshot
471
+ try { persistLiveContext(taskId, updatedState, wamRoot); } catch {}
472
+
473
+ // ---- Context Assembly ----
474
+ const inject = [];
475
+
476
+ const reqs = updatedState.requirements || [];
477
+ const pend = reqs.filter((r) => r.status !== "done");
478
+ const objective = updatedState.contract?.objective || analysis.intent?.goal || null;
479
+ if (!(analysis.intent?.classification === "trivial" || analysis.strategy === "FAST")) {
480
+ if (updatedState.phase === "PROPOSED" && !updatedState.contractDisplayed) {
481
+ const visible = reqs.slice(0, MAX_VISIBLE_REQS);
482
+ const overflow = reqs.length - MAX_VISIBLE_REQS;
483
+ inject.push(
484
+ `Objective: ${objective || "pending"}`,
485
+ `Remaining (${pend.length}/${reqs.length}):`,
486
+ ...visible.map((r) => ` - ${truncate(r.title, 120)}`),
487
+ ...(overflow > 0 ? [` ... y ${overflow} más`] : []),
488
+ );
489
+ updatedState.contractDisplayed = true;
490
+ persistTaskState(taskId, updatedState, wamRoot);
491
+ }
492
+ }
493
+
494
+ try { initMemory(wamRoot); } catch {}
495
+ try { updateProjectMemo(analysis, wamRoot); } catch {}
496
+ try {
497
+ const pack = assembleContext({
498
+ prompt: promptText,
499
+ taskId,
500
+ classification: analysis.intent?.classification,
501
+ mode: analysis.strategy,
502
+ projectPath: wamRoot,
503
+ budget: cfg.contextBudget || 4000,
504
+ taskState: updatedState,
505
+ skillRegistry: waitAMinute.loadBundledRegistry(),
506
+ selectedSkills: analysis.skills?.selected || [],
507
+ });
508
+ if (pack.lines.length) {
509
+ inject.push(pack.lines.join("\n") + `\n[wam pack ${pack.budget_used}/${pack.budget} tok ${pack.levels.N0 ? "N0" : ""}${pack.levels.N1 ? "+N1" : ""}${pack.levels.N2 ? "+N2" : ""}${pack.levels.N3 ? "+N3" : ""}]`);
510
+ }
511
+ try { createSnapshot(taskId, updatedState, wamRoot); } catch {}
512
+ } catch {}
513
+
514
+ if (gate.blocked) {
515
+ const maxGateReqs = 5;
516
+ const pendingItems = gate.pending || [];
517
+ const visiblePending = pendingItems.slice(0, maxGateReqs);
518
+ const overflowPending = pendingItems.length - maxGateReqs;
519
+ const pendingList = visiblePending.length > 0
520
+ ? "\n Requisitos pendientes:\n " + visiblePending.map((p) => `- ${truncate(p, 150)}`).join("\n ") + (overflowPending > 0 ? `\n ...(+${overflowPending} más)` : "") + "\n"
521
+ : "";
522
+ const gateHold = `⛔ [wait-a-minute] COMPLETION GATE: faltan ${gate.pending.length} requisito(s). No declare DONE.${pendingList} Continuar con: ${updatedState.nextAction}`;
523
+ emitTextPart(output, gateHold, { sessionID: input.sessionID, messageID: output.message?.id || input.messageID });
524
+ if (input?.parts && input.parts.length > 0) {
525
+ const tp = input.parts.find((p) => p.type === "text" && typeof p.text === "string");
526
+ if (tp) tp.text = gateHold;
527
+ }
528
+ } else if (gate.allDone) {
529
+ updateTaskMemory(taskId, {
530
+ summary: [
531
+ "# Task Summary", "", "## Objective",
532
+ `- ${taskId} (${analysis.intent?.classification || "task"})`, "", "## Completed",
533
+ ...(updatedState.contract?.requirements || []).map((r) => `- ${r}`), "", "## Verification",
534
+ ...(updatedState.requirements || []).map((r) => `- ${r.id}: ${r.status}`), "", "## Status", "COMPLETED",
535
+ ].join("\n"),
536
+ }, wamRoot);
537
+ addRecentChange({
538
+ date: new Date().toISOString().slice(0, 10),
539
+ scope: taskId,
540
+ changes: updatedState.contract?.requirements || [],
541
+ verification: `requisitos completos: ${(updatedState.requirements || []).length}`,
542
+ }, wamRoot);
543
+ try {
544
+ writeCavemanSummary(taskId, updatedState, wamRoot, [
545
+ "date:", new Date().toISOString().slice(0, 10),
546
+ "scope:", taskId,
547
+ "status:", "COMPLETED",
548
+ "completed:",
549
+ ...(updatedState.contract?.requirements || []).map((r) => `- ${r}`),
550
+ "verification:",
551
+ ...(updatedState.requirements || []).map((r) => `- ${r.id}: ${r.status}`),
552
+ ].join("\n"));
553
+ } catch {}
554
+ try {
555
+ closeSession({
556
+ sessionId: getSessionId(wamRoot),
557
+ taskId,
558
+ summary: (updatedState.contract?.requirements || []).join("; "),
559
+ candidates: (updatedState.requirements || []).map((r) => ({ id: r.id, title: r.title, evidence: r.evidence || [] })),
560
+ }, wamRoot);
561
+ } catch {}
562
+ }
563
+
564
+ if (updatedState.contract?.status === "APPROVED") {
565
+ inject.push(...delegationLines(updatedState));
566
+ }
567
+ if (inject.length > 0) {
568
+ emitTextPart(output, inject.join("\n") + "\n", { sessionID: input.sessionID, messageID: output.message?.id || input.messageID });
569
+ }
570
+
571
+ if (updatedState.contract?.status !== "APPROVED" && updatedState.phase !== "DONE") {
572
+ waitAMinute.presentValidation({
573
+ analysis: {
574
+ ...analysis,
575
+ contractStatus: updatedState.contract.status,
576
+ phase: updatedState.phase,
577
+ completionContract: updatedState.contract,
578
+ },
579
+ ctx: output,
580
+ meta: { sessionID: input.sessionID, messageID: output.message?.id || input.messageID },
581
+ });
582
+ }
583
+
584
+ input.waitAnalysis = analysis;
585
+ } catch (err) {
586
+ console.error("[wait-a-minute] Pre-flight analysis failed:", err);
587
+ }
588
+ }
589
+
590
+ /**
591
+ * Extracts the prompt text from a chat.message input/output pair.
592
+ * Extracted as a utility so it can be tested independently.
593
+ *
594
+ * @param {Object} input - chat.message input
595
+ * @param {Object} output - chat.message output
596
+ * @returns {string}
597
+ */
598
+ export function extractPrompt(input, output) {
599
+ // Read ONLY from the user's own message. Never from `output.parts`:
600
+ // WAM writes synthetic injections there, so reading them back is the
601
+ // self-consumption bug that caused the re-entrancy loop.
602
+ const srcParts = input?.message?.parts || input?.parts || [];
603
+ if (srcParts.length > 0) {
604
+ const textPart = srcParts.find(
605
+ (p) => p.type === "text" && typeof p.text === "string" && !isWamSynthetic(p),
606
+ );
607
+ return textPart?.text || "";
608
+ }
609
+ if (input?.text && typeof input.text === "string") return input.text;
610
+ return "";
611
+ }