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
@@ -0,0 +1,501 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { cavemanify } from "../skills/engine.js";
4
+
5
+ const StorageProvider = {
6
+ read: (p) => fs.readFileSync(p, "utf-8"),
7
+ write: (p, data) => { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, data, "utf-8"); },
8
+ exists: (p) => fs.existsSync(p),
9
+ readdir: (p) => fs.readdirSync(p),
10
+ rm: (p, opts) => fs.rmSync(p, opts)
11
+ };
12
+
13
+ // -- Operational Memory for Wait a Minute (WAM) ----------------------------
14
+ // Memoria operacional persistente, legible por humanos, en .wam/.
15
+ // Conocimiento DERIVADO: no es fuente de verdad (spec operational-memory).
16
+ // Task state canonical: .wam/tasks/<id>/state.yaml (engine.js) — este módulo
17
+ // deriva artifacts humanos (evidence.md/summary.md) sin reemplazarlo.
18
+
19
+ const CONTEXT_FILES = {
20
+ project: "project.md",
21
+ architecture: "architecture.md",
22
+ recentChanges: "recent-changes.md",
23
+ decisions: "decisions.md",
24
+ constraints: "constraints.md",
25
+ };
26
+
27
+ const WAM_DIRS = ["context", "tasks", "skills", "cache", "history"];
28
+
29
+ const WAM_GITIGNORE = `# .wam — memoria operacional (spec §17, versionado configurable)
30
+ # Candidato a versionar (conocimiento operacional estable):
31
+ # context/project.md
32
+ # context/architecture.md
33
+ # context/decisions.md
34
+ # context/constraints.md
35
+ # Normalmente regenerable / runtime (ignorar):
36
+ cache/
37
+ tasks/
38
+ history/
39
+ skills/
40
+ .engram/
41
+ `;
42
+
43
+ const SECRET_PATTERNS = [
44
+ /(api[_-]?key|apikey|token|secret|password|passwd|bearer|authorization|auth)\s*[:=]\s*["']?[A-Za-z0-9_\-./]{8,}["']?/gi,
45
+ /\b(sk|pk|ghp|github_pat)_[A-Za-z0-9_\-]{12,}\b/g,
46
+ ];
47
+
48
+ const VALID_SOURCES = new Set(["observed", "inferred", "user-decided"]);
49
+
50
+ const CONFIDENCE_STRING_TO_NUM = { high: 0.9, medium: 0.5, low: 0.2 };
51
+
52
+ export function normalizeConfidence(v) {
53
+ if (typeof v === "number" && Number.isFinite(v)) return Math.max(0, Math.min(1, v));
54
+ if (typeof v === "string") {
55
+ const trimmed = v.trim();
56
+ if (trimmed !== "" && !isNaN(Number(trimmed))) {
57
+ return Math.max(0, Math.min(1, Number(trimmed)));
58
+ }
59
+ const n = CONFIDENCE_STRING_TO_NUM[trimmed.toLowerCase()];
60
+ return n != null ? n : 0.5;
61
+ }
62
+ return 0.5;
63
+ }
64
+
65
+ export function confidenceLabel(n) {
66
+ const x = normalizeConfidence(n);
67
+ return x >= 0.7 ? "high" : x >= 0.4 ? "medium" : "low";
68
+ }
69
+
70
+ // -- helpers -----------------------------------------------------------------
71
+
72
+ function rootDir(root) {
73
+ return root || process.cwd();
74
+ }
75
+
76
+ function contextDir(root) {
77
+ return path.join(rootDir(root), ".wam", "context");
78
+ }
79
+
80
+ function nowIso() {
81
+ return new Date().toISOString();
82
+ }
83
+
84
+ export function redact(text = "") {
85
+ if (!text) return text;
86
+ let out = String(text);
87
+ for (const re of SECRET_PATTERNS) {
88
+ out = out.replace(re, (m) => {
89
+ const sep = m.search(/[:=]/);
90
+ if (sep === -1) return "<redacted>";
91
+ return m.slice(0, sep + 1) + " <redacted>";
92
+ });
93
+ }
94
+ return out;
95
+ }
96
+
97
+ function splitFrontmatter(content) {
98
+ const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(content || "");
99
+ if (!m) return { meta: {}, body: content || "" };
100
+ const meta = {};
101
+ for (const line of m[1].split(/\r?\n/)) {
102
+ const i = line.indexOf(":");
103
+ if (i === -1) continue;
104
+ const k = line.slice(0, i).trim();
105
+ let v = line.slice(i + 1).trim();
106
+ if (
107
+ v.length >= 2 &&
108
+ ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")))
109
+ ) {
110
+ v = v.slice(1, -1);
111
+ }
112
+ meta[k] = v;
113
+ }
114
+ return { meta, body: m[2] || "" };
115
+ }
116
+
117
+ function frontmatter(meta) {
118
+ const keys = ["source", "confidence", "last_verified", "status"];
119
+ const lines = ["---"];
120
+ for (const k of keys) {
121
+ const v = meta[k];
122
+ if (v === undefined || v === null || v === "") continue;
123
+ lines.push(`${k}: ${String(v).includes(" ") ? `"${v}"` : v}`);
124
+ }
125
+ lines.push("---");
126
+ return lines.join("\n");
127
+ }
128
+
129
+ function resolveDoc(doc) {
130
+ if (CONTEXT_FILES[doc]) return CONTEXT_FILES[doc];
131
+ if (typeof doc === "string" && doc.endsWith(".md")) return doc;
132
+ return `${doc}.md`;
133
+ }
134
+
135
+ function readDoc(doc, root) {
136
+ const file = path.join(contextDir(root), resolveDoc(doc));
137
+ if (!StorageProvider.exists(file)) return { file, meta: {}, body: "" };
138
+ return { file, ...splitFrontmatter(StorageProvider.read(file)) };
139
+ }
140
+
141
+ function sectionsOf(body) {
142
+ return (body || "")
143
+ .split("\n## ")
144
+ .map((p, i) => (i === 0 ? p : `## ${p}`))
145
+ .filter((p) => p.trim());
146
+ }
147
+
148
+ function firstLine(s) {
149
+ // primera línea INFORMATIVA: salta headings (#/##) y líneas vacías
150
+ const m = (s || "").split("\n").find((l) => l.trim() && !/^#+\s/.test(l.trim()));
151
+ return m ? m.trim() : "";
152
+ }
153
+
154
+ // -- init (espec §3: solo estructura, sin contenido ficticio, idempotente) ---
155
+
156
+ export function initMemory(root) {
157
+ const wam = path.join(rootDir(root), ".wam");
158
+ const created = !StorageProvider.exists(wam);
159
+ if (created) {
160
+ for (const d of WAM_DIRS) {
161
+ if (d !== "context") {
162
+ StorageProvider.write(path.join(wam, d, ".gitkeep"), "");
163
+ } else {
164
+ fs.mkdirSync(path.join(wam, d), { recursive: true });
165
+ }
166
+ }
167
+ const gi = path.join(wam, ".gitignore");
168
+ if (!StorageProvider.exists(gi)) StorageProvider.write(gi, WAM_GITIGNORE);
169
+ }
170
+ return created;
171
+ }
172
+
173
+ // -- context docs (espec §4-§8, §15) ----------------------------------------
174
+
175
+ export function updateContext(doc, body, metadata = {}, root) {
176
+ const meta = {
177
+ source: VALID_SOURCES.has(metadata.source) ? metadata.source : "observed",
178
+ confidence: normalizeConfidence(metadata.confidence).toFixed(2),
179
+ last_verified: metadata.last_verified || nowIso(),
180
+ status: metadata.status || "current",
181
+ };
182
+ const file = path.join(contextDir(root), resolveDoc(doc));
183
+ StorageProvider.write(file, frontmatter(meta) + "\n" + redact(String(body)).trim() + "\n");
184
+ return { ok: true, file };
185
+ }
186
+
187
+ export function markStale(doc, { lastVerified } = {}, root) {
188
+ const { file, meta, body } = readDoc(doc, root);
189
+ if (!StorageProvider.exists(file)) return { ok: false, reason: "doc no existe" };
190
+ meta.status = "stale";
191
+ meta.last_verified = lastVerified || new Date(Date.now() - 30 * 86400000).toISOString();
192
+ StorageProvider.write(file, frontmatter(meta) + "\n" + body.trim() + "\n");
193
+ return { ok: true, file, status: "stale" };
194
+ }
195
+
196
+ export function addRecentChange({ date, scope, changes = [], verification = "" } = {}, root) {
197
+ if (!date) return { ok: false, reason: "date requerido" };
198
+ const { body } = readDoc("recentChanges", root);
199
+ const heading = `## ${date}${scope ? ` — ${scope}` : ""}`;
200
+ if ((body || "").includes(heading)) return { ok: true, skipped: true };
201
+ const section = [
202
+ heading,
203
+ ...changes.map((c) => `- ${c}`),
204
+ ...(verification ? ["", `Verification: ${verification}`] : []),
205
+ ].join("\n");
206
+ const sections = sectionsOf(body);
207
+ const header =
208
+ sections.length && sections[0].trim() === "# Recent Changes"
209
+ ? sections[0].trim()
210
+ : "# Recent Changes";
211
+ const rest = sections.slice(1);
212
+ const all = [header, section, ...rest];
213
+ return updateContext(
214
+ "recentChanges",
215
+ all.join("\n\n"),
216
+ {
217
+ source: "observed",
218
+ confidence: 0.9,
219
+ last_verified: /^\d{4}-\d{2}-\d{2}/.test(date)
220
+ ? `${date}T00:00:00.000Z`
221
+ : nowIso(),
222
+ },
223
+ root
224
+ );
225
+ }
226
+
227
+ function parseDecisionEntries(body) {
228
+ const list = [];
229
+ for (const p of sectionsOf(body)) {
230
+ const idm = /^##\s+(.+)$/m.exec(p);
231
+ if (!idm) continue;
232
+ const datem = /^- date:\s*(.+)$/m.exec(p);
233
+ const statusm = /^- status:\s*(.+)$/m.exec(p);
234
+ const srcm = /^- source:\s*(.+)$/m.exec(p);
235
+ const confm = /^- confidence:\s*(.+)$/m.exec(p);
236
+ const decisionm = /^- decision:\s*(.+)$/m.exec(p);
237
+ const reasonm = /^- reason:\s*(.+)$/m.exec(p);
238
+ const id = idm[1].trim();
239
+ list.push({
240
+ id,
241
+ date: datem ? datem[1].trim() : undefined,
242
+ status: statusm ? statusm[1].trim() : undefined,
243
+ source: srcm ? srcm[1].trim() : "inferred",
244
+ confidence: confm ? confm[1].trim() : undefined,
245
+ decision: decisionm ? decisionm[1].trim() : undefined,
246
+ reason: reasonm ? reasonm[1].trim() : undefined,
247
+ raw: p,
248
+ });
249
+ }
250
+ return list;
251
+ }
252
+
253
+ function renderDecisionEntry(d) {
254
+ return [
255
+ `## ${d.id}`,
256
+ `- date: ${d.date}`,
257
+ `- status: ${d.status}`,
258
+ `- source: ${d.source}`,
259
+ `- confidence: ${d.confidence}`,
260
+ `- decision: ${d.decision}`,
261
+ ...(d.reason ? [`- reason: ${d.reason}`] : []),
262
+ ].join("\n");
263
+ }
264
+
265
+ export function getDecision(id, root) {
266
+ const { body } = readDoc("decisions", root);
267
+ const entries = parseDecisionEntries(body);
268
+ return entries.find((e) => e.id === id);
269
+ }
270
+
271
+ // espe §7 + §10: preserva user-decided ante inferencias de menor autoridad
272
+ export function recordDecision(entry = {}, root) {
273
+ const { id, decision } = entry;
274
+ if (!id || !decision) return { ok: false, reason: "id y decision requeridos" };
275
+ const source = VALID_SOURCES.has(entry.source) ? entry.source : "inferred";
276
+ const { body } = readDoc("decisions", root);
277
+ const entries = parseDecisionEntries(body);
278
+ const prior = entries.find((e) => e.id === id);
279
+ if (prior && prior.source === "user-decided" && source !== "user-decided") {
280
+ return { ok: true, preserved: "user-decided", conflicting: true };
281
+ }
282
+ const cleaned = entries.filter((e) => e.id !== id).map((e) => e.raw);
283
+ const rendered = renderDecisionEntry({
284
+ id,
285
+ date: entry.date || nowIso().slice(0, 10),
286
+ status: entry.status || "accepted",
287
+ source,
288
+ confidence: normalizeConfidence(entry.confidence).toFixed(2),
289
+ decision,
290
+ reason: entry.reason || "",
291
+ });
292
+ const newBody = ["# Decisions", ...cleaned, rendered].join("\n\n");
293
+ return updateContext(
294
+ "decisions",
295
+ newBody,
296
+ { source, confidence: normalizeConfidence(entry.confidence) },
297
+ root
298
+ );
299
+ }
300
+
301
+ export function addConstraint(text, { source = "inferred", confidence = 0.5, lastVerified } = {}, root) {
302
+ if (!text) return { ok: false, reason: "text requerido" };
303
+ const safe = redact(String(text).trim());
304
+ const { body } = readDoc("constraints", root);
305
+ const lines = (body || "").split("\n").filter((l) => l.trim());
306
+ const norm = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
307
+ const existing = lines.filter((l) => l.startsWith("- ")).map((l) => l.replace(/^\-\s+/, ""));
308
+ const dup = existing.find((l) => norm(l.replace(/\s*\[source:.*$/, "")) === norm(safe));
309
+ if (dup) {
310
+ if (/source:\s*user-decided/.test(dup) && source !== "user-decided") {
311
+ return { ok: true, preserved: "user-decided", conflicting: true };
312
+ }
313
+ return { ok: true, skipped: true };
314
+ }
315
+ const confNum = normalizeConfidence(confidence);
316
+ const entry = `- ${safe} [source: ${source} | confidence: ${confNum.toFixed(2)} | last_verified: ${lastVerified || nowIso()}]`;
317
+ const newBody = ["# Constraints", ...lines, entry].join("\n");
318
+ return updateContext("constraints", newBody, { source, confidence: confNum }, root);
319
+ }
320
+
321
+ /**
322
+ * Compacts decisions.md by removing excessive blank lines between entries.
323
+ * Call periodically or after recordDecision to keep file size manageable.
324
+ */
325
+ export function compactDecisions(root) {
326
+ const { body } = readDoc("decisions", root);
327
+ if (!body) return { ok: false, reason: "no decisions file" };
328
+
329
+ const lines = body.split("\n");
330
+ const compacted = [];
331
+ let prevBlank = false;
332
+
333
+ for (const line of lines) {
334
+ const isBlank = line.trim() === "";
335
+ if (isBlank && prevBlank) continue;
336
+ compacted.push(line);
337
+ prevBlank = isBlank;
338
+ }
339
+
340
+ const newBody = compacted.join("\n").replace(/\n{3,}/g, "\n\n");
341
+ return updateContext("decisions", newBody, { source: "compaction", confidence: 1.0 }, root);
342
+ }
343
+
344
+ // -- task memory (espec §14, derivado de state.yaml — no duplica la fuente) --
345
+
346
+ export function updateTaskMemory(taskId, { evidence = "", summary = "" } = {}, root) {
347
+ if (!taskId) return { ok: false, reason: "taskId requerido" };
348
+ const dir = path.join(rootDir(root), ".wam", "tasks", taskId);
349
+ StorageProvider.write(path.join(dir, ".gitkeep"), "");
350
+ if (evidence) StorageProvider.write(path.join(dir, "evidence.md"), redact(evidence).trim() + "\n");
351
+ if (summary) {
352
+ StorageProvider.write(path.join(dir, "summary.md"), redact(summary).trim() + "\n");
353
+ StorageProvider.write(path.join(dir, "caveman-summary.md"), cavemanify(redact(summary)).trim() + "\n");
354
+ }
355
+ return { ok: true, dir };
356
+ }
357
+
358
+ // -- contexto vivo (auto-adaptativo a la tarea en ejecución) ----------------
359
+
360
+ /**
361
+ * Snapshot vivo de la tarea activa en .wam/tasks/<taskId>/context.md.
362
+ * Se actualiza en cada mensaje (chat.message) y se sobreescribe al cambiar
363
+ * de tarea — el contexto siempre refleja lo que el agente está ejecutando.
364
+ * Taxonomía WAM: artifacts de ejecución viven bajo tasks/<id>/, no bajo
365
+ * context/. context/ se reserva para conocimiento operacional estable
366
+ * (project/architecture/decisions/constraints).
367
+ */
368
+ export function updateLiveContext(taskId, state = {}, root) {
369
+ const dir = path.join(rootDir(root), ".wam", "tasks", taskId);
370
+ const reqs = state.requirements || [];
371
+ const pend = reqs.filter((r) => r.status !== "done" && r.status !== "verified").length;
372
+ const unverified = reqs.filter((r) => r.status === "done").length;
373
+ const unknowns = (state.unknowns || []).filter((u) => u.status === "blocking");
374
+ const body = [
375
+ "# Live Task Context",
376
+ `task: ${taskId} — ${state.phase || "?"} / ${state.contract?.status || "?"}`,
377
+ `req: ${pend} pend${unverified ? ` | ${unverified} sin verificar` : ""} de ${reqs.length}`,
378
+ `next: ${state.nextAction || "—"}`,
379
+ ...(unknowns.length ? [`blocking: ${unknowns.map((u) => `${u.id} ${u.question || u.statement}`).join(" | ")}`] : []),
380
+ ].join("\n");
381
+ StorageProvider.write(path.join(dir, "context.md"), body + "\n");
382
+ return { ok: true };
383
+ }
384
+
385
+ // -- recuperación de contexto (espec §13) -----------------------------------
386
+
387
+ const SUBPROJ_SKIP = new Set(["node_modules", ".git", ".wam", "upstream", ".cache", "dist", "build", "coverage", ".next", "vendor"]);
388
+
389
+ /**
390
+ * Detecta subproyectos (repos git hijos) con package.json bajo el root.
391
+ * Útil cuando el root es multi-repo sin stack propio (ej. comparador-precios
392
+ * con backend/frontend/scraper) — la raíz no tiene package.json pero los
393
+ * repos hijos sí. Cada repo se reporta con su lenguaje dominante.
394
+ */
395
+ function detectSubprojects(root, maxDepth = 2) {
396
+ const out = [];
397
+ const seen = new Set();
398
+ const stackOf = (pkg) => {
399
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
400
+ const langs = [];
401
+ if (deps.typescript) langs.push("typescript");
402
+ if (deps.react || deps["react-dom"] || deps.next) langs.push("react");
403
+ if (deps["@nestjs/core"] || deps.nestjs) langs.push("nestjs");
404
+ if (deps.express) langs.push("express");
405
+ if (deps.vue) langs.push("vue");
406
+ if (deps.svelte) langs.push("svelte");
407
+ if (deps.python || deps.flask || deps.django) langs.push("python");
408
+ return langs;
409
+ };
410
+ const walk = (dir, depth) => {
411
+ if (depth > maxDepth) return;
412
+ let entries;
413
+ try {
414
+ entries = fs.readdirSync(dir, { withFileTypes: true });
415
+ } catch {
416
+ return;
417
+ }
418
+ for (const e of entries) {
419
+ if (!e.isDirectory() || SUBPROJ_SKIP.has(e.name)) continue;
420
+ const p = path.join(dir, e.name);
421
+ if (seen.has(p)) continue;
422
+ seen.add(p);
423
+ try {
424
+ if (fs.existsSync(path.join(p, ".git"))) {
425
+ const pkgPath = path.join(p, "package.json");
426
+ if (fs.existsSync(pkgPath)) {
427
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
428
+ const langs = stackOf(pkg);
429
+ out.push(`Subproyecto ${path.basename(p)} (${pkg.name || path.basename(p)}${langs.length ? `: ${langs.join("+")}` : ""})`);
430
+ } else {
431
+ out.push(`Subproyecto ${path.basename(p)} (repo git sin package.json)`);
432
+ }
433
+ continue; // no descender dentro de repos hijos
434
+ }
435
+ walk(p, depth + 1);
436
+ } catch {}
437
+ }
438
+ };
439
+ // El propio root: si es repo git con package.json, reportarlo como Proyecto
440
+ try {
441
+ const pkgPath = path.join(root, "package.json");
442
+ if (fs.existsSync(pkgPath)) {
443
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
444
+ const langs = stackOf(pkg);
445
+ out.push(`Proyecto ${path.basename(root)} (${pkg.name || path.basename(root)}${langs.length ? `: ${langs.join("+")}` : ""})`);
446
+ seen.add(root);
447
+ }
448
+ } catch {}
449
+ walk(root, 1);
450
+ return out;
451
+ }
452
+
453
+ export function updateProjectMemo(analysis, root) {
454
+ const pi = analysis?.project || analysis?.projectInfo;
455
+ const known = [];
456
+ if (pi) {
457
+ if (pi.detected_stack && pi.detected_stack !== "unknown") known.push(`Stack: ${pi.detected_stack}`);
458
+ if (pi.architecture && pi.architecture !== "unknown") known.push(`Arquitectura: ${pi.architecture}`);
459
+ for (const f of pi.relevant_files || []) known.push(`Artefacto: ${f}`);
460
+ }
461
+ const inferred = pi?.inferred || [];
462
+ const assumed = pi?.assumed || [];
463
+ // Raíz multi-repo sin stack propio → detectar subproyectos observados
464
+ if (!known.length && root) {
465
+ known.push(...detectSubprojects(root));
466
+ }
467
+ if (!known.length && !inferred.length && !assumed.length) return;
468
+ const body = [
469
+ "# Project Context",
470
+ "",
471
+ ...(known.length ? ["## Stack (observed)", ...known.map((k) => `- ${k}`)] : []),
472
+ ...(inferred.length ? ["", "## Inferred", ...inferred.map((i) => `- ${i}`)] : []),
473
+ ...(assumed.length ? ["", "## Assumed", ...assumed.map((a) => `- ${a}`)] : []),
474
+ ].join("\n");
475
+ const { project } = getOperationalContext(root);
476
+ if (project.body.trim() === body.trim()) return;
477
+ updateContext("project", body, { source: "observed", confidence: inferred.length ? 0.5 : 0.9 }, root);
478
+ }
479
+
480
+ export function getOperationalContext(root) {
481
+ const ctx = {};
482
+ for (const [key, name] of Object.entries(CONTEXT_FILES)) {
483
+ const { meta, body } = readDoc(key, root);
484
+ ctx[key] = { name, meta, body };
485
+ }
486
+ return ctx;
487
+ }
488
+
489
+ export function summarizeOperationalContext(root) {
490
+ const ctx = getOperationalContext(root);
491
+ const lines = [];
492
+ const projectFirst = firstLine(ctx.project.body);
493
+ if (projectFirst) lines.push(`Proyecto: ${projectFirst.slice(0, 100)}`);
494
+ const rc = (ctx.recentChanges.body.match(/^##\s/gm) || []).length;
495
+ if (rc) lines.push(`Cambios recientes: ${rc} sección(es)`);
496
+ const dec = (ctx.decisions.body.match(/^##\s/gm) || []).length;
497
+ if (dec) lines.push(`Decisiones: ${dec}`);
498
+ const cons = (ctx.constraints.body.match(/^- /gm) || []).length;
499
+ if (cons) lines.push(`Restricciones: ${cons}`);
500
+ return lines.length ? lines.join(" | ") : "";
501
+ }
@@ -0,0 +1,16 @@
1
+ export function createMigrationRunner(migrations) {
2
+ return {
3
+ run: (from, to) => {
4
+ const applied = [];
5
+ const skipped = [];
6
+ for (const m of migrations) {
7
+ if (m.version > from && m.version <= to) {
8
+ applied.push(m.version);
9
+ } else {
10
+ skipped.push(m.version);
11
+ }
12
+ }
13
+ return { applied, skipped };
14
+ }
15
+ };
16
+ }
@@ -0,0 +1,39 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+
4
+ export const PersistenceManager = {
5
+ getCanonicalPath(type, taskId = null, filename = null) {
6
+ const root = process.cwd();
7
+ let base = path.join(root, ".wam");
8
+
9
+ switch (type) {
10
+ case 'context':
11
+ base = path.join(base, 'context');
12
+ break;
13
+ case 'task':
14
+ base = path.join(base, 'tasks', taskId);
15
+ break;
16
+ case 'traces':
17
+ base = path.join(base, 'traces');
18
+ break;
19
+ case 'history':
20
+ base = path.join(base, 'history');
21
+ break;
22
+ default:
23
+ throw new Error(`Tipo de persistencia no reconocido: ${type}`);
24
+ }
25
+
26
+ if (!fs.existsSync(base)) fs.mkdirSync(base, { recursive: true });
27
+
28
+ if (filename === 'task-context.md') filename = 'context.md';
29
+
30
+ return filename ? path.join(base, filename) : base;
31
+ },
32
+
33
+ cleanLegacy(filePath) {
34
+ if (fs.existsSync(filePath)) {
35
+ fs.unlinkSync(filePath);
36
+ console.log(`[PersistenceManager] Hard-deleted legacy file: ${filePath}`);
37
+ }
38
+ }
39
+ };