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,162 @@
1
+ // strategy-identity.js
2
+ // Canonical hashing and identity for approvedStrategy objects.
3
+ //
4
+ // Exports:
5
+ // - createStrategyIdentity(rawStrategy): builds an identity { canonical, hash, ephemeral }
6
+ // - normalizeStrategy(rawStrategy): returns the canonical projection (volatile fields
7
+ // removed, keys sorted recursively, arrays normalized for stability)
8
+ // - hashStrategy(rawStrategy | identity): returns sha256 of canonical JSON
9
+ // - sameStrategy(a, b): strict-equality of canonical hashes
10
+ //
11
+ // Design goals:
12
+ // - Stable across property reordering (top-level + nested object keys sorted).
13
+ // - Stable across volatile/ephemeral fields (timestamps, IDs, attempt counters).
14
+ // - Pure: no I/O, no Date.now() side effects, no randomness.
15
+ // - Deterministic JSON via canonical projection.
16
+
17
+ import { createHash } from "node:crypto";
18
+
19
+ // Volatile keys do NOT affect strategy semantics and MUST be stripped before hashing.
20
+ const VOLATILE_KEYS = new Set([
21
+ "createdAt",
22
+ "updatedAt",
23
+ "modifiedAt",
24
+ "instanceId",
25
+ "runId",
26
+ "correlationId",
27
+ "requestId",
28
+ "sessionId",
29
+ "traceId",
30
+ "attempt",
31
+ "durationMs",
32
+ "timestamp",
33
+ "ts",
34
+ "id",
35
+ "_id",
36
+ ]);
37
+
38
+ // Keys whose value is an array of strategy rules (allowed/prohibited actions).
39
+ // Arrays under these keys are sorted by their string representation to remain stable.
40
+ const SORTED_ARRAY_KEYS = new Set([
41
+ "allowedActions",
42
+ "prohibitedActions",
43
+ ]);
44
+
45
+ function isPlainObject(value) {
46
+ if (value === null || typeof value !== "object") return false;
47
+ const proto = Object.getPrototypeOf(value);
48
+ return proto === Object.prototype || proto === null;
49
+ }
50
+
51
+ function isVolatileKey(key) {
52
+ return VOLATILE_KEYS.has(key);
53
+ }
54
+
55
+ function stableKey(value) {
56
+ if (value === null) return " ";
57
+ if (typeof value === "string") return `s:${value}`;
58
+ if (typeof value === "number") return `n:${value}`;
59
+ if (typeof value === "boolean") return `b:${value}`;
60
+ if (Array.isArray(value)) return `a:${value.map(stableKey).join("|")}`;
61
+ if (typeof value === "object") {
62
+ const keys = Object.keys(value).sort();
63
+ return `o:${keys.map((k) => `${k}=${stableKey(value[k])}`).join("&")}`;
64
+ }
65
+ return `u:${String(value)}`;
66
+ }
67
+
68
+ function stableSortArray(arr) {
69
+ if (arr.length === 0) return arr;
70
+ const indexed = arr.map((v, i) => ({ i, k: stableKey(v) }));
71
+ indexed.sort((a, b) => {
72
+ if (a.k < b.k) return -1;
73
+ if (a.k > b.k) return 1;
74
+ return a.i - b.i;
75
+ });
76
+ return indexed.map((x) => arr[x.i]);
77
+ }
78
+
79
+ function canonicalizeValue(value) {
80
+ if (value === null || value === undefined) return null;
81
+ if (Array.isArray(value)) {
82
+ const arr = value.map((v) => canonicalizeValue(v));
83
+ return stableSortArray(arr);
84
+ }
85
+ if (typeof value === "object") {
86
+ if (!isPlainObject(value)) return value;
87
+ const sortedKeys = Object.keys(value).sort();
88
+ const out = {};
89
+ for (const key of sortedKeys) {
90
+ if (isVolatileKey(key)) continue;
91
+ out[key] = canonicalizeValue(value[key]);
92
+ }
93
+ return out;
94
+ }
95
+ return value;
96
+ }
97
+
98
+ function normalizeStrategy(rawStrategy) {
99
+ if (rawStrategy === null || rawStrategy === undefined) return null;
100
+ if (typeof rawStrategy !== "object") return null;
101
+ return canonicalizeValue(rawStrategy);
102
+ }
103
+
104
+ function hashStrategy(input) {
105
+ const normalized =
106
+ input && typeof input === "object" && input.__normalized === true
107
+ ? input.canonical
108
+ : normalizeStrategy(input);
109
+ if (normalized === null) return null;
110
+ const json = JSON.stringify(normalized);
111
+ return createHash("sha256").update(json).digest("hex");
112
+ }
113
+
114
+ function collectEphemeralKeys(value, prefix = "") {
115
+ const found = [];
116
+ if (value === null || value === undefined) return found;
117
+ if (typeof value !== "object" || Array.isArray(value)) return found;
118
+ if (!isPlainObject(value)) return found;
119
+ for (const key of Object.keys(value)) {
120
+ const path = prefix ? `${prefix}.${key}` : key;
121
+ if (isVolatileKey(key)) {
122
+ found.push(path);
123
+ } else {
124
+ const child = value[key];
125
+ if (child && typeof child === "object" && !Array.isArray(child) && isPlainObject(child)) {
126
+ found.push(...collectEphemeralKeys(child, path));
127
+ }
128
+ }
129
+ }
130
+ return found;
131
+ }
132
+
133
+ function createStrategyIdentity(rawStrategy) {
134
+ const canonical = normalizeStrategy(rawStrategy);
135
+ const hash = canonical === null ? null : hashStrategy({ __normalized: true, canonical });
136
+ const ephemeral = collectEphemeralKeys(rawStrategy);
137
+ return {
138
+ canonical,
139
+ hash,
140
+ ephemeral,
141
+ __normalized: true,
142
+ };
143
+ }
144
+
145
+ function sameStrategy(a, b) {
146
+ const ha = hashStrategy(a);
147
+ const hb = hashStrategy(b);
148
+ if (ha === null && hb === null) return true;
149
+ if (ha === null || hb === null) return false;
150
+ return ha === hb;
151
+ }
152
+
153
+ export {
154
+ createStrategyIdentity,
155
+ normalizeStrategy,
156
+ hashStrategy,
157
+ sameStrategy,
158
+ // exported for tests / advanced use:
159
+ canonicalizeValue as _canonicalizeValue,
160
+ VOLATILE_KEYS as _VOLATILE_KEYS,
161
+ SORTED_ARRAY_KEYS as _SORTED_ARRAY_KEYS,
162
+ };
@@ -0,0 +1,7 @@
1
+ export function createToolInterceptor() {
2
+ return {
3
+ intercept(tool, args, handler) {
4
+ return handler(tool, args);
5
+ },
6
+ };
7
+ }
@@ -0,0 +1,63 @@
1
+ export const EXECUTION_STATUS = Object.freeze({
2
+ SUCCEEDED: "SUCCEEDED",
3
+ FAILED: "FAILED",
4
+ UNKNOWN: "UNKNOWN",
5
+ });
6
+
7
+ export const OUTCOME_STATUS = Object.freeze({
8
+ SATISFIED: "SATISFIED",
9
+ NOT_SATISFIED: "NOT_SATISFIED",
10
+ UNKNOWN: "UNKNOWN",
11
+ });
12
+
13
+ const RUNTIME_ERROR_PATTERNS = [
14
+ /\bchromium\b.*\b(missing|not\s+found|not\s+installed|executable\s+missing)/i,
15
+ /BROWSER_(EXECUTABLE_MISSING|NOT_FOUND|MISSING)/,
16
+ /\b(failed|error|cannot|not\s+found|null|undefined)\b/i,
17
+ /\bENOENT\b/,
18
+ /\bexecutable\s+file\s+not\s+found/i,
19
+ ];
20
+
21
+ function detectRuntimeError(actual, unexpected, output) {
22
+ if (actual?.status === "failure" || actual?.status === "error") return true;
23
+ const text = JSON.stringify({ actual, unexpected, output });
24
+ return RUNTIME_ERROR_PATTERNS.some((re) => re.test(text));
25
+ }
26
+
27
+ export function classifyToolResult({ result, actual, unexpected, output, expected } = {}) {
28
+ const hasOutput = result !== undefined && result !== null;
29
+ const outputError = String(output?.error || "").toLowerCase();
30
+ const executionFailed =
31
+ outputError.length > 0 ||
32
+ (result === undefined && !hasOutput);
33
+
34
+ const execution = executionFailed ? EXECUTION_STATUS.FAILED : EXECUTION_STATUS.SUCCEEDED;
35
+
36
+ const expectedIsSuccess = expected?.expected?.status === "success" && (expected?.expected?.exitCode === undefined || expected?.expected?.exitCode === 0);
37
+ const actualSignalsSuccess =
38
+ actual?.status === "success" ||
39
+ (typeof result === "string" && !unexpected?.error);
40
+ const actualSignalsFailure = detectRuntimeError(actual, unexpected, output);
41
+
42
+ let outcome;
43
+ if (!expectedIsSuccess) {
44
+ outcome = OUTCOME_STATUS.UNKNOWN;
45
+ } else if (actualSignalsFailure) {
46
+ outcome = OUTCOME_STATUS.NOT_SATISFIED;
47
+ } else if (actualSignalsSuccess) {
48
+ outcome = OUTCOME_STATUS.SATISFIED;
49
+ } else {
50
+ outcome = OUTCOME_STATUS.UNKNOWN;
51
+ }
52
+
53
+ return { execution, outcome };
54
+ }
55
+
56
+ export function isSatisfied(result) {
57
+ return result?.outcome === OUTCOME_STATUS.SATISFIED;
58
+ }
59
+
60
+ export function isRuntimeFailure(result) {
61
+ return result?.execution === EXECUTION_STATUS.FAILED &&
62
+ result?.outcome === OUTCOME_STATUS.NOT_SATISFIED;
63
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Canary Deploy
3
+ *
4
+ * Staged rollout with health-check tracking and auto-abort on threshold
5
+ * breach. The canary evaluator is dependency-injected so production code
6
+ * uses real telemetry while tests can simulate any health trajectory.
7
+ *
8
+ * API:
9
+ * canaryDeploy({
10
+ * releaseId,
11
+ * rolloutPercent, // integer 0..100, size of canary cohort
12
+ * durationMs, // observation window
13
+ * healthCheck, // () => { ok, completionRate, errorRate }
14
+ * thresholds, // { minCompletionRate, maxErrorRate }
15
+ * tickMs, // polling interval (default 10ms)
16
+ * now, // injectable clock (default Date.now)
17
+ * })
18
+ * -> Promise<{
19
+ * status: "advanced" | "aborted",
20
+ * reason: string,
21
+ * observations: Array<{ at, ok, completionRate, errorRate }>,
22
+ * }>
23
+ */
24
+
25
+ const DEFAULT_THRESHOLDS = {
26
+ minCompletionRate: 0.9, // drop > 10% relative triggers abort
27
+ maxErrorRate: 0.05, // spike > 5% triggers abort
28
+ };
29
+
30
+ const DEFAULT_TICK_MS = 10;
31
+
32
+ /**
33
+ * Run a canary rollout. Resolves once the canary either advances (healthy
34
+ * for the full window) or auto-aborts (breach detected). Never rejects:
35
+ * failures surface as `{ status: "aborted", reason }`.
36
+ */
37
+ export async function canaryDeploy(opts) {
38
+ const {
39
+ releaseId,
40
+ rolloutPercent,
41
+ durationMs,
42
+ healthCheck,
43
+ thresholds = DEFAULT_THRESHOLDS,
44
+ tickMs = DEFAULT_TICK_MS,
45
+ now = () => Date.now(),
46
+ } = opts || {};
47
+
48
+ if (typeof releaseId !== "string" || !releaseId) {
49
+ return {
50
+ status: "aborted",
51
+ reason: "releaseId required",
52
+ observations: [],
53
+ };
54
+ }
55
+ if (!Number.isFinite(rolloutPercent) || rolloutPercent < 0 || rolloutPercent > 100) {
56
+ return {
57
+ status: "aborted",
58
+ reason: "rolloutPercent must be 0..100",
59
+ observations: [],
60
+ };
61
+ }
62
+ if (!Number.isFinite(durationMs) || durationMs <= 0) {
63
+ return {
64
+ status: "aborted",
65
+ reason: "durationMs must be > 0",
66
+ observations: [],
67
+ };
68
+ }
69
+ if (typeof healthCheck !== "function") {
70
+ return {
71
+ status: "aborted",
72
+ reason: "healthCheck function required",
73
+ observations: [],
74
+ };
75
+ }
76
+
77
+ const minCompletion = thresholds.minCompletionRate ?? DEFAULT_THRESHOLDS.minCompletionRate;
78
+ const maxError = thresholds.maxErrorRate ?? DEFAULT_THRESHOLDS.maxErrorRate;
79
+
80
+ // Real-time clock for pacing + deadline; injectable `now` is for `at` timestamps.
81
+ const realStart = Date.now();
82
+ const realDeadline = realStart + durationMs;
83
+ const observations = [];
84
+
85
+ while (true) {
86
+ let sample;
87
+ try {
88
+ sample = await healthCheck();
89
+ } catch (err) {
90
+ return {
91
+ status: "aborted",
92
+ reason: `health check threw: ${err && err.message ? err.message : String(err)}`,
93
+ observations,
94
+ };
95
+ }
96
+
97
+ const observation = {
98
+ at: now(),
99
+ ok: !!sample?.ok,
100
+ completionRate: typeof sample?.completionRate === "number" ? sample.completionRate : null,
101
+ errorRate: typeof sample?.errorRate === "number" ? sample.errorRate : null,
102
+ };
103
+ observations.push(observation);
104
+
105
+ if (!observation.ok) {
106
+ return {
107
+ status: "aborted",
108
+ reason: "health check reported unhealthy",
109
+ observations,
110
+ };
111
+ }
112
+ if (
113
+ observation.completionRate !== null &&
114
+ observation.completionRate < minCompletion
115
+ ) {
116
+ return {
117
+ status: "aborted",
118
+ reason: `completion rate ${observation.completionRate} < ${minCompletion}`,
119
+ observations,
120
+ };
121
+ }
122
+ if (observation.errorRate !== null && observation.errorRate > maxError) {
123
+ return {
124
+ status: "aborted",
125
+ reason: `error rate ${observation.errorRate} > ${maxError}`,
126
+ observations,
127
+ };
128
+ }
129
+
130
+ if (Date.now() >= realDeadline) {
131
+ return {
132
+ status: "advanced",
133
+ reason: "canary healthy for full window",
134
+ observations,
135
+ };
136
+ }
137
+ await sleep(tickMs);
138
+ }
139
+ }
140
+
141
+ function sleep(ms) {
142
+ return new Promise((resolve) => setTimeout(resolve, ms));
143
+ }
@@ -0,0 +1,59 @@
1
+ export function createMetricsCollector() {
2
+ const series = new Map();
3
+
4
+ function key(name, tags) {
5
+ const tagStr = tags ? Object.keys(tags).sort().map(k => `${k}=${tags[k]}`).join(',') : '';
6
+ return `${name}|${tagStr}`;
7
+ }
8
+
9
+ function record(name, value, tags) {
10
+ const k = key(name, tags);
11
+ if (!series.has(k)) {
12
+ series.set(k, { name, tags: tags || {}, values: [] });
13
+ }
14
+ series.get(k).values.push(value);
15
+ }
16
+
17
+ function snapshotFor(entry) {
18
+ const values = entry.values;
19
+ if (values.length === 0) {
20
+ return { count: 0, sum: 0, min: 0, max: 0, avg: 0, p50: 0, p95: 0, p99: 0 };
21
+ }
22
+ const sorted = [...values].sort((a, b) => a - b);
23
+ const sum = values.reduce((a, b) => a + b, 0);
24
+ const count = values.length;
25
+ const pct = (p) => {
26
+ if (sorted.length === 1) return sorted[0];
27
+ const idx = Math.min(sorted.length - 1, Math.max(0, Math.round(p * (sorted.length - 1))));
28
+ return sorted[idx];
29
+ };
30
+ return {
31
+ count,
32
+ sum,
33
+ min: sorted[0],
34
+ max: sorted[count - 1],
35
+ avg: sum / count,
36
+ p50: pct(0.5),
37
+ p95: pct(0.95),
38
+ p99: pct(0.99),
39
+ };
40
+ }
41
+
42
+ function getSnapshot() {
43
+ const out = [];
44
+ for (const entry of series.values()) {
45
+ out.push({
46
+ name: entry.name,
47
+ tags: entry.tags,
48
+ ...snapshotFor(entry),
49
+ });
50
+ }
51
+ return out;
52
+ }
53
+
54
+ function reset() {
55
+ series.clear();
56
+ }
57
+
58
+ return { record, getSnapshot, reset };
59
+ }
@@ -0,0 +1,6 @@
1
+ export function recover(error, context) {
2
+ if (context && context.retry) return { recovered: true, action: 'retry' };
3
+ if (context && context.fallback) return { recovered: true, action: 'fallback' };
4
+ if (context && context.circuitOpen) return { recovered: false, action: 'circuit-open' };
5
+ return { recovered: false, error };
6
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Event Bus — minimal pub/sub.
3
+ *
4
+ * Synchronous event delivery with off() to detach handlers. Designed for
5
+ * production hardening (M5-50) — inter-module decoupling.
6
+ */
7
+
8
+ /**
9
+ * @typedef {Object} EventBus
10
+ * @property {(event: string, handler: Function) => () => void} on
11
+ * @property {(event: string, handler: Function) => void} off
12
+ * @property {(event: string, payload?: unknown) => void} emit
13
+ * @property {(event: string) => number} listenerCount
14
+ * @property {() => void} removeAllListeners
15
+ */
16
+
17
+ /**
18
+ * Create a new event bus.
19
+ * @returns {EventBus}
20
+ */
21
+ export function createEventBus() {
22
+ /** @type {Map<string, Set<Function>>} */
23
+ const map = new Map();
24
+
25
+ function on(event, handler) {
26
+ if (typeof handler !== "function") throw new TypeError("handler must be function");
27
+ let set = map.get(event);
28
+ if (!set) {
29
+ set = new Set();
30
+ map.set(event, set);
31
+ }
32
+ set.add(handler);
33
+ return () => off(event, handler);
34
+ }
35
+
36
+ function off(event, handler) {
37
+ const set = map.get(event);
38
+ if (!set) return;
39
+ set.delete(handler);
40
+ if (set.size === 0) map.delete(event);
41
+ }
42
+
43
+ function emit(event, payload) {
44
+ const set = map.get(event);
45
+ if (!set) return;
46
+ // copy to allow off() during iteration
47
+ for (const fn of [...set]) {
48
+ try {
49
+ fn(payload, { event });
50
+ } catch (_err) {
51
+ // swallow listener errors — pub/sub isolation
52
+ }
53
+ }
54
+ }
55
+
56
+ function listenerCount(event) {
57
+ const set = map.get(event);
58
+ return set ? set.size : 0;
59
+ }
60
+
61
+ function removeAllListeners() {
62
+ map.clear();
63
+ }
64
+
65
+ return { on, off, emit, listenerCount, removeAllListeners };
66
+ }
@@ -0,0 +1,57 @@
1
+ export function createHealthChecker() {
2
+ const checks = new Map();
3
+
4
+ function register(name, fn) {
5
+ if (typeof fn !== 'function') {
6
+ throw new TypeError(`Health check "${name}" must be a function`);
7
+ }
8
+ checks.set(name, fn);
9
+ }
10
+
11
+ function unregister(name) {
12
+ return checks.delete(name);
13
+ }
14
+
15
+ async function runOne(name) {
16
+ const fn = checks.get(name);
17
+ if (!fn) {
18
+ return { name, status: 'unknown', error: 'not registered' };
19
+ }
20
+ const started = Date.now();
21
+ try {
22
+ const result = await fn();
23
+ const latencyMs = Date.now() - started;
24
+ // Treat thrown exception as fail; explicit return shape otherwise.
25
+ if (result && typeof result === 'object' && 'status' in result) {
26
+ return { name, ...result, latencyMs };
27
+ }
28
+ return { name, status: 'ok', latencyMs, details: result };
29
+ } catch (err) {
30
+ return {
31
+ name,
32
+ status: 'fail',
33
+ latencyMs: Date.now() - started,
34
+ error: err && err.message ? err.message : String(err),
35
+ };
36
+ }
37
+ }
38
+
39
+ async function check(name) {
40
+ if (name) return runOne(name);
41
+ const results = await Promise.all(
42
+ Array.from(checks.keys()).map((n) => runOne(n))
43
+ );
44
+ const overall = results.every((r) => r.status === 'ok')
45
+ ? 'ok'
46
+ : results.some((r) => r.status === 'ok')
47
+ ? 'degraded'
48
+ : 'fail';
49
+ return { overall, checks: results, timestamp: Date.now() };
50
+ }
51
+
52
+ function list() {
53
+ return Array.from(checks.keys());
54
+ }
55
+
56
+ return { register, unregister, check, list };
57
+ }
@@ -0,0 +1,39 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const LOG_FILE = process.env.WAM_LOG_FILE || path.join(process.cwd(), '.wam', 'plugin.log');
5
+
6
+ /**
7
+ * Simple diagnostic logger for wait-a-minute-plugin.
8
+ * Prefixes: [ISO-Timestamp] [LEVEL] [category] Message
9
+ */
10
+ export const logger = {
11
+ _log(level, category, message) {
12
+ const timestamp = new Date().toISOString();
13
+ const formattedMessage = `[${timestamp}] [${level}] [${category}] ${message}\n`;
14
+
15
+ try {
16
+ const logDir = path.dirname(LOG_FILE);
17
+ if (!fs.existsSync(logDir)) {
18
+ fs.mkdirSync(logDir, { recursive: true });
19
+ }
20
+ fs.appendFileSync(LOG_FILE, formattedMessage, 'utf8');
21
+ } catch (err) {
22
+ console.error(`[LOGGER-ERROR] Failed to write to log file: ${err.message}`);
23
+ }
24
+ },
25
+
26
+ info(category, message) {
27
+ this._log('INFO', category, message);
28
+ },
29
+
30
+ warn(category, message) {
31
+ this._log('WARN', category, message);
32
+ },
33
+
34
+ error(category, message) {
35
+ this._log('ERROR', category, message);
36
+ }
37
+ };
38
+
39
+