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,153 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ const OPENCODE_DB_PATH = path.join(
6
+ process.env.HOME || "/home/nicolas",
7
+ ".local/share/opencode/opencode.db"
8
+ );
9
+
10
+ let dbInstance = null;
11
+
12
+ function getDb() {
13
+ if (dbInstance) return dbInstance;
14
+ if (!fs.existsSync(OPENCODE_DB_PATH)) {
15
+ console.error("[wait-a-minute] OpenCode DB not found:", OPENCODE_DB_PATH);
16
+ return null;
17
+ }
18
+ try {
19
+ dbInstance = new DatabaseSync(OPENCODE_DB_PATH, { readOnly: true });
20
+ return dbInstance;
21
+ } catch (err) {
22
+ console.error("[wait-a-minute] Failed to open OpenCode DB:", err.message);
23
+ return null;
24
+ }
25
+ }
26
+
27
+ export function getSessionMessages(sessionId, limit = 20) {
28
+ const db = getDb();
29
+ if (!db) return [];
30
+
31
+ try {
32
+ const stmt = db.prepare(`
33
+ SELECT id, time_created, data
34
+ FROM message
35
+ WHERE session_id = ?
36
+ ORDER BY time_created DESC
37
+ LIMIT ?
38
+ `);
39
+ const rows = stmt.all(sessionId, limit);
40
+ return rows.map((row) => ({
41
+ id: row.id,
42
+ timeCreated: row.timeCreated,
43
+ ...JSON.parse(row.data),
44
+ }));
45
+ } catch (err) {
46
+ console.error("[wait-a-minute] Failed to query session messages:", err.message);
47
+ return [];
48
+ }
49
+ }
50
+
51
+ export function getSessionParts(sessionId, limit = 50) {
52
+ const db = getDb();
53
+ if (!db) return [];
54
+
55
+ try {
56
+ const stmt = db.prepare(`
57
+ SELECT p.id, p.time_created, p.data, p.message_id
58
+ FROM part p
59
+ JOIN message m ON p.message_id = m.id
60
+ WHERE m.session_id = ?
61
+ ORDER BY p.time_created DESC
62
+ LIMIT ?
63
+ `);
64
+ const rows = stmt.all(sessionId, limit);
65
+ return rows.map((row) => ({
66
+ id: row.id,
67
+ timeCreated: row.timeCreated,
68
+ messageId: row.messageId,
69
+ ...JSON.parse(row.data),
70
+ }));
71
+ } catch (err) {
72
+ console.error("[wait-a-minute] Failed to query session parts:", err.message);
73
+ return [];
74
+ }
75
+ }
76
+
77
+ export function getToolCallsFromSession(sessionId, limit = 100) {
78
+ const parts = getSessionParts(sessionId, limit);
79
+ return parts
80
+ .filter((p) => p.type === "tool" && p.tool)
81
+ .map((p) => ({
82
+ tool: p.tool,
83
+ callId: p.callID,
84
+ command: p.state?.input?.command,
85
+ output: p.state?.output,
86
+ exitCode: p.state?.metadata?.exit,
87
+ startTime: p.time?.start,
88
+ endTime: p.time?.end,
89
+ }));
90
+ }
91
+
92
+ export function getTextFromSession(sessionId, limit = 10) {
93
+ const parts = getSessionParts(sessionId, limit * 5);
94
+ return parts
95
+ .filter((p) => p.type === "text" && p.text)
96
+ .map((p) => ({
97
+ text: p.text,
98
+ messageId: p.messageId,
99
+ timeCreated: p.timeCreated,
100
+ }));
101
+ }
102
+
103
+ export function findSessionBySlug(slug) {
104
+ const db = getDb();
105
+ if (!db) return null;
106
+
107
+ try {
108
+ const stmt = db.prepare(`
109
+ SELECT id, title, time_created
110
+ FROM session
111
+ WHERE id LIKE ?
112
+ LIMIT 1
113
+ `);
114
+ const row = stmt.get(`%${slug}%`);
115
+ return row || null;
116
+ } catch (err) {
117
+ console.error("[wait-a-minute] Failed to find session:", err.message);
118
+ return null;
119
+ }
120
+ }
121
+
122
+ export function getRecentActions(sessionId, minutes = 30) {
123
+ const db = getDb();
124
+ if (!db) return [];
125
+
126
+ const cutoff = Date.now() - minutes * 60 * 1000;
127
+ try {
128
+ const stmt = db.prepare(`
129
+ SELECT p.id, p.time_created, p.data
130
+ FROM part p
131
+ JOIN message m ON p.message_id = m.id
132
+ WHERE m.session_id = ?
133
+ AND p.data LIKE '%"type":"tool"%'
134
+ AND p.time_created > ?
135
+ ORDER BY p.time_created DESC
136
+ `);
137
+ const rows = stmt.all(sessionId, cutoff);
138
+ return rows.map((row) => {
139
+ const data = JSON.parse(row.data);
140
+ return {
141
+ id: row.id,
142
+ timeCreated: row.timeCreated,
143
+ tool: data.tool,
144
+ command: data.state?.input?.command,
145
+ output: data.state?.output,
146
+ exitCode: data.state?.metadata?.exit,
147
+ };
148
+ });
149
+ } catch (err) {
150
+ console.error("[wait-a-minute] Failed to get recent actions:", err.message);
151
+ return [];
152
+ }
153
+ }
@@ -0,0 +1,419 @@
1
+ /**
2
+ * Orchestration — funciones puras de decisión/estado extraídas de index.js.
3
+ *
4
+ * Este módulo contiene Toda la lógica de decisión del ciclo de vida:
5
+ * - Gestión de fases (PROPOSED→APPROVED→IMPLEMENTING→VERIFYING→DONE)
6
+ * - Transiciones de estado (approve/reject/edit contract, mark requirement)
7
+ * - Completion gate (evaluate, apply phase transition)
8
+ *
9
+ * Las funciones aquí son PURAS: toman estado como entrada, retornan
10
+ * decisión como salida. Sin side effects (no persisten, no leen, no escriben).
11
+ *
12
+ * index.js (OpenCode Adapter) importa estas funciones y se encarga de
13
+ * los side effects: getTaskState, persistTaskState, updateLiveContext, etc.
14
+ */
15
+
16
+ import { evaluateRequirement as evaluateRequirementChecks } from "../verification/verification.js";
17
+ import { truncate } from "../execution/formatting.js";
18
+
19
+ // ═══════════════════════════════════════════════════════════════
20
+ // Constantes de fase
21
+ // ═══════════════════════════════════════════════════════════════
22
+
23
+ export const PHASE_PROPOSED = "PROPOSED";
24
+ export const PHASE_APPROVED = "APPROVED";
25
+ export const PHASE_IMPLEMENTING = "IMPLEMENTING";
26
+ export const PHASE_VERIFYING = "VERIFYING";
27
+ export const PHASE_DONE = "DONE";
28
+ export const PHASE_ASKING = "ASKING";
29
+ export const PHASE_WAITING = "WAITING";
30
+
31
+ /**
32
+ * Transiciones válidas de fase.
33
+ * Cada clave es una fase origen, el array fases destino permitidas.
34
+ */
35
+ export const PHASE_TRANSITIONS = Object.freeze({
36
+ [PHASE_PROPOSED]: [PHASE_IMPLEMENTING, PHASE_WAITING, PHASE_ASKING],
37
+ [PHASE_IMPLEMENTING]: [PHASE_VERIFYING, PHASE_DONE, PHASE_ASKING],
38
+ [PHASE_VERIFYING]: [PHASE_IMPLEMENTING, PHASE_DONE],
39
+ [PHASE_DONE]: [],
40
+ [PHASE_ASKING]: [PHASE_PROPOSED, PHASE_IMPLEMENTING],
41
+ [PHASE_WAITING]: [PHASE_PROPOSED, PHASE_IMPLEMENTING],
42
+ });
43
+
44
+ /**
45
+ * Valida si una transición de fase es permitida.
46
+ * @param {string} from - Fase origen
47
+ * @param {string} to - Fase destino
48
+ * @returns {boolean}
49
+ */
50
+ export function canTransitionPhase(from, to) {
51
+ return (PHASE_TRANSITIONS[from] || []).includes(to);
52
+ }
53
+
54
+ // ═══════════════════════════════════════════════════════════════
55
+ // nextActionFrom — cálculo puro del próximo action
56
+ // ═══════════════════════════════════════════════════════════════
57
+
58
+ // Eliminada definición local de truncate, usando import de formatting.js
59
+
60
+ /**
61
+ * Determina la próxima acción sugerida según el estado de requirements.
62
+ * @param {Object} state - Estado de la tarea
63
+ * @returns {string}
64
+ */
65
+ export function nextActionFrom(state) {
66
+ const pending = (state?.requirements || []).find(
67
+ (r) => !["done", "verified"].includes(r.status)
68
+ );
69
+ if (pending)
70
+ return `Implementar ${truncate(pending.title)} (${pending.id} ${pending.status})`;
71
+ const unverified = (state?.requirements || []).find((r) => r.status === "done");
72
+ if (unverified)
73
+ return `Verificar ${truncate(unverified.title)} — /wam progress ${unverified.id} verified <evidencia>`;
74
+ if (state?.requirements?.length) {
75
+ // If all requirements are verified, we are done
76
+ const allVerified = (state.requirements || []).every(r => r.status === "verified");
77
+ if (allVerified) {
78
+ return "Tarea completa — contrato verificado";
79
+ }
80
+ return "Verificar requisitos completos antes de DONE";
81
+ }
82
+ return "Continuar tarea";
83
+ }
84
+
85
+ // ═══════════════════════════════════════════════════════════════
86
+ // Completion Gate — evaluación pura
87
+ // ═══════════════════════════════════════════════════════════════
88
+
89
+ /**
90
+ * Evalúa si una tarea puede completarse.
91
+ * PURE: toma state + promptText, retorna gate result sin side effects.
92
+ * @param {Object} state - Estado de la tarea
93
+ * @param {string} promptText - Texto del prompt del usuario
94
+ * @returns {{ blocked: boolean, allDone?: boolean, verifying?: boolean, pending?: string[], autoApprove?: boolean }}
95
+ */
96
+ export function evaluateCompletionGate(state, promptText) {
97
+ const lower = (promptText || "").toLowerCase().trim();
98
+
99
+ if (lower.includes("aprobar contrato") || lower.includes("continuar")) {
100
+ return { blocked: false, allDone: false, autoApprove: true };
101
+ }
102
+
103
+ const doneClaims =
104
+ /(^|\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;
105
+ if (!doneClaims.test(lower)) return { blocked: false };
106
+
107
+ // 1. Blocking unknowns
108
+ const blockingUnknowns = (state?.contract?.unknowns || []).filter(
109
+ (u) => u.status === "blocking"
110
+ );
111
+ if (blockingUnknowns.length > 0) {
112
+ return {
113
+ blocked: true,
114
+ pending: blockingUnknowns.map(
115
+ (u) => `${u.id} — ${u.question} (DECISION_CRITICAL sin responder)`
116
+ ),
117
+ };
118
+ }
119
+
120
+ // 2. Blocking assumptions
121
+ const blockingAssumptions = (state?.contract?.assumptions || []).filter(
122
+ (a) =>
123
+ a.classification === "DECISION_CRITICAL" && a.status !== "resolved"
124
+ );
125
+ if (blockingAssumptions.length > 0) {
126
+ return {
127
+ blocked: true,
128
+ pending: blockingAssumptions.map(
129
+ (a) => `${a.id} — ${a.statement} (DECISION_CRITICAL sin resolver)`
130
+ ),
131
+ };
132
+ }
133
+
134
+ // 3. Pending requirements (not done/verified, or done without evidence)
135
+ const pending = (state?.requirements || []).filter(
136
+ (r) =>
137
+ (r.status !== "done" && r.status !== "verified") ||
138
+ (r.status === "done" && !(r.evidence || []).length)
139
+ );
140
+ if (pending.length > 0) {
141
+ return {
142
+ blocked: true,
143
+ pending: pending.map((r) => {
144
+ const missingEvidence =
145
+ r.status === "done" && !(r.evidence || []).length;
146
+ return `${r.id} — ${r.title}${missingEvidence ? " (sin evidencia)" : ""}`;
147
+ }),
148
+ };
149
+ }
150
+
151
+ // 4. Failing criteria
152
+ const failingCriteria = (state?.completionContract?.criteria || []).filter(
153
+ (c) => c.status === "FAIL" || c.status === "UNKNOWN"
154
+ );
155
+ if (failingCriteria.length > 0) {
156
+ return {
157
+ blocked: true,
158
+ pending: failingCriteria.map(
159
+ (c) => `${c.id} — ${c.criterion} (${c.status})`
160
+ ),
161
+ };
162
+ }
163
+
164
+ // 5. Contract must be APPROVED
165
+ if (state?.contract?.status !== "APPROVED") {
166
+ return {
167
+ blocked: true,
168
+ pending: [
169
+ `contrato ${state.contract?.status || "PROPOSED"} — aprobar con /wam contract approve antes de DONE`,
170
+ ],
171
+ };
172
+ }
173
+
174
+ // 6. All requirements must be verified
175
+ const unverified = (state?.requirements || []).filter(
176
+ (r) => r.status !== "verified"
177
+ );
178
+ if (unverified.length > 0) {
179
+ return {
180
+ blocked: true,
181
+ verifying: true,
182
+ pending: unverified.map(
183
+ (r) => `${r.id} — ${r.title} (verificar: /wam progress ${r.id} verified <evidencia>)`
184
+ ),
185
+ };
186
+ }
187
+
188
+ // 7. Contract checks must pass
189
+ const checks = state?.contract?.checks || [];
190
+ if (checks.length > 0) {
191
+ const byReq = new Map();
192
+ for (const c of checks) {
193
+ if (!byReq.has(c.reqId)) byReq.set(c.reqId, []);
194
+ byReq.get(c.reqId).push(c);
195
+ }
196
+ const checkFailures = [];
197
+ for (const [reqId, list] of byReq) {
198
+ const verdict = evaluateRequirementChecks(list, list);
199
+ if (verdict.status !== "VERIFIED")
200
+ checkFailures.push(`${reqId}: ${verdict.reason || "checks pendientes"}`);
201
+ }
202
+ if (checkFailures.length > 0) {
203
+ return { blocked: true, verifying: true, pending: checkFailures };
204
+ }
205
+ }
206
+
207
+ return { blocked: false, allDone: true };
208
+ }
209
+
210
+ /**
211
+ * Determina la transición de fase resultante de un completion gate evaluation.
212
+ * PURE: retorna la nueva fase y nextAction sin persistir.
213
+ * @param {Object} state - Estado actual
214
+ * @param {{ blocked: boolean, allDone?: boolean, verifying?: boolean }} gate - Resultado del gate
215
+ * @returns {{ phase: string, nextAction: string }}
216
+ */
217
+ export function applyPhaseTransition(state, gate) {
218
+ if (gate.blocked) {
219
+ return {
220
+ phase: gate.verifying ? PHASE_VERIFYING : PHASE_IMPLEMENTING,
221
+ nextAction: nextActionFrom(state),
222
+ };
223
+ }
224
+ if (gate.allDone) {
225
+ return {
226
+ phase: PHASE_DONE,
227
+ nextAction: "Tarea completa — contrato verificado",
228
+ };
229
+ }
230
+ return {
231
+ phase: state?.phase || PHASE_PROPOSED,
232
+ nextAction: nextActionFrom(state),
233
+ };
234
+ }
235
+
236
+ // ═══════════════════════════════════════════════════════════════
237
+ // Contract transitions — decision logic (pure)
238
+ // ═══════════════════════════════════════════════════════════════
239
+
240
+ /**
241
+ * Lógica para aprobar un contrato (PROPOSED → APPROVED, phase → IMPLEMENTING).
242
+ * PURE: retorna los cambios a aplicar sobre el estado.
243
+ * @param {Object} state - Estado actual
244
+ * @returns {{ ok: boolean, reason?: string, status?: string, phase?: string, nextAction?: string, approvedStrategy?: Object }}
245
+ */
246
+ export function approveContractLogic(state) {
247
+ if (!state) return { ok: false, reason: "Sin estado de tarea" };
248
+
249
+ const blocking = (state.contract?.unknowns || []).filter(
250
+ (u) => u.status === "blocking"
251
+ );
252
+ if (blocking.length > 0) {
253
+ return {
254
+ ok: false,
255
+ reason: `${blocking[0].id} DECISION_CRITICAL sin responder (bloquea aprobación): ${blocking[0].question}`,
256
+ };
257
+ }
258
+
259
+ const blockingAssumptions = (state.contract?.assumptions || []).filter(
260
+ (a) =>
261
+ a.classification === "DECISION_CRITICAL" && a.status !== "resolved"
262
+ );
263
+ if (blockingAssumptions.length > 0) {
264
+ return {
265
+ ok: false,
266
+ reason: `${blockingAssumptions[0].id} DECISION_CRITICAL sin resolver (bloquea aprobación): ${blockingAssumptions[0].statement} — /wam resolve ${blockingAssumptions[0].id} <evidencia>`,
267
+ };
268
+ }
269
+
270
+ const inferredStrategy =
271
+ (state.contract?.objective ||
272
+ state.intent?.goal ||
273
+ state.requirements?.[0]?.title ||
274
+ state.lastAction?.split("\n")[0] ||
275
+ "task execution"
276
+ ).slice(0, 100);
277
+
278
+ const allowedActions = [
279
+ "write",
280
+ "edit",
281
+ "bash",
282
+ "sh",
283
+ "task",
284
+ "todowrite",
285
+ "pty_spawn",
286
+ "pty_write",
287
+ "pty_kill",
288
+ "openspec",
289
+ "openspec new change",
290
+ "openspec instructions",
291
+ "openspec validate",
292
+ "openspec archive",
293
+ "git status",
294
+ "git diff",
295
+ "git log",
296
+ "git commit",
297
+ "npm test",
298
+ "pnpm test",
299
+ "pnpm install",
300
+ "crear change OpenSpec",
301
+ "delegar via Task",
302
+ ];
303
+
304
+ const prohibitedActions = [
305
+ "delete production data",
306
+ "drop database",
307
+ "production deploy",
308
+ "credential modification",
309
+ "scope expansion",
310
+ "destructive operation",
311
+ ];
312
+
313
+ const invalidationConditions = [
314
+ "environment cannot satisfy required runtime",
315
+ "required dependency unavailable and unfixable",
316
+ "explicit user retraction",
317
+ "verified evidence contradicts strategy at architectural level",
318
+ ];
319
+
320
+ return {
321
+ ok: true,
322
+ status: "APPROVED",
323
+ phase: state.phase === PHASE_DONE ? state.phase : PHASE_IMPLEMENTING,
324
+ nextAction: nextActionFrom(state),
325
+ approvedStrategy: {
326
+ strategy: inferredStrategy,
327
+ approvedAt: Date.now(),
328
+ scope:
329
+ state.requirements?.map((r) => r.title).join(", ") || inferredStrategy,
330
+ allowedActions,
331
+ prohibitedActions,
332
+ invalidationConditions,
333
+ status: "ACTIVE",
334
+ },
335
+ };
336
+ }
337
+
338
+ /**
339
+ * Lógica para rechazar un contrato (→ REJECTED, phase → WAITING).
340
+ * PURE.
341
+ * @param {Object} state
342
+ * @returns {{ ok: boolean, status: string, phase: string, nextAction: string }}
343
+ */
344
+ export function rejectContractLogic(state) {
345
+ return {
346
+ ok: true,
347
+ status: "REJECTED",
348
+ phase: PHASE_WAITING,
349
+ nextAction: "Revisar contrato con el usuario",
350
+ };
351
+ }
352
+
353
+ /**
354
+ * Lógica para editar un contrato (→ vuelve a PROPOSED).
355
+ * PURE.
356
+ * @param {Object} state
357
+ * @returns {{ ok: boolean, status: string, phase: string, nextAction: string }}
358
+ */
359
+ export function editContractLogic(state) {
360
+ return {
361
+ ok: true,
362
+ status: "PROPOSED",
363
+ phase: PHASE_PROPOSED,
364
+ nextAction: "Revisar contrato — /wam contract approve o edit",
365
+ };
366
+ }
367
+
368
+ // ═══════════════════════════════════════════════════════════════
369
+ // markRequirement — decision logic (pure)
370
+ // ═══════════════════════════════════════════════════════════════
371
+
372
+ /**
373
+ * Lógica para marcar un requisito como done/pending/verified.
374
+ * PURE: valida y retorna cambios al estado. No persiste.
375
+ * @param {Object} state - Estado actual
376
+ * @param {string} reqId - ID del requisito
377
+ * @param {"done"|"pending"|"verified"} status - Nuevo estado
378
+ * @param {string} evidence - Evidencia
379
+ * @returns {{ ok: boolean, reason?: string, phase?: string, nextAction?: string }}
380
+ */
381
+ export function markRequirementLogic(state, reqId, status, evidence) {
382
+ if (!state) return { ok: false, reason: "Sin estado de tarea" };
383
+
384
+ const req = (state.requirements || []).find((r) => r.id === reqId);
385
+ if (!req) return { ok: false, reason: `Requisito ${reqId} no existe` };
386
+
387
+ if (
388
+ (status === "done" || status === "verified") &&
389
+ !(evidence && evidence.trim())
390
+ ) {
391
+ return {
392
+ ok: false,
393
+ reason: `Requisito ${reqId}: evidencia requerida para marcar ${status}`,
394
+ };
395
+ }
396
+
397
+ if (status === "verified" && req.status !== "done") {
398
+ return {
399
+ ok: false,
400
+ reason: `Requisito ${reqId}: marcar done antes de verified`,
401
+ };
402
+ }
403
+
404
+ // Aplicar cambios al estado (retornar nuevo estado, no mutar)
405
+ let newPhase = state.phase;
406
+ let newNextAction = nextActionFrom(state);
407
+
408
+ // Si todos los requirements están verified → auto-advance a VERIFYING
409
+ if (status === "verified") {
410
+ const allVerified = (state.requirements || []).every(
411
+ (r) => r.status === "verified"
412
+ );
413
+ if (allVerified && state.phase !== PHASE_DONE) {
414
+ newPhase = PHASE_VERIFYING;
415
+ }
416
+ }
417
+
418
+ return { ok: true, phase: newPhase, nextAction: newNextAction };
419
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Provenance tracking for synthetic parts injected by WaitAMinute.
3
+ * Prevents re-entrancy and self-consumption bugs.
4
+ */
5
+
6
+ export const WAM_META = "wam";
7
+
8
+ /**
9
+ * Tags a part as synthetic and injected by WAM.
10
+ * @param {Object} part
11
+ * @param {Object} [options]
12
+ * @param {string} [options.messageID]
13
+ * @param {string} [options.phase]
14
+ * @param {Function} [options.idFactory]
15
+ */
16
+ export function tagWamPart(part, { messageID, phase, idFactory } = {}) {
17
+ const tagged = {
18
+ ...part,
19
+ synthetic: true,
20
+ metadata: {
21
+ ...(part.metadata || {}),
22
+ [WAM_META]: true,
23
+ wamMessageId: messageID ?? null,
24
+ wamPhase: phase ?? null,
25
+ },
26
+ };
27
+ if (idFactory && !tagged.id) {
28
+ tagged.id = idFactory();
29
+ }
30
+ return tagged;
31
+ }
32
+
33
+ /**
34
+ * Checks if a part was injected by WAM.
35
+ */
36
+ export function isWamSynthetic(part) {
37
+ return !!part && part.synthetic === true && part?.metadata?.[WAM_META] === true;
38
+ }
39
+
40
+ /**
41
+ * Checks if a collection of parts contains a marker for a specific messageID.
42
+ */
43
+ export function hasWamMarkerFor(parts, messageID) {
44
+ return Array.isArray(parts) && parts.some(p => isWamSynthetic(p) && (messageID == null || p.metadata?.wamMessageId === messageID));
45
+ }
46
+
47
+ /**
48
+ * Injects WAM parts into a target sink (parts array or system array).
49
+ */
50
+ export function injectWamParts(target, textsOrParts, { messageID, phase, idFactory, position = "prepend" } = {}) {
51
+ const sink = Array.isArray(target.parts) ? target.parts : (Array.isArray(target.system) ? target.system : null);
52
+ if (!sink) return;
53
+
54
+ const list = Array.isArray(textsOrParts) ? textsOrParts : [textsOrParts];
55
+ const parts = list.map(item => {
56
+ const part = typeof item === "string" ? { type: "text", text: item } : item;
57
+ return tagWamPart(part, { messageID, phase, idFactory });
58
+ });
59
+
60
+ if (position === "append") {
61
+ sink.push(...parts);
62
+ } else {
63
+ sink.unshift(...parts);
64
+ }
65
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Rate Limiter — fixed-window counter.
3
+ *
4
+ * Limits calls to N per windowMs milliseconds. Returns a function that
5
+ * records a call and indicates if it's allowed. Production hardening (M5-54).
6
+ */
7
+
8
+ /**
9
+ * @typedef {Object} RateLimiter
10
+ * @property {() => boolean} tryAcquire
11
+ * @property {() => number} remaining
12
+ * @property {() => number} used
13
+ * @property {() => void} reset
14
+ */
15
+
16
+ /**
17
+ * @param {{limit?: number, windowMs?: number}} opts
18
+ * @returns {RateLimiter}
19
+ */
20
+ export function createRateLimiter(opts = {}) {
21
+ const limit = opts.limit ?? 5;
22
+ const windowMs = opts.windowMs ?? 1000;
23
+ let windowStart = Date.now();
24
+ let count = 0;
25
+
26
+ function tryAcquire() {
27
+ const now = Date.now();
28
+ if (now - windowStart >= windowMs) {
29
+ windowStart = now;
30
+ count = 0;
31
+ }
32
+ if (count >= limit) return false;
33
+ count++;
34
+ return true;
35
+ }
36
+
37
+ function remaining() {
38
+ const now = Date.now();
39
+ if (now - windowStart >= windowMs) return limit;
40
+ return Math.max(0, limit - count);
41
+ }
42
+
43
+ function used() {
44
+ const now = Date.now();
45
+ if (now - windowStart >= windowMs) return 0;
46
+ return count;
47
+ }
48
+
49
+ function reset() {
50
+ windowStart = Date.now();
51
+ count = 0;
52
+ }
53
+
54
+ return { tryAcquire, remaining, used, reset };
55
+ }