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.
- package/LICENSE +21 -0
- package/README.md +318 -2
- package/index.js +1836 -0
- package/package.json +70 -4
- package/skills/codebase-design/SKILL.md +114 -0
- package/skills/design-contract/SKILL.md +27 -0
- package/skills/design-critique/SKILL.md +19 -0
- package/skills/design-system/SKILL.md +17 -0
- package/skills/design-system/references/image-analysis.md +14 -0
- package/skills/design-system/references/tokens.md +32 -0
- package/skills/design-system/references/ui-rules.md +14 -0
- package/skills/diagnosing-bugs/SKILL.md +138 -0
- package/skills/frontend-design/SKILL.md +36 -0
- package/skills/handoff/SKILL.md +16 -0
- package/skills/improve-codebase-architecture/SKILL.md +71 -0
- package/skills/linter-negative/SKILL.md +74 -0
- package/skills/no-slop-ui/SKILL.md +26 -0
- package/skills/prototype/SKILL.md +26 -0
- package/skills/registry.json +44087 -0
- package/skills/tastemaker/SKILL.md +8 -0
- package/skills/tastemaker/references/tastemaker.md +14 -0
- package/skills/tdd/SKILL.md +38 -0
- package/skills/visual-review/SKILL.md +103 -0
- package/skills/visual-review/references/review-gates.md +42 -0
- package/skills/visual-review/references/surface-archetypes.md +37 -0
- package/skills/wizard/SKILL.md +44 -0
- package/skills/writing-for-agents/SKILL.md +81 -0
- package/src/cognition/cognition-store.js +251 -0
- package/src/cognition/cognitive-state.js +126 -0
- package/src/cognition/hypothesis-manager.js +113 -0
- package/src/cognition/observation-engine.js +112 -0
- package/src/cognition/observation-provenance.js +268 -0
- package/src/context/active-context-boundary.js +68 -0
- package/src/context/assembly.js +494 -0
- package/src/context/assessment-rules.js +55 -0
- package/src/context/capsule-staleness.js +267 -0
- package/src/context/capsules.js +2 -0
- package/src/context/compare-array.js +98 -0
- package/src/context/compare-object.js +89 -0
- package/src/context/compare-values.js +42 -0
- package/src/context/context-assembly-contract.js +71 -0
- package/src/context/context-budget-manager.js +232 -0
- package/src/context/context-builder.js +49 -0
- package/src/context/context-cache.js +55 -0
- package/src/context/context-capture.js +243 -0
- package/src/context/context-classification.js +199 -0
- package/src/context/context-compaction.js +179 -0
- package/src/context/context-decision-audit.js +267 -0
- package/src/context/context-demotion.js +173 -0
- package/src/context/context-dictionary.js +106 -0
- package/src/context/context-drift-detection.js +225 -0
- package/src/context/context-evaluation.js +374 -0
- package/src/context/context-event-ingress.js +100 -0
- package/src/context/context-event-normalization.js +224 -0
- package/src/context/context-feedback-loop.js +216 -0
- package/src/context/context-freshness.js +21 -0
- package/src/context/context-graph-builder.js +71 -0
- package/src/context/context-graph.js +451 -0
- package/src/context/context-history-relevance.js +321 -0
- package/src/context/context-interception.js +145 -0
- package/src/context/context-invalidation-events.js +154 -0
- package/src/context/context-lifecycle.js +170 -0
- package/src/context/context-loop-prevention.js +347 -0
- package/src/context/context-manager.js +340 -0
- package/src/context/context-memory-layers.js +265 -0
- package/src/context/context-minimality.js +166 -0
- package/src/context/context-normalization.js +130 -0
- package/src/context/context-optimization-metrics.js +434 -0
- package/src/context/context-output-policy.js +135 -0
- package/src/context/context-pack-authority.js +157 -0
- package/src/context/context-promotion.js +172 -0
- package/src/context/context-query-contract.js +101 -0
- package/src/context/context-ranking.js +164 -0
- package/src/context/context-rate-distortion.js +261 -0
- package/src/context/context-recovery.js +209 -0
- package/src/context/context-retrieval-integration.js +60 -0
- package/src/context/context-retrieval.js +93 -0
- package/src/context/context-router.js +457 -0
- package/src/context/context-routing-evaluation.js +331 -0
- package/src/context/context-routing-observability.js +296 -0
- package/src/context/context-routing-shadow.js +237 -0
- package/src/context/context-scope.js +181 -0
- package/src/context/context-snapshot.js +226 -0
- package/src/context/context-source-registry.js +95 -0
- package/src/context/context-sufficiency-gate.js +120 -0
- package/src/context/context-sufficiency-oracle.js +167 -0
- package/src/context/context-telemetry.js +176 -0
- package/src/context/context.js +633 -0
- package/src/context/dependency-closure.js +246 -0
- package/src/context/page-fault-tracker.js +275 -0
- package/src/context/prompt-engine.js +74 -0
- package/src/context/runtime-context-graph.js +260 -0
- package/src/context/selection.js +2 -0
- package/src/context/session.js +2 -0
- package/src/context/sufficiency-contract.js +300 -0
- package/src/context/tokenizer.js +17 -0
- package/src/context-benchmark-router.mjs +125 -0
- package/src/evidence/evidence-engine.js +161 -0
- package/src/evidence/evidence-factory.js +3 -0
- package/src/evidence/evidence-freshness.js +463 -0
- package/src/evidence/evidence-gap.js +76 -0
- package/src/evidence/evidence-lineage.js +525 -0
- package/src/evidence/evidence.js +136 -0
- package/src/evidence/freshness-validation.js +187 -0
- package/src/evidence/lineage.js +4 -0
- package/src/execution/action-evaluation.js +179 -0
- package/src/execution/circuit-breaker.js +70 -0
- package/src/execution/claim-interception.js +63 -0
- package/src/execution/execution-engine.js +258 -0
- package/src/execution/execution-intent.js +69 -0
- package/src/execution/execution-state.js +89 -0
- package/src/execution/formatting.js +60 -0
- package/src/execution/l1-sanitize.js +16 -0
- package/src/execution/l2-resource-guards.js +19 -0
- package/src/execution/l3-invariant-checker.js +5 -0
- package/src/execution/l4-containment.js +6 -0
- package/src/execution/runtime-guards.js +106 -0
- package/src/execution/strategy-identity.js +162 -0
- package/src/execution/tool-interceptor.js +7 -0
- package/src/execution/tool-result-classifier.js +63 -0
- package/src/integration/canary-deploy.js +143 -0
- package/src/integration/collector.js +59 -0
- package/src/integration/error-recovery.js +6 -0
- package/src/integration/event-bus.js +66 -0
- package/src/integration/health-checker.js +57 -0
- package/src/integration/logger.js +39 -0
- package/src/integration/message-handler.js +611 -0
- package/src/integration/opencode-db.js +153 -0
- package/src/integration/orchestration.js +419 -0
- package/src/integration/part-provenance.js +65 -0
- package/src/integration/rate-limiter.js +55 -0
- package/src/integration/release-gate.js +336 -0
- package/src/integration/rollback-manager.js +164 -0
- package/src/integration/router-adapter.js +269 -0
- package/src/integration/telemetry.js +45 -0
- package/src/integration/update-checker.js +10 -0
- package/src/persistence/event-sourcing.js +75 -0
- package/src/persistence/memory.js +501 -0
- package/src/persistence/migration-runner.js +16 -0
- package/src/persistence/persistence-manager.js +39 -0
- package/src/persistence/transaction-log.js +157 -0
- package/src/policy/action-capabilities.js +116 -0
- package/src/policy/action-policy.js +4 -0
- package/src/policy/assessment-engine.js +104 -0
- package/src/policy/assumption-tracking.js +66 -0
- package/src/policy/budget-enforcer.js +7 -0
- package/src/policy/config-validator.js +102 -0
- package/src/policy/execution-assessment.js +142 -0
- package/src/policy/failure-classification.js +5 -0
- package/src/policy/feature-flags.js +65 -0
- package/src/policy/governance-enforcement.js +118 -0
- package/src/policy/locale.js +15 -0
- package/src/policy/multi-signer.js +151 -0
- package/src/policy/policy-state-machine.js +97 -0
- package/src/policy/ponytail-constraint.js +38 -0
- package/src/policy/project-inspector.js +93 -0
- package/src/policy/query-builder.js +14 -0
- package/src/policy/request-classifier.js +79 -0
- package/src/policy/risk-engine.js +266 -0
- package/src/policy/scope-enforcement.js +140 -0
- package/src/policy/secret-scanner.js +68 -0
- package/src/policy/skill-routing.js +104 -0
- package/src/policy/strategy-capabilities.js +211 -0
- package/src/policy/strategy.js +6 -0
- package/src/policy/uncertainty.js +117 -0
- package/src/shared/container.js +44 -0
- package/src/shared/decision-types.js +24 -0
- package/src/shared/simple-test.js +160 -0
- package/src/shared/wam-events.js +52 -0
- package/src/skills/engine.js +1705 -0
- package/src/skills/skill-registry.js +19 -0
- package/src/skills/skill-routing.js +340 -0
- package/src/state/lifecycle-manager.js +113 -0
- package/src/state/operational-state.js +198 -0
- package/src/state/projections.js +47 -0
- package/src/state/replay-engine.js +5 -0
- package/src/state/requirement-ownership.js +20 -0
- package/src/state/requirement-state.js +152 -0
- package/src/state/run-event-identity.js +172 -0
- package/src/state/state-engine.js +19 -0
- package/src/state/state-machine.js +119 -0
- package/src/state/state-schema.js +318 -0
- package/src/state/state-store.js +335 -0
- package/src/state/state-transition.js +84 -0
- package/src/state/task-dependencies.js +287 -0
- package/src/state/task-execution.js +217 -0
- package/src/state/task-lifecycle.js +59 -0
- package/src/state/task-runs.js +361 -0
- package/src/state/task-state-file.js +86 -0
- package/src/state/task-state.js +41 -0
- package/src/state/task-status.js +54 -0
- package/src/state/task-store.js +48 -0
- package/src/state/wam-state.js +148 -0
- package/src/verification/completion-gate-simple.js +8 -0
- package/src/verification/completion-gate.js +183 -0
- package/src/verification/contract.js +9 -0
- package/src/verification/contradiction-resolution.js +255 -0
- package/src/verification/diagnostic-engine.js +659 -0
- package/src/verification/false-completion-prevention.js +100 -0
- package/src/verification/hook-validator.js +6 -0
- package/src/verification/requirements.js +6 -0
- package/src/verification/verification-context.js +162 -0
- package/src/verification/verification-lifecycle.js +66 -0
- package/src/verification/verification-model.js +89 -0
- package/src/verification/verification-persistence.js +129 -0
- package/src/verification/verification-policy.js +127 -0
- package/src/verification/verification.js +415 -0
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Capsules — Context Selection Engine.
|
|
3
|
+
*
|
|
4
|
+
* OpenSpec changes: context-memory-model, context-selection-budget,
|
|
5
|
+
* session-context-continuity, refactor-context-engine.
|
|
6
|
+
*
|
|
7
|
+
* Rol: ciclo de vida de cápsula, identidad de sesión, selección determinística
|
|
8
|
+
* bajo presupuesto (N3) y retrieval por relevancia. NO decide N0/N1/N2 (esos
|
|
9
|
+
* niveles los emite assembly.js, Context Pack Builder).
|
|
10
|
+
*
|
|
11
|
+
* Unidades: Context Capsules (.wam/capsules/<id>.json), identidad de sesión
|
|
12
|
+
* (.wam/session.json), paquete de contexto por tarea. Sin vector DB ni
|
|
13
|
+
* embeddings; capa de alias determinística (closed set) para tokens sinónimos.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { generateContract, evaluateContract, mergeAssumptions } from "./sufficiency-contract.js";
|
|
19
|
+
|
|
20
|
+
const LEVELS = ["L1", "L2", "L3", "L4"];
|
|
21
|
+
const LIFECYCLE = ["candidate", "active", "superseded", "stale", "invalidated"];
|
|
22
|
+
const PROVENANCE = ["user_decided", "observed", "inferred"];
|
|
23
|
+
|
|
24
|
+
export const LEGACY_SESSION_ID = "legacy";
|
|
25
|
+
|
|
26
|
+
const ALIASES = {
|
|
27
|
+
auth: "authentication",
|
|
28
|
+
authentication: "auth",
|
|
29
|
+
db: "database",
|
|
30
|
+
database: "db",
|
|
31
|
+
postgres: "postgresql",
|
|
32
|
+
postgresql: "postgres",
|
|
33
|
+
jwt: "token",
|
|
34
|
+
ts: "typescript",
|
|
35
|
+
typescript: "ts",
|
|
36
|
+
py: "python",
|
|
37
|
+
python: "py",
|
|
38
|
+
k8s: "kubernetes",
|
|
39
|
+
kubernetes: "k8s",
|
|
40
|
+
api: "interface",
|
|
41
|
+
ui: "frontend",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// --- CHANGE: session identity derived by root + OpenCode sessionID only ---
|
|
45
|
+
|
|
46
|
+
function capsulesDir(root) {
|
|
47
|
+
return path.join(root || process.cwd(), ".wam", "capsules");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sessionFile(root) {
|
|
51
|
+
return path.join(root || process.cwd(), ".wam", "session.json");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function genId(prefix) {
|
|
55
|
+
return `${prefix}_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function nowIso() {
|
|
59
|
+
return new Date().toISOString();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fileExists(p) {
|
|
63
|
+
try {
|
|
64
|
+
return fs.existsSync(p);
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// -- Change 3: session identity derived per root --------------------------------
|
|
71
|
+
// Session ID is scoped to (root, OpenCode sessionID), never global.
|
|
72
|
+
// Different roots NEVER share session identity.
|
|
73
|
+
|
|
74
|
+
export function getSessionId(root, sessionID) {
|
|
75
|
+
const file = sessionFile(root);
|
|
76
|
+
const key = `${root}:${sessionID || "default"}`;
|
|
77
|
+
// Per-(root,session) cache — never shared across roots
|
|
78
|
+
if (!getSessionId._cache) getSessionId._cache = new Map();
|
|
79
|
+
if (getSessionId._cache.has(key)) return getSessionId._cache.get(key);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
if (fileExists(file)) {
|
|
83
|
+
const s = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
84
|
+
if (s.session_id) {
|
|
85
|
+
getSessionId._cache.set(key, s.session_id);
|
|
86
|
+
return s.session_id;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch {}
|
|
90
|
+
const sid = genId("ses");
|
|
91
|
+
try {
|
|
92
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
93
|
+
fs.writeFileSync(file, JSON.stringify({ session_id: sid, created_at: nowIso() }, null, 2));
|
|
94
|
+
} catch {}
|
|
95
|
+
getSessionId._cache.set(key, sid);
|
|
96
|
+
return sid;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function resetSessionCache() {
|
|
100
|
+
if (getSessionId._cache) getSessionId._cache.clear();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// -- Change 1: capsule CRUD ------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
function capsuleFile(root, contextId) {
|
|
106
|
+
return path.join(capsulesDir(root), `${contextId}.json`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function listCapsules(root, { level, lifecycle, sessionId } = {}) {
|
|
110
|
+
const dir = capsulesDir(root);
|
|
111
|
+
let ids = [];
|
|
112
|
+
try {
|
|
113
|
+
ids = fs.readdirSync(dir).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5));
|
|
114
|
+
} catch {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
return ids
|
|
118
|
+
.map((id) => getCapsule(id, root))
|
|
119
|
+
.filter(Boolean)
|
|
120
|
+
.filter((c) => !level || c.level === level)
|
|
121
|
+
.filter((c) => !lifecycle || c.lifecycle === lifecycle)
|
|
122
|
+
.filter((c) => matchesSession(c, sessionId))
|
|
123
|
+
.sort((a, b) => (a.importance || 0) - (b.importance || 0));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function matchesSession(capsule, sessionId) {
|
|
127
|
+
if (sessionId === undefined) return true;
|
|
128
|
+
const csid = capsule.session_id;
|
|
129
|
+
if (csid == null) return sessionId === null || sessionId === LEGACY_SESSION_ID;
|
|
130
|
+
if (sessionId === null) return false;
|
|
131
|
+
return csid === sessionId;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function migrateLegacyCapsules(root = process.cwd(), { dryRun = false } = {}) {
|
|
135
|
+
const dir = capsulesDir(root);
|
|
136
|
+
let ids = [];
|
|
137
|
+
try {
|
|
138
|
+
ids = fs.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
139
|
+
} catch {
|
|
140
|
+
return { ok: true, migrated: [], dryRun };
|
|
141
|
+
}
|
|
142
|
+
const migrated = [];
|
|
143
|
+
for (const file of ids) {
|
|
144
|
+
const full = path.join(dir, file);
|
|
145
|
+
let capsule;
|
|
146
|
+
try {
|
|
147
|
+
capsule = JSON.parse(fs.readFileSync(full, "utf-8"));
|
|
148
|
+
} catch {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (!Object.prototype.hasOwnProperty.call(capsule, "session_id")) {
|
|
152
|
+
migrated.push({ file, context_id: capsule.context_id });
|
|
153
|
+
if (!dryRun) {
|
|
154
|
+
capsule.session_id = LEGACY_SESSION_ID;
|
|
155
|
+
fs.writeFileSync(full, JSON.stringify(capsule, null, 2));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { ok: true, migrated, dryRun };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function getCapsule(contextId, root) {
|
|
163
|
+
try {
|
|
164
|
+
return JSON.parse(fs.readFileSync(capsuleFile(root, contextId), "utf-8"));
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Crea una Context Capsule. `level` por defecto L3 Session (lo más conservador).
|
|
172
|
+
* Provenance inferred NO puede promoverse a L1 silenciosamente (ver promoteCapsule).
|
|
173
|
+
*/
|
|
174
|
+
export function createCapsule(entry, root) {
|
|
175
|
+
const contextId = entry.context_id || genId("cap");
|
|
176
|
+
const level = LEVELS.includes(entry.level) ? entry.level : "L3";
|
|
177
|
+
const provenance = PROVENANCE.includes(entry.provenance) ? entry.provenance : "inferred";
|
|
178
|
+
const capsule = {
|
|
179
|
+
context_id: contextId,
|
|
180
|
+
session_id: entry.session_id || getSessionId(root),
|
|
181
|
+
level,
|
|
182
|
+
lifecycle: LIFECYCLE.includes(entry.lifecycle) ? entry.lifecycle : "active",
|
|
183
|
+
purpose: entry.purpose || "",
|
|
184
|
+
scope: entry.scope || "",
|
|
185
|
+
importance: clampNum(entry.importance, 1, 10, 5),
|
|
186
|
+
confidence: clampNum(entry.confidence, 0, 1, 0.5),
|
|
187
|
+
provenance,
|
|
188
|
+
created_at: nowIso(),
|
|
189
|
+
updated_at: nowIso(),
|
|
190
|
+
mutation_rate: ["low", "medium", "high"].includes(entry.mutation_rate) ? entry.mutation_rate : "medium",
|
|
191
|
+
reuse_probability: ["low", "medium", "high"].includes(entry.reuse_probability)
|
|
192
|
+
? entry.reuse_probability
|
|
193
|
+
: "medium",
|
|
194
|
+
dependencies: Array.isArray(entry.dependencies) ? entry.dependencies : [],
|
|
195
|
+
supersedes: entry.supersedes || null,
|
|
196
|
+
superseded_by: null,
|
|
197
|
+
retrieval_hints: Array.isArray(entry.retrieval_hints) ? entry.retrieval_hints : [],
|
|
198
|
+
content: entry.content || "",
|
|
199
|
+
evidence: entry.evidence || "",
|
|
200
|
+
};
|
|
201
|
+
persistCapsule(root, capsule);
|
|
202
|
+
// supersession: la anterior deja de ser current
|
|
203
|
+
if (capsule.supersedes) {
|
|
204
|
+
const prior = getCapsule(capsule.supersedes, root);
|
|
205
|
+
if (prior && prior.lifecycle !== "invalidated") {
|
|
206
|
+
prior.lifecycle = "superseded";
|
|
207
|
+
prior.superseded_by = contextId;
|
|
208
|
+
prior.updated_at = nowIso();
|
|
209
|
+
persistCapsule(root, prior);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return capsule;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function updateCapsule(contextId, patch, root) {
|
|
216
|
+
const c = getCapsule(contextId, root);
|
|
217
|
+
if (!c) return { ok: false, reason: `Cápsula ${contextId} no existe` };
|
|
218
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
219
|
+
if (k === "context_id" || k === "session_id") continue;
|
|
220
|
+
if (k === "level" && !LEVELS.includes(v)) continue;
|
|
221
|
+
if (k === "lifecycle" && !LIFECYCLE.includes(v)) continue;
|
|
222
|
+
if (k === "provenance" && !PROVENANCE.includes(v)) continue;
|
|
223
|
+
c[k] = v;
|
|
224
|
+
}
|
|
225
|
+
c.updated_at = nowIso();
|
|
226
|
+
persistCapsule(root, c);
|
|
227
|
+
return { ok: true, capsule: c };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function markStale(contextId, root) {
|
|
231
|
+
return updateCapsule(contextId, { lifecycle: "stale" }, root);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function invalidateCapsule(contextId, root) {
|
|
235
|
+
return updateCapsule(contextId, { lifecycle: "invalidated" }, root);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function supersedeCapsule(oldId, newId, root) {
|
|
239
|
+
const c = getCapsule(oldId, root);
|
|
240
|
+
if (!c) return { ok: false, reason: `Cápsula ${oldId} no existe` };
|
|
241
|
+
c.lifecycle = "superseded";
|
|
242
|
+
c.superseded_by = newId;
|
|
243
|
+
c.updated_at = nowIso();
|
|
244
|
+
persistCapsule(root, c);
|
|
245
|
+
return { ok: true };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Promoción conservadora entre niveles.
|
|
250
|
+
* - inferred → L1 exige aprobación explícita (approvedBy) — nunca silenciosa.
|
|
251
|
+
* - L4 → descartable (no promovible).
|
|
252
|
+
*/
|
|
253
|
+
export function promoteCapsule(contextId, targetLevel, { approvedBy = "", evidence = "", root } = {}) {
|
|
254
|
+
const c = getCapsule(contextId, root);
|
|
255
|
+
if (!c) return { ok: false, reason: `Cápsula ${contextId} no existe` };
|
|
256
|
+
if (!LEVELS.includes(targetLevel)) return { ok: false, reason: `Nivel inválido ${targetLevel}` };
|
|
257
|
+
if (c.level === "L4") return { ok: false, reason: "L4 es descartable — no promovible" };
|
|
258
|
+
const cur = LEVELS.indexOf(c.level);
|
|
259
|
+
const tgt = LEVELS.indexOf(targetLevel);
|
|
260
|
+
if (tgt >= cur) {
|
|
261
|
+
return { ok: false, reason: `Promoción debe subir hacia L1 (${c.level} → ${targetLevel} es descenso o igual)` };
|
|
262
|
+
}
|
|
263
|
+
if (targetLevel === "L1" && c.provenance === "inferred" && !approvedBy) {
|
|
264
|
+
return { ok: false, reason: "inferred no puede promoverse a L1 silenciosamente — requiere aprobación" };
|
|
265
|
+
}
|
|
266
|
+
c.level = targetLevel;
|
|
267
|
+
c.lifecycle = "active";
|
|
268
|
+
if (evidence) c.evidence = evidence;
|
|
269
|
+
if (approvedBy) c.promotion = { approved_by: approvedBy, at: nowIso(), from: LEVELS[cur] };
|
|
270
|
+
c.updated_at = nowIso();
|
|
271
|
+
persistCapsule(root, c);
|
|
272
|
+
return { ok: true, capsule: c };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function persistCapsule(root, capsule) {
|
|
276
|
+
try {
|
|
277
|
+
const dir = capsulesDir(root);
|
|
278
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
279
|
+
fs.writeFileSync(capsuleFile(root, capsule.context_id), JSON.stringify(capsule, null, 2));
|
|
280
|
+
} catch {}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function clampNum(v, min, max, dflt) {
|
|
284
|
+
const n = Number(v);
|
|
285
|
+
if (!Number.isFinite(n)) return dflt;
|
|
286
|
+
return Math.max(min, Math.min(max, n));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// -- Change 2: context selection bajo presupuesto --------------------------------
|
|
290
|
+
|
|
291
|
+
function tokenize(text = "") {
|
|
292
|
+
const raw = (text || "")
|
|
293
|
+
.toLowerCase()
|
|
294
|
+
.replace(/[^a-z0-9áéíóúñü\s]/g, " ")
|
|
295
|
+
.split(/\s+/)
|
|
296
|
+
.filter((w) => w.length > 2);
|
|
297
|
+
const out = new Set();
|
|
298
|
+
for (const w of raw) {
|
|
299
|
+
out.add(w);
|
|
300
|
+
const aliased = ALIASES[w];
|
|
301
|
+
if (aliased) out.add(aliased);
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function overlapScore(aTokens, bTokens) {
|
|
307
|
+
if (!aTokens.size || !bTokens.size) return 0;
|
|
308
|
+
let hit = 0;
|
|
309
|
+
for (const t of bTokens) if (aTokens.has(t)) hit++;
|
|
310
|
+
return hit / Math.max(aTokens.size, 1);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function freshness(capsule, now = Date.now()) {
|
|
314
|
+
const days = Math.max(0, (now - new Date(capsule.updated_at).getTime()) / 86400000);
|
|
315
|
+
return Math.max(0.1, 1 - days / 30);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function capsuleTokens(c) {
|
|
319
|
+
return tokenize([c.purpose, c.scope, ...(c.retrieval_hints || []), c.content].join(" "));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function utility(capsule, taskTokens, now) {
|
|
323
|
+
const relevance = overlapScore(taskTokens, capsuleTokens(capsule));
|
|
324
|
+
const importance = (capsule.importance || 5) / 10;
|
|
325
|
+
const fresh = freshness(capsule, now);
|
|
326
|
+
const confidence = capsule.confidence || 0.5;
|
|
327
|
+
const tokenCost = Math.max(1, estimateCapsuleTokens(capsule));
|
|
328
|
+
return { relevance, importance, fresh, confidence, tokenCost, value: relevance * importance * fresh * confidence / tokenCost };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function estimateCapsuleTokens(capsule) {
|
|
332
|
+
const text = [capsule.purpose, capsule.scope, ...(capsule.retrieval_hints || []), capsule.content].join(" ");
|
|
333
|
+
return Math.ceil(text.length / 4);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Construye el Context Package de la tarea bajo presupuesto.
|
|
338
|
+
* L1 activa = base siempre; L2/L3 por utility; dependencias resueltas;
|
|
339
|
+
* redundancia eliminada; observabilidad en selection-log.jsonl.
|
|
340
|
+
*/
|
|
341
|
+
export function selectContext(task, { budget = 8000, root, sessionId, log = true } = {}) {
|
|
342
|
+
const now = Date.now();
|
|
343
|
+
const taskTokens = tokenize(task);
|
|
344
|
+
const capsules = listCapsules(root, sessionId ? { sessionId } : {})
|
|
345
|
+
.filter((c) => c.lifecycle === "active" && c.level !== "L4");
|
|
346
|
+
|
|
347
|
+
const selected = [];
|
|
348
|
+
const rationale = [];
|
|
349
|
+
let used = 0;
|
|
350
|
+
|
|
351
|
+
// 1. L1 base (siempre)
|
|
352
|
+
const l1 = capsules
|
|
353
|
+
.filter((c) => c.level === "L1" && (c.content || "").trim().length > 0)
|
|
354
|
+
.sort((a, b) => (b.importance || 0) - (a.importance || 0));
|
|
355
|
+
for (const c of l1) {
|
|
356
|
+
const t = estimateCapsuleTokens(c);
|
|
357
|
+
if (used + t > budget) {
|
|
358
|
+
rationale.push(`${c.context_id}: L1 excede presupuesto restante (omitido)`);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
selected.push(c);
|
|
362
|
+
used += t;
|
|
363
|
+
rationale.push(`${c.context_id}: L1 foundation (importance ${c.importance})`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 2. L2/L3 por utility (solo capsules con contenido real — sin contenido no hay conocimiento)
|
|
367
|
+
const candidates = capsules
|
|
368
|
+
.filter((c) => (c.level === "L2" || c.level === "L3") && (c.content || "").trim().length > 0)
|
|
369
|
+
.map((c) => ({ c, ...utility(c, taskTokens, now) }))
|
|
370
|
+
.filter((u) => u.relevance > 0)
|
|
371
|
+
.sort((a, b) => b.value - a.value);
|
|
372
|
+
|
|
373
|
+
const seenContent = new Set();
|
|
374
|
+
for (const u of candidates) {
|
|
375
|
+
const c = u.c;
|
|
376
|
+
if (c.superseded_by) {
|
|
377
|
+
rationale.push(`${c.context_id}: superseded por ${c.superseded_by} (omitido)`);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
// redundancia: mismo contenido normalizado ya incluido (scope NO es
|
|
381
|
+
// identidad — dos capsules del mismo módulo son conocimiento distinto)
|
|
382
|
+
const contentKey = (c.content || c.purpose || "").toLowerCase().trim().slice(0, 120);
|
|
383
|
+
if (seenContent.has(contentKey)) {
|
|
384
|
+
rationale.push(`${c.context_id}: duplica contenido de capsule ya incluida (omitido)`);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const t = estimateCapsuleTokens(c);
|
|
388
|
+
if (used + t > budget) {
|
|
389
|
+
rationale.push(`${c.context_id}: excede presupuesto (utility ${u.value.toFixed(4)})`);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
selected.push(c);
|
|
393
|
+
used += t;
|
|
394
|
+
seenContent.add(contentKey);
|
|
395
|
+
rationale.push(`${c.context_id}: utility ${u.value.toFixed(4)} (rel ${u.relevance.toFixed(2)} × imp ${u.importance.toFixed(2)} × fresh ${u.fresh.toFixed(2)} × conf ${u.confidence.toFixed(2)})`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// 3. dependencias (transitivas, dedupe)
|
|
399
|
+
const byId = new Map(capsules.map((c) => [c.context_id, c]));
|
|
400
|
+
const seen = new Set(selected.map((c) => c.context_id));
|
|
401
|
+
const queue = selected.flatMap((c) => c.dependencies || []);
|
|
402
|
+
while (queue.length) {
|
|
403
|
+
const depId = queue.shift();
|
|
404
|
+
if (seen.has(depId)) continue;
|
|
405
|
+
const dep = byId.get(depId);
|
|
406
|
+
if (!dep || dep.lifecycle !== "active") continue;
|
|
407
|
+
const t = estimateCapsuleTokens(dep);
|
|
408
|
+
if (used + t > budget) {
|
|
409
|
+
rationale.push(`${depId}: dependencia excede presupuesto (omitida)`);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
selected.push(dep);
|
|
413
|
+
seen.add(depId);
|
|
414
|
+
used += t;
|
|
415
|
+
rationale.push(`${depId}: dependencia requerida`);
|
|
416
|
+
queue.push(...(dep.dependencies || []));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 4. sufficiency — contract-based instead of lexical
|
|
420
|
+
let contract = generateContract(task, "contract-1");
|
|
421
|
+
contract = evaluateContract(contract, selected);
|
|
422
|
+
contract = mergeAssumptions(contract, []);
|
|
423
|
+
const missing = contract.missing;
|
|
424
|
+
const sufficiency = contract.sufficient ? "ok" : "insufficient";
|
|
425
|
+
|
|
426
|
+
const pkg = {
|
|
427
|
+
task,
|
|
428
|
+
selected_ids: selected.map((c) => c.context_id),
|
|
429
|
+
capsules: selected,
|
|
430
|
+
rationale,
|
|
431
|
+
budget_used: used,
|
|
432
|
+
budget,
|
|
433
|
+
sufficiency,
|
|
434
|
+
missing,
|
|
435
|
+
contract,
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
if (log) {
|
|
439
|
+
try {
|
|
440
|
+
const logFile = path.join(root || process.cwd(), ".wam", "traces", "selection-log.jsonl");
|
|
441
|
+
fs.mkdirSync(path.dirname(logFile), { recursive: true });
|
|
442
|
+
fs.appendFileSync(logFile, JSON.stringify({ timestamp: nowIso(), task, selected_ids: pkg.selected_ids, budget_used: used, budget, sufficiency }) + "\n");
|
|
443
|
+
} catch {}
|
|
444
|
+
}
|
|
445
|
+
return pkg;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Recuperación bajo demanda. Distingue "no cargado" de "no existe":
|
|
450
|
+
* retorna { ok, capsules, message } sin fabricar resultados.
|
|
451
|
+
*/
|
|
452
|
+
export function retrieveContext(query, { limit = 5, root } = {}) {
|
|
453
|
+
const qTokens = tokenize(query);
|
|
454
|
+
const hits = listCapsules(root)
|
|
455
|
+
.filter((c) => c.lifecycle === "active" || c.lifecycle === "candidate")
|
|
456
|
+
.map((c) => ({ c, rel: overlapScore(qTokens, capsuleTokens(c)) }))
|
|
457
|
+
.filter((h) => h.rel > 0)
|
|
458
|
+
.sort((a, b) => b.rel - a.rel)
|
|
459
|
+
.slice(0, limit)
|
|
460
|
+
.map((h) => ({ capsule: h.c, relevance: h.rel }));
|
|
461
|
+
if (!hits.length) {
|
|
462
|
+
return { ok: false, capsules: [], message: `Sin cápsulas para "${query}" — contexto no disponible (no se fabrica)` };
|
|
463
|
+
}
|
|
464
|
+
return { ok: true, capsules: hits, message: `${hits.length} cápsula(s) encontrada(s)` };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// -- Change 3: session continuity -------------------------------------------------
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Cierre de sesión: registra progreso + candidatos (sin auto-promoción).
|
|
471
|
+
* Retorna el log; L3 de la sesión permanece salvo promoción explícita.
|
|
472
|
+
*/
|
|
473
|
+
export function closeSession({ sessionId, taskId, summary = "", candidates = [] } = {}, root) {
|
|
474
|
+
const logFile = path.join(root || process.cwd(), ".wam", "context", "sessions.log.jsonl");
|
|
475
|
+
const entry = {
|
|
476
|
+
timestamp: nowIso(),
|
|
477
|
+
session_id: sessionId || getSessionId(root),
|
|
478
|
+
task_id: taskId || null,
|
|
479
|
+
summary: summary.slice(0, 2000),
|
|
480
|
+
candidates,
|
|
481
|
+
auto_promoted: [],
|
|
482
|
+
};
|
|
483
|
+
try {
|
|
484
|
+
fs.mkdirSync(path.dirname(logFile), { recursive: true });
|
|
485
|
+
fs.appendFileSync(logFile, JSON.stringify(entry) + "\n");
|
|
486
|
+
} catch {}
|
|
487
|
+
return entry;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export function getSessionLog(root) {
|
|
491
|
+
const logFile = path.join(root || process.cwd(), ".wam", "context", "sessions.log.jsonl");
|
|
492
|
+
try {
|
|
493
|
+
return fs.readFileSync(logFile, "utf-8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
494
|
+
} catch {
|
|
495
|
+
return [];
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// -- Change 4: multi-repo root resolution -----------------------------------
|
|
500
|
+
// opencode se abre en un proyecto multi-repo (ej. comparador-precios con
|
|
501
|
+
// scraper/api/frontend internos). El .wam debe vivir en el repo git objetivo
|
|
502
|
+
// del mensaje, NO en el cwd del proceso ni en la raíz del proyecto.
|
|
503
|
+
|
|
504
|
+
function findGitRepos(root, maxDepth = 2) {
|
|
505
|
+
// Si el root ya es repo git, NO hay repos hijos que buscar (caso común:
|
|
506
|
+
// opencode abierto DENTRO de un repo) — cero escaneo del filesystem.
|
|
507
|
+
try {
|
|
508
|
+
if (fs.existsSync(path.join(root, ".git"))) return [];
|
|
509
|
+
} catch {
|
|
510
|
+
return [];
|
|
511
|
+
}
|
|
512
|
+
const found = [];
|
|
513
|
+
const seen = new Set();
|
|
514
|
+
const walk = (dir, depth) => {
|
|
515
|
+
if (depth > maxDepth) return;
|
|
516
|
+
let entries;
|
|
517
|
+
try {
|
|
518
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
519
|
+
} catch {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
for (const e of entries) {
|
|
523
|
+
if (!e.isDirectory()) continue;
|
|
524
|
+
if (e.name === "node_modules" || e.name === ".git" || e.name === ".wam" || e.name === "upstream" || e.name === ".cache" || e.name === "dist" || e.name === "build") continue;
|
|
525
|
+
const p = path.join(dir, e.name);
|
|
526
|
+
if (seen.has(p)) continue;
|
|
527
|
+
seen.add(p);
|
|
528
|
+
try {
|
|
529
|
+
if (fs.existsSync(path.join(p, ".git"))) found.push(p);
|
|
530
|
+
else walk(p, depth + 1);
|
|
531
|
+
} catch {}
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
walk(root, 0);
|
|
535
|
+
return found;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function readPkgName(repoRoot) {
|
|
539
|
+
try {
|
|
540
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf-8"));
|
|
541
|
+
return pkg.name || "";
|
|
542
|
+
} catch {
|
|
543
|
+
return "";
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const PATH_RE = /(?:^|\s|["'(])((\/[a-zA-Z0-9_.-]+)+(?:\/[a-zA-Z0-9_.-]+)*\.(?:ts|tsx|js|jsx|mjs|cjs|json|md|py|go|rs|java|css|html|yaml|yml|sh)(?::\d+)?)(?=\s|["')]|$)/g;
|
|
548
|
+
|
|
549
|
+
export function extractPaths(prompt = "") {
|
|
550
|
+
const out = [];
|
|
551
|
+
for (const m of (prompt || "").matchAll(PATH_RE)) {
|
|
552
|
+
const p = m[1].replace(/:\d+$/, "");
|
|
553
|
+
if (!out.includes(p)) out.push(p);
|
|
554
|
+
}
|
|
555
|
+
return out;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const repoScanCache = new Map();
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Resuelve el root de .wam para un mensaje:
|
|
562
|
+
* 1. paths explícitos del prompt → repo git que los contiene
|
|
563
|
+
* 2. term-match del prompt contra nombre del repo + package.json name
|
|
564
|
+
* 3. fallback: sessionRoot
|
|
565
|
+
*/
|
|
566
|
+
export function resolveWamRoot(prompt = "", sessionRoot = process.cwd()) {
|
|
567
|
+
const repos = [sessionRoot];
|
|
568
|
+
if (repoScanCache.has(sessionRoot)) {
|
|
569
|
+
repos.push(...repoScanCache.get(sessionRoot));
|
|
570
|
+
} else {
|
|
571
|
+
const nested = findGitRepos(sessionRoot).filter((r) => r !== sessionRoot);
|
|
572
|
+
repoScanCache.set(sessionRoot, nested);
|
|
573
|
+
repos.push(...nested);
|
|
574
|
+
}
|
|
575
|
+
const repoSet = repos.filter((r) => fileExists(path.join(r, ".git")));
|
|
576
|
+
if (!repoSet.length) return sessionRoot;
|
|
577
|
+
|
|
578
|
+
for (const p of extractPaths(prompt)) {
|
|
579
|
+
const abs = path.resolve(sessionRoot, p);
|
|
580
|
+
for (const repo of repoSet) {
|
|
581
|
+
if (abs === repo || abs.startsWith(repo + path.sep)) return repo;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const promptTokens = tokenize(prompt);
|
|
586
|
+
let best = sessionRoot;
|
|
587
|
+
let bestScore = 0;
|
|
588
|
+
for (const repo of repoSet) {
|
|
589
|
+
const score = overlapScore(promptTokens, tokenize(`${path.basename(repo)} ${readPkgName(repo)}`));
|
|
590
|
+
if (score > bestScore) {
|
|
591
|
+
best = repo;
|
|
592
|
+
bestScore = score;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
// Sin match y sessionRoot no es repo → continuidad: repo hijo con memoria
|
|
596
|
+
// .wam más reciente (el usuario sigue donde dejó el trabajo, no donde nació
|
|
597
|
+
// el proceso). Evita que mensajes sin señal de repo caigan a la raíz vacía.
|
|
598
|
+
// NOTA: mtime se usa como tiebreaker ÚNICAMENTE entre repos que tienen ya
|
|
599
|
+
// memoria .wam — NO es la señal primaria. La resolución primaria es siempre
|
|
600
|
+
// por señales explícitas (paths del prompt) o coincidencia de repo.
|
|
601
|
+
if (best === sessionRoot && !repoSet.includes(sessionRoot) && repoSet.length) {
|
|
602
|
+
const withMemory = repoSet
|
|
603
|
+
.map((r) => {
|
|
604
|
+
const wamRoot = path.join(r, ".wam", "tasks");
|
|
605
|
+
try {
|
|
606
|
+
const entries = fs.readdirSync(wamRoot, { withFileTypes: true });
|
|
607
|
+
let latest = 0;
|
|
608
|
+
for (const e of entries) {
|
|
609
|
+
if (!e.isDirectory()) continue;
|
|
610
|
+
const tf = path.join(wamRoot, e.name, "context.md");
|
|
611
|
+
try {
|
|
612
|
+
const m = fs.statSync(tf).mtimeMs;
|
|
613
|
+
if (m > latest) latest = m;
|
|
614
|
+
} catch {}
|
|
615
|
+
}
|
|
616
|
+
if (!latest) {
|
|
617
|
+
try { latest = fs.statSync(path.join(r, ".wam")).mtimeMs; } catch {}
|
|
618
|
+
}
|
|
619
|
+
return latest ? { r, mtime: latest } : null;
|
|
620
|
+
} catch {
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
})
|
|
624
|
+
.filter(Boolean)
|
|
625
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
626
|
+
if (withMemory.length) return withMemory[0].r;
|
|
627
|
+
}
|
|
628
|
+
return best;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
export function clearRepoCache() {
|
|
632
|
+
repoScanCache.clear();
|
|
633
|
+
}
|