@aisy/core 0.1.0

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 (270) hide show
  1. package/LICENSE +202 -0
  2. package/dist/agent-loop/index.d.ts +4 -0
  3. package/dist/agent-loop/index.d.ts.map +1 -0
  4. package/dist/agent-loop/index.js +352 -0
  5. package/dist/agent-loop/index.js.map +1 -0
  6. package/dist/agent-loop/types.d.ts +183 -0
  7. package/dist/agent-loop/types.d.ts.map +1 -0
  8. package/dist/agent-loop/types.js +3 -0
  9. package/dist/agent-loop/types.js.map +1 -0
  10. package/dist/bin/aisy.d.ts +3 -0
  11. package/dist/bin/aisy.d.ts.map +1 -0
  12. package/dist/bin/aisy.js +14 -0
  13. package/dist/bin/aisy.js.map +1 -0
  14. package/dist/cli/index.d.ts +17 -0
  15. package/dist/cli/index.d.ts.map +1 -0
  16. package/dist/cli/index.js +114 -0
  17. package/dist/cli/index.js.map +1 -0
  18. package/dist/context-engine/index.d.ts +4 -0
  19. package/dist/context-engine/index.d.ts.map +1 -0
  20. package/dist/context-engine/index.js +126 -0
  21. package/dist/context-engine/index.js.map +1 -0
  22. package/dist/context-engine/types.d.ts +54 -0
  23. package/dist/context-engine/types.d.ts.map +1 -0
  24. package/dist/context-engine/types.js +4 -0
  25. package/dist/context-engine/types.js.map +1 -0
  26. package/dist/eval/index.d.ts +20 -0
  27. package/dist/eval/index.d.ts.map +1 -0
  28. package/dist/eval/index.js +128 -0
  29. package/dist/eval/index.js.map +1 -0
  30. package/dist/eval/types.d.ts +62 -0
  31. package/dist/eval/types.d.ts.map +1 -0
  32. package/dist/eval/types.js +17 -0
  33. package/dist/eval/types.js.map +1 -0
  34. package/dist/gateway/index.d.ts +5 -0
  35. package/dist/gateway/index.d.ts.map +1 -0
  36. package/dist/gateway/index.js +288 -0
  37. package/dist/gateway/index.js.map +1 -0
  38. package/dist/gateway/types.d.ts +194 -0
  39. package/dist/gateway/types.d.ts.map +1 -0
  40. package/dist/gateway/types.js +94 -0
  41. package/dist/gateway/types.js.map +1 -0
  42. package/dist/goals/index.d.ts +11 -0
  43. package/dist/goals/index.d.ts.map +1 -0
  44. package/dist/goals/index.js +21 -0
  45. package/dist/goals/index.js.map +1 -0
  46. package/dist/goals/types.d.ts +47 -0
  47. package/dist/goals/types.d.ts.map +1 -0
  48. package/dist/goals/types.js +5 -0
  49. package/dist/goals/types.js.map +1 -0
  50. package/dist/index.d.ts +56 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +50 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/mcp/index.d.ts +5 -0
  55. package/dist/mcp/index.d.ts.map +1 -0
  56. package/dist/mcp/index.js +215 -0
  57. package/dist/mcp/index.js.map +1 -0
  58. package/dist/mcp/types.d.ts +148 -0
  59. package/dist/mcp/types.d.ts.map +1 -0
  60. package/dist/mcp/types.js +4 -0
  61. package/dist/mcp/types.js.map +1 -0
  62. package/dist/memory/index.d.ts +6 -0
  63. package/dist/memory/index.d.ts.map +1 -0
  64. package/dist/memory/index.js +419 -0
  65. package/dist/memory/index.js.map +1 -0
  66. package/dist/memory/types.d.ts +131 -0
  67. package/dist/memory/types.d.ts.map +1 -0
  68. package/dist/memory/types.js +33 -0
  69. package/dist/memory/types.js.map +1 -0
  70. package/dist/nightly/index.d.ts +4 -0
  71. package/dist/nightly/index.d.ts.map +1 -0
  72. package/dist/nightly/index.js +470 -0
  73. package/dist/nightly/index.js.map +1 -0
  74. package/dist/nightly/types.d.ts +326 -0
  75. package/dist/nightly/types.d.ts.map +1 -0
  76. package/dist/nightly/types.js +3 -0
  77. package/dist/nightly/types.js.map +1 -0
  78. package/dist/observability/index.d.ts +11 -0
  79. package/dist/observability/index.d.ts.map +1 -0
  80. package/dist/observability/index.js +396 -0
  81. package/dist/observability/index.js.map +1 -0
  82. package/dist/observability/types.d.ts +139 -0
  83. package/dist/observability/types.d.ts.map +1 -0
  84. package/dist/observability/types.js +4 -0
  85. package/dist/observability/types.js.map +1 -0
  86. package/dist/onboarding/index.d.ts +16 -0
  87. package/dist/onboarding/index.d.ts.map +1 -0
  88. package/dist/onboarding/index.js +787 -0
  89. package/dist/onboarding/index.js.map +1 -0
  90. package/dist/onboarding/interactive.d.ts +23 -0
  91. package/dist/onboarding/interactive.d.ts.map +1 -0
  92. package/dist/onboarding/interactive.js +45 -0
  93. package/dist/onboarding/interactive.js.map +1 -0
  94. package/dist/onboarding/types.d.ts +388 -0
  95. package/dist/onboarding/types.d.ts.map +1 -0
  96. package/dist/onboarding/types.js +35 -0
  97. package/dist/onboarding/types.js.map +1 -0
  98. package/dist/orchestration/index.d.ts +8 -0
  99. package/dist/orchestration/index.d.ts.map +1 -0
  100. package/dist/orchestration/index.js +706 -0
  101. package/dist/orchestration/index.js.map +1 -0
  102. package/dist/orchestration/types.d.ts +391 -0
  103. package/dist/orchestration/types.d.ts.map +1 -0
  104. package/dist/orchestration/types.js +30 -0
  105. package/dist/orchestration/types.js.map +1 -0
  106. package/dist/personality/index.d.ts +65 -0
  107. package/dist/personality/index.d.ts.map +1 -0
  108. package/dist/personality/index.js +339 -0
  109. package/dist/personality/index.js.map +1 -0
  110. package/dist/personality/types.d.ts +103 -0
  111. package/dist/personality/types.d.ts.map +1 -0
  112. package/dist/personality/types.js +15 -0
  113. package/dist/personality/types.js.map +1 -0
  114. package/dist/provider/index.d.ts +4 -0
  115. package/dist/provider/index.d.ts.map +1 -0
  116. package/dist/provider/index.js +236 -0
  117. package/dist/provider/index.js.map +1 -0
  118. package/dist/provider/types.d.ts +180 -0
  119. package/dist/provider/types.d.ts.map +1 -0
  120. package/dist/provider/types.js +4 -0
  121. package/dist/provider/types.js.map +1 -0
  122. package/dist/runtime/agent-cards.d.ts +14 -0
  123. package/dist/runtime/agent-cards.d.ts.map +1 -0
  124. package/dist/runtime/agent-cards.js +90 -0
  125. package/dist/runtime/agent-cards.js.map +1 -0
  126. package/dist/runtime/agent-runner.d.ts +30 -0
  127. package/dist/runtime/agent-runner.d.ts.map +1 -0
  128. package/dist/runtime/agent-runner.js +37 -0
  129. package/dist/runtime/agent-runner.js.map +1 -0
  130. package/dist/runtime/budget.d.ts +15 -0
  131. package/dist/runtime/budget.d.ts.map +1 -0
  132. package/dist/runtime/budget.js +24 -0
  133. package/dist/runtime/budget.js.map +1 -0
  134. package/dist/runtime/delegation-driver.d.ts +11 -0
  135. package/dist/runtime/delegation-driver.d.ts.map +1 -0
  136. package/dist/runtime/delegation-driver.js +132 -0
  137. package/dist/runtime/delegation-driver.js.map +1 -0
  138. package/dist/runtime/exact-cache.d.ts +10 -0
  139. package/dist/runtime/exact-cache.d.ts.map +1 -0
  140. package/dist/runtime/exact-cache.js +30 -0
  141. package/dist/runtime/exact-cache.js.map +1 -0
  142. package/dist/runtime/execute-tool.d.ts +29 -0
  143. package/dist/runtime/execute-tool.d.ts.map +1 -0
  144. package/dist/runtime/execute-tool.js +80 -0
  145. package/dist/runtime/execute-tool.js.map +1 -0
  146. package/dist/runtime/guardian.d.ts +9 -0
  147. package/dist/runtime/guardian.d.ts.map +1 -0
  148. package/dist/runtime/guardian.js +41 -0
  149. package/dist/runtime/guardian.js.map +1 -0
  150. package/dist/runtime/hook-gate.d.ts +17 -0
  151. package/dist/runtime/hook-gate.d.ts.map +1 -0
  152. package/dist/runtime/hook-gate.js +56 -0
  153. package/dist/runtime/hook-gate.js.map +1 -0
  154. package/dist/runtime/memory-adapter.d.ts +6 -0
  155. package/dist/runtime/memory-adapter.d.ts.map +1 -0
  156. package/dist/runtime/memory-adapter.js +38 -0
  157. package/dist/runtime/memory-adapter.js.map +1 -0
  158. package/dist/runtime/nightly-adapters.d.ts +48 -0
  159. package/dist/runtime/nightly-adapters.d.ts.map +1 -0
  160. package/dist/runtime/nightly-adapters.js +139 -0
  161. package/dist/runtime/nightly-adapters.js.map +1 -0
  162. package/dist/runtime/nightly-generator.d.ts +10 -0
  163. package/dist/runtime/nightly-generator.d.ts.map +1 -0
  164. package/dist/runtime/nightly-generator.js +335 -0
  165. package/dist/runtime/nightly-generator.js.map +1 -0
  166. package/dist/runtime/onboarding-node.d.ts +6 -0
  167. package/dist/runtime/onboarding-node.d.ts.map +1 -0
  168. package/dist/runtime/onboarding-node.js +356 -0
  169. package/dist/runtime/onboarding-node.js.map +1 -0
  170. package/dist/runtime/provider-anthropic.d.ts +43 -0
  171. package/dist/runtime/provider-anthropic.d.ts.map +1 -0
  172. package/dist/runtime/provider-anthropic.js +148 -0
  173. package/dist/runtime/provider-anthropic.js.map +1 -0
  174. package/dist/runtime/provider-cli.d.ts +18 -0
  175. package/dist/runtime/provider-cli.d.ts.map +1 -0
  176. package/dist/runtime/provider-cli.js +73 -0
  177. package/dist/runtime/provider-cli.js.map +1 -0
  178. package/dist/runtime/provider-openai.d.ts +30 -0
  179. package/dist/runtime/provider-openai.d.ts.map +1 -0
  180. package/dist/runtime/provider-openai.js +114 -0
  181. package/dist/runtime/provider-openai.js.map +1 -0
  182. package/dist/runtime/providers.d.ts +43 -0
  183. package/dist/runtime/providers.d.ts.map +1 -0
  184. package/dist/runtime/providers.js +72 -0
  185. package/dist/runtime/providers.js.map +1 -0
  186. package/dist/runtime/sandbox-bash.d.ts +21 -0
  187. package/dist/runtime/sandbox-bash.d.ts.map +1 -0
  188. package/dist/runtime/sandbox-bash.js +51 -0
  189. package/dist/runtime/sandbox-bash.js.map +1 -0
  190. package/dist/runtime/scoped-tool-executor.d.ts +10 -0
  191. package/dist/runtime/scoped-tool-executor.d.ts.map +1 -0
  192. package/dist/runtime/scoped-tool-executor.js +30 -0
  193. package/dist/runtime/scoped-tool-executor.js.map +1 -0
  194. package/dist/runtime/session-log.d.ts +6 -0
  195. package/dist/runtime/session-log.d.ts.map +1 -0
  196. package/dist/runtime/session-log.js +54 -0
  197. package/dist/runtime/session-log.js.map +1 -0
  198. package/dist/runtime/settings.d.ts +24 -0
  199. package/dist/runtime/settings.d.ts.map +1 -0
  200. package/dist/runtime/settings.js +29 -0
  201. package/dist/runtime/settings.js.map +1 -0
  202. package/dist/runtime/spawn-plan.d.ts +13 -0
  203. package/dist/runtime/spawn-plan.d.ts.map +1 -0
  204. package/dist/runtime/spawn-plan.js +107 -0
  205. package/dist/runtime/spawn-plan.js.map +1 -0
  206. package/dist/runtime/spend.d.ts +41 -0
  207. package/dist/runtime/spend.d.ts.map +1 -0
  208. package/dist/runtime/spend.js +0 -0
  209. package/dist/runtime/spend.js.map +1 -0
  210. package/dist/runtime/sub-agent-runner.d.ts +19 -0
  211. package/dist/runtime/sub-agent-runner.d.ts.map +1 -0
  212. package/dist/runtime/sub-agent-runner.js +47 -0
  213. package/dist/runtime/sub-agent-runner.js.map +1 -0
  214. package/dist/safety/grants.d.ts +7 -0
  215. package/dist/safety/grants.d.ts.map +1 -0
  216. package/dist/safety/grants.js +53 -0
  217. package/dist/safety/grants.js.map +1 -0
  218. package/dist/safety/index.d.ts +72 -0
  219. package/dist/safety/index.d.ts.map +1 -0
  220. package/dist/safety/index.js +464 -0
  221. package/dist/safety/index.js.map +1 -0
  222. package/dist/safety/types.d.ts +254 -0
  223. package/dist/safety/types.d.ts.map +1 -0
  224. package/dist/safety/types.js +3 -0
  225. package/dist/safety/types.js.map +1 -0
  226. package/dist/skills/index.d.ts +4 -0
  227. package/dist/skills/index.d.ts.map +1 -0
  228. package/dist/skills/index.js +463 -0
  229. package/dist/skills/index.js.map +1 -0
  230. package/dist/skills/types.d.ts +177 -0
  231. package/dist/skills/types.d.ts.map +1 -0
  232. package/dist/skills/types.js +3 -0
  233. package/dist/skills/types.js.map +1 -0
  234. package/dist/testing/clock.d.ts +8 -0
  235. package/dist/testing/clock.d.ts.map +1 -0
  236. package/dist/testing/clock.js +13 -0
  237. package/dist/testing/clock.js.map +1 -0
  238. package/dist/testing/effect-verifier.d.ts +15 -0
  239. package/dist/testing/effect-verifier.d.ts.map +1 -0
  240. package/dist/testing/effect-verifier.js +27 -0
  241. package/dist/testing/effect-verifier.js.map +1 -0
  242. package/dist/testing/index.d.ts +5 -0
  243. package/dist/testing/index.d.ts.map +1 -0
  244. package/dist/testing/index.js +5 -0
  245. package/dist/testing/index.js.map +1 -0
  246. package/dist/testing/provider-fake.d.ts +14 -0
  247. package/dist/testing/provider-fake.d.ts.map +1 -0
  248. package/dist/testing/provider-fake.js +18 -0
  249. package/dist/testing/provider-fake.js.map +1 -0
  250. package/dist/testing/sandbox-stub.d.ts +15 -0
  251. package/dist/testing/sandbox-stub.d.ts.map +1 -0
  252. package/dist/testing/sandbox-stub.js +15 -0
  253. package/dist/testing/sandbox-stub.js.map +1 -0
  254. package/dist/tools/index.d.ts +11 -0
  255. package/dist/tools/index.d.ts.map +1 -0
  256. package/dist/tools/index.js +0 -0
  257. package/dist/tools/index.js.map +1 -0
  258. package/dist/tools/types.d.ts +138 -0
  259. package/dist/tools/types.d.ts.map +1 -0
  260. package/dist/tools/types.js +4 -0
  261. package/dist/tools/types.js.map +1 -0
  262. package/dist/triggers/index.d.ts +4 -0
  263. package/dist/triggers/index.d.ts.map +1 -0
  264. package/dist/triggers/index.js +187 -0
  265. package/dist/triggers/index.js.map +1 -0
  266. package/dist/triggers/types.d.ts +74 -0
  267. package/dist/triggers/types.d.ts.map +1 -0
  268. package/dist/triggers/types.js +5 -0
  269. package/dist/triggers/types.js.map +1 -0
  270. package/package.json +36 -0
@@ -0,0 +1,706 @@
1
+ // Orchestration — Component 11
2
+ // Deterministic control plane for multi-step work: coordinator-workers topology
3
+ // (ADR-0021), Loop Guardian retry cap (ADR-0020), generations (ADR-0005).
4
+ // See docs/specs/11-orchestration.md.
5
+ import { randomUUID, createHash } from 'node:crypto';
6
+ export { ScopeConflictError, ScopeViolationError } from './types.js';
7
+ import { ScopeConflictError, ScopeViolationError, } from './types.js';
8
+ // ---------------------------------------------------------------------------
9
+ // Glob matching — minimal, deterministic. Worker scopes use glob paths
10
+ // ('src/api/**'); `touched` carries concrete paths checked against them
11
+ // (spec §3 WorkerScope, §5.1 "code checks touched ⊆ scope.owns").
12
+ // ---------------------------------------------------------------------------
13
+ function globToRegExp(glob) {
14
+ let re = '';
15
+ for (let i = 0; i < glob.length; i++) {
16
+ const c = glob[i];
17
+ if (c === '*') {
18
+ if (glob[i + 1] === '*') {
19
+ re += '.*';
20
+ i++;
21
+ }
22
+ else {
23
+ re += '[^/]*';
24
+ }
25
+ }
26
+ else if (c === '?') {
27
+ re += '[^/]';
28
+ }
29
+ else {
30
+ re += c.replace(/[.+^${}()|[\]\\]/, '\\$&');
31
+ }
32
+ }
33
+ return new RegExp(`^${re}$`);
34
+ }
35
+ export function globMatches(glob, path) {
36
+ return globToRegExp(glob).test(path);
37
+ }
38
+ /** Literal prefix of a glob — everything before the first wildcard. */
39
+ function globRoot(glob) {
40
+ const wildcardIdx = glob.search(/[*?]/);
41
+ return wildcardIdx === -1 ? glob : glob.slice(0, wildcardIdx);
42
+ }
43
+ /**
44
+ * Conservative may-overlap test between two scope patterns: equal patterns or
45
+ * one literal root truly *containing* the other are treated as overlapping.
46
+ * Containment only counts at a path-segment boundary — the character after the
47
+ * shorter root in the longer one must be a `/` (or the shorter root already
48
+ * ends in `/`, or the roots are equal). Otherwise a bare prefix like
49
+ * 'src/api' would wrongly match the disjoint 'src/apiv2/**'.
50
+ * Used for the pairwise write-disjointness assertion (ADR-0021, AC-11-1).
51
+ */
52
+ function patternsMayOverlap(a, b) {
53
+ if (a === b)
54
+ return true;
55
+ const ra = globRoot(a);
56
+ const rb = globRoot(b);
57
+ const [shorter, longer] = ra.length <= rb.length ? [ra, rb] : [rb, ra];
58
+ if (!longer.startsWith(shorter))
59
+ return false;
60
+ return longer.length === shorter.length || shorter.endsWith('/') || longer[shorter.length] === '/';
61
+ }
62
+ function overlappingPaths(ownsA, ownsB) {
63
+ const overlap = [];
64
+ for (const a of ownsA) {
65
+ for (const b of ownsB) {
66
+ if (patternsMayOverlap(a, b))
67
+ overlap.push(a === b ? a : `${a} ~ ${b}`);
68
+ }
69
+ }
70
+ return overlap;
71
+ }
72
+ // ---------------------------------------------------------------------------
73
+ // Loop Guardian — sliding-window cycle detector for periods 1/2/3 (ADR-0020).
74
+ // A cycle repeating more than `maxRepeats` times latches a STOP; the verdict
75
+ // persists across further checks (no auto-resume, spec §7 / AC-11-7). Cycles
76
+ // of period ≥4 are by design invisible here and are caught by the global
77
+ // budget cap instead (spec §5.2 / AC-11-8).
78
+ // ---------------------------------------------------------------------------
79
+ const DEFAULT_MAX_REPEATS = 3;
80
+ const DEFAULT_WINDOW_SIZE = 32;
81
+ export function makeLoopGuardian(config = {}) {
82
+ const maxRepeats = config.maxRepeats ?? DEFAULT_MAX_REPEATS;
83
+ const windowSize = config.windowSize ?? DEFAULT_WINDOW_SIZE;
84
+ let window = [];
85
+ let latched;
86
+ return {
87
+ check(step) {
88
+ // STOP is latched — the run never auto-resumes (AC-11-7).
89
+ if (latched !== undefined)
90
+ return latched;
91
+ window.push(step);
92
+ if (window.length > windowSize)
93
+ window.shift();
94
+ for (const period of [1, 2, 3]) {
95
+ if (window.length < period * 2)
96
+ continue;
97
+ // The candidate cycle is the last `period` actions; count how many
98
+ // times it repeats consecutively at the tail of the window.
99
+ const block = window.slice(-period).map(s => s.actionId);
100
+ let repeats = 1;
101
+ let pos = window.length - 2 * period;
102
+ while (pos >= 0) {
103
+ let same = true;
104
+ for (let i = 0; i < period; i++) {
105
+ if (window[pos + i].actionId !== block[i]) {
106
+ same = false;
107
+ break;
108
+ }
109
+ }
110
+ if (!same)
111
+ break;
112
+ repeats++;
113
+ pos -= period;
114
+ }
115
+ if (repeats > maxRepeats) {
116
+ latched = { stop: true, period, repeatCount: repeats, windowSnapshot: [...window] };
117
+ return latched;
118
+ }
119
+ }
120
+ return { pass: true };
121
+ },
122
+ reset() {
123
+ window = [];
124
+ latched = undefined;
125
+ },
126
+ };
127
+ }
128
+ // ---------------------------------------------------------------------------
129
+ // Coordinator — decomposition, scope-checked spawn, journal reconciliation
130
+ // (ADR-0021). Scope assignment and all invariants below are code (100%
131
+ // adherence); only the *wording* of intents/entries defers to the model.
132
+ // ---------------------------------------------------------------------------
133
+ /** §7: with the Loop Guardian unavailable the iteration cap tightens to this floor. */
134
+ const CONSERVATIVE_ITERATION_FLOOR = 25;
135
+ /** §4 example scope contract — default per-worker budget slice. */
136
+ const DEFAULT_WORKER_ITERATIONS = 40;
137
+ const DEFAULT_WORKER_SPEND_USD = 0.5;
138
+ export function makeCoordinator(deps) {
139
+ const runId = `r-${randomUUID().slice(0, 8)}`;
140
+ let generationId = 'g-1';
141
+ const spawnedScopes = new Map();
142
+ /** Workers that attempted an out-of-scope write; excluded from the merge (§7). */
143
+ const faulted = new Set();
144
+ /** Mirror of budget.json.nested — proves which bound stopped the run (§5.2). */
145
+ const nested = { loopGuardianTrips: 0, planReplans: 0, skillFailures: {} };
146
+ let haltReason;
147
+ let degraded = false;
148
+ /**
149
+ * Coordinator-controlled monotonic seq. The journal `seq` is a tamper signal
150
+ * (§4), so a worker must never assign its own — a hostile/buggy worker could
151
+ * forge collisions or gaps. The coordinator alone advances it (§8 repudiation).
152
+ */
153
+ let nextSeq = 1;
154
+ function emit(kind, payload) {
155
+ deps.emit({ kind, runId, ts: new Date().toISOString(), payload });
156
+ }
157
+ /** Halt fail-closed on the first cap reached; never proceeds past it (§5.2). */
158
+ function halt(reason, extra) {
159
+ if (haltReason !== undefined)
160
+ return;
161
+ haltReason = reason;
162
+ emit('run.terminated', { reason, nested: { ...nested }, ...extra });
163
+ }
164
+ // Cold start (§7 / AC-11-14): no run state exists yet — fail closed by
165
+ // construction: fresh g-1, empty journal, fresh ledger, zero workers, and
166
+ // no orphan-resumption capability at all.
167
+ //
168
+ // Guardian heartbeat probe (§7 / AC-11-15): if Observability's Loop
169
+ // Guardian is unreachable, do not run unattended at normal caps — flag the
170
+ // run degraded and tighten the iteration cap to the conservative floor.
171
+ let startupVerdict;
172
+ try {
173
+ startupVerdict = deps.loopGuardian.check({ actionId: '__heartbeat__', seq: 0 });
174
+ if (!('stop' in startupVerdict))
175
+ deps.loopGuardian.reset();
176
+ }
177
+ catch {
178
+ degraded = true;
179
+ }
180
+ emit('run.started', {
181
+ generationId,
182
+ degraded,
183
+ iterationCapFloor: degraded ? CONSERVATIVE_ITERATION_FLOOR : undefined,
184
+ });
185
+ // Precedence (§5.2 / AC-11-9): the Loop-Guardian STOP is the innermost
186
+ // bound and fires before any budget charge — a latched STOP halts here.
187
+ if (startupVerdict !== undefined && 'stop' in startupVerdict) {
188
+ nested.loopGuardianTrips++;
189
+ halt('loop-guardian', { reviewCard: { windowSnapshot: startupVerdict.windowSnapshot } });
190
+ }
191
+ function checkScope(touched, scope) {
192
+ // §5.1: touched ⊆ owns ∧ touched ∩ doNotTouch = ∅; doNotTouch overrides owns.
193
+ const insideDeny = touched.filter(p => scope.doNotTouch.some(g => globMatches(g, p)));
194
+ if (insideDeny.length > 0)
195
+ return { paths: insideDeny, reason: 'inside-doNotTouch' };
196
+ const outsideOwns = touched.filter(p => !scope.owns.some(g => globMatches(g, p)));
197
+ if (outsideOwns.length > 0)
198
+ return { paths: outsideOwns, reason: 'outside-owns' };
199
+ return undefined;
200
+ }
201
+ function abortReconcile(reason) {
202
+ // §7 seq gap / tamper: reconciliation aborts, run is halted as untrusted.
203
+ halt('untrusted-journal', { integrity: reason });
204
+ return { aborted: reason };
205
+ }
206
+ /**
207
+ * Dead-end fork (§5.3 / ADR-0005). The model would distill lessons (~70%); the
208
+ * fork itself is code. The new generation carries constitution + lessons only —
209
+ * the failed transcript is dropped — and per-generation counters reset, but the
210
+ * run-level spend cap is NOT reset (no budget-reset evasion, §8). Switching
211
+ * `generationId` is the deterministic transcript drop: subsequent entries are
212
+ * stamped with the fresh generation, never the dead-ended one.
213
+ */
214
+ function forkOnDeadEnd(lessons) {
215
+ const newGen = deps.generationManager.fork(runId, lessons);
216
+ const parent = generationId;
217
+ generationId = newGen;
218
+ nested.loopGuardianTrips = 0;
219
+ nested.planReplans = 0;
220
+ nested.skillFailures = {};
221
+ emit('generation.forked', { generationId: newGen, parent });
222
+ return newGen;
223
+ }
224
+ return {
225
+ decompose(task, _gen) {
226
+ // Decomposition is a model call in production (~70%, spec §5.1); the
227
+ // deterministic carve here is one worker per affected path. Each
228
+ // worker's doNotTouch explicitly denies every peer's lane.
229
+ const paths = [...new Set(task.affectedPaths)];
230
+ const briefs = paths.map((path, i) => ({
231
+ workerId: `w-${i + 1}`,
232
+ intent: `Apply '${task.description}' within ${path}`,
233
+ scope: {
234
+ owns: [path],
235
+ doNotTouch: paths.filter(p => p !== path),
236
+ taskClass: 'reasoning',
237
+ budgetSlice: { iterations: DEFAULT_WORKER_ITERATIONS, spendUsd: DEFAULT_WORKER_SPEND_USD },
238
+ },
239
+ }));
240
+ // Code-enforced (ADR-0021 / AC-11-1): scopes must be pairwise
241
+ // write-disjoint before any spawn; an overlap halts the run instead.
242
+ for (let i = 0; i < briefs.length; i++) {
243
+ for (let j = i + 1; j < briefs.length; j++) {
244
+ const overlap = overlappingPaths(briefs[i].scope.owns, briefs[j].scope.owns);
245
+ if (overlap.length > 0) {
246
+ throw new ScopeConflictError(briefs[i].workerId, briefs[j].workerId, overlap);
247
+ }
248
+ }
249
+ }
250
+ return briefs;
251
+ },
252
+ async spawn(brief) {
253
+ if (haltReason !== undefined) {
254
+ throw new Error(`run halted (${haltReason}); refusing to spawn worker '${brief.workerId}'`);
255
+ }
256
+ // Reject any scope overlapping an already-spawned worker (AC-11-1).
257
+ for (const [otherId, otherScope] of spawnedScopes) {
258
+ const overlap = overlappingPaths(brief.scope.owns, otherScope.owns);
259
+ if (overlap.length > 0) {
260
+ throw new ScopeConflictError(otherId, brief.workerId, overlap);
261
+ }
262
+ }
263
+ spawnedScopes.set(brief.workerId, brief.scope);
264
+ emit('worker.spawned', { workerId: brief.workerId, scope: brief.scope });
265
+ // The handle deliberately exposes NO peer channel — no sendToPeer /
266
+ // message / peers. The no-peer-to-peer invariant is enforced by
267
+ // absence of capability (ADR-0021 / AC-11-3); the only sink a worker
268
+ // has is appendDecision into the shared journal.
269
+ const handle = {
270
+ workerId: brief.workerId,
271
+ appendDecision: (partial) => {
272
+ // Fail closed (§7): a worker that already committed a scope violation
273
+ // is faulted and excluded from the merge — it must not be able to
274
+ // write any further entries into the shared journal (AC-11-2).
275
+ if (faulted.has(brief.workerId)) {
276
+ throw new ScopeViolationError(brief.workerId, partial.touched, 'outside-owns');
277
+ }
278
+ const violation = checkScope(partial.touched, brief.scope);
279
+ if (violation !== undefined) {
280
+ // Fail closed (§7): reject the append, mark the worker faulted
281
+ // so its output never enters the merge (AC-11-2).
282
+ faulted.add(brief.workerId);
283
+ emit('scope.violation', {
284
+ workerId: brief.workerId,
285
+ violatingPaths: violation.paths,
286
+ reason: violation.reason,
287
+ });
288
+ throw new ScopeViolationError(brief.workerId, violation.paths, violation.reason);
289
+ }
290
+ // Seq is coordinator-controlled (§4 / §8): override any worker-supplied
291
+ // value with a fresh monotonic seq so the journal stays tamper-evident.
292
+ const entry = {
293
+ runId,
294
+ generationId,
295
+ workerId: brief.workerId,
296
+ ...partial,
297
+ seq: nextSeq++,
298
+ };
299
+ deps.journal.append(entry);
300
+ emit('journal.appended', { workerId: brief.workerId, seq: entry.seq });
301
+ },
302
+ done: Promise.resolve(),
303
+ };
304
+ return handle;
305
+ },
306
+ reconcile(targetRunId) {
307
+ const all = deps.journal.read(targetRunId);
308
+ // Integrity first (AC-11-5/6): `seq` must be strictly monotonic with no
309
+ // gaps. A gap means loss/tampering; a duplicate means mutation. Both
310
+ // fail closed — no merge is ever produced from an untrusted journal.
311
+ const seqs = all.map(e => e.seq).sort((a, b) => a - b);
312
+ for (let i = 1; i < seqs.length; i++) {
313
+ const delta = seqs[i] - seqs[i - 1];
314
+ if (delta === 0)
315
+ return abortReconcile('untrusted');
316
+ if (delta > 1)
317
+ return abortReconcile('seq-gap');
318
+ }
319
+ // Faulted workers' decisions never enter the merge (§7 scope violation).
320
+ const entries = all.filter(e => !faulted.has(e.workerId));
321
+ // Contradiction scan (AC-11-4 / ADR-0021): two entries conflict when they
322
+ // touch a shared resource and chose *different* options for it. Only one
323
+ // decision can hold per resource, so distinct `decidedFor` on a shared path
324
+ // is incompatible — regardless of whether either recorded a competing
325
+ // `decidedAgainst` (which is often '' when there was no rival option). The
326
+ // earlier FOR==AGAINST-only test silently glued together exactly this common
327
+ // case, the failure ADR-0021 exists to prevent.
328
+ const conflicts = [];
329
+ for (let i = 0; i < entries.length; i++) {
330
+ for (let j = i + 1; j < entries.length; j++) {
331
+ const a = entries[i];
332
+ const b = entries[j];
333
+ if (a.decidedFor === b.decidedFor)
334
+ continue;
335
+ const shared = a.touched.find(p => b.touched.includes(p));
336
+ if (shared === undefined)
337
+ continue;
338
+ conflicts.push({ entryA: a, entryB: b, resource: shared });
339
+ }
340
+ }
341
+ if (conflicts.length === 0) {
342
+ emit('run.reconciled', { entries: entries.length, conflicts: 0 });
343
+ return { merged: entries };
344
+ }
345
+ // Deterministic resolution (§5.1): one merger, not dialogue — the
346
+ // earliest decision (lowest seq) wins and the coordinator records its
347
+ // own journal entry; the losing decision is excluded from the merge.
348
+ const excluded = new Set();
349
+ const resolutions = [];
350
+ let resolutionSeq = (seqs[seqs.length - 1] ?? 0) + 1;
351
+ for (const c of conflicts) {
352
+ const winner = c.entryA.seq <= c.entryB.seq ? c.entryA : c.entryB;
353
+ const loser = winner === c.entryA ? c.entryB : c.entryA;
354
+ excluded.add(loser);
355
+ const resolution = {
356
+ runId: targetRunId,
357
+ generationId,
358
+ workerId: 'coordinator',
359
+ seq: resolutionSeq++,
360
+ decidedFor: winner.decidedFor,
361
+ decidedAgainst: loser.decidedFor,
362
+ because: `coordinator resolution over '${c.resource}': ` +
363
+ `earliest decision (seq ${winner.seq}, ${winner.workerId}) is the single source of truth`,
364
+ touched: [],
365
+ ts: new Date().toISOString(),
366
+ };
367
+ deps.journal.append(resolution);
368
+ resolutions.push(resolution);
369
+ }
370
+ const merged = [...entries.filter(e => !excluded.has(e)), ...resolutions];
371
+ emit('run.reconciled', { entries: merged.length, conflicts: conflicts.length });
372
+ return { merged };
373
+ },
374
+ // §5.2 global-budget backstop (AC-11-8/9). Every iteration / tool dispatch
375
+ // is charged here; the run never proceeds past the first cap reached. A
376
+ // `global-budget` cap is a dead-end trigger (§5.3) → fork a fresh generation.
377
+ charge(cost) {
378
+ // Fail-closed: once halted, never charge or proceed again.
379
+ if (haltReason !== undefined) {
380
+ return { capped: true, reason: 'global-budget' };
381
+ }
382
+ const verdict = deps.budgetGuard.charge(runId, cost);
383
+ if ('capped' in verdict) {
384
+ emit('budget.capped', { reason: verdict.reason });
385
+ halt(verdict.reason);
386
+ if (verdict.reason === 'global-budget') {
387
+ // Dead-end: distillation is the model's job in production; the fork is code.
388
+ forkOnDeadEnd([{ summary: `run hit global budget cap` }]);
389
+ }
390
+ }
391
+ return verdict;
392
+ },
393
+ // ADR-0025 / AC-11-10: advisory only. N≥3 failures lower the strategy's
394
+ // priority in a worker's choice (recorded in the nested ledger); this
395
+ // never emits run.terminated and never blocks a spawn.
396
+ onSkillFailure(skill) {
397
+ nested.skillFailures[skill] = (nested.skillFailures[skill] ?? 0) + 1;
398
+ },
399
+ };
400
+ }
401
+ // ---------------------------------------------------------------------------
402
+ // First-class sub-agent delegation (ADR-0039, spec §5.4/§5.5).
403
+ //
404
+ // A goal-DAG of DelegationTasks, each served by a sub-agent whose capabilities
405
+ // are fixed by an AgentCard (the model cannot self-widen). State hands off
406
+ // without loss: every delegation owns a hash-chained shard; the parent receives
407
+ // only a compact TaskObservation. Reuses the §5.1 scope-disjointness logic, the
408
+ // ADR-0020 Loop Guardian, and ScopeConflictError — nothing here is a new runtime.
409
+ // ---------------------------------------------------------------------------
410
+ const SHARD_GENESIS = '0'.repeat(64);
411
+ /** Deterministic JSON: object keys sorted recursively (stable hashing input). */
412
+ function stableStringify(value) {
413
+ return JSON.stringify(value, (_k, v) => v !== null && typeof v === 'object' && !Array.isArray(v)
414
+ ? Object.fromEntries(Object.keys(v)
415
+ .sort()
416
+ .map(k => [k, v[k]]))
417
+ : v);
418
+ }
419
+ /**
420
+ * Deep clone a shard payload so neither the caller (who keeps a reference to
421
+ * what it passed to append) nor a post-mortem shard() reader can mutate an
422
+ * "immutable" audit entry after the fact. structuredClone handles cycles/Dates;
423
+ * the rare non-cloneable payload (e.g. a function) falls back to the original.
424
+ */
425
+ function safeClone(v) {
426
+ try {
427
+ return structuredClone(v);
428
+ }
429
+ catch {
430
+ return v;
431
+ }
432
+ }
433
+ function cloneEntry(e) {
434
+ return { ...e, payload: safeClone(e.payload) };
435
+ }
436
+ function shardEntryHash(e) {
437
+ const payloadHash = createHash('sha256').update(stableStringify(e.payload), 'utf8').digest('hex');
438
+ return createHash('sha256')
439
+ .update(stableStringify({
440
+ delegationId: e.delegationId,
441
+ seq: e.seq,
442
+ prevHash: e.prevHash,
443
+ kind: e.kind,
444
+ ts: e.ts,
445
+ payloadHash,
446
+ }), 'utf8')
447
+ .digest('hex');
448
+ }
449
+ function isPlanDAG(plan) {
450
+ return Array.isArray(plan.nodes);
451
+ }
452
+ /**
453
+ * Normalize a Core (01) linear plan into a degenerate goal-DAG: each step
454
+ * becomes a node depending on the previous one, preserving sequential order
455
+ * (AC-11-16). A PlanDAG is returned unchanged. The Core `Plan` shape itself is
456
+ * never modified — the union lives here, at the orchestration layer.
457
+ */
458
+ function normalizePlan(plan) {
459
+ if (isPlanDAG(plan))
460
+ return { nodes: [...plan.nodes], edges: [...plan.edges] };
461
+ const nodes = plan.steps.map((step, i) => ({
462
+ taskId: `s${i + 1}`,
463
+ intent: step.intent ?? '',
464
+ assignedTo: null,
465
+ dependsOn: i > 0 ? [`s${i}`] : [],
466
+ scope: { owns: [], doNotTouch: [], taskClass: 'reasoning' },
467
+ budgetSlice: { iterations: DEFAULT_WORKER_ITERATIONS, spendUsd: DEFAULT_WORKER_SPEND_USD },
468
+ outputContract: '',
469
+ retryPolicy: { maxReplans: 0, maxIterations: DEFAULT_WORKER_ITERATIONS },
470
+ }));
471
+ const edges = nodes.slice(1).map((n, i) => ({ from: `s${i + 1}`, to: n.taskId }));
472
+ return { nodes, edges };
473
+ }
474
+ export function makeDelegationManager(plan, deps) {
475
+ const runId = `r-${randomUUID().slice(0, 8)}`;
476
+ const dagPlan = normalizePlan(plan);
477
+ const tasksById = new Map(dagPlan.nodes.map(t => [t.taskId, t]));
478
+ const completed = new Set();
479
+ const failed = new Set();
480
+ const skipped = new Set();
481
+ const delegations = new Map();
482
+ const checkpoints = new Map();
483
+ const runBudget = { iterations: 0, spendUsd: 0, wallMs: 0 };
484
+ const delegationIdFor = (taskId) => `d-${taskId}`;
485
+ function emit(kind, payload) {
486
+ deps.emit({ kind, runId, ts: new Date().toISOString(), payload });
487
+ }
488
+ function isTerminal(taskId) {
489
+ return completed.has(taskId) || failed.has(taskId) || skipped.has(taskId);
490
+ }
491
+ /** Owns of every delegation still holding write scope (active or resumed). */
492
+ function activeOwns(exceptId) {
493
+ const out = [];
494
+ for (const [id, st] of delegations) {
495
+ if (id === exceptId)
496
+ continue;
497
+ if (st.status === 'active' || st.status === 'resumed')
498
+ out.push({ id, owns: st.owns });
499
+ }
500
+ return out;
501
+ }
502
+ function addCost(cost) {
503
+ runBudget.iterations += cost.iterations;
504
+ runBudget.spendUsd += cost.spendUsd;
505
+ runBudget.wallMs += cost.wallMs;
506
+ }
507
+ function appendShard(state, delegationId, kind, payload) {
508
+ const prev = state.entries[state.entries.length - 1];
509
+ const seq = (prev?.seq ?? 0) + 1;
510
+ const prevHash = prev?.hash ?? SHARD_GENESIS;
511
+ const ts = new Date().toISOString();
512
+ // Sever the caller's reference: store an independent deep copy so the
513
+ // append-only shard stays tamper-evident (a circular payload fails fast at
514
+ // the hash step below, never reaching storage).
515
+ const stored = safeClone(payload);
516
+ const base = { delegationId, seq, prevHash, kind, payload: stored, ts };
517
+ const entry = { ...base, hash: shardEntryHash(base) };
518
+ state.entries.push(entry);
519
+ return cloneEntry(entry);
520
+ }
521
+ function writeCheckpoint(delegationId, state) {
522
+ const last = state.entries[state.entries.length - 1];
523
+ checkpoints.set(delegationId, {
524
+ delegationId,
525
+ taskId: state.task.taskId,
526
+ scope: state.task.scope,
527
+ snapshotPrefixHash: last?.hash ?? SHARD_GENESIS,
528
+ lastSeq: last?.seq ?? 0,
529
+ });
530
+ }
531
+ function makeHandle(delegationId, state) {
532
+ return {
533
+ delegationId,
534
+ taskId: state.task.taskId,
535
+ card: state.card,
536
+ owns: [...state.owns],
537
+ writableMcp: [...state.writableMcp],
538
+ permitsTool: (name) => state.permittedTools.has(name),
539
+ permitsMcp: (server) => state.writableMcp.includes(server),
540
+ append: (kind, payload) => appendShard(state, delegationId, kind, payload),
541
+ shard: () => state.entries.map(cloneEntry),
542
+ get guardian() {
543
+ return state.guardian;
544
+ },
545
+ complete: (summary, result, cost) => {
546
+ state.status = 'completed';
547
+ completed.add(state.task.taskId);
548
+ addCost(cost);
549
+ emit('delegation.completed', { delegationId, taskId: state.task.taskId });
550
+ return { delegationId, status: 'completed', summary, touched: [...state.owns], result, cost };
551
+ },
552
+ fail: (summary, cost) => {
553
+ state.status = 'failed';
554
+ failed.add(state.task.taskId);
555
+ addCost(cost);
556
+ writeCheckpoint(delegationId, state);
557
+ emit('delegation.failed', { delegationId, taskId: state.task.taskId });
558
+ return { delegationId, status: 'failed', summary, touched: [...state.owns], result: undefined, cost };
559
+ },
560
+ };
561
+ }
562
+ return {
563
+ dag() {
564
+ return { nodes: [...dagPlan.nodes], edges: [...dagPlan.edges] };
565
+ },
566
+ readySet() {
567
+ // Deterministic: input order, a task ready only when every dependency has
568
+ // COMPLETED and it has not itself run / been spawned (AC-11-16).
569
+ return dagPlan.nodes.filter(t => !isTerminal(t.taskId) &&
570
+ !delegations.has(delegationIdFor(t.taskId)) &&
571
+ t.dependsOn.every(dep => completed.has(dep)));
572
+ },
573
+ spawn(taskId, requested) {
574
+ const task = tasksById.get(taskId);
575
+ if (task === undefined)
576
+ throw new Error(`unknown delegation task '${taskId}'`);
577
+ const delegationId = delegationIdFor(taskId);
578
+ if (delegations.has(delegationId)) {
579
+ throw new Error(`delegation '${delegationId}' already spawned — use resume()`);
580
+ }
581
+ if (failed.has(taskId) || skipped.has(taskId)) {
582
+ throw new Error(`task '${taskId}' is terminal (failed/cascade-skipped) and not runnable`);
583
+ }
584
+ // Deterministic ready-set: refuse a task whose upstream has not completed.
585
+ const unmet = task.dependsOn.filter(d => !completed.has(d));
586
+ if (unmet.length > 0) {
587
+ throw new Error(`task '${taskId}' not ready: unmet dependencies [${unmet.join(', ')}]`);
588
+ }
589
+ if (task.assignedTo === null) {
590
+ throw new Error(`task '${taskId}' has no assigned AgentCard`);
591
+ }
592
+ const card = deps.resolveCard(task.assignedTo);
593
+ if (card === undefined)
594
+ throw new Error(`AgentCard '${task.assignedTo}' not found`);
595
+ // §5.5 scope composition. The granted lane is owns minus doNotTouch; a
596
+ // declared skill writing outside it is a ScopeConflictError and no
597
+ // sub-agent starts. owns = task.scope.owns ∪ skill-touched, which equals
598
+ // task.scope.owns precisely because skill-touched ⊆ the granted lane.
599
+ const inGrantedLane = (p) => task.scope.owns.some(g => globMatches(g, p)) &&
600
+ !task.scope.doNotTouch.some(g => globMatches(g, p));
601
+ const skillTouched = card.skills.flatMap(s => deps.skillTouchedPaths(s));
602
+ const outOfLane = skillTouched.filter(p => !inGrantedLane(p));
603
+ if (outOfLane.length > 0) {
604
+ emit('scope.violation', { delegationId, taskId, reason: 'skill-outside-lane', paths: outOfLane });
605
+ throw new ScopeConflictError(delegationId, `card:${card.name}`, outOfLane);
606
+ }
607
+ const owns = [...task.scope.owns];
608
+ // Pairwise write-disjointness across ALL active delegations (§5.5).
609
+ for (const other of activeOwns()) {
610
+ const overlap = overlappingPaths(owns, other.owns);
611
+ if (overlap.length > 0)
612
+ throw new ScopeConflictError(other.id, delegationId, overlap);
613
+ }
614
+ // Capabilities are the card's; any model-emitted request that exceeds the
615
+ // card is ignored — the card is the sole authority (AC-11-17).
616
+ void requested;
617
+ const permittedTools = new Set(Object.keys(card.toolTiers));
618
+ const writableMcp = card.mcpAllowlist.filter(s => deps.mcpWritable(s));
619
+ const state = {
620
+ task,
621
+ card,
622
+ owns,
623
+ writableMcp,
624
+ permittedTools,
625
+ entries: [],
626
+ guardian: makeLoopGuardian({}),
627
+ status: 'active',
628
+ };
629
+ delegations.set(delegationId, state);
630
+ emit('delegation.spawned', { delegationId, taskId, owns, card: card.name });
631
+ return makeHandle(delegationId, state);
632
+ },
633
+ resume(delegationId) {
634
+ const state = delegations.get(delegationId);
635
+ if (state === undefined)
636
+ throw new Error(`cannot resume unknown delegation '${delegationId}'`);
637
+ const cp = checkpoints.get(delegationId);
638
+ if (cp === undefined)
639
+ throw new Error(`no checkpoint for '${delegationId}' — nothing to resume`);
640
+ // Resume from checkpoint.lastSeq: the shard already holds entries up to
641
+ // lastSeq, so further appends continue the chain. Reset ONLY the local
642
+ // Loop Guardian; the run-level budget is inherited (no budget-reset
643
+ // evasion, §8) and downstream cascade state is untouched (AC-11-20).
644
+ failed.delete(state.task.taskId);
645
+ state.status = 'resumed';
646
+ state.guardian = makeLoopGuardian({});
647
+ emit('delegation.resumed', { delegationId, fromSeq: cp.lastSeq });
648
+ return makeHandle(delegationId, state);
649
+ },
650
+ schedule() {
651
+ // Cascade-skip: any non-terminal task with a failed or already-skipped
652
+ // ancestor is skipped, transitively, with an explicit journal entry — no
653
+ // silent drop (AC-11-20). Once skipped, a task is terminal and not re-emitted.
654
+ let changed = true;
655
+ while (changed) {
656
+ changed = false;
657
+ for (const t of dagPlan.nodes) {
658
+ if (isTerminal(t.taskId))
659
+ continue;
660
+ const blocked = t.dependsOn.some(d => failed.has(d) || skipped.has(d));
661
+ if (blocked) {
662
+ skipped.add(t.taskId);
663
+ emit('cascade-skip', { taskId: t.taskId, reason: 'upstream-failed' });
664
+ changed = true;
665
+ }
666
+ }
667
+ }
668
+ const ready = dagPlan.nodes
669
+ .filter(t => !isTerminal(t.taskId) &&
670
+ !delegations.has(delegationIdFor(t.taskId)) &&
671
+ t.dependsOn.every(dep => completed.has(dep)))
672
+ .map(t => t.taskId);
673
+ const cascadeSkipped = dagPlan.nodes.filter(t => skipped.has(t.taskId)).map(t => t.taskId);
674
+ return { ready, cascadeSkipped };
675
+ },
676
+ runBudgetSpent() {
677
+ return { ...runBudget };
678
+ },
679
+ verifyShardChain(delegationId) {
680
+ const state = delegations.get(delegationId);
681
+ if (state === undefined)
682
+ return false;
683
+ let prev = SHARD_GENESIS;
684
+ for (let i = 0; i < state.entries.length; i++) {
685
+ const e = state.entries[i];
686
+ if (e.seq !== i + 1)
687
+ return false;
688
+ if (e.prevHash !== prev)
689
+ return false;
690
+ const recomputed = shardEntryHash({
691
+ delegationId: e.delegationId,
692
+ seq: e.seq,
693
+ prevHash: e.prevHash,
694
+ kind: e.kind,
695
+ payload: e.payload,
696
+ ts: e.ts,
697
+ });
698
+ if (recomputed !== e.hash)
699
+ return false;
700
+ prev = e.hash;
701
+ }
702
+ return true;
703
+ },
704
+ };
705
+ }
706
+ //# sourceMappingURL=index.js.map