@agilno-tech/rivet 0.1.0-alpha.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 (283) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +6 -0
  3. package/README.md +59 -0
  4. package/bin/cli.js +42 -0
  5. package/dist/agents/backend-django-agent/SKILL.md +35 -0
  6. package/dist/agents/backend-nestjs-agent/SKILL.md +36 -0
  7. package/dist/agents/boss-agent/SKILL.md +28 -0
  8. package/dist/agents/bounded-worker/SKILL.md +28 -0
  9. package/dist/agents/data-performance-agent/SKILL.md +32 -0
  10. package/dist/agents/delivery-ticketing-agent/SKILL.md +36 -0
  11. package/dist/agents/devops-agent/SKILL.md +34 -0
  12. package/dist/agents/engineering-manager/SKILL.md +28 -0
  13. package/dist/agents/frontend-nextjs-agent/SKILL.md +34 -0
  14. package/dist/agents/frontend-web-agent/SKILL.md +31 -0
  15. package/dist/agents/incident-response-agent/SKILL.md +42 -0
  16. package/dist/agents/jest-agent/SKILL.md +77 -0
  17. package/dist/agents/mobile-agent/SKILL.md +32 -0
  18. package/dist/agents/playwright-agent/SKILL.md +54 -0
  19. package/dist/agents/product-design-manager/SKILL.md +27 -0
  20. package/dist/agents/qa-agent/SKILL.md +29 -0
  21. package/dist/agents/quality-manager/SKILL.md +28 -0
  22. package/dist/agents/vitest-agent/SKILL.md +77 -0
  23. package/dist/governance/pre-push-rules.md +75 -0
  24. package/dist/governance/prompt-hygiene.md +22 -0
  25. package/dist/governance/review-checklist.md +24 -0
  26. package/dist/governance/safety-and-data.md +27 -0
  27. package/dist/governance/usage-rules.md +21 -0
  28. package/dist/mandatory/address-pr-feedback/SKILL.md +196 -0
  29. package/dist/mandatory/agentic-goal/SKILL.md +42 -0
  30. package/dist/mandatory/agentic-status/SKILL.md +74 -0
  31. package/dist/mandatory/apply-design/SKILL.md +228 -0
  32. package/dist/mandatory/check-ac/SKILL.md +124 -0
  33. package/dist/mandatory/create-pr-and-commit/SKILL.md +304 -0
  34. package/dist/mandatory/design/SKILL.md +312 -0
  35. package/dist/mandatory/feature-workflow/SKILL.md +87 -0
  36. package/dist/mandatory/hotfix/SKILL.md +195 -0
  37. package/dist/mandatory/pre-push/SKILL.md +272 -0
  38. package/dist/mandatory/project-context/SKILL.md +180 -0
  39. package/dist/mandatory/release-docs/SKILL.md +191 -0
  40. package/dist/mandatory/review-pr/SKILL.md +323 -0
  41. package/dist/mandatory/security-review/SKILL.md +157 -0
  42. package/dist/skills/api-contract/SKILL.md +77 -0
  43. package/dist/skills/backend-django/SKILL.md +27 -0
  44. package/dist/skills/backend-nestjs/SKILL.md +65 -0
  45. package/dist/skills/bug-ticket-creation/SKILL.md +43 -0
  46. package/dist/skills/cloudwatch-troubleshooting/SKILL.md +73 -0
  47. package/dist/skills/database-migration/SKILL.md +97 -0
  48. package/dist/skills/debugging/SKILL.md +32 -0
  49. package/dist/skills/devops-infra/SKILL.md +26 -0
  50. package/dist/skills/documentation/SKILL.md +24 -0
  51. package/dist/skills/frontend-nextjs/SKILL.md +27 -0
  52. package/dist/skills/incident-postmortem/SKILL.md +38 -0
  53. package/dist/skills/kubernetes-troubleshooting/SKILL.md +71 -0
  54. package/dist/skills/mobile-react-native/SKILL.md +37 -0
  55. package/dist/skills/postgres-analytics/SKILL.md +29 -0
  56. package/dist/skills/product-jira-ticketing/SKILL.md +28 -0
  57. package/dist/skills/qa-bug-analysis/SKILL.md +27 -0
  58. package/dist/skills/refactoring/SKILL.md +32 -0
  59. package/dist/skills/sprint-planning/SKILL.md +32 -0
  60. package/dist/skills/testing-quality/SKILL.md +62 -0
  61. package/dist/v2/protocols/agent-orchestration.md +49 -0
  62. package/dist/v2/protocols/delivery.md +45 -0
  63. package/dist/v2/protocols/design-authority.md +29 -0
  64. package/dist/v2/protocols/goal-graph.md +44 -0
  65. package/dist/v2/protocols/pattern-first-development.md +29 -0
  66. package/dist/v2/protocols/qa-evidence.md +29 -0
  67. package/dist/v2/protocols/security.md +29 -0
  68. package/dist/v2/schemas/event.schema.json +86 -0
  69. package/dist/v2/schemas/evidence.schema.json +94 -0
  70. package/dist/v2/schemas/feature-decomposition.schema.json +48 -0
  71. package/dist/v2/schemas/feature-plan.schema.json +95 -0
  72. package/dist/v2/schemas/goal-graph.schema.json +61 -0
  73. package/dist/v2/schemas/integration.schema.json +119 -0
  74. package/dist/v2/schemas/orchestration.schema.json +69 -0
  75. package/dist/v2/schemas/project.schema.json +209 -0
  76. package/dist/v2/schemas/providers.schema.json +142 -0
  77. package/dist/v2/schemas/quality.schema.json +50 -0
  78. package/dist/v2/schemas/work-action.schema.json +44 -0
  79. package/dist/v2/schemas/work-request.schema.json +253 -0
  80. package/dist/v2/templates/evidence/qa-bundle.json +194 -0
  81. package/dist/v2/templates/github-actions/rivet-deploy.yml +98 -0
  82. package/dist/v2/templates/harness/SKILL.md +90 -0
  83. package/dist/v2/templates/project/.rivet/orchestration.yaml +42 -0
  84. package/dist/v2/templates/project/.rivet/project.yaml +14 -0
  85. package/dist/v2/templates/project/.rivet/providers.yaml +8 -0
  86. package/dist/v2/templates/project/.rivet/quality.yaml +16 -0
  87. package/package.json +58 -0
  88. package/protocols/agent-orchestration.md +49 -0
  89. package/protocols/delivery.md +45 -0
  90. package/protocols/design-authority.md +29 -0
  91. package/protocols/goal-graph.md +44 -0
  92. package/protocols/pattern-first-development.md +29 -0
  93. package/protocols/qa-evidence.md +29 -0
  94. package/protocols/security.md +29 -0
  95. package/schemas/event.schema.json +86 -0
  96. package/schemas/evidence.schema.json +94 -0
  97. package/schemas/feature-decomposition.schema.json +48 -0
  98. package/schemas/feature-plan.schema.json +95 -0
  99. package/schemas/goal-graph.schema.json +61 -0
  100. package/schemas/integration.schema.json +119 -0
  101. package/schemas/orchestration.schema.json +69 -0
  102. package/schemas/project.schema.json +209 -0
  103. package/schemas/providers.schema.json +142 -0
  104. package/schemas/quality.schema.json +50 -0
  105. package/schemas/work-action.schema.json +44 -0
  106. package/schemas/work-request.schema.json +253 -0
  107. package/src/adapters/confluence.js +138 -0
  108. package/src/adapters/contract.js +657 -0
  109. package/src/adapters/factory.js +122 -0
  110. package/src/adapters/figma.js +178 -0
  111. package/src/adapters/fixtures.js +183 -0
  112. package/src/adapters/github.js +314 -0
  113. package/src/adapters/http.js +704 -0
  114. package/src/adapters/jira.js +195 -0
  115. package/src/adapters/linear.js +171 -0
  116. package/src/adapters/node-transport.js +59 -0
  117. package/src/cli/integration-setup-prompt.js +32 -0
  118. package/src/cli/interrupt.js +25 -0
  119. package/src/cli/main.js +596 -0
  120. package/src/cli/output.js +174 -0
  121. package/src/cli/parse-args.js +246 -0
  122. package/src/cli/project-discovery.js +70 -0
  123. package/src/cli/task-confirmation.js +38 -0
  124. package/src/cli/task-presentation.js +22 -0
  125. package/src/clients/claude.js +214 -0
  126. package/src/clients/codex.js +180 -0
  127. package/src/clients/compatibility.js +23 -0
  128. package/src/clients/contract.js +224 -0
  129. package/src/clients/fake.js +170 -0
  130. package/src/clients/process-runner.js +657 -0
  131. package/src/clients/result-contract.js +78 -0
  132. package/src/commands/delivery-publish.js +80 -0
  133. package/src/commands/delivery-remote.js +230 -0
  134. package/src/commands/delivery-review-update.js +39 -0
  135. package/src/commands/delivery-tracker.js +94 -0
  136. package/src/commands/delivery-transition.js +79 -0
  137. package/src/commands/delivery.js +223 -0
  138. package/src/commands/dependency-approval.js +21 -0
  139. package/src/commands/doctor.js +191 -0
  140. package/src/commands/evidence.js +44 -0
  141. package/src/commands/feature.js +212 -0
  142. package/src/commands/goals.js +231 -0
  143. package/src/commands/human-run.js +208 -0
  144. package/src/commands/human-task.js +278 -0
  145. package/src/commands/init.js +848 -0
  146. package/src/commands/install.js +814 -0
  147. package/src/commands/integration-setup.js +65 -0
  148. package/src/commands/integrations.js +54 -0
  149. package/src/commands/models.js +158 -0
  150. package/src/commands/orchestrate.js +61 -0
  151. package/src/commands/preflight.js +125 -0
  152. package/src/commands/protocols.js +228 -0
  153. package/src/commands/repositories.js +77 -0
  154. package/src/commands/setup-remote.js +41 -0
  155. package/src/commands/setup.js +214 -0
  156. package/src/commands/status.js +224 -0
  157. package/src/commands/support.js +44 -0
  158. package/src/commands/task-completion.js +104 -0
  159. package/src/commands/uninstall.js +234 -0
  160. package/src/commands/verify.js +171 -0
  161. package/src/commands/work.js +220 -0
  162. package/src/commands/worktrees.js +52 -0
  163. package/src/config/command-readiness.js +218 -0
  164. package/src/config/commands.js +206 -0
  165. package/src/config/defaults.js +55 -0
  166. package/src/config/load.js +230 -0
  167. package/src/config/validate.js +560 -0
  168. package/src/delivery/bitbucket-review.js +123 -0
  169. package/src/delivery/branch-publication.js +23 -0
  170. package/src/delivery/contract.js +379 -0
  171. package/src/delivery/github-deployment.js +132 -0
  172. package/src/delivery/github-review.js +5 -0
  173. package/src/delivery/github.js +382 -0
  174. package/src/delivery/gitlab-review.js +5 -0
  175. package/src/delivery/gitlab.js +395 -0
  176. package/src/delivery/prepare.js +90 -0
  177. package/src/delivery/publication-process.js +33 -0
  178. package/src/delivery/publication-transport.js +112 -0
  179. package/src/delivery/review-request.js +184 -0
  180. package/src/delivery/review-update.js +74 -0
  181. package/src/delivery/service.js +400 -0
  182. package/src/delivery/store.js +76 -0
  183. package/src/delivery/tracker-target.js +51 -0
  184. package/src/delivery/tracker-transition.js +158 -0
  185. package/src/delivery/tracker.js +164 -0
  186. package/src/discovery/git.js +85 -0
  187. package/src/discovery/portable.js +108 -0
  188. package/src/discovery/project.js +345 -0
  189. package/src/discovery/tools.js +145 -0
  190. package/src/evaluations/approval.js +20 -0
  191. package/src/evaluations/cost-policy.js +20 -0
  192. package/src/evaluations/harness-attempt.js +113 -0
  193. package/src/evaluations/live-runner.js +55 -0
  194. package/src/evaluations/profile.js +28 -0
  195. package/src/evaluations/report.js +11 -0
  196. package/src/evaluations/text-attempt.js +18 -0
  197. package/src/evidence/checksum.js +87 -0
  198. package/src/evidence/collect.js +727 -0
  199. package/src/evidence/validate.js +354 -0
  200. package/src/feature/accepted-integration.js +46 -0
  201. package/src/feature/actions.js +37 -0
  202. package/src/feature/client-profile.js +56 -0
  203. package/src/feature/decomposition-contract.js +58 -0
  204. package/src/feature/host-execution.js +633 -0
  205. package/src/feature/host-lock-recovery.js +96 -0
  206. package/src/feature/host-run-lock.js +32 -0
  207. package/src/feature/local-approval.js +183 -0
  208. package/src/feature/plan-contract.js +215 -0
  209. package/src/feature/planner.js +253 -0
  210. package/src/feature/run-store.js +241 -0
  211. package/src/feature/runtime-bridge.js +840 -0
  212. package/src/feature/verification-report.js +168 -0
  213. package/src/feature/workflow.js +375 -0
  214. package/src/git/client.js +509 -0
  215. package/src/git/integration-worktree.js +153 -0
  216. package/src/git/reconcile.js +222 -0
  217. package/src/git/reservations.js +450 -0
  218. package/src/git/worktrees.js +478 -0
  219. package/src/graph/completion.js +189 -0
  220. package/src/graph/fixtures.js +116 -0
  221. package/src/graph/reducer.js +124 -0
  222. package/src/graph/scheduler.js +252 -0
  223. package/src/graph/validate.js +106 -0
  224. package/src/install/managed.js +579 -0
  225. package/src/install/project-reference.js +33 -0
  226. package/src/install/project-runtime.js +142 -0
  227. package/src/install/runtime-integrity.cjs +141 -0
  228. package/src/integrations/capabilities.js +69 -0
  229. package/src/integrations/host-observation.js +26 -0
  230. package/src/integrations/registry.js +56 -0
  231. package/src/models/delegate.js +102 -0
  232. package/src/models/profiles.js +53 -0
  233. package/src/models/protocols.js +208 -0
  234. package/src/models/registry.js +83 -0
  235. package/src/models/transport.js +59 -0
  236. package/src/policy/approvals.js +251 -0
  237. package/src/policy/authority.js +282 -0
  238. package/src/policy/budget.js +149 -0
  239. package/src/policy/command-bootstrap.js +219 -0
  240. package/src/policy/commands.js +803 -0
  241. package/src/prompts/launch-contract.js +43 -0
  242. package/src/prompts/planning-contract.js +92 -0
  243. package/src/protocols/presentation.js +53 -0
  244. package/src/protocols/project.js +289 -0
  245. package/src/quality/runner.js +497 -0
  246. package/src/quality/traceability.js +215 -0
  247. package/src/repositories/identity.js +59 -0
  248. package/src/repositories/index.js +2 -0
  249. package/src/repositories/provider.js +210 -0
  250. package/src/runtime/application.js +353 -0
  251. package/src/runtime/dependency-directory.js +33 -0
  252. package/src/runtime/harness-discovery.js +121 -0
  253. package/src/runtime/heartbeat.js +47 -0
  254. package/src/runtime/instance-store.js +112 -0
  255. package/src/runtime/orchestrator.js +1133 -0
  256. package/src/runtime/portable-dependencies.js +94 -0
  257. package/src/runtime/recovery.js +190 -0
  258. package/src/runtime/retry.js +54 -0
  259. package/src/runtime/supervisor.js +166 -0
  260. package/src/runtime/worktree-bootstrap.js +169 -0
  261. package/src/state/event-store.js +333 -0
  262. package/src/state/lock.js +266 -0
  263. package/src/state/paths.js +274 -0
  264. package/src/state/redact.js +106 -0
  265. package/src/state/snapshot-store.js +310 -0
  266. package/src/status/public/app.js +96 -0
  267. package/src/status/public/index.html +37 -0
  268. package/src/status/public/styles.css +50 -0
  269. package/src/status/server.js +323 -0
  270. package/src/status/view-model.js +356 -0
  271. package/src/support/bundle.js +195 -0
  272. package/src/support/failure-report.js +96 -0
  273. package/src/work-request/contract.js +250 -0
  274. package/src/work-request/host.js +78 -0
  275. package/src/work-request/local.js +163 -0
  276. package/src/work-request/tracker.js +105 -0
  277. package/templates/evidence/qa-bundle.json +194 -0
  278. package/templates/github-actions/rivet-deploy.yml +98 -0
  279. package/templates/harness/SKILL.md +90 -0
  280. package/templates/project/.rivet/orchestration.yaml +42 -0
  281. package/templates/project/.rivet/project.yaml +14 -0
  282. package/templates/project/.rivet/providers.yaml +8 -0
  283. package/templates/project/.rivet/quality.yaml +16 -0
@@ -0,0 +1,94 @@
1
+ import {createHash,randomUUID} from 'node:crypto';
2
+ import {lstat,mkdir,writeFile} from 'node:fs/promises';
3
+ import {dirname,join} from 'node:path';
4
+ import {inspectDependencyDirectory as ownedDirectory,readDependencyFile as regularFile} from './dependency-directory.js';
5
+ import {compileProjectDependencies} from '../config/commands.js';
6
+ import {assertGitClient} from '../git/client.js';
7
+ import {createApprovalReceipt,createApprovalRegistry} from '../policy/approvals.js';
8
+ import {createAuthorityEnvelope} from '../policy/authority.js';
9
+ import {runCommand,verifyCommandExecutable} from '../policy/commands.js';
10
+ import {WorktreeBootstrapError,verifyBootstrapCheckout} from './worktree-bootstrap.js';
11
+
12
+ const DIRECTORY='.rivet-deps';
13
+ const MARKER='.rivet-owner.json';
14
+ const hash=bytes=>createHash('sha256').update(bytes).digest('hex');
15
+ const fail=(reason='unsafe-checkout')=>{throw new WorktreeBootstrapError(reason);};
16
+ function cancelled(signal){signal?.throwIfAborted();}
17
+
18
+ async function inputsFor(value,gitClient,config) {
19
+ let total=0;const files=[];
20
+ for(const path of config.inputs) {
21
+ // Every ancestor is a real directory; a tracked symlink never becomes an input.
22
+ let cursor=value.worktreePath;
23
+ for(const part of path.split('/').slice(0,-1)) {cursor=join(cursor,part);const entry=await lstat(cursor);if(!entry.isDirectory()||entry.isSymbolicLink())fail();}
24
+ const bytes=await regularFile(join(value.worktreePath,path),8*1024*1024);
25
+ total+=bytes.length;if(total>16*1024*1024)fail();
26
+ const tracked=await gitClient.inspectTrackedFile(value.worktreePath,value.expectedCommit,path);
27
+ const blob=createHash(tracked.blob.length===40?'sha1':'sha256').update(`blob ${bytes.length}\0`).update(bytes).digest('hex');
28
+ if(blob!==tracked.blob)fail();
29
+ files.push(Object.freeze({path,sha256:hash(bytes)}));
30
+ }
31
+ return Object.freeze(files);
32
+ }
33
+ export async function inspectPortableDependencies(value,options) {
34
+ try {
35
+ assertGitClient(options.gitClient);
36
+ const dependencies=compileProjectDependencies({schemaVersion:3,dependencies:value.dependencies});
37
+ if(!dependencies)fail('invalid-input');
38
+ cancelled(options.signal);
39
+ await verifyBootstrapCheckout(value,options.gitClient);
40
+ const ownership=await ownedDirectory(value.worktreePath);
41
+ const inputs=await inputsFor(value,options.gitClient,dependencies);
42
+ return Object.freeze({kind:'portable',projectRoot:value.projectRoot,worktreePath:value.worktreePath,expectedCommit:value.expectedCommit,expectedBranch:value.expectedBranch,inputs,steps:dependencies.steps,provides:dependencies.provides,ownership});
43
+ }catch(error){if(error instanceof WorktreeBootstrapError)throw error;cancelled(options.signal);fail();}
44
+ }
45
+ function samePlan(a,b){return JSON.stringify(a)===JSON.stringify(b);}
46
+ async function directoryIdentity(path) {const stat=await lstat(path,{bigint:true});return `${stat.dev}:${stat.ino}`;}
47
+ async function ensureOwned(root,prior,signal) {
48
+ if(prior)return prior;
49
+ const path=join(root,DIRECTORY);
50
+ // mkdir without recursive is the ownership claim; never adopt an existing entry.
51
+ cancelled(signal);
52
+ await mkdir(path,{mode:0o700});
53
+ cancelled(signal);
54
+ await writeFile(join(path,'.gitignore'),'*\n',{flag:'wx',mode:0o600});
55
+ cancelled(signal);
56
+ await writeFile(join(path,MARKER),JSON.stringify({kind:'rivet-dependencies',version:1,root,rootIdentity:await directoryIdentity(root),directoryIdentity:await directoryIdentity(path),token:randomUUID()}),{flag:'wx',mode:0o600});
57
+ return ownedDirectory(root);
58
+ }
59
+ export async function bootstrapPortableDependencies(value,options) {
60
+ const plan=await inspectPortableDependencies(value,options);
61
+ if(typeof options.confirm!=='function'||typeof options.resolveCommandExecutable!=='function')fail('invalid-input');
62
+ cancelled(options.signal);
63
+ if(await options.confirm(plan)!==true)return Object.freeze({status:'declined'});
64
+ cancelled(options.signal);
65
+ if(!samePlan(plan,await inspectPortableDependencies(value,options)))fail();
66
+ cancelled(options.signal);
67
+ let ownership;
68
+ try {ownership=await ensureOwned(plan.worktreePath,plan.ownership,options.signal);}catch{fail();}
69
+ async function revalidate() {
70
+ cancelled(options.signal);
71
+ const current=await inspectPortableDependencies(value,options);
72
+ if(!samePlan({...plan,ownership},current))fail('checkout-changed');
73
+ }
74
+ const results=[];
75
+ for(const step of plan.steps) {
76
+ await revalidate();
77
+ const executable=await options.resolveCommandExecutable(step.argv[0],{execution:'argv',worktree:plan.worktreePath,cwd:step.cwd});
78
+ await verifyCommandExecutable(executable,{execution:'argv'});
79
+ await revalidate();
80
+ const nowMs=Date.now(),commandId=step.id;
81
+ const registry=createApprovalRegistry({approvers:[{id:'human-owner',principal:'human'}]});
82
+ const approval=createApprovalReceipt({id:`approval-${randomUUID()}`,approverId:'human-owner',approverPrincipal:'human',subjectId:'rivet-bootstrap',action:'dependency.install',resource:`command:${commandId}:${plan.worktreePath}`,policyId:'dependency.install',decision:'approved',expiresAt:new Date(nowMs+60000).toISOString(),singleUse:true});
83
+ const result=await runCommand({worktree:plan.worktreePath,authority:createAuthorityEnvelope({actorId:'rivet-bootstrap',principal:'agent',actions:['dependency.install'],ownedPaths:[],providers:[],commands:[commandId]}),commands:{[commandId]:{execution:'argv',executable,args:step.argv.slice(1),action:'dependency.install',elevated:true,approvalPolicyId:'dependency.install',approverId:'human-owner'}},environment:{PATH:[dirname(executable),dirname(process.execPath),'/usr/bin','/bin'].join(':')},timeoutMs:10*60000,maxOutputBytes:16384,maxStreamOutputBytes:16384},{actorId:'rivet-bootstrap',commandId,cwd:step.cwd,approval,approvalRegistry:registry,nowMs},{signal:options.signal});
84
+ results.push(result);
85
+ await revalidate();
86
+ if(result.status!=='success')return Object.freeze({status:'failed',result,results:Object.freeze(results)});
87
+ }
88
+ for(const name of plan.provides) {
89
+ const executable=await options.resolveCommandExecutable(name,{execution:'argv',worktree:plan.worktreePath,cwd:'.'});
90
+ await verifyCommandExecutable(executable,{execution:'argv'});
91
+ }
92
+ await revalidate();
93
+ return Object.freeze({status:'ready',results:Object.freeze(results)});
94
+ }
@@ -0,0 +1,190 @@
1
+ import { evaluateAuthority } from '../policy/authority.js';
2
+ import { claimApproval } from '../policy/approvals.js';
3
+ import { EVIDENCE_TYPES } from '../config/defaults.js';
4
+ import { validatedGraphSnapshot } from '../graph/validate.js';
5
+ import { RuntimeError, appendRuntimeEvent, remainingRuntimeNodeBudget, runtimeTransaction, transitionRuntimeState } from './orchestrator.js';
6
+ import { heartbeatStatus } from './heartbeat.js';
7
+ import { retryDecision, retryPolicySchedule } from './retry.js';
8
+
9
+ function fail(reason) { throw new RuntimeError(reason); }
10
+ function version(value) { if (!Number.isSafeInteger(value) || value < 0 || value >= Number.MAX_SAFE_INTEGER) fail('invalid-runtime-input'); return value; }
11
+ function safeId(value) { return typeof value === 'string' && /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(value) && value.length <= 64; }
12
+
13
+ function capture(input, allowed, required = allowed) {
14
+ try {
15
+ if (!input || typeof input !== 'object' || Array.isArray(input)) fail('invalid-runtime-input');
16
+ const keys = Reflect.ownKeys(input);
17
+ if (keys.some(key => typeof key !== 'string' || !allowed.includes(key))) fail('invalid-runtime-input');
18
+ const result = {};
19
+ for (const key of keys) {
20
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
21
+ if (!descriptor?.enumerable) fail('invalid-runtime-input');
22
+ result[key] = input[key];
23
+ }
24
+ if (required.some(key => !Object.hasOwn(result, key))) fail('invalid-runtime-input');
25
+ return result;
26
+ } catch (error) { if (error instanceof RuntimeError) throw error; fail('invalid-runtime-input'); }
27
+ }
28
+ function now(value) { const result = value ?? 0; if (!Number.isSafeInteger(result) || result < 0) fail('invalid-runtime-input'); return result; }
29
+ function reason(value) { if (typeof value !== 'string' || value.length < 1 || value.length > 1000 || /[\u0000\r\n]/.test(value)) fail('invalid-runtime-input'); return value; }
30
+ function structuralNode(state, nodeId, authority, action) {
31
+ if (!safeId(nodeId)) fail('invalid-runtime-input');
32
+ const node = state.graph.nodes.find(item => item.id === nodeId); const parent = state.graph.nodes.find(item => item.id === node?.parentId);
33
+ if (!node || !parent || authority?.actorId !== parent.owner.id) fail('approval-required');
34
+ const decision = evaluateAuthority(authority, { actorId: authority.actorId, action, resource: node.id });
35
+ if (decision.decision !== 'allow') fail('approval-required');
36
+ return node;
37
+ }
38
+ function retryBound(state, node) {
39
+ const attempts = state.attempts[node.id] ?? 1; const maximum = Math.min((state.limits?.retries ?? 2) + 1, 10);
40
+ if (attempts >= maximum || (state.usage?.retries ?? 0) >= (state.limits?.retries ?? 2)) fail('invalid-runtime-input');
41
+ state.attempts[node.id] = attempts + 1; state.usage ??= { tokens: 0, costUsd: '0', retries: 0, timeMinutes: 0, taskLimit: 0 }; state.usage.retries += 1;
42
+ }
43
+ function reopenGraph(state, nowMs, actorId) {
44
+ if (!['blocked', 'failed'].includes(state.terminal)) return;
45
+ if (['blocked', 'failed'].includes(state.graph.status)) transitionRuntimeState(state, state.graph, 'corrective', nowMs, { role: 'manager', id: actorId });
46
+ if (state.graph.status === 'corrective') transitionRuntimeState(state, state.graph, 'running', nowMs, { role: 'manager', id: actorId });
47
+ state.terminal = null;
48
+ }
49
+
50
+ export async function retryNode(instance, input, runtimeOptions = null) {
51
+ const value = capture(input, ['expectedVersion', 'nodeId', 'authority', 'reason', 'nowMs'], ['expectedVersion', 'nodeId', 'authority', 'reason']);
52
+ const retryReason = reason(value.reason); const nowMs = now(value.nowMs);
53
+ return runtimeTransaction(instance, value.expectedVersion, state => {
54
+ const node = structuralNode(state, value.nodeId, value.authority, 'orchestration.retry');
55
+ if (!['blocked', 'failed', 'corrective'].includes(node.status)) fail('invalid-runtime-input');
56
+ let delayMs = 0;
57
+ if (runtimeOptions?.retryPolicy) {
58
+ const decision = retryDecision(runtimeOptions.retryPolicy, { attempt: state.attempts[node.id] ?? 1, classification: 'provider-transient' });
59
+ if (!decision.retry || (state.usage?.retries ?? 0) >= (state.limits?.retries ?? 0)) fail('invalid-runtime-input');
60
+ state.attempts[node.id] = decision.nextAttempt; state.usage.retries += 1; delayMs = decision.delayMs;
61
+ } else retryBound(state, node);
62
+ delete state.heartbeats[node.id]; delete state.lastHeartbeatAt[node.id];
63
+ if (['failed', 'blocked'].includes(node.status)) transitionRuntimeState(state, node, 'corrective', nowMs, node.owner);
64
+ if (delayMs === 0) transitionRuntimeState(state, node, 'ready', nowMs, node.owner);
65
+ else { state.retryAtMs ??= {}; state.retryAtMs[node.id] = nowMs + delayMs; }
66
+ appendRuntimeEvent(state, nowMs, { actor: { role: 'manager', id: value.authority.actorId }, event: { type: 'retry', nodeId: node.id, retryReason } });
67
+ reopenGraph(state, nowMs, value.authority.actorId);
68
+ return state;
69
+ }, runtimeOptions?.retryPolicy ? retryPolicySchedule(runtimeOptions.retryPolicy) : null);
70
+ }
71
+
72
+ export async function cancelNode(instance, input) {
73
+ const value = capture(input, ['expectedVersion', 'nodeId', 'authority', 'nowMs'], ['expectedVersion', 'nodeId', 'authority']); const nowMs = now(value.nowMs);
74
+ return runtimeTransaction(instance, value.expectedVersion, state => {
75
+ const node = structuralNode(state, value.nodeId, value.authority, 'orchestration.cancel');
76
+ if (node.status === 'cancelled') return null;
77
+ if (['completed', 'archived'].includes(node.status)) fail('invalid-runtime-input');
78
+ const cancelled = new Set([node.id]);
79
+ for (let changed = true; changed;) {
80
+ changed = false;
81
+ for (const candidate of state.graph.nodes) if (!cancelled.has(candidate.id) && candidate.dependencies.some(id => cancelled.has(id))) { cancelled.add(candidate.id); changed = true; }
82
+ }
83
+ for (const candidate of state.graph.nodes) if (cancelled.has(candidate.id) && !['completed', 'archived', 'cancelled'].includes(candidate.status)) {
84
+ transitionRuntimeState(state, candidate, 'cancelled', nowMs, candidate.owner);
85
+ const intent = state.launchIntents[candidate.id];
86
+ if (intent && ['committed', 'prepared', 'started'].includes(intent.status)) { delete intent.preparationClaim; intent.status = 'complete'; }
87
+ if (intent) delete intent.retryAtMs;
88
+ if (state.retryAtMs) delete state.retryAtMs[candidate.id];
89
+ }
90
+ if (state.graph.status !== 'cancelled') transitionRuntimeState(state, state.graph, 'cancelled', nowMs, node.owner);
91
+ state.terminal = 'cancelled'; return state;
92
+ });
93
+ }
94
+
95
+ export async function cancelGoal(instance, input) {
96
+ const value = capture(input, ['expectedVersion', 'authority', 'nowMs'], ['expectedVersion', 'authority']); const nowMs = now(value.nowMs);
97
+ return runtimeTransaction(instance, value.expectedVersion, state => {
98
+ const decision = evaluateAuthority(value.authority, { actorId: value.authority?.actorId, action: 'orchestration.cancel', resource: instance.id });
99
+ if (value.authority?.principal !== 'human' || decision.decision !== 'allow') fail('approval-required');
100
+ if (state.terminal === 'cancelled') return null;
101
+ if (state.terminal === 'completed') fail('invalid-runtime-input');
102
+ for (const node of state.graph.nodes) if (!['completed', 'cancelled', 'archived'].includes(node.status)) {
103
+ transitionRuntimeState(state, node, 'cancelled', nowMs, { role: 'human', id: value.authority.actorId });
104
+ const intent = state.launchIntents[node.id];
105
+ if (intent && ['committed', 'prepared', 'started'].includes(intent.status)) { delete intent.preparationClaim; intent.status = 'complete'; }
106
+ if (intent) delete intent.retryAtMs;
107
+ if (state.retryAtMs) delete state.retryAtMs[node.id];
108
+ }
109
+ if (state.graph.status !== 'cancelled') transitionRuntimeState(state, state.graph, 'cancelled', nowMs, { role: 'human', id: value.authority.actorId });
110
+ state.terminal = 'cancelled'; return state;
111
+ });
112
+ }
113
+
114
+ export async function createCorrectiveNode(instance, input) {
115
+ const captured = capture(input, ['expectedVersion', 'sourceNodeId', 'nodeId', 'ownerId', 'authority', 'reason', 'evidenceRefs', 'evidenceTypes', 'nowMs'], ['expectedVersion', 'sourceNodeId', 'nodeId', 'ownerId', 'authority', 'reason', 'evidenceRefs']);
116
+ const evidenceRefs = Array.isArray(captured.evidenceRefs) ? [...captured.evidenceRefs] : captured.evidenceRefs; const retryReason = reason(captured.reason); const nowMs = now(captured.nowMs);
117
+ if (![captured.sourceNodeId, captured.nodeId, captured.ownerId].every(safeId) || !Array.isArray(evidenceRefs) || evidenceRefs.length < 1 || evidenceRefs.length > 8 || evidenceRefs.some(item => !safeId(item)) || new Set(evidenceRefs).size !== evidenceRefs.length) fail('invalid-runtime-input');
118
+ return runtimeTransaction(instance, captured.expectedVersion, state => {
119
+ if (state.graph.nodes.some(node => node.id === captured.nodeId) || state.graph.nodes.length >= 1000) fail('invalid-runtime-input');
120
+ const source = structuralNode(state, captured.sourceNodeId, captured.authority, 'orchestration.correct');
121
+ if (!['failed', 'blocked', 'corrective'].includes(source.status)) fail('invalid-runtime-input');
122
+ retryBound(state, source);
123
+ const evidenceTypes = captured.evidenceTypes === undefined ? [...source.requiredEvidenceTypes] : Array.isArray(captured.evidenceTypes) ? [...captured.evidenceTypes] : captured.evidenceTypes;
124
+ if (!Array.isArray(evidenceTypes) || evidenceTypes.length !== evidenceRefs.length || evidenceTypes.length > 8
125
+ || evidenceTypes.some(item => !EVIDENCE_TYPES.includes(item)) || new Set(evidenceTypes).size !== evidenceTypes.length) fail('invalid-runtime-input');
126
+ evidenceTypes.sort(); evidenceRefs.sort();
127
+ const inheritedEvidenceTypes = [...source.requiredEvidenceTypes].sort();
128
+ if (evidenceTypes.length !== inheritedEvidenceTypes.length
129
+ || evidenceTypes.some((item, index) => item !== inheritedEvidenceTypes[index])) fail('invalid-runtime-input');
130
+ const remaining = remainingRuntimeNodeBudget(state, source);
131
+ if (remaining.timeMinutes < 1 || remaining.tokenLimit < 1 || remaining.taskLimit < 1) fail('invalid-runtime-input');
132
+ const correctiveCost = Number(remaining.costUsd);
133
+ if (!Number.isFinite(correctiveCost) || correctiveCost <= 0) fail('invalid-runtime-input');
134
+ const corrective = { id: captured.nodeId, parentId: source.parentId, objective: `Correct ${source.id}: ${retryReason}`.slice(0, 2000), owner: { role: 'worker', id: captured.ownerId }, dependencies: [...source.dependencies], authorityScopes: [...source.authorityScopes], budget: { ...remaining, costUsd: correctiveCost }, completionProfile: source.completionProfile, requiredEvidenceTypes: evidenceTypes, evidenceRefs: [...evidenceRefs], status: 'corrective' };
135
+ if (source.status === 'blocked') transitionRuntimeState(state, source, 'failed', nowMs, source.owner);
136
+ if (state.graph.nodes.find(node => node.id === source.id).status === 'corrective') transitionRuntimeState(state, source, 'failed', nowMs, source.owner);
137
+ transitionRuntimeState(state, source, 'archived', nowMs, source.owner);
138
+ const nodes = state.graph.nodes.map(node => ({ ...node, dependencies: node.dependencies.map(id => id === source.id ? corrective.id : id) }));
139
+ state.graph = validatedGraphSnapshot({ ...state.graph, nodes: [...nodes, corrective] });
140
+ appendRuntimeEvent(state, nowMs, { actor: { role: 'manager', id: captured.authority.actorId }, event: { type: 'retry', nodeId: source.id, retryReason: `corrective:${retryReason}` } });
141
+ reopenGraph(state, nowMs, captured.authority.actorId);
142
+ return state;
143
+ });
144
+ }
145
+
146
+ export async function recoverStalledNode(instance, input) {
147
+ const value = capture(input, ['expectedVersion', 'nodeId', 'authority', 'nowMs', 'reason']); const nowMs = now(value.nowMs); const retryReason = reason(value.reason);
148
+ return runtimeTransaction(instance, value.expectedVersion, state => {
149
+ const node = structuralNode(state, value.nodeId, value.authority, 'orchestration.recover');
150
+ const intent = state.launchIntents[node.id]; const heartbeat = state.heartbeats[node.id];
151
+ if (!['reserved', 'running'].includes(node.status) || !intent || !['committed', 'prepared', 'started'].includes(intent.status) || !heartbeat) fail('invalid-runtime-input');
152
+ const liveness = heartbeatStatus(heartbeat, { nowMs, expected: { instanceId: instance.id, nodeId: node.id, actorId: node.owner.id, leaseId: intent.reservationId } });
153
+ if (liveness.status !== 'stalled') fail('invalid-runtime-input');
154
+ retryBound(state, node);
155
+ if (node.status === 'running') {
156
+ transitionRuntimeState(state, node, 'corrective', nowMs, node.owner);
157
+ transitionRuntimeState(state, node, 'ready', nowMs, node.owner);
158
+ } else transitionRuntimeState(state, node, 'ready', nowMs, node.owner);
159
+ delete state.heartbeats[node.id]; delete state.lastHeartbeatAt[node.id];
160
+ delete intent.preparationClaim; intent.status = 'recovered'; appendRuntimeEvent(state, nowMs, { actor: { role: 'manager', id: value.authority.actorId }, event: { type: 'retry', nodeId: node.id, retryReason } }); return state;
161
+ });
162
+ }
163
+
164
+ export async function recoverRuntimeLock(target, input) {
165
+ const value = capture(input, ['expectedVersion', 'nowMs', 'authority', 'approval', 'approvalRegistry', 'expectedApproverId']);
166
+ if (!target || !safeId(target.id) || typeof target.version !== 'function' || typeof target.inspectLock !== 'function' || typeof target.recoverLock !== 'function') fail('invalid-runtime-input');
167
+ const expectedVersion = version(value.expectedVersion); const nowMs = now(value.nowMs);
168
+ if (await target.version() !== expectedVersion) fail('version-conflict');
169
+ const lock = capture(await target.inspectLock(), ['ownerId', 'acquiredAtMs', 'expiresAtMs', 'ownerAlive']);
170
+ if (!safeId(lock.ownerId) || !Number.isSafeInteger(lock.acquiredAtMs) || !Number.isSafeInteger(lock.expiresAtMs) || lock.acquiredAtMs < 0 || lock.expiresAtMs <= lock.acquiredAtMs || nowMs < lock.expiresAtMs || lock.ownerAlive !== false) fail('invalid-runtime-input');
171
+ const decision = evaluateAuthority(value.authority, { actorId: value.authority?.actorId, action: 'orchestration.recover', resource: target.id });
172
+ if (decision.decision !== 'allow') fail('approval-required');
173
+ const claim = claimApproval(value.approval, { subjectId: value.authority.actorId, action: 'orchestration.recover', resource: target.id, policyId: 'runtime.stale-lock-recovery' }, { registry: value.approvalRegistry, expectedApproverId: value.expectedApproverId, requireHumanApprover: true, requireSingleUse: true, nowMs });
174
+ if (!claim.valid) fail('approval-required');
175
+ try {
176
+ if (!claim.finalize()) fail('approval-required');
177
+ await target.recoverLock(Object.freeze({ expectedVersion, expectedOwnerId: lock.ownerId, expectedExpiresAtMs: lock.expiresAtMs, nowMs, requireOwnerDead: true }));
178
+ if (!claim.publish()) fail('approval-required');
179
+ return Object.freeze({ recovered: true, expectedVersion });
180
+ } catch (error) {
181
+ try {
182
+ const observedLock = capture(await target.inspectLock(), ['ownerId', 'acquiredAtMs', 'expiresAtMs', 'ownerAlive']);
183
+ const unchanged = observedLock.ownerId === lock.ownerId && observedLock.acquiredAtMs === lock.acquiredAtMs
184
+ && observedLock.expiresAtMs === lock.expiresAtMs && observedLock.ownerAlive === lock.ownerAlive;
185
+ if (unchanged) { if (!claim.rollback()) claim.release(); }
186
+ else claim.publish();
187
+ } catch { /* Ambiguous recovery remains finalized and unusable. */ }
188
+ throw error;
189
+ }
190
+ }
@@ -0,0 +1,54 @@
1
+ const ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
2
+ const policies = new WeakSet();
3
+ export const MAX_RETRY_DELAY_MS = 3_600_000;
4
+
5
+ export class RetryPolicyError extends Error {
6
+ constructor(reason = 'invalid-retry-policy') {
7
+ super('Runtime retry policy is invalid.');
8
+ this.name = 'RetryPolicyError';
9
+ this.code = 'ERR_RUNTIME_RETRY_POLICY';
10
+ this.safeMessage = this.message;
11
+ this.details = Object.freeze({ reason });
12
+ }
13
+ }
14
+
15
+ function fail(reason) { throw new RetryPolicyError(reason); }
16
+
17
+ export function createRetryPolicy(input) {
18
+ try {
19
+ if (!input || typeof input !== 'object' || Array.isArray(input)) fail('invalid-retry-policy');
20
+ const keys = Reflect.ownKeys(input);
21
+ if (keys.some(key => typeof key !== 'string' || !['maxAttempts', 'delaysMs', 'retryable'].includes(key))) fail('invalid-retry-policy');
22
+ const maxAttempts = input.maxAttempts;
23
+ const delays = input.delaysMs;
24
+ const retryable = input.retryable;
25
+ if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1 || maxAttempts > 16
26
+ || !Array.isArray(delays) || delays.length !== Math.max(0, maxAttempts - 1) || delays.length > 15
27
+ || delays.some(value => !Number.isSafeInteger(value) || value < 0 || value > MAX_RETRY_DELAY_MS)
28
+ || !Array.isArray(retryable) || retryable.length > 32
29
+ || retryable.some(value => typeof value !== 'string' || value.length > 64 || !ID.test(value))
30
+ || new Set(retryable).size !== retryable.length) fail('invalid-retry-policy');
31
+ const policy = Object.freeze({ maxAttempts, delaysMs: Object.freeze([...delays]), retryable: Object.freeze([...retryable].sort()) });
32
+ policies.add(policy);
33
+ return policy;
34
+ } catch (error) { if (error instanceof RetryPolicyError) throw error; fail('invalid-retry-policy'); }
35
+ }
36
+
37
+ export function retryDecision(policy, input) {
38
+ try {
39
+ if (!policies.has(policy) || !input || typeof input !== 'object' || Array.isArray(input)) fail('invalid-retry-decision');
40
+ const keys = Reflect.ownKeys(input);
41
+ if (keys.some(key => typeof key !== 'string' || !['attempt', 'classification'].includes(key))) fail('invalid-retry-decision');
42
+ const attempt = input.attempt;
43
+ const classification = input.classification;
44
+ if (!Number.isSafeInteger(attempt) || attempt < 1 || attempt > 16 || typeof classification !== 'string' || !ID.test(classification)) fail('invalid-retry-decision');
45
+ if (!policy.retryable.includes(classification)) return Object.freeze({ retry: false, reason: 'not-retryable' });
46
+ if (attempt >= policy.maxAttempts) return Object.freeze({ retry: false, reason: 'attempts-exhausted' });
47
+ return Object.freeze({ retry: true, nextAttempt: attempt + 1, delayMs: policy.delaysMs[attempt - 1] });
48
+ } catch (error) { if (error instanceof RetryPolicyError) throw error; fail('invalid-retry-decision'); }
49
+ }
50
+
51
+ export function retryPolicySchedule(policy) {
52
+ if (!policies.has(policy)) fail('invalid-retry-policy');
53
+ return policy.delaysMs;
54
+ }
@@ -0,0 +1,166 @@
1
+ const TERMINAL = new Set(['complete', 'completed', 'blocked', 'cancelled', 'budget-exhausted', 'failed']);
2
+ const ABORTED_GETTER = Object.getOwnPropertyDescriptor(AbortSignal.prototype, 'aborted')?.get;
3
+ const ADD_ABORT_LISTENER = AbortSignal.prototype.addEventListener;
4
+ const REMOVE_ABORT_LISTENER = AbortSignal.prototype.removeEventListener;
5
+
6
+ export class SupervisorError extends Error {
7
+ constructor(reason = 'invalid-watch') {
8
+ super('Orchestration watch request is invalid.');
9
+ this.name = 'SupervisorError'; this.code = 'ERR_RUNTIME_SUPERVISOR'; this.safeMessage = this.message;
10
+ this.details = Object.freeze({ reason });
11
+ }
12
+ }
13
+
14
+ function fail(reason) { throw new SupervisorError(reason); }
15
+
16
+ function readClock(now, prior) {
17
+ let value;
18
+ try { value = now(); } catch { fail('clock-failed'); }
19
+ if (Number.isSafeInteger(value) && Number.isSafeInteger(prior) && value < prior) fail('clock-regressed');
20
+ return value;
21
+ }
22
+
23
+ function captureSignal(value) {
24
+ if (value === undefined) return Object.freeze({ signal: undefined, release() {} });
25
+ try {
26
+ if (!(value instanceof AbortSignal) || typeof ABORTED_GETTER !== 'function') fail('invalid-watch');
27
+ const controller = new AbortController(); const relayAbort = () => controller.abort();
28
+ const aborted = ABORTED_GETTER.call(value);
29
+ if (typeof aborted !== 'boolean') fail('invalid-watch');
30
+ let listening = false;
31
+ if (aborted) controller.abort();
32
+ else {
33
+ ADD_ABORT_LISTENER.call(value, 'abort', relayAbort, { once: true }); listening = true;
34
+ if (ABORTED_GETTER.call(value)) relayAbort();
35
+ }
36
+ return Object.freeze({
37
+ signal: controller.signal,
38
+ release() {
39
+ if (!listening) return;
40
+ listening = false;
41
+ try { REMOVE_ABORT_LISTENER.call(value, 'abort', relayAbort); } catch {}
42
+ },
43
+ });
44
+ } catch { fail('invalid-watch'); }
45
+ }
46
+
47
+ function captureTickResult(value) {
48
+ let terminal; let version;
49
+ try {
50
+ if (!value || typeof value !== 'object' || Array.isArray(value)) fail('tick-output');
51
+ terminal = value.terminal; version = value.version;
52
+ } catch { fail('tick-output'); }
53
+ if (TERMINAL.has(terminal)) return Object.freeze({ terminal, version });
54
+ if (terminal !== null || !Number.isSafeInteger(version) || version < 0) fail('tick-output');
55
+ return Object.freeze({ terminal, version });
56
+ }
57
+
58
+ async function boundedWait(wait, intervalMs, signal) {
59
+ const controller = new AbortController(); const timeoutToken = Object.freeze(Object.create(null)); const abortToken = Object.freeze(Object.create(null));
60
+ let timer; let resolveAbort; let externallyAborted = false; let timedOut = false;
61
+ const relayAbort = () => { externallyAborted = true; resolveAbort?.(abortToken); controller.abort(); };
62
+ signal?.addEventListener('abort', relayAbort, { once: true });
63
+ try {
64
+ const operation = Promise.resolve().then(() => wait(intervalMs, controller.signal))
65
+ .then(() => Object.freeze({ ok: true }), () => Object.freeze({ ok: false }));
66
+ const timeout = new Promise(resolve => { timer = setTimeout(() => { timedOut = true; resolve(timeoutToken); controller.abort(); }, intervalMs); });
67
+ const aborted = new Promise(resolve => { resolveAbort = resolve; if (signal?.aborted) relayAbort(); });
68
+ const result = await Promise.race([operation, timeout, aborted]);
69
+ if (result === abortToken || externallyAborted) return 'aborted';
70
+ if (result === timeoutToken || timedOut) return 'timeout';
71
+ if (result !== timeoutToken && result.ok !== true) fail('wait-failed');
72
+ return 'settled';
73
+ } finally {
74
+ if (timer !== undefined) clearTimeout(timer);
75
+ signal?.removeEventListener('abort', relayAbort);
76
+ }
77
+ }
78
+
79
+ async function boundedTick(runtime, tick, instance, expectedVersion, timeoutMs, signal) {
80
+ const controller = new AbortController();
81
+ const timeoutToken = Object.freeze({ status: 'timeout' });
82
+ const abortToken = Object.freeze({ status: 'aborted' });
83
+ const lateToken = Object.freeze({ status: 'late' });
84
+ let expired = false; let timer; let resolveAbort; let timeoutRemaining = timeoutMs;
85
+ const relayAbort = () => { controller.abort(); resolveAbort?.(abortToken); };
86
+ signal?.addEventListener('abort', relayAbort, { once: true });
87
+ try {
88
+ const operation = Promise.resolve()
89
+ .then(() => tick.call(runtime, instance, { expectedVersion, signal: controller.signal }))
90
+ .then(
91
+ value => expired ? lateToken : Object.freeze({ status: 'settled', ok: true, value }),
92
+ () => expired ? lateToken : Object.freeze({ status: 'settled', ok: false }),
93
+ );
94
+ const timeout = new Promise(resolve => {
95
+ const schedule = () => {
96
+ const delay = Math.min(timeoutRemaining, 2_147_483_647);
97
+ timer = setTimeout(() => {
98
+ timeoutRemaining -= delay;
99
+ if (timeoutRemaining > 0) schedule();
100
+ else { expired = true; controller.abort(); resolve(timeoutToken); }
101
+ }, delay);
102
+ };
103
+ schedule();
104
+ });
105
+ const aborted = new Promise(resolve => { resolveAbort = resolve; if (signal?.aborted) relayAbort(); });
106
+ const result = await Promise.race([operation, timeout, aborted]);
107
+ if (result === timeoutToken || result === abortToken) { expired = true; return result; }
108
+ if (result.ok !== true) fail('tick-failed');
109
+ return result;
110
+ } finally {
111
+ expired = true;
112
+ if (timer !== undefined) clearTimeout(timer);
113
+ signal?.removeEventListener('abort', relayAbort);
114
+ }
115
+ }
116
+
117
+ export async function watchOrchestration(runtime, instance, input) {
118
+ let tick; let expectedVersion; let intervalMs; let maxTicks; let deadlineMs; let now; let wait; let signal;
119
+ try {
120
+ if (!runtime || !instance || !input || typeof input !== 'object' || Array.isArray(input)) fail('invalid-watch');
121
+ const prototype = Object.getPrototypeOf(input);
122
+ if (prototype !== Object.prototype && prototype !== null) fail('invalid-watch');
123
+ const allowed = new Set(['expectedVersion', 'intervalMs', 'maxTicks', 'deadlineMs', 'now', 'wait', 'signal']);
124
+ const required = ['expectedVersion', 'intervalMs', 'maxTicks', 'deadlineMs', 'now', 'wait'];
125
+ const keys = Reflect.ownKeys(input);
126
+ if (keys.length > allowed.size || keys.some(key => typeof key !== 'string' || !allowed.has(key))
127
+ || required.some(key => !keys.includes(key))) fail('invalid-watch');
128
+ for (const key of keys) if (!Object.getOwnPropertyDescriptor(input, key)?.enumerable) fail('invalid-watch');
129
+ tick = runtime.tick;
130
+ if (typeof tick !== 'function') fail('invalid-watch');
131
+ expectedVersion = input.expectedVersion;
132
+ intervalMs = input.intervalMs;
133
+ maxTicks = input.maxTicks;
134
+ deadlineMs = input.deadlineMs;
135
+ now = input.now;
136
+ wait = input.wait;
137
+ signal = input.signal;
138
+ } catch { fail('invalid-watch'); }
139
+ if (!Number.isSafeInteger(expectedVersion) || expectedVersion < 0 || !Number.isSafeInteger(intervalMs) || intervalMs < 1 || intervalMs > 60_000
140
+ || !Number.isSafeInteger(maxTicks) || maxTicks < 1 || maxTicks > 10_000 || !Number.isSafeInteger(deadlineMs) || deadlineMs < 0
141
+ || typeof now !== 'function' || typeof wait !== 'function') fail('invalid-watch');
142
+ const signalBoundary = captureSignal(signal); signal = signalBoundary.signal;
143
+ try {
144
+ let version = expectedVersion; let lastClock;
145
+ for (let ticks = 1; ticks <= maxTicks; ticks += 1) {
146
+ if (signal?.aborted) return Object.freeze({ terminal: 'cancelled', ticks: ticks - 1 });
147
+ const current = readClock(now, lastClock); lastClock = current;
148
+ if (!Number.isSafeInteger(current) || current < 0 || current > deadlineMs) return Object.freeze({ terminal: 'deadline', ticks: ticks - 1 });
149
+ const remainingForTick = deadlineMs - current;
150
+ if (remainingForTick <= 0) return Object.freeze({ terminal: 'deadline', ticks: ticks - 1 });
151
+ const tickOutcome = await boundedTick(runtime, tick, instance, version, remainingForTick, signal);
152
+ if (tickOutcome.status === 'timeout') return Object.freeze({ terminal: 'deadline', ticks });
153
+ if (tickOutcome.status === 'aborted') return Object.freeze({ terminal: 'cancelled', ticks });
154
+ const result = captureTickResult(tickOutcome.value);
155
+ if (TERMINAL.has(result.terminal)) return Object.freeze({ terminal: result.terminal, ticks });
156
+ version = result.version;
157
+ if (ticks === maxTicks) return Object.freeze({ terminal: 'max-ticks', ticks });
158
+ const afterTick = readClock(now, lastClock); lastClock = afterTick;
159
+ const remaining = deadlineMs - afterTick;
160
+ if (!Number.isSafeInteger(remaining) || remaining <= 0) return Object.freeze({ terminal: 'deadline', ticks });
161
+ const waitOutcome = await boundedWait(wait, Math.min(intervalMs, remaining), signal);
162
+ if (waitOutcome === 'aborted') return Object.freeze({ terminal: 'cancelled', ticks });
163
+ }
164
+ fail('invalid-watch');
165
+ } finally { signalBoundary.release(); }
166
+ }