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,237 @@
1
+ /**
2
+ * WAM Context Routing — Shadow Mode
3
+ *
4
+ * Computes what context WAM would send for a task, without modifying
5
+ * OpenCode context. Used for evaluation against the current context pack.
6
+ */
7
+
8
+ import { ContextGraph } from "./context-graph.js";
9
+ import { getResumeContext, getPreviousFailures, getPreviousDecisions } from "../state/task-runs.js";
10
+ import { getDependencies, getUpstream, detectMissingDependencies, detectContextGaps } from "../state/task-dependencies.js";
11
+ import { resolveContext } from "./context-router.js";
12
+ import { getTaskState } from "../skills/engine.js";
13
+
14
+ // -- Types --
15
+
16
+ /**
17
+ * @typedef {Object} ContextSnapshot
18
+ * @property {string[]} nodeIds
19
+ * @property {number} tokenEstimate
20
+ * @property {string[]} sources - Where each piece of context came from
21
+ */
22
+
23
+ /**
24
+ * @typedef {Object} ContextGap
25
+ * @property {string} type
26
+ * @property {string} severity
27
+ * @property {string} description
28
+ */
29
+
30
+ /**
31
+ * @typedef {Object} ShadowContextResult
32
+ * @property {string} taskId
33
+ * @property {ContextSnapshot} currentContext
34
+ * @property {ContextSnapshot} proposedContext
35
+ * @property {ContextGap[]} missing
36
+ * @property {{ current: number, proposed: number }} tokenEstimate
37
+ * @property {string[]} routingDecisions - Why each piece was selected
38
+ */
39
+
40
+ // -- Token estimation --
41
+
42
+ function estimateTokens(text = "") {
43
+ return Math.ceil((text || "").length / 4);
44
+ }
45
+
46
+ function nodeTokens(node) {
47
+ return estimateTokens(node.content) + 20;
48
+ }
49
+
50
+ // -- Current Context Pack (simulates what WAM currently sends) --
51
+
52
+ /**
53
+ * Simulate the current WAM context pack for a task.
54
+ * This represents the N0-N3 model.
55
+ */
56
+ export function getCurrentContextPack(taskId, root) {
57
+ const sources = [];
58
+ const nodeIds = [];
59
+ let tokens = 0;
60
+
61
+ // N0: Global policy (minimal)
62
+ sources.push("N0:global-policy");
63
+ tokens += 50;
64
+
65
+ // N2: Task state (required)
66
+ const state = getTaskState(taskId, root);
67
+ if (state) {
68
+ nodeIds.push(`task-${taskId}`);
69
+ const taskTokens = estimateTokens(JSON.stringify(state));
70
+ tokens += taskTokens;
71
+ sources.push(`N2:task-state (${taskTokens} tokens)`);
72
+ }
73
+
74
+ // N3: Session context (top-level only)
75
+ sources.push("N3:session-context");
76
+ tokens += 200;
77
+
78
+ return { nodeIds, tokenEstimate: tokens, sources };
79
+ }
80
+
81
+ // -- Proposed Context (shadow routing) --
82
+
83
+ /**
84
+ * Compute proposed context using dependency-aware routing.
85
+ * Combines: task state + dependencies + evidence + relevant history.
86
+ */
87
+ export function getProposedContext(taskId, graph, root) {
88
+ const sources = [];
89
+ const nodeIds = [];
90
+ let tokens = 0;
91
+
92
+ // 1. Current task state (always included)
93
+ nodeIds.push(taskId);
94
+ sources.push("task-state");
95
+ tokens += 200;
96
+
97
+ // 2. Required dependencies (from graph)
98
+ if (graph) {
99
+ const deps = graph.getDependencies(taskId);
100
+ for (const dep of deps) {
101
+ nodeIds.push(dep.id);
102
+ tokens += nodeTokens(dep);
103
+ sources.push(`dependency:${dep.type}`);
104
+ }
105
+
106
+ // 3. Upstream context (transitive deps)
107
+ const upstream = graph.getUpstream(taskId, 3);
108
+ for (const u of upstream) {
109
+ if (!nodeIds.includes(u.id)) {
110
+ nodeIds.push(u.id);
111
+ tokens += nodeTokens(u);
112
+ sources.push(`upstream:${u.type}`);
113
+ }
114
+ }
115
+ }
116
+
117
+ // 4. Required evidence
118
+ if (graph) {
119
+ const evidenceNodes = graph.getNodes("evidence");
120
+ for (const e of evidenceNodes) {
121
+ if (!nodeIds.includes(e.id)) {
122
+ // Check if this evidence supports any of our dependencies
123
+ const supports = graph.getEdgesFrom(e.id);
124
+ const relevant = supports.some((s) => nodeIds.includes(s.to));
125
+ if (relevant) {
126
+ nodeIds.push(e.id);
127
+ tokens += nodeTokens(e);
128
+ sources.push("evidence:supports-dependency");
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ // 5. Relevant execution history (last 2 failures + 3 decisions)
135
+ const failures = getPreviousFailures(taskId, root);
136
+ for (const f of failures.slice(-2)) {
137
+ nodeIds.push(`exec-failure-${f.id}`);
138
+ tokens += estimateTokens(f.outcome || "") + 20;
139
+ sources.push(`history:failure`);
140
+ }
141
+
142
+ const decisions = getPreviousDecisions(taskId, root, 3);
143
+ for (const d of decisions) {
144
+ nodeIds.push(`exec-decision-${d.executionId}`);
145
+ tokens += estimateTokens(d.decision) + 20;
146
+ sources.push(`history:decision`);
147
+ }
148
+
149
+ // 6. Context gaps
150
+ let gaps = [];
151
+ if (graph) {
152
+ // Detect missing deps by checking edges from this task
153
+ const edges = graph.getEdgesFrom(taskId);
154
+ for (const edge of edges) {
155
+ if (["requires_output", "requires_completion", "requires_evidence",
156
+ "requires_decision", "depends_on_artifact"].includes(edge.type)) {
157
+ const targetNode = graph.getNode(edge.to);
158
+ if (!targetNode) {
159
+ gaps.push({ type: "missing_dependency", severity: "blocking", description: `Output ${edge.to} not found` });
160
+ } else if (!targetNode.verified) {
161
+ gaps.push({ type: "unverified_dependency", severity: "warning", description: `Output ${edge.to} not verified` });
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ return {
168
+ nodeIds,
169
+ tokenEstimate: tokens,
170
+ sources,
171
+ gaps,
172
+ };
173
+ }
174
+
175
+ // -- Shadow Router --
176
+
177
+ /**
178
+ * Run shadow routing: compute both current and proposed context.
179
+ * Never modifies OpenCode input.
180
+ */
181
+ export function shadowRoute(taskId, graph, root) {
182
+ const current = getCurrentContextPack(taskId, root);
183
+ const proposed = getProposedContext(taskId, graph, root);
184
+ const missing = proposed.gaps || [];
185
+
186
+ const routingDecisions = proposed.sources.map((s) => `Include: ${s}`);
187
+
188
+ if (missing.length > 0) {
189
+ routingDecisions.push(`Block: ${missing.length} context gaps detected`);
190
+ }
191
+
192
+ return {
193
+ taskId,
194
+ currentContext: {
195
+ nodeIds: current.nodeIds,
196
+ tokenEstimate: current.tokenEstimate,
197
+ sources: current.sources,
198
+ },
199
+ proposedContext: {
200
+ nodeIds: proposed.nodeIds,
201
+ tokenEstimate: proposed.tokenEstimate,
202
+ sources: proposed.sources,
203
+ },
204
+ missing,
205
+ tokenEstimate: {
206
+ current: current.tokenEstimate,
207
+ proposed: proposed.tokenEstimate,
208
+ },
209
+ routingDecisions,
210
+ };
211
+ }
212
+
213
+ // -- Evaluation Helpers --
214
+
215
+ /**
216
+ * Check if proposed context includes all required items from current context.
217
+ */
218
+ export function hasRequiredContext(proposed, required) {
219
+ return required.every((r) => proposed.nodeIds.includes(r));
220
+ }
221
+
222
+ /**
223
+ * Compute context overlap between current and proposed.
224
+ */
225
+ export function computeOverlap(current, proposed) {
226
+ const currentSet = new Set(current.nodeIds);
227
+ const proposedSet = new Set(proposed.nodeIds);
228
+ const intersection = [...currentSet].filter((id) => proposedSet.has(id));
229
+ const union = new Set([...currentSet, ...proposedSet]);
230
+
231
+ return {
232
+ shared: intersection.length,
233
+ onlyInCurrent: currentSet.size - intersection.length,
234
+ onlyInProposed: proposedSet.size - intersection.length,
235
+ jaccard: union.size > 0 ? intersection.length / union.size : 0,
236
+ };
237
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Context Scope — define and assign scope boundaries to context items.
3
+ *
4
+ * Scopes: GLOBAL, PROJECT, TASK, REQUIREMENT, ACTION, SESSION, TURN.
5
+ *
6
+ * Scope determines the visibility and lifetime of a context item.
7
+ * A context item's scope is assigned by analyzing the item's metadata,
8
+ * type, and associated identifiers.
9
+ */
10
+
11
+ /** @typedef {"GLOBAL"|"PROJECT"|"TASK"|"REQUIREMENT"|"ACTION"|"SESSION"|"TURN"} ContextScope */
12
+
13
+ /**
14
+ * @typedef {Object} ScopeResult
15
+ * @property {ContextScope} scope
16
+ * @property {number} confidence 0-1
17
+ * @property {string} reason
18
+ */
19
+
20
+ // Scope hierarchy (smaller = more specific)
21
+ const SCOPE_LEVELS = ["GLOBAL", "PROJECT", "TASK", "REQUIREMENT", "ACTION", "SESSION", "TURN"];
22
+
23
+ // Type-to-scope hints
24
+ const TYPE_SCOPE_HINTS = {
25
+ tool_call: "ACTION",
26
+ tool_result: "ACTION",
27
+ agent_message: "SESSION",
28
+ system_message: "GLOBAL",
29
+ file_modification: "ACTION",
30
+ git_change: "PROJECT",
31
+ requirement_change: "REQUIREMENT",
32
+ verification_result: "ACTION",
33
+ clarification_request: "SESSION",
34
+ plan: "TASK",
35
+ rationale: "SESSION",
36
+ assumption: "SESSION",
37
+ };
38
+
39
+ /**
40
+ * Infer scope from metadata fields.
41
+ * @param {Object} metadata
42
+ * @returns {ContextScope|null}
43
+ */
44
+ function inferFromMetadata(metadata) {
45
+ if (!metadata || typeof metadata !== "object") return null;
46
+
47
+ // Explicit scope in metadata (uppercase normalized)
48
+ if (metadata.scope) {
49
+ const raw = String(metadata.scope).toUpperCase();
50
+ if (SCOPE_LEVELS.includes(raw)) {
51
+ return raw;
52
+ }
53
+ }
54
+
55
+ // Task ID implies TASK scope (or more specific if requirementId present)
56
+ if (metadata.taskId || metadata.task_id) {
57
+ if (metadata.requirementId || metadata.requirement_id) {
58
+ return "REQUIREMENT";
59
+ }
60
+ if (metadata.actionId || metadata.action_id) {
61
+ return "ACTION";
62
+ }
63
+ return "TASK";
64
+ }
65
+
66
+ // Requirement ID implies REQUIREMENT scope
67
+ if (metadata.requirementId || metadata.requirement_id) {
68
+ return "REQUIREMENT";
69
+ }
70
+
71
+ // Session ID implies SESSION scope
72
+ if (metadata.sessionId || metadata.session_id || metadata.session) {
73
+ return "SESSION";
74
+ }
75
+
76
+ // Turn number implies TURN scope
77
+ if (metadata.turn !== undefined || metadata.turnNumber !== undefined) {
78
+ return "TURN";
79
+ }
80
+
81
+ // Project name/id implies PROJECT scope
82
+ if (metadata.projectId || metadata.project_id || metadata.project) {
83
+ return "PROJECT";
84
+ }
85
+
86
+ return null;
87
+ }
88
+
89
+ /**
90
+ * Infer scope from item type and content.
91
+ * @param {Object} item
92
+ * @returns {ContextScope|null}
93
+ */
94
+ function inferFromType(item) {
95
+ const type = (item.type || "").toLowerCase();
96
+ return TYPE_SCOPE_HINTS[type] || null;
97
+ }
98
+
99
+ /**
100
+ * Assign scope to a context item.
101
+ * @param {Object} item - Context item with type, metadata, content
102
+ * @param {Object} [metadata] - Additional context for scoping decisions
103
+ * @returns {ScopeResult}
104
+ */
105
+ export function scopeContextItem(item, metadata = {}) {
106
+ if (!item || typeof item !== "object") {
107
+ return { scope: "SESSION", confidence: 0.3, reason: "default_fallback" };
108
+ }
109
+
110
+ const combinedMetadata = { ...item.metadata, ...metadata };
111
+
112
+ // 1. Explicit scope in metadata (highest priority)
113
+ const explicit = inferFromMetadata(combinedMetadata);
114
+ if (explicit) {
115
+ return { scope: explicit, confidence: 0.95, reason: "explicit_metadata" };
116
+ }
117
+
118
+ // 2. Type-based inference
119
+ const typeScope = inferFromType(item);
120
+ if (typeScope) {
121
+ return { scope: typeScope, confidence: 0.8, reason: `type:${item.type}` };
122
+ }
123
+
124
+ // 3. Content-based keywords
125
+ const text = typeof item.content === "string"
126
+ ? item.content
127
+ : typeof item.summary === "string"
128
+ ? item.summary
129
+ : JSON.stringify(item.content || "");
130
+
131
+ const lower = text.toLowerCase();
132
+
133
+ if (metadata.globalScope || combinedMetadata.globalScope || lower.includes("global")) {
134
+ return { scope: "GLOBAL", confidence: 0.7, reason: "global_signal" };
135
+ }
136
+ if (lower.includes("project") || combinedMetadata.project) {
137
+ return { scope: "PROJECT", confidence: 0.7, reason: "project_signal" };
138
+ }
139
+ if (lower.includes("requirement") || combinedMetadata.requirement) {
140
+ return { scope: "REQUIREMENT", confidence: 0.7, reason: "requirement_signal" };
141
+ }
142
+
143
+ // 4. Default: SESSION scope
144
+ return { scope: "SESSION", confidence: 0.6, reason: "default_session" };
145
+ }
146
+
147
+ /**
148
+ * Normalize a scope string to a valid ContextScope.
149
+ * @param {string} scope
150
+ * @returns {ContextScope}
151
+ */
152
+ export function normalizeScope(scope) {
153
+ if (!scope) return "SESSION";
154
+ const upper = String(scope).toUpperCase();
155
+ if (SCOPE_LEVELS.includes(upper)) return upper;
156
+ return "SESSION";
157
+ }
158
+
159
+ /**
160
+ * Get parent scope (one level up in hierarchy).
161
+ * @param {ContextScope} scope
162
+ * @returns {ContextScope}
163
+ */
164
+ export function parentScope(scope) {
165
+ const idx = SCOPE_LEVELS.indexOf(scope);
166
+ if (idx <= 0) return scope; // GLOBAL has no parent
167
+ return SCOPE_LEVELS[idx - 1];
168
+ }
169
+
170
+ /**
171
+ * Check if a scope covers another (is at least as broad).
172
+ * @param {ContextScope} container
173
+ * @param {ContextScope} contained
174
+ * @returns {boolean}
175
+ */
176
+ export function scopeCovers(container, contained) {
177
+ const cIdx = SCOPE_LEVELS.indexOf(container);
178
+ const sIdx = SCOPE_LEVELS.indexOf(contained);
179
+ if (cIdx < 0 || sIdx < 0) return false;
180
+ return cIdx <= sIdx;
181
+ }
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Context Snapshot — continuation invalidation.
3
+ * Detects when context has become stale and needs rebuild.
4
+ *
5
+ * States: VALID | STALE | INVALID
6
+ */
7
+
8
+ import crypto from "node:crypto";
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+
12
+ const SNAPSHOT_DIR = ".wam/snapshots";
13
+
14
+ /**
15
+ * @typedef {Object} ContextSnapshot
16
+ * @property {string} taskId
17
+ * @property {string} gitRevision
18
+ * @property {string} relevantFilesHash
19
+ * @property {string} projectContextHash
20
+ * @property {string} taskStateHash
21
+ * @property {number} createdAt
22
+ */
23
+
24
+ /**
25
+ * @typedef {"VALID" | "STALE" | "INVALID"} SnapshotStatus
26
+ */
27
+
28
+ /**
29
+ * @typedef {Object} SnapshotCheck
30
+ * @property {SnapshotStatus} status
31
+ * @property {string} reason
32
+ * @property {string[]} changedSignals
33
+ */
34
+
35
+ function hashString(str = "") {
36
+ return crypto.createHash("sha256").update(str || "").digest("hex").slice(0, 16);
37
+ }
38
+
39
+ function hashFiles(filePaths, root) {
40
+ const parts = [];
41
+ for (const fp of filePaths) {
42
+ const full = path.join(root, fp);
43
+ try {
44
+ const content = fs.readFileSync(full, "utf-8");
45
+ parts.push(`${fp}:${hashString(content)}`);
46
+ } catch {
47
+ parts.push(`${fp}:missing`);
48
+ }
49
+ }
50
+ return hashString(parts.join("|"));
51
+ }
52
+
53
+ function getGitRevision(root) {
54
+ try {
55
+ const head = fs.readFileSync(path.join(root, ".git", "HEAD"), "utf-8").trim();
56
+ const ref = head.replace(/^ref:\s*/, "");
57
+ const refPath = path.join(root, ".git", ref);
58
+ if (fs.existsSync(refPath)) {
59
+ return fs.readFileSync(refPath, "utf-8").trim().slice(0, 12);
60
+ }
61
+ return head.slice(0, 12);
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ function getRelevantFiles(root) {
68
+ const candidates = [
69
+ "package.json",
70
+ "package-lock.json",
71
+ "yarn.lock",
72
+ "pnpm-lock.yaml",
73
+ "tsconfig.json",
74
+ "nest-cli.json",
75
+ "angular.json",
76
+ "AGENTS.md",
77
+ ".opencode/opencode.jsonc",
78
+ ];
79
+ return candidates.filter((f) => fs.existsSync(path.join(root, f)));
80
+ }
81
+
82
+ function getProjectContextHash(root) {
83
+ const relevant = getRelevantFiles(root);
84
+ return hashFiles(relevant, root);
85
+ }
86
+
87
+ function getTaskStateHash(taskState) {
88
+ const serialized = JSON.stringify({
89
+ phase: taskState?.phase,
90
+ contractStatus: taskState?.contract?.status,
91
+ requirements: taskState?.requirements?.map((r) => ({
92
+ id: r.id,
93
+ status: r.status,
94
+ })),
95
+ approvedStrategy: taskState?.approvedStrategy,
96
+ });
97
+ return hashString(serialized);
98
+ }
99
+
100
+ function snapshotDir(root) {
101
+ const dir = path.join(root || process.cwd(), SNAPSHOT_DIR);
102
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
103
+ return dir;
104
+ }
105
+
106
+ function snapshotPath(taskId, root) {
107
+ return path.join(snapshotDir(root), `${taskId}.json`);
108
+ }
109
+
110
+ /**
111
+ * Create a context snapshot for a task.
112
+ */
113
+ export function createSnapshot(taskId, taskState, root) {
114
+ const snapshot = {
115
+ taskId,
116
+ gitRevision: getGitRevision(root),
117
+ relevantFilesHash: getProjectContextHash(root),
118
+ projectContextHash: hashString(getRelevantFiles(root).join(",")),
119
+ taskStateHash: getTaskStateHash(taskState),
120
+ createdAt: Date.now(),
121
+ };
122
+ fs.writeFileSync(snapshotPath(taskId, root), JSON.stringify(snapshot, null, 2));
123
+ return snapshot;
124
+ }
125
+
126
+ /**
127
+ * Load an existing snapshot for a task.
128
+ */
129
+ export function loadSnapshot(taskId, root) {
130
+ try {
131
+ const raw = fs.readFileSync(snapshotPath(taskId, root), "utf-8");
132
+ return JSON.parse(raw);
133
+ } catch {
134
+ return null;
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Check if a continuation can use the fast-path.
140
+ *
141
+ * @param {string} taskId
142
+ * @param {Object} currentTaskState
143
+ * @param {string} root
144
+ * @returns {SnapshotCheck}
145
+ */
146
+ export function checkContinuation(taskId, currentTaskState, root, collector = null) {
147
+ const finish = (result) => {
148
+ if (collector) collector.record(result.status === "VALID" ? "Snapshot_hit" : "Snapshot_miss");
149
+ return result;
150
+ };
151
+ const previous = loadSnapshot(taskId, root);
152
+ if (!previous) {
153
+ return finish({ status: "STALE", reason: "no-previous-snapshot", changedSignals: ["no-snapshot"] });
154
+ }
155
+
156
+ const changed = [];
157
+
158
+ // Check git revision
159
+ const currentGit = getGitRevision(root);
160
+ if (currentGit && previous.gitRevision && currentGit !== previous.gitRevision) {
161
+ changed.push("git-revision");
162
+ }
163
+
164
+ // Check relevant files (package.json, config, etc.)
165
+ const currentFilesHash = getProjectContextHash(root);
166
+ if (currentFilesHash !== previous.relevantFilesHash) {
167
+ changed.push("relevant-files");
168
+ }
169
+
170
+ // Check task state
171
+ const currentStateHash = getTaskStateHash(currentTaskState);
172
+ if (currentStateHash !== previous.taskStateHash) {
173
+ changed.push("task-state");
174
+ }
175
+
176
+ if (changed.length === 0) {
177
+ return finish({ status: "VALID", reason: "no-changes", changedSignals: [] });
178
+ }
179
+
180
+ // Task state changes = INVALID (need full rebuild)
181
+ if (changed.includes("task-state")) {
182
+ return finish({ status: "INVALID", reason: "task-state-changed", changedSignals: changed });
183
+ }
184
+
185
+ // Other changes = STALE (need partial rebuild)
186
+ return finish({ status: "STALE", reason: "context-changed", changedSignals: changed });
187
+ }
188
+
189
+ /**
190
+ * Determine which context levels need rebuild based on changed signals.
191
+ *
192
+ * @param {string[]} changedSignals
193
+ * @returns {{ rebuildN1: boolean, rebuildN2: boolean, rebuildN3: boolean }}
194
+ */
195
+ export function rebuildScope(changedSignals, collector = null) {
196
+ if (collector) {
197
+ collector.record("Reconstruction_count");
198
+ collector.record("Context_reconstructed");
199
+ }
200
+ const scope = { rebuildN1: false, rebuildN2: false, rebuildN3: false };
201
+
202
+ for (const signal of changedSignals) {
203
+ switch (signal) {
204
+ case "relevant-files":
205
+ case "git-revision":
206
+ // Project-level changes → rebuild N1 (project context) and N3 (skills)
207
+ scope.rebuildN1 = true;
208
+ scope.rebuildN3 = true;
209
+ break;
210
+ case "task-state":
211
+ // Task-level changes → rebuild everything
212
+ scope.rebuildN1 = true;
213
+ scope.rebuildN2 = true;
214
+ scope.rebuildN3 = true;
215
+ break;
216
+ case "no-snapshot":
217
+ // No previous snapshot → full rebuild
218
+ scope.rebuildN1 = true;
219
+ scope.rebuildN2 = true;
220
+ scope.rebuildN3 = true;
221
+ break;
222
+ }
223
+ }
224
+
225
+ return scope;
226
+ }