psyclaw 0.25.1 → 0.26.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 (271) hide show
  1. package/README.md +111 -60
  2. package/{apps → dist/apps}/panel/index.html +50 -28
  3. package/dist/src/adapters/pi/extension.d.ts +19 -0
  4. package/dist/src/adapters/pi/extension.js +735 -0
  5. package/dist/src/adapters/pi/extension.js.map +1 -0
  6. package/dist/src/adapters/pi/model.d.ts +72 -0
  7. package/dist/src/adapters/pi/model.js +152 -0
  8. package/dist/src/adapters/pi/model.js.map +1 -0
  9. package/dist/src/adapters/pi/resources.d.ts +23 -0
  10. package/dist/src/adapters/pi/resources.js +54 -0
  11. package/dist/src/adapters/pi/resources.js.map +1 -0
  12. package/dist/src/adapters/pi/rpc.d.ts +58 -0
  13. package/dist/src/adapters/pi/rpc.js +253 -0
  14. package/dist/src/adapters/pi/rpc.js.map +1 -0
  15. package/dist/src/adapters/pi/session.d.ts +30 -0
  16. package/dist/src/adapters/pi/session.js +64 -0
  17. package/dist/src/adapters/pi/session.js.map +1 -0
  18. package/dist/src/agents/catalog.d.ts +30 -0
  19. package/dist/src/agents/catalog.js +118 -0
  20. package/dist/src/agents/catalog.js.map +1 -0
  21. package/dist/src/agents/discover.d.ts +28 -0
  22. package/dist/src/agents/discover.js +127 -0
  23. package/dist/src/agents/discover.js.map +1 -0
  24. package/dist/src/agents/import.d.ts +67 -0
  25. package/dist/src/agents/import.js +789 -0
  26. package/dist/src/agents/import.js.map +1 -0
  27. package/dist/src/analysis/hooks.d.ts +81 -0
  28. package/dist/src/analysis/hooks.js +139 -0
  29. package/dist/src/analysis/hooks.js.map +1 -0
  30. package/dist/src/analysis/pre-registration.d.ts +18 -0
  31. package/dist/src/analysis/pre-registration.js +86 -0
  32. package/dist/src/analysis/pre-registration.js.map +1 -0
  33. package/dist/src/branding.d.ts +44 -0
  34. package/dist/src/branding.js +128 -0
  35. package/dist/src/branding.js.map +1 -0
  36. package/dist/src/chat.d.ts +16 -0
  37. package/dist/src/chat.js +95 -0
  38. package/dist/src/chat.js.map +1 -0
  39. package/dist/src/cli.d.ts +2 -0
  40. package/dist/src/cli.js +316 -0
  41. package/dist/src/cli.js.map +1 -0
  42. package/dist/src/core/citations.d.ts +44 -0
  43. package/dist/src/core/citations.js +48 -0
  44. package/dist/src/core/citations.js.map +1 -0
  45. package/dist/src/core/contracts.d.ts +119 -0
  46. package/dist/src/core/contracts.js +2 -0
  47. package/dist/src/core/contracts.js.map +1 -0
  48. package/dist/src/core/docx.d.ts +11 -0
  49. package/dist/src/core/docx.js +46 -0
  50. package/dist/src/core/docx.js.map +1 -0
  51. package/dist/src/core/doi.d.ts +41 -0
  52. package/dist/src/core/doi.js +82 -0
  53. package/dist/src/core/doi.js.map +1 -0
  54. package/dist/src/core/evidence-policy.d.ts +24 -0
  55. package/dist/src/core/evidence-policy.js +308 -0
  56. package/dist/src/core/evidence-policy.js.map +1 -0
  57. package/dist/src/core/filekind.d.ts +15 -0
  58. package/dist/src/core/filekind.js +82 -0
  59. package/dist/src/core/filekind.js.map +1 -0
  60. package/dist/src/core/hash.d.ts +2 -0
  61. package/dist/src/core/hash.js +17 -0
  62. package/dist/src/core/hash.js.map +1 -0
  63. package/dist/src/core/pricing.d.ts +38 -0
  64. package/dist/src/core/pricing.js +110 -0
  65. package/dist/src/core/pricing.js.map +1 -0
  66. package/dist/src/core/redact.d.ts +12 -0
  67. package/dist/src/core/redact.js +48 -0
  68. package/dist/src/core/redact.js.map +1 -0
  69. package/dist/src/core/references.d.ts +55 -0
  70. package/dist/src/core/references.js +102 -0
  71. package/dist/src/core/references.js.map +1 -0
  72. package/dist/src/core/schemas.d.ts +162 -0
  73. package/dist/src/core/schemas.js +160 -0
  74. package/dist/src/core/schemas.js.map +1 -0
  75. package/dist/src/extension.d.ts +1 -0
  76. package/dist/src/extension.js +2 -0
  77. package/dist/src/extension.js.map +1 -0
  78. package/dist/src/index.d.ts +72 -0
  79. package/dist/src/index.js +73 -0
  80. package/dist/src/index.js.map +1 -0
  81. package/dist/src/install/installer.d.ts +147 -0
  82. package/dist/src/install/installer.js +721 -0
  83. package/dist/src/install/installer.js.map +1 -0
  84. package/dist/src/integrations/contracts.d.ts +35 -0
  85. package/dist/src/integrations/contracts.js +27 -0
  86. package/dist/src/integrations/contracts.js.map +1 -0
  87. package/dist/src/integrations/mcp.d.ts +155 -0
  88. package/dist/src/integrations/mcp.js +826 -0
  89. package/dist/src/integrations/mcp.js.map +1 -0
  90. package/dist/src/knowledge-map/contracts.d.ts +20 -0
  91. package/dist/src/knowledge-map/contracts.js +2 -0
  92. package/dist/src/knowledge-map/contracts.js.map +1 -0
  93. package/dist/src/knowledge-map/index.d.ts +4 -0
  94. package/dist/src/knowledge-map/index.js +39 -0
  95. package/dist/src/knowledge-map/index.js.map +1 -0
  96. package/dist/src/literature/contracts.d.ts +106 -0
  97. package/dist/src/literature/contracts.js +2 -0
  98. package/dist/src/literature/contracts.js.map +1 -0
  99. package/dist/src/literature/fulltext.d.ts +7 -0
  100. package/dist/src/literature/fulltext.js +24 -0
  101. package/dist/src/literature/fulltext.js.map +1 -0
  102. package/dist/src/literature/knowledge-map.d.ts +2 -0
  103. package/dist/src/literature/knowledge-map.js +20 -0
  104. package/dist/src/literature/knowledge-map.js.map +1 -0
  105. package/dist/src/literature/ledger.d.ts +3 -0
  106. package/dist/src/literature/ledger.js +17 -0
  107. package/dist/src/literature/ledger.js.map +1 -0
  108. package/dist/src/literature/metadata.d.ts +6 -0
  109. package/dist/src/literature/metadata.js +32 -0
  110. package/dist/src/literature/metadata.js.map +1 -0
  111. package/dist/src/memory/store.d.ts +22 -0
  112. package/dist/src/memory/store.js +73 -0
  113. package/dist/src/memory/store.js.map +1 -0
  114. package/dist/src/orchestration/contracts.d.ts +61 -0
  115. package/dist/src/orchestration/contracts.js +2 -0
  116. package/dist/src/orchestration/contracts.js.map +1 -0
  117. package/dist/src/orchestration/pi-executor.d.ts +28 -0
  118. package/dist/src/orchestration/pi-executor.js +169 -0
  119. package/dist/src/orchestration/pi-executor.js.map +1 -0
  120. package/dist/src/orchestration/runner.d.ts +196 -0
  121. package/dist/src/orchestration/runner.js +991 -0
  122. package/dist/src/orchestration/runner.js.map +1 -0
  123. package/dist/src/orchestration/scheduler.d.ts +8 -0
  124. package/dist/src/orchestration/scheduler.js +335 -0
  125. package/dist/src/orchestration/scheduler.js.map +1 -0
  126. package/dist/src/panel/events.d.ts +20 -0
  127. package/dist/src/panel/events.js +93 -0
  128. package/dist/src/panel/events.js.map +1 -0
  129. package/dist/src/panel/extension.d.ts +8 -0
  130. package/dist/src/panel/extension.js +115 -0
  131. package/dist/src/panel/extension.js.map +1 -0
  132. package/dist/src/panel/literature-map.d.ts +26 -0
  133. package/dist/src/panel/literature-map.js +34 -0
  134. package/dist/src/panel/literature-map.js.map +1 -0
  135. package/dist/src/panel/projection.d.ts +51 -0
  136. package/dist/src/panel/projection.js +288 -0
  137. package/dist/src/panel/projection.js.map +1 -0
  138. package/dist/src/panel/server.d.ts +46 -0
  139. package/dist/src/panel/server.js +1610 -0
  140. package/dist/src/panel/server.js.map +1 -0
  141. package/dist/src/panel/usage.d.ts +53 -0
  142. package/dist/src/panel/usage.js +150 -0
  143. package/dist/src/panel/usage.js.map +1 -0
  144. package/dist/src/project/bootstrap.d.ts +10 -0
  145. package/dist/src/project/bootstrap.js +83 -0
  146. package/dist/src/project/bootstrap.js.map +1 -0
  147. package/dist/src/project/hitl.d.ts +12 -0
  148. package/dist/src/project/hitl.js +43 -0
  149. package/dist/src/project/hitl.js.map +1 -0
  150. package/dist/src/project/jsonl.d.ts +16 -0
  151. package/dist/src/project/jsonl.js +93 -0
  152. package/dist/src/project/jsonl.js.map +1 -0
  153. package/dist/src/project/manuscript.d.ts +21 -0
  154. package/dist/src/project/manuscript.js +74 -0
  155. package/dist/src/project/manuscript.js.map +1 -0
  156. package/dist/src/project/paths.d.ts +38 -0
  157. package/dist/src/project/paths.js +166 -0
  158. package/dist/src/project/paths.js.map +1 -0
  159. package/dist/src/project/versions.d.ts +10 -0
  160. package/dist/src/project/versions.js +14 -0
  161. package/dist/src/project/versions.js.map +1 -0
  162. package/dist/src/psyclaw-theme.d.ts +88 -0
  163. package/dist/src/psyclaw-theme.js +89 -0
  164. package/dist/src/psyclaw-theme.js.map +1 -0
  165. package/dist/src/research/brief.d.ts +11 -0
  166. package/dist/src/research/brief.js +81 -0
  167. package/dist/src/research/brief.js.map +1 -0
  168. package/dist/src/research/ledger.d.ts +12 -0
  169. package/dist/src/research/ledger.js +43 -0
  170. package/dist/src/research/ledger.js.map +1 -0
  171. package/dist/src/run.d.ts +20 -0
  172. package/dist/src/run.js +34 -0
  173. package/dist/src/run.js.map +1 -0
  174. package/dist/src/setup.d.ts +54 -0
  175. package/dist/src/setup.js +221 -0
  176. package/dist/src/setup.js.map +1 -0
  177. package/dist/src/skills/contracts.d.ts +105 -0
  178. package/dist/src/skills/contracts.js +3 -0
  179. package/dist/src/skills/contracts.js.map +1 -0
  180. package/dist/src/skills/preflight.d.ts +10 -0
  181. package/dist/src/skills/preflight.js +39 -0
  182. package/dist/src/skills/preflight.js.map +1 -0
  183. package/dist/src/skills/registry.d.ts +28 -0
  184. package/dist/src/skills/registry.js +540 -0
  185. package/dist/src/skills/registry.js.map +1 -0
  186. package/dist/src/style/artifact.d.ts +6 -0
  187. package/dist/src/style/artifact.js +61 -0
  188. package/dist/src/style/artifact.js.map +1 -0
  189. package/dist/src/style/cli-ui.d.ts +41 -0
  190. package/dist/src/style/cli-ui.js +119 -0
  191. package/dist/src/style/cli-ui.js.map +1 -0
  192. package/dist/src/style/contracts.d.ts +34 -0
  193. package/dist/src/style/contracts.js +2 -0
  194. package/dist/src/style/contracts.js.map +1 -0
  195. package/dist/src/style/registry.d.ts +16 -0
  196. package/dist/src/style/registry.js +50 -0
  197. package/dist/src/style/registry.js.map +1 -0
  198. package/dist/src/trust/manifest.d.ts +42 -0
  199. package/dist/src/trust/manifest.js +53 -0
  200. package/dist/src/trust/manifest.js.map +1 -0
  201. package/dist/src/trust/sbom.d.ts +54 -0
  202. package/dist/src/trust/sbom.js +77 -0
  203. package/dist/src/trust/sbom.js.map +1 -0
  204. package/dist/src/tui/app.d.ts +3 -0
  205. package/dist/src/tui/app.js +103 -0
  206. package/dist/src/tui/app.js.map +1 -0
  207. package/dist/src/tui/model.d.ts +13 -0
  208. package/dist/src/tui/model.js +25 -0
  209. package/dist/src/tui/model.js.map +1 -0
  210. package/dist/src/updates/check.d.ts +43 -0
  211. package/dist/src/updates/check.js +68 -0
  212. package/dist/src/updates/check.js.map +1 -0
  213. package/dist/src/updates/manifest.d.ts +19 -0
  214. package/dist/src/updates/manifest.js +58 -0
  215. package/dist/src/updates/manifest.js.map +1 -0
  216. package/dist/src/updates/registry.d.ts +21 -0
  217. package/dist/src/updates/registry.js +82 -0
  218. package/dist/src/updates/registry.js.map +1 -0
  219. package/dist/src/updates/skills.d.ts +15 -0
  220. package/dist/src/updates/skills.js +39 -0
  221. package/dist/src/updates/skills.js.map +1 -0
  222. package/dist/src/updates/status.d.ts +7 -0
  223. package/dist/src/updates/status.js +37 -0
  224. package/dist/src/updates/status.js.map +1 -0
  225. package/dist/src/updates/update.d.ts +43 -0
  226. package/dist/src/updates/update.js +128 -0
  227. package/dist/src/updates/update.js.map +1 -0
  228. package/dist/src/wizard.d.ts +7 -0
  229. package/dist/src/wizard.js +90 -0
  230. package/dist/src/wizard.js.map +1 -0
  231. package/dist/src/workflows/analysis-delegation.d.ts +3 -0
  232. package/dist/src/workflows/analysis-delegation.js +96 -0
  233. package/dist/src/workflows/analysis-delegation.js.map +1 -0
  234. package/dist/src/workflows/expert-review-runner.d.ts +29 -0
  235. package/dist/src/workflows/expert-review-runner.js +35 -0
  236. package/dist/src/workflows/expert-review-runner.js.map +1 -0
  237. package/dist/src/workflows/expert-review.d.ts +140 -0
  238. package/dist/src/workflows/expert-review.js +111 -0
  239. package/dist/src/workflows/expert-review.js.map +1 -0
  240. package/dist/src/workflows/institutional-fulltext.d.ts +16 -0
  241. package/dist/src/workflows/institutional-fulltext.js +110 -0
  242. package/dist/src/workflows/institutional-fulltext.js.map +1 -0
  243. package/dist/src/workflows/literature-review.d.ts +3 -0
  244. package/dist/src/workflows/literature-review.js +76 -0
  245. package/dist/src/workflows/literature-review.js.map +1 -0
  246. package/dist/src/workflows/meta-analysis.d.ts +38 -0
  247. package/dist/src/workflows/meta-analysis.js +185 -0
  248. package/dist/src/workflows/meta-analysis.js.map +1 -0
  249. package/dist/src/workflows/publish.d.ts +51 -0
  250. package/dist/src/workflows/publish.js +153 -0
  251. package/dist/src/workflows/publish.js.map +1 -0
  252. package/dist/src/workflows/spec.d.ts +51 -0
  253. package/dist/src/workflows/spec.js +157 -0
  254. package/dist/src/workflows/spec.js.map +1 -0
  255. package/dist/src/workflows/style-contract.d.ts +24 -0
  256. package/dist/src/workflows/style-contract.js +23 -0
  257. package/dist/src/workflows/style-contract.js.map +1 -0
  258. package/dist/src/workflows/writing-review.d.ts +15 -0
  259. package/dist/src/workflows/writing-review.js +82 -0
  260. package/dist/src/workflows/writing-review.js.map +1 -0
  261. package/package.json +20 -8
  262. package/scripts/rebrand-pi.mjs +6 -5
  263. package/scripts/__pycache__/build_dist.cpython-314.pyc +0 -0
  264. package/scripts/__pycache__/build_whitepaper_docx.cpython-314.pyc +0 -0
  265. package/scripts/build_preprint_docx.py +0 -729
  266. package/scripts/build_preprint_figures.py +0 -238
  267. package/scripts/build_three_line_table_fixture.py +0 -56
  268. package/scripts/build_whitepaper_docx.py +0 -768
  269. package/scripts/check-branding.mjs +0 -159
  270. package/scripts/copy-panel.mjs +0 -11
  271. package/scripts/verify_docx.py +0 -41
@@ -0,0 +1,991 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { isAbsolute, join, relative, resolve } from "node:path";
3
+ import { lstatSync, readFileSync, realpathSync } from "node:fs";
4
+ import { readFile } from "node:fs/promises";
5
+ import { Type } from "typebox";
6
+ import { Compile } from "typebox/compile";
7
+ import { sha256Text } from "../core/hash.js";
8
+ import { atomicWriteFile } from "../project/jsonl.js";
9
+ import { projectPaths } from "../project/paths.js";
10
+ import { JsonlMemoryStore } from "../memory/store.js";
11
+ import { nextReadyBatch, validatePlan } from "./scheduler.js";
12
+ const EffectSchema = Type.Union([
13
+ Type.Literal("read"),
14
+ Type.Literal("write"),
15
+ Type.Literal("network"),
16
+ Type.Literal("destructive"),
17
+ ]);
18
+ const CompletionContractSchema = Type.Object({
19
+ requiredArtifacts: Type.Array(Type.String({ minLength: 1 })),
20
+ requiredReceiptEffects: Type.Array(EffectSchema),
21
+ mustPassGates: Type.Array(Type.String({ minLength: 1 })),
22
+ }, { additionalProperties: false });
23
+ const TaskNodeSchema = Type.Object({
24
+ id: Type.String({ minLength: 1 }),
25
+ role: Type.Union([
26
+ Type.Literal("planner"),
27
+ Type.Literal("researcher"),
28
+ Type.Literal("analyst"),
29
+ Type.Literal("critic"),
30
+ Type.Literal("writer"),
31
+ Type.Literal("verifier"),
32
+ ]),
33
+ objective: Type.String({ minLength: 1 }),
34
+ deps: Type.Array(Type.String({ minLength: 1 })),
35
+ ownedPaths: Type.Array(Type.String({ minLength: 1 })),
36
+ parallelSafe: Type.Boolean(),
37
+ inputs: Type.Array(Type.String()),
38
+ outputs: Type.Array(Type.String()),
39
+ // Kept optional for compatibility with existing v1 plans. Runtime still
40
+ // defaults to read-only and never implicitly grants network/destructive.
41
+ allowedEffects: Type.Optional(Type.Array(EffectSchema)),
42
+ completionContract: CompletionContractSchema,
43
+ }, { additionalProperties: false });
44
+ export const PlanSchema = Type.Object({
45
+ schemaVersion: Type.Literal("psyclaw/plan/v1"),
46
+ runId: Type.String({ minLength: 1 }),
47
+ tasks: Type.Array(TaskNodeSchema, { minItems: 1 }),
48
+ budget: Type.Object({
49
+ maxTurns: Type.Integer({ minimum: 1 }),
50
+ maxWorkers: Type.Integer({ minimum: 1, maximum: 4 }),
51
+ }, { additionalProperties: false }),
52
+ horizon: Type.Optional(Type.Object({
53
+ strategy: Type.Literal("hierarchical-plan-act-reflect"),
54
+ maxIterations: Type.Integer({ minimum: 1 }),
55
+ reflectionEvery: Type.Integer({ minimum: 1 }),
56
+ }, { additionalProperties: false })),
57
+ }, { additionalProperties: false });
58
+ const compiledPlan = Compile(PlanSchema);
59
+ export class PlanSchemaError extends Error {
60
+ diagnostics;
61
+ constructor(diagnostics) {
62
+ super(`Plan schema invalid: ${diagnostics.join("; ")}`);
63
+ this.name = "PlanSchemaError";
64
+ this.diagnostics = diagnostics;
65
+ }
66
+ }
67
+ /** Parse and validate the only value a planner is allowed to hand to runtime. */
68
+ export function validatePlanSchema(value) {
69
+ if (!compiledPlan.Check(value)) {
70
+ const diagnostics = [...compiledPlan.Errors(value)].map((error) => {
71
+ const path = "path" in error && typeof error.path === "string" ? error.path : "/";
72
+ return `${path}: ${error.message}`;
73
+ });
74
+ throw new PlanSchemaError(diagnostics);
75
+ }
76
+ const plan = value;
77
+ const diagnostics = [];
78
+ if (!isSafeRunId(plan.runId))
79
+ diagnostics.push("/runId: unsafe run id");
80
+ const ids = new Set();
81
+ for (const [index, task] of plan.tasks.entries()) {
82
+ if (!isSafeTaskId(task.id))
83
+ diagnostics.push(`/tasks/${index}/id: unsafe task id`);
84
+ if (ids.has(task.id))
85
+ diagnostics.push(`/tasks/${index}/id: duplicate task id`);
86
+ ids.add(task.id);
87
+ if (task.allowedEffects && new Set(task.allowedEffects).size !== task.allowedEffects.length) {
88
+ diagnostics.push(`/tasks/${index}/allowedEffects: duplicate effect`);
89
+ }
90
+ for (const [depIndex, dep] of task.deps.entries()) {
91
+ if (!isSafeTaskId(dep))
92
+ diagnostics.push(`/tasks/${index}/deps/${depIndex}: unsafe task id`);
93
+ }
94
+ }
95
+ if (diagnostics.length > 0)
96
+ throw new PlanSchemaError(diagnostics);
97
+ return plan;
98
+ }
99
+ /** Alias used by integrations that call the boundary an "accepted plan". */
100
+ export const acceptPlan = validatePlanSchema;
101
+ /** Execute a planner with no ambient tool context, then enforce the Plan schema. */
102
+ export async function planOnly(planner, request) {
103
+ return validatePlanSchema(await planner(request));
104
+ }
105
+ /**
106
+ * A deliberately small coordinator. It owns scheduling and acceptance, while
107
+ * the executor owns the actual Pi/skill/MCP call. No model output can mutate
108
+ * task state without passing the checks below.
109
+ */
110
+ export class BoundedOrchestrator {
111
+ options;
112
+ checkpointWrite = Promise.resolve();
113
+ constructor(options) {
114
+ this.options = options;
115
+ }
116
+ async run(planValue) {
117
+ let plan;
118
+ try {
119
+ plan = validatePlanSchema(planValue);
120
+ }
121
+ catch (error) {
122
+ const message = error instanceof Error ? error.message : "Plan schema invalid";
123
+ return this.blockedWithoutPlan(String(message));
124
+ }
125
+ const diagnostics = validatePlan(plan);
126
+ const policyDiagnostics = validateRunnerPolicy(plan, this.options);
127
+ const allDiagnostics = [...diagnostics, ...policyDiagnostics].filter((item) => item.severity === "block").map((item) => item.reason);
128
+ if (allDiagnostics.length > 0) {
129
+ return this.blocked(plan, allDiagnostics);
130
+ }
131
+ const checkpoint = emptyCheckpoint(plan, this.options);
132
+ const state = { checkpoint, reportByTask: new Map() };
133
+ await this.persist(state.checkpoint);
134
+ await this.emit({ type: "planned", runId: plan.runId, at: this.timestamp() });
135
+ return this.execute(plan, state);
136
+ }
137
+ async resume(planValue) {
138
+ let plan;
139
+ try {
140
+ plan = validatePlanSchema(planValue);
141
+ }
142
+ catch (error) {
143
+ const message = error instanceof Error ? error.message : "Plan schema invalid";
144
+ return this.blockedWithoutPlan(String(message));
145
+ }
146
+ const diagnostics = [...validatePlan(plan), ...validateRunnerPolicy(plan, this.options)]
147
+ .filter((item) => item.severity === "block").map((item) => item.reason);
148
+ if (diagnostics.length > 0)
149
+ return this.blocked(plan, diagnostics);
150
+ const path = this.checkpointPath(plan.runId);
151
+ if (!path)
152
+ return this.blocked(plan, ["checkpoint path is required for resume"]);
153
+ let checkpoint;
154
+ try {
155
+ this.assertCheckpointPath(path);
156
+ checkpoint = parseCheckpoint(JSON.parse(await readFile(path, "utf8")));
157
+ }
158
+ catch (error) {
159
+ return this.blocked(plan, [`checkpoint unavailable or invalid: ${error instanceof Error ? error.message : String(error)}`]);
160
+ }
161
+ const expectedHash = hashPlan(plan);
162
+ if (checkpoint.runId !== plan.runId)
163
+ return this.blocked(plan, ["checkpoint run id mismatch"]);
164
+ if (checkpoint.planHash !== expectedHash)
165
+ return this.blocked(plan, ["checkpoint plan hash mismatch"]);
166
+ if (this.options.inputDigest !== undefined && checkpoint.inputDigest !== this.options.inputDigest) {
167
+ return this.blocked(plan, ["checkpoint input digest drift"]);
168
+ }
169
+ if (checkpoint.inputDigest !== undefined && this.options.inputDigest === undefined) {
170
+ return this.blocked(plan, ["checkpoint input digest must be supplied for resume"]);
171
+ }
172
+ if (this.options.specRevision !== undefined && checkpoint.specRevision !== this.options.specRevision) {
173
+ return this.blocked(plan, ["checkpoint spec revision drift"]);
174
+ }
175
+ if (checkpoint.specRevision !== undefined && this.options.specRevision === undefined) {
176
+ return this.blocked(plan, ["checkpoint spec revision must be supplied for resume"]);
177
+ }
178
+ // A process crash can leave a task marked running. It has no live worker
179
+ // after restart, so make it pending and issue a fresh dispatch id. A
180
+ // result carrying the old id can then never overwrite the resumed task.
181
+ for (const item of Object.values(checkpoint.tasks)) {
182
+ if (item.status === "running") {
183
+ item.status = "pending";
184
+ delete item.dispatchId;
185
+ }
186
+ }
187
+ if (checkpoint.budgetUsed > effectiveBudget(plan, this.options)) {
188
+ return this.blocked(plan, ["checkpoint budget already exceeds the requested budget"]);
189
+ }
190
+ const planTaskIds = new Set(plan.tasks.map((task) => task.id));
191
+ const checkpointTaskIds = new Set(Object.keys(checkpoint.tasks));
192
+ if (planTaskIds.size !== checkpointTaskIds.size || [...planTaskIds].some((id) => !checkpointTaskIds.has(id))) {
193
+ return this.blocked(plan, ["checkpoint task set does not match plan"]);
194
+ }
195
+ // A persisted success is only a hint. Re-run the exact completion
196
+ // contract before allowing its dependants to proceed; files and receipts
197
+ // may have changed while the process was down.
198
+ for (const task of plan.tasks) {
199
+ const item = checkpoint.tasks[task.id];
200
+ if (item?.status !== "succeeded")
201
+ continue;
202
+ const reaccepted = item.report
203
+ ? verifyWorkerCompletion(task, {
204
+ report: item.report,
205
+ artifacts: item.artifactRecords ?? item.artifacts,
206
+ receipts: item.receipts,
207
+ passedGates: item.passedGates,
208
+ }, {
209
+ readOnly: this.taskIsReadOnly(task),
210
+ ...(this.options.root !== undefined ? { artifactRoot: this.options.root } : {}),
211
+ expectedRunId: plan.runId,
212
+ expectedTaskId: task.id,
213
+ ...(item.dispatchId !== undefined ? { expectedDispatchId: item.dispatchId } : {}),
214
+ ...(this.options.specRevision !== undefined ? { expectedSpecRevision: this.options.specRevision } : {}),
215
+ allowedEffects: this.allowedEffects(task),
216
+ })
217
+ : { ok: false, reasons: ["succeeded checkpoint has no worker report"] };
218
+ if (!reaccepted.ok) {
219
+ item.status = "pending";
220
+ delete item.dispatchId;
221
+ delete item.report;
222
+ item.artifacts = [];
223
+ delete item.artifactRecords;
224
+ item.receipts = [];
225
+ item.passedGates = [];
226
+ }
227
+ }
228
+ const state = {
229
+ checkpoint: {
230
+ ...checkpoint,
231
+ budgetLimit: effectiveBudget(plan, this.options),
232
+ updatedAt: this.timestamp(),
233
+ },
234
+ reportByTask: new Map(Object.entries(checkpoint.tasks)
235
+ .filter(([, item]) => item.status === "succeeded" && item.report !== undefined)
236
+ .map(([id, item]) => [id, item.report])),
237
+ };
238
+ await this.persist(state.checkpoint);
239
+ return this.execute(plan, state);
240
+ }
241
+ async execute(plan, state) {
242
+ const diagnostics = [];
243
+ const tasks = state.checkpoint.tasks;
244
+ const completed = new Set(Object.entries(tasks).filter(([, item]) => item.status === "succeeded").map(([id]) => id));
245
+ const maxWorkers = effectiveWorkers(plan, this.options);
246
+ const controller = new AbortController();
247
+ while (completed.size < plan.tasks.length) {
248
+ if (await this.options.pauseRequested?.()) {
249
+ await this.persist(state.checkpoint);
250
+ await this.emit({ type: "checkpoint", runId: plan.runId, at: this.timestamp(), message: "pause requested by human" });
251
+ return this.result(plan, state, "paused", [...diagnostics, "pause requested by human"]);
252
+ }
253
+ const running = Object.entries(tasks)
254
+ .filter(([, item]) => item.status === "running")
255
+ .map(([id]) => plan.tasks.find((task) => task.id === id))
256
+ .filter((task) => task !== undefined);
257
+ const ready = nextReadyBatch(plan, completed, running).slice(0, maxWorkers);
258
+ const pending = plan.tasks.filter((task) => tasks[task.id]?.status === "pending");
259
+ if (ready.length === 0) {
260
+ if (pending.length === 0)
261
+ break;
262
+ const unresolved = pending.map((task) => task.id).join(", ");
263
+ const reason = `no schedulable tasks; unresolved or failed dependencies: ${unresolved}`;
264
+ diagnostics.push(reason);
265
+ await this.markBlocked(state, reason);
266
+ return this.result(plan, state, "blocked", diagnostics);
267
+ }
268
+ const remainingBudget = state.checkpoint.budgetLimit - state.checkpoint.budgetUsed;
269
+ if (remainingBudget <= 0) {
270
+ diagnostics.push("worker turn budget exhausted");
271
+ await this.persist(state.checkpoint);
272
+ return this.result(plan, state, "paused", diagnostics);
273
+ }
274
+ const dispatchable = ready.slice(0, remainingBudget);
275
+ if (dispatchable.length < ready.length)
276
+ diagnostics.push("worker turn budget will pause before all ready tasks run");
277
+ const dispatches = dispatchable.map((task) => this.dispatch(plan, state, task, controller.signal));
278
+ await Promise.all(dispatches);
279
+ for (const task of dispatchable) {
280
+ if (tasks[task.id]?.status === "succeeded")
281
+ completed.add(task.id);
282
+ if (tasks[task.id]?.status === "failed" || tasks[task.id]?.status === "blocked") {
283
+ const reason = tasks[task.id]?.report?.blockers.join("; ") || `task ${task.id} failed`;
284
+ diagnostics.push(reason);
285
+ await this.persist(state.checkpoint);
286
+ return this.result(plan, state, "blocked", diagnostics);
287
+ }
288
+ }
289
+ if (dispatchable.length < ready.length) {
290
+ await this.persist(state.checkpoint);
291
+ return this.result(plan, state, "paused", diagnostics);
292
+ }
293
+ }
294
+ await this.persist(state.checkpoint);
295
+ await this.emit({ type: "completed", runId: plan.runId, at: this.timestamp(), message: "all task completion contracts passed" });
296
+ return this.result(plan, state, "completed", diagnostics);
297
+ }
298
+ async dispatch(plan, state, task, signal) {
299
+ const entry = state.checkpoint.tasks[task.id];
300
+ if (!entry || entry.status !== "pending")
301
+ return;
302
+ const attempt = entry.attempt + 1;
303
+ const dispatchId = `${plan.runId}:${task.id}:${attempt}:${randomUUID().slice(0, 8)}`;
304
+ entry.status = "running";
305
+ entry.attempt = attempt;
306
+ entry.dispatchId = dispatchId;
307
+ state.checkpoint.budgetUsed += 1;
308
+ await this.persist(state.checkpoint);
309
+ await this.emit({ type: "started", runId: plan.runId, at: this.timestamp(), taskId: task.id, dispatchId });
310
+ let raw;
311
+ try {
312
+ raw = await this.options.executor(task, {
313
+ runId: plan.runId,
314
+ dispatchId,
315
+ attempt,
316
+ task,
317
+ readOnly: this.taskIsReadOnly(task),
318
+ signal,
319
+ });
320
+ }
321
+ catch (error) {
322
+ // A thrown executor result is never treated as success.
323
+ entry.status = "failed";
324
+ entry.report = failedReport(task.id, dispatchId, `executor threw: ${error instanceof Error ? error.message : String(error)}`);
325
+ await this.draftReflection(plan, task, entry.report.summary, entry.report.blockers);
326
+ await this.persist(state.checkpoint);
327
+ return;
328
+ }
329
+ // The entry may have been superseded by a resume/abort while the worker
330
+ // was away. A late result is observed but cannot overwrite current state.
331
+ if (entry.status !== "running" || entry.dispatchId !== dispatchId)
332
+ return;
333
+ const normalized = normalizeWorkerResult(raw);
334
+ const acceptance = verifyWorkerCompletion(task, normalized, {
335
+ readOnly: this.taskIsReadOnly(task),
336
+ writtenPaths: state.checkpoint.writtenPaths,
337
+ reservedPaths: plan.tasks
338
+ .filter((candidate) => candidate.id !== task.id && state.checkpoint.tasks[candidate.id]?.status === "running")
339
+ .flatMap((candidate) => candidate.ownedPaths),
340
+ expectedRunId: plan.runId,
341
+ expectedTaskId: task.id,
342
+ expectedDispatchId: dispatchId,
343
+ ...(this.options.specRevision !== undefined ? { expectedSpecRevision: this.options.specRevision } : {}),
344
+ ...(this.options.root !== undefined ? { artifactRoot: this.options.root } : {}),
345
+ allowedEffects: this.allowedEffects(task),
346
+ seenIdempotencyKeys: Object.values(state.checkpoint.tasks).flatMap((item) => item.receipts
347
+ .map((receipt) => receipt.idempotencyKey)
348
+ .filter((key) => typeof key === "string")),
349
+ });
350
+ if (!acceptance.ok) {
351
+ entry.status = "blocked";
352
+ entry.report = failedReport(task.id, dispatchId, acceptance.reasons.join("; "), normalized.report);
353
+ await this.draftReflection(plan, task, entry.report.summary, entry.report.blockers);
354
+ await this.persist(state.checkpoint);
355
+ await this.emit({ type: "blocked", runId: plan.runId, at: this.timestamp(), taskId: task.id, dispatchId, message: acceptance.reasons.join("; ") });
356
+ return;
357
+ }
358
+ entry.status = "succeeded";
359
+ entry.report = normalized.report;
360
+ entry.artifacts = normalized.artifacts;
361
+ if (normalized.artifactRecords && normalized.artifactRecords.length > 0) {
362
+ entry.artifactRecords = normalized.artifactRecords;
363
+ }
364
+ entry.receipts = normalized.receipts;
365
+ entry.passedGates = normalized.passedGates;
366
+ for (const path of normalized.report.filesModified)
367
+ state.checkpoint.writtenPaths.push(normalizePath(path));
368
+ state.reportByTask.set(task.id, normalized.report);
369
+ await this.persist(state.checkpoint);
370
+ await this.emit({ type: "completed", runId: plan.runId, at: this.timestamp(), taskId: task.id, dispatchId });
371
+ }
372
+ /** Persist failure experience as a pending lesson; human approval promotes it. */
373
+ async draftReflection(plan, task, summary, blockers) {
374
+ if (!this.options.root)
375
+ return;
376
+ const now = this.timestamp();
377
+ try {
378
+ await new JsonlMemoryStore(this.options.root).draft({
379
+ id: `lesson_${plan.runId}_${task.id}_${Date.now()}`,
380
+ kind: "lesson",
381
+ scope: "project",
382
+ content: { strategy: plan.horizon?.strategy ?? "plan-act", task: task.objective, summary, blockers: [...blockers] },
383
+ sourceRefs: [`run:${plan.runId}`, `task:${task.id}`],
384
+ confidence: 0.5,
385
+ status: "pending",
386
+ createdAt: now,
387
+ updatedAt: now,
388
+ });
389
+ }
390
+ catch {
391
+ // Reflection must never turn an already-recorded worker failure into a new failure.
392
+ }
393
+ }
394
+ checkpointPath(runId) {
395
+ return this.options.checkpointPath ?? (this.options.root ? join(projectPaths(this.options.root).runs, `${runId}.checkpoint.json`) : undefined);
396
+ }
397
+ allowedEffects(task) {
398
+ const declared = task.allowedEffects;
399
+ // `allowWrites` is a host ceiling, not an implicit task permission. A
400
+ // planner must explicitly declare `allowedEffects: ["read", "write"]`
401
+ // before a write can be accepted; omitted declarations remain read-only.
402
+ const requested = declared === undefined ? ["read"] : declared;
403
+ // Each effect class needs its own explicit host approval: write from
404
+ // allowWrites, network from allowNetwork, destructive from allowDestructive.
405
+ // allowWrites never implicitly grants network or destructive.
406
+ const effective = [];
407
+ for (const effect of requested) {
408
+ if (effect === "read")
409
+ effective.push("read");
410
+ else if (effect === "write" && this.options.allowWrites)
411
+ effective.push("write");
412
+ else if (effect === "network" && this.options.allowNetwork)
413
+ effective.push("network");
414
+ else if (effect === "destructive" && this.options.allowDestructive)
415
+ effective.push("destructive");
416
+ }
417
+ return new Set(effective);
418
+ }
419
+ taskAllowsWrite(task) {
420
+ return this.allowedEffects(task).has("write");
421
+ }
422
+ taskIsReadOnly(task) {
423
+ return [...this.allowedEffects(task)].every((effect) => effect === "read");
424
+ }
425
+ async persist(checkpoint) {
426
+ const path = this.checkpointPath(checkpoint.runId);
427
+ if (!path)
428
+ return;
429
+ this.assertCheckpointPath(path);
430
+ const operation = this.checkpointWrite.catch(() => undefined).then(async () => {
431
+ checkpoint.updatedAt = this.timestamp();
432
+ await atomicWriteFile(path, `${JSON.stringify(checkpoint, null, 2)}\n`);
433
+ await this.emit({ type: "checkpoint", runId: checkpoint.runId, at: checkpoint.updatedAt });
434
+ });
435
+ this.checkpointWrite = operation.catch(() => undefined);
436
+ await operation;
437
+ }
438
+ async markBlocked(state, reason) {
439
+ for (const item of Object.values(state.checkpoint.tasks)) {
440
+ if (item.status === "pending")
441
+ item.status = "blocked";
442
+ }
443
+ await this.persist(state.checkpoint);
444
+ await this.emit({ type: "blocked", runId: state.checkpoint.runId, at: this.timestamp(), message: reason });
445
+ }
446
+ async emit(event) {
447
+ await this.options.onEvent?.(event);
448
+ }
449
+ timestamp() {
450
+ return (this.options.now ?? (() => new Date().toISOString()))();
451
+ }
452
+ assertCheckpointPath(path) {
453
+ if (!this.options.root) {
454
+ if (this.options.checkpointPath)
455
+ throw new Error("custom checkpoint path requires a project root");
456
+ return;
457
+ }
458
+ const base = resolve(this.options.root);
459
+ const candidate = resolve(path);
460
+ const rel = relative(base, candidate).replaceAll("\\", "/");
461
+ if (!rel || rel === ".." || rel.startsWith("../") || isAbsolute(rel) || rel.toLowerCase().startsWith(".git/") || rel.toLowerCase() === ".git") {
462
+ throw new Error("checkpoint path must remain inside the project root");
463
+ }
464
+ // Check existing paths and every ancestor. A symlink introduced after the
465
+ // lexical check must not redirect an atomic checkpoint write outside root.
466
+ const rootReal = realpathSync(base);
467
+ let current = candidate;
468
+ while (true) {
469
+ try {
470
+ const stat = lstatSync(current);
471
+ if (stat.isSymbolicLink())
472
+ throw new Error("checkpoint path ancestor symlink is not allowed");
473
+ const real = realpathSync(current);
474
+ const realRel = relative(rootReal, real).replaceAll("\\", "/");
475
+ if (realRel === ".." || realRel.startsWith("../") || isAbsolute(realRel)) {
476
+ throw new Error("checkpoint path resolves outside the project root");
477
+ }
478
+ if (realRel === "" && current !== base)
479
+ throw new Error("checkpoint path resolves to project root");
480
+ return;
481
+ }
482
+ catch (error) {
483
+ if (error.code !== "ENOENT")
484
+ throw error;
485
+ const parent = resolve(current, "..");
486
+ if (parent === current)
487
+ throw new Error("checkpoint path has no existing project ancestor");
488
+ current = parent;
489
+ }
490
+ }
491
+ }
492
+ blocked(plan, diagnostics) {
493
+ const checkpoint = emptyCheckpoint(plan, this.options);
494
+ for (const task of plan.tasks) {
495
+ const item = checkpoint.tasks[task.id];
496
+ if (item)
497
+ item.status = "blocked";
498
+ }
499
+ return {
500
+ runId: plan.runId,
501
+ status: "blocked",
502
+ planHash: hashPlan(plan),
503
+ budgetUsed: 0,
504
+ checkpoint,
505
+ reports: [],
506
+ diagnostics,
507
+ };
508
+ }
509
+ blockedWithoutPlan(reason) {
510
+ return { runId: "unknown", status: "blocked", planHash: "", budgetUsed: 0, reports: [], diagnostics: [reason] };
511
+ }
512
+ result(plan, state, status, diagnostics) {
513
+ return {
514
+ runId: plan.runId,
515
+ status,
516
+ planHash: state.checkpoint.planHash,
517
+ budgetUsed: state.checkpoint.budgetUsed,
518
+ checkpoint: state.checkpoint,
519
+ reports: [...state.reportByTask.values()],
520
+ diagnostics,
521
+ };
522
+ }
523
+ }
524
+ /** Alias kept short for callers that do not need the implementation name. */
525
+ export const Orchestrator = BoundedOrchestrator;
526
+ export async function runPlan(plan, options) {
527
+ return new BoundedOrchestrator(options).run(plan);
528
+ }
529
+ export async function resumePlan(plan, options) {
530
+ return new BoundedOrchestrator(options).resume(plan);
531
+ }
532
+ export function verifyWorkerCompletion(task, result, policy = { readOnly: true }) {
533
+ const normalized = normalizeWorkerResult(result);
534
+ const reasons = [];
535
+ if (!normalized.schemaValid)
536
+ return { ok: false, reasons: ["worker report schema invalid"] };
537
+ const report = normalized.report;
538
+ if (report.taskId !== task.id)
539
+ reasons.push("worker report task id mismatch");
540
+ if (policy.expectedTaskId !== undefined && report.taskId !== policy.expectedTaskId)
541
+ reasons.push("worker report task id does not match dispatch");
542
+ if (policy.expectedDispatchId !== undefined && report.dispatchId !== policy.expectedDispatchId)
543
+ reasons.push("late or stale worker dispatch result");
544
+ if (report.outcome !== "succeeded")
545
+ reasons.push(`worker outcome is ${report.outcome}`);
546
+ if (report.verification.some((item) => item.exitCode !== 0))
547
+ reasons.push("worker verification command failed");
548
+ if (normalized.specRevision !== undefined && normalized.specRevision.trim() === "")
549
+ reasons.push("worker spec revision is empty");
550
+ if (policy.expectedSpecRevision !== undefined && normalized.specRevision !== policy.expectedSpecRevision) {
551
+ reasons.push("worker spec revision mismatch or missing");
552
+ }
553
+ const modifications = report.filesModified.map(normalizePath);
554
+ if (policy.readOnly && modifications.length > 0)
555
+ reasons.push("read-only worker reported file modifications");
556
+ for (const path of modifications) {
557
+ if (isProtectedPath(path))
558
+ reasons.push(`protected path modification denied: ${path}`);
559
+ if (!policy.readOnly && !isOwnedPath(task.ownedPaths, path))
560
+ reasons.push(`file modification outside owned paths: ${path}`);
561
+ if (policy.writtenPaths?.some((existing) => pathsOverlap(existing, path)))
562
+ reasons.push(`parallel write overlap: ${path}`);
563
+ if (policy.reservedPaths?.some((reserved) => pathsOverlap(reserved, path)))
564
+ reasons.push(`parallel write overlap: ${path}`);
565
+ }
566
+ const contract = task.completionContract;
567
+ const artifactRecords = normalized.artifactRecords ?? normalized.artifacts.map((path) => ({ path }));
568
+ if (contract.requiredArtifacts.length > 0 && !policy.artifactRoot) {
569
+ reasons.push("required artifacts cannot be verified without a project root");
570
+ }
571
+ for (const artifact of artifactRecords) {
572
+ if (typeof artifact.path !== "string" || artifact.path.trim() === "") {
573
+ reasons.push("artifact path must be a non-empty string");
574
+ continue;
575
+ }
576
+ if (artifact.sha256 !== undefined && !isSha256(artifact.sha256)) {
577
+ reasons.push(`artifact hash is invalid: ${artifact.path}`);
578
+ continue;
579
+ }
580
+ if (policy.artifactRoot) {
581
+ const check = verifyArtifactOnDisk(policy.artifactRoot, artifact);
582
+ if (!check.ok)
583
+ reasons.push(check.reason);
584
+ }
585
+ }
586
+ for (const error of normalized.artifactErrors ?? [])
587
+ reasons.push(error);
588
+ for (const required of contract.requiredArtifacts) {
589
+ const requiredPath = normalizePath(required);
590
+ const record = artifactRecords.find((artifact) => normalizePath(artifact.path) === requiredPath);
591
+ if (!record) {
592
+ reasons.push(`missing required artifact: ${required}`);
593
+ continue;
594
+ }
595
+ // A required artifact must be produced under this task's owned paths or
596
+ // outputs; a pre-existing arbitrary file (e.g. package.json) cannot stand in.
597
+ if (!isOwnedOrOutput(task, requiredPath)) {
598
+ reasons.push(`required artifact outside owned paths/outputs: ${required}`);
599
+ }
600
+ if (record.sha256 === undefined || !isSha256(record.sha256)) {
601
+ reasons.push(`required artifact missing a valid sha256: ${required}`);
602
+ }
603
+ if (policy.artifactRoot) {
604
+ const check = verifyArtifactOnDisk(policy.artifactRoot, record);
605
+ if (!check.ok)
606
+ reasons.push(check.reason);
607
+ }
608
+ }
609
+ for (const requiredEffect of contract.requiredReceiptEffects) {
610
+ if (!normalized.receipts.some((receipt) => isReceipt(receipt) && receipt.effect === requiredEffect && receipt.ok && receipt.approval !== "denied")) {
611
+ reasons.push(`missing successful receipt effect: ${requiredEffect}`);
612
+ }
613
+ }
614
+ if (modifications.length > 0 && !normalized.receipts.some((receipt) => (isReceipt(receipt) && receipt.effect === "write" && receipt.ok && receipt.approval === "approved" &&
615
+ typeof receipt.idempotencyKey === "string" && receipt.idempotencyKey.length > 0))) {
616
+ reasons.push("file modification lacks approved idempotent write receipt");
617
+ }
618
+ for (const gate of contract.mustPassGates) {
619
+ if (!normalized.passedGates.includes(gate))
620
+ reasons.push(`required gate not passed: ${gate}`);
621
+ }
622
+ const seenKeys = new Set(policy.seenIdempotencyKeys ?? []);
623
+ for (const receipt of normalized.receipts) {
624
+ if (!isReceipt(receipt)) {
625
+ reasons.push("worker receipt schema invalid");
626
+ continue;
627
+ }
628
+ if (policy.expectedRunId !== undefined && receipt.runId !== policy.expectedRunId)
629
+ reasons.push("worker receipt run id mismatch");
630
+ if (policy.expectedTaskId !== undefined && receipt.taskId !== policy.expectedTaskId)
631
+ reasons.push("worker receipt task id mismatch");
632
+ if (policy.readOnly && receipt.effect !== "read")
633
+ reasons.push(`read-only worker requested ${receipt.effect} effect`);
634
+ if (policy.allowedEffects && !policy.allowedEffects.has(receipt.effect)) {
635
+ reasons.push(`undeclared ${receipt.effect} effect: ${receipt.tool}`);
636
+ }
637
+ if (receipt.effect !== "read" && (receipt.approval !== "approved" || !isValidIdempotencyKey(receipt.idempotencyKey))) {
638
+ reasons.push(`side-effect receipt lacks approval/idempotency: ${receipt.tool}`);
639
+ }
640
+ if (receipt.idempotencyKey !== undefined) {
641
+ if (seenKeys.has(receipt.idempotencyKey))
642
+ reasons.push(`duplicate idempotency key: ${receipt.idempotencyKey}`);
643
+ seenKeys.add(receipt.idempotencyKey);
644
+ }
645
+ }
646
+ return { ok: reasons.length === 0, reasons: [...new Set(reasons)] };
647
+ }
648
+ function normalizeWorkerResult(raw) {
649
+ if (isNormalizedResult(raw))
650
+ return raw;
651
+ if (isExecutionResult(raw)) {
652
+ const schemaValid = isWorkerReport(raw.report);
653
+ const report = schemaValid ? raw.report : invalidReport(raw.report);
654
+ const artifactRecords = [];
655
+ const artifactErrors = [];
656
+ if (raw.artifacts !== undefined && !Array.isArray(raw.artifacts))
657
+ artifactErrors.push("worker artifacts must be an array");
658
+ const supplied = Array.isArray(raw.artifacts) ? raw.artifacts : report.filesModified;
659
+ for (const item of supplied) {
660
+ if (typeof item === "string")
661
+ artifactRecords.push({ path: item });
662
+ else if (typeof item === "object" && item !== null && typeof item.path === "string" &&
663
+ (item.sha256 === undefined || typeof item.sha256 === "string")) {
664
+ artifactRecords.push({ path: item.path, ...(item.sha256 !== undefined ? { sha256: item.sha256 } : {}) });
665
+ }
666
+ else
667
+ artifactErrors.push("worker artifact record invalid");
668
+ }
669
+ return {
670
+ report,
671
+ artifacts: artifactRecords.map((item) => item.path),
672
+ artifactRecords,
673
+ artifactErrors,
674
+ receipts: Array.isArray(raw.receipts) ? raw.receipts : reportReceipts(report),
675
+ passedGates: Array.isArray(raw.passedGates) && raw.passedGates.every((item) => typeof item === "string")
676
+ ? raw.passedGates
677
+ : reportGates(report),
678
+ ...(raw.specRevision !== undefined ? { specRevision: raw.specRevision } : {}),
679
+ schemaValid,
680
+ };
681
+ }
682
+ const schemaValid = isWorkerReport(raw);
683
+ const report = schemaValid ? raw : invalidReport(raw);
684
+ return {
685
+ report,
686
+ artifacts: report.filesModified,
687
+ artifactRecords: report.filesModified.map((path) => ({ path })),
688
+ artifactErrors: [],
689
+ receipts: reportReceipts(report),
690
+ passedGates: reportGates(report),
691
+ schemaValid,
692
+ };
693
+ }
694
+ function isExecutionResult(value) {
695
+ return typeof value === "object" && value !== null && "report" in value;
696
+ }
697
+ function isNormalizedResult(value) {
698
+ if (typeof value !== "object" || value === null)
699
+ return false;
700
+ const candidate = value;
701
+ return typeof candidate.schemaValid === "boolean" &&
702
+ Array.isArray(candidate.artifactRecords) &&
703
+ Array.isArray(candidate.artifacts) &&
704
+ candidate.report !== undefined;
705
+ }
706
+ function invalidReport(value) {
707
+ const candidate = typeof value === "object" && value !== null ? value : {};
708
+ return {
709
+ schemaVersion: candidate.schemaVersion === "psyclaw/worker-report/v1" ? candidate.schemaVersion : "psyclaw/worker-report/v1",
710
+ taskId: typeof candidate.taskId === "string" ? candidate.taskId : "",
711
+ dispatchId: typeof candidate.dispatchId === "string" ? candidate.dispatchId : "",
712
+ outcome: "failed",
713
+ summary: typeof candidate.summary === "string" ? candidate.summary : "invalid worker report",
714
+ filesModified: Array.isArray(candidate.filesModified) && candidate.filesModified.every((item) => typeof item === "string") ? candidate.filesModified : [],
715
+ verification: Array.isArray(candidate.verification) ? candidate.verification : [],
716
+ blockers: ["worker report schema invalid"],
717
+ };
718
+ }
719
+ function reportReceipts(report) {
720
+ const candidate = report.receipts;
721
+ return Array.isArray(candidate) ? candidate : [];
722
+ }
723
+ function reportGates(report) {
724
+ const candidate = report.passedGates;
725
+ return Array.isArray(candidate) && candidate.every((item) => typeof item === "string") ? candidate : [];
726
+ }
727
+ function isWorkerReport(value) {
728
+ if (typeof value !== "object" || value === null)
729
+ return false;
730
+ const report = value;
731
+ return (report.schemaVersion === "psyclaw/worker-report/v1" &&
732
+ typeof report.taskId === "string" && report.taskId.length > 0 &&
733
+ typeof report.dispatchId === "string" && report.dispatchId.length > 0 &&
734
+ (report.outcome === "succeeded" || report.outcome === "blocked" || report.outcome === "failed") &&
735
+ typeof report.summary === "string" &&
736
+ Array.isArray(report.filesModified) && report.filesModified.every((item) => typeof item === "string") &&
737
+ Array.isArray(report.verification) && report.verification.every((item) => (typeof item === "object" && item !== null && typeof item.command === "string" &&
738
+ typeof item.exitCode === "number" && Number.isInteger(item.exitCode))) &&
739
+ Array.isArray(report.blockers) && report.blockers.every((item) => typeof item === "string"));
740
+ }
741
+ function isReceipt(value) {
742
+ if (typeof value !== "object" || value === null)
743
+ return false;
744
+ const receipt = value;
745
+ return receipt.schemaVersion === "psyclaw/tool-receipt/v1" &&
746
+ typeof receipt.runId === "string" && receipt.runId.length > 0 &&
747
+ typeof receipt.taskId === "string" && receipt.taskId.length > 0 &&
748
+ typeof receipt.tool === "string" && receipt.tool.length > 0 &&
749
+ (receipt.effect === "read" || receipt.effect === "write" || receipt.effect === "network" || receipt.effect === "destructive") &&
750
+ (receipt.approval === "not-needed" || receipt.approval === "approved" || receipt.approval === "denied") &&
751
+ typeof receipt.ok === "boolean" &&
752
+ typeof receipt.startedAt === "string" && isTimestamp(receipt.startedAt) &&
753
+ typeof receipt.finishedAt === "string" && isTimestamp(receipt.finishedAt) &&
754
+ new Date(receipt.finishedAt).getTime() >= new Date(receipt.startedAt).getTime() &&
755
+ (receipt.idempotencyKey === undefined || isValidIdempotencyKey(receipt.idempotencyKey));
756
+ }
757
+ function isSafeRunId(value) {
758
+ return /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value) && value !== "." && value !== "..";
759
+ }
760
+ function isSafeTaskId(value) {
761
+ return /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value) && value !== "." && value !== "..";
762
+ }
763
+ function isTimestamp(value) {
764
+ // Date.parse accepts surprising values such as `0` or locale strings. A
765
+ // receipt is an audit boundary, so require an unambiguous UTC ISO form.
766
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value) && Number.isFinite(Date.parse(value));
767
+ }
768
+ function isSha256(value) {
769
+ return /^[a-f0-9]{64}$/i.test(value);
770
+ }
771
+ function isValidIdempotencyKey(value) {
772
+ return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/.test(value);
773
+ }
774
+ function verifyArtifactOnDisk(root, artifact) {
775
+ const normalized = artifact.path.trim().replaceAll("\\", "/");
776
+ if (isAbsoluteOrTraversal(normalized))
777
+ return { ok: false, reason: `artifact path escapes project root: ${artifact.path}` };
778
+ const candidate = resolve(root, normalized);
779
+ const base = resolve(root);
780
+ const rel = relative(base, candidate).replaceAll("\\", "/");
781
+ if (!rel || rel === ".." || rel.startsWith("../") || isAbsolute(rel)) {
782
+ return { ok: false, reason: `artifact path escapes project root: ${artifact.path}` };
783
+ }
784
+ try {
785
+ const stat = lstatSync(candidate);
786
+ if (!stat.isFile() || stat.isSymbolicLink())
787
+ return { ok: false, reason: `required artifact is not a regular file: ${artifact.path}` };
788
+ const rootReal = realpathSync(base);
789
+ const real = realpathSync(candidate);
790
+ const realRel = relative(rootReal, real).replaceAll("\\", "/");
791
+ if (!realRel || realRel === ".." || realRel.startsWith("../") || isAbsolute(realRel)) {
792
+ return { ok: false, reason: `artifact resolves outside project root: ${artifact.path}` };
793
+ }
794
+ if (artifact.sha256 !== undefined) {
795
+ const actual = createHash("sha256").update(readFileSync(candidate)).digest("hex");
796
+ if (actual.toLowerCase() !== artifact.sha256.toLowerCase())
797
+ return { ok: false, reason: `artifact hash mismatch: ${artifact.path}` };
798
+ }
799
+ return { ok: true };
800
+ }
801
+ catch (error) {
802
+ if (error.code === "ENOENT")
803
+ return { ok: false, reason: `required artifact does not exist: ${artifact.path}` };
804
+ return { ok: false, reason: `artifact could not be verified: ${artifact.path}` };
805
+ }
806
+ }
807
+ function failedReport(taskId, dispatchId, reason, base) {
808
+ return {
809
+ schemaVersion: "psyclaw/worker-report/v1",
810
+ taskId,
811
+ dispatchId,
812
+ outcome: "failed",
813
+ summary: base?.summary ?? "worker did not satisfy completion contract",
814
+ filesModified: base?.filesModified ?? [],
815
+ verification: base?.verification ?? [],
816
+ blockers: [...(base?.blockers ?? []), reason],
817
+ };
818
+ }
819
+ function emptyCheckpoint(plan, options) {
820
+ const tasks = {};
821
+ for (const task of plan.tasks) {
822
+ tasks[task.id] = { status: "pending", attempt: 0, artifacts: [], receipts: [], passedGates: [] };
823
+ }
824
+ const inputDigest = options.inputDigest;
825
+ const specRevision = options.specRevision;
826
+ return {
827
+ schemaVersion: "psyclaw/orchestrator-checkpoint/v1",
828
+ runId: plan.runId,
829
+ planHash: hashPlan(plan),
830
+ ...(inputDigest !== undefined ? { inputDigest } : {}),
831
+ ...(specRevision !== undefined ? { specRevision } : {}),
832
+ budgetLimit: effectiveBudget(plan, options),
833
+ budgetUsed: 0,
834
+ tasks,
835
+ writtenPaths: [],
836
+ updatedAt: (options.now ?? (() => new Date().toISOString()))(),
837
+ };
838
+ }
839
+ function parseCheckpoint(value) {
840
+ if (typeof value !== "object" || value === null)
841
+ throw new Error("checkpoint must be an object");
842
+ const checkpoint = value;
843
+ if (checkpoint.schemaVersion !== "psyclaw/orchestrator-checkpoint/v1" ||
844
+ typeof checkpoint.runId !== "string" || typeof checkpoint.planHash !== "string" ||
845
+ typeof checkpoint.budgetLimit !== "number" || !Number.isInteger(checkpoint.budgetLimit) || checkpoint.budgetLimit < 1 ||
846
+ typeof checkpoint.budgetUsed !== "number" || !Number.isInteger(checkpoint.budgetUsed) || checkpoint.budgetUsed < 0 ||
847
+ typeof checkpoint.tasks !== "object" || checkpoint.tasks === null || !Array.isArray(checkpoint.writtenPaths) ||
848
+ !checkpoint.writtenPaths.every((item) => typeof item === "string")) {
849
+ throw new Error("checkpoint schema invalid");
850
+ }
851
+ if (!isSafeRunId(checkpoint.runId) || !isSha256(checkpoint.planHash) || !isTimestamp(checkpoint.updatedAt ?? "")) {
852
+ throw new Error("checkpoint identity or timestamp invalid");
853
+ }
854
+ if (!Object.getPrototypeOf(checkpoint.tasks) || Object.getPrototypeOf(checkpoint.tasks) !== Object.prototype) {
855
+ throw new Error("checkpoint tasks prototype invalid");
856
+ }
857
+ const tasks = {};
858
+ for (const [id, raw] of Object.entries(checkpoint.tasks)) {
859
+ if (!isSafeTaskId(id))
860
+ throw new Error(`checkpoint task id invalid: ${id}`);
861
+ if (!isTaskCheckpoint(raw))
862
+ throw new Error(`checkpoint task invalid: ${id}`);
863
+ tasks[id] = raw;
864
+ }
865
+ return {
866
+ schemaVersion: "psyclaw/orchestrator-checkpoint/v1",
867
+ runId: checkpoint.runId,
868
+ planHash: checkpoint.planHash,
869
+ ...(typeof checkpoint.inputDigest === "string" ? { inputDigest: checkpoint.inputDigest } : {}),
870
+ ...(typeof checkpoint.specRevision === "string" ? { specRevision: checkpoint.specRevision } : {}),
871
+ budgetLimit: checkpoint.budgetLimit,
872
+ budgetUsed: checkpoint.budgetUsed,
873
+ tasks,
874
+ writtenPaths: checkpoint.writtenPaths,
875
+ updatedAt: typeof checkpoint.updatedAt === "string" && checkpoint.updatedAt.length > 0
876
+ ? checkpoint.updatedAt
877
+ : new Date().toISOString(),
878
+ };
879
+ }
880
+ function isTaskCheckpoint(value) {
881
+ if (typeof value !== "object" || value === null)
882
+ return false;
883
+ const item = value;
884
+ return (item.status === "pending" || item.status === "running" || item.status === "succeeded" || item.status === "failed" || item.status === "blocked") &&
885
+ typeof item.attempt === "number" && Number.isInteger(item.attempt) && item.attempt >= 0 &&
886
+ Array.isArray(item.artifacts) && item.artifacts.every((entry) => typeof entry === "string") &&
887
+ (item.artifactRecords === undefined || (Array.isArray(item.artifactRecords) && item.artifactRecords.every((entry) => (typeof entry === "object" && entry !== null && typeof entry.path === "string" &&
888
+ (entry.sha256 === undefined || (typeof entry.sha256 === "string" && isSha256(entry.sha256))))))) &&
889
+ Array.isArray(item.receipts) && item.receipts.every((entry) => isReceipt(entry)) &&
890
+ Array.isArray(item.passedGates) && item.passedGates.every((entry) => typeof entry === "string") &&
891
+ (item.report === undefined || isWorkerReport(item.report));
892
+ }
893
+ function validateRunnerPolicy(plan, options) {
894
+ const diagnostics = [];
895
+ const allowWrites = options.allowWrites ?? false;
896
+ const maxWorkers = effectiveWorkers(plan, options);
897
+ if (options.maxWorkers !== undefined && (!Number.isInteger(options.maxWorkers) || options.maxWorkers < 1 || options.maxWorkers > 4)) {
898
+ diagnostics.push({ severity: "block", reason: "maxWorkers override must be between 1 and 4" });
899
+ }
900
+ if (maxWorkers < 1 || maxWorkers > 4)
901
+ diagnostics.push({ severity: "block", reason: "effective maxWorkers must be between 1 and 4" });
902
+ if (options.maxTurns !== undefined && (!Number.isInteger(options.maxTurns) || options.maxTurns < 1)) {
903
+ diagnostics.push({ severity: "block", reason: "maxTurns must be a positive integer" });
904
+ }
905
+ for (const task of plan.tasks) {
906
+ for (const rawPath of [...task.ownedPaths, ...task.outputs]) {
907
+ if (isAbsoluteOrTraversal(rawPath)) {
908
+ diagnostics.push({ taskId: task.id, severity: "block", reason: `unsafe owned path: ${rawPath}` });
909
+ continue;
910
+ }
911
+ const path = normalizePath(rawPath);
912
+ if (isProtectedPath(path))
913
+ diagnostics.push({ taskId: task.id, severity: "block", reason: `protected owned path: ${rawPath}` });
914
+ if (path === "" || path === "." || path.startsWith("../") || path === "..") {
915
+ diagnostics.push({ taskId: task.id, severity: "block", reason: `unsafe owned path: ${rawPath}` });
916
+ }
917
+ }
918
+ if (!allowWrites && task.outputs.length > 0 && task.parallelSafe === false) {
919
+ // Outputs are declarations, not a permission; leave them schedulable as
920
+ // read-only tasks. Actual modifications are rejected at report time.
921
+ }
922
+ }
923
+ return diagnostics;
924
+ }
925
+ function effectiveWorkers(plan, options) {
926
+ const requested = options.maxWorkers ?? plan.budget.maxWorkers;
927
+ return Math.min(4, plan.budget.maxWorkers, requested);
928
+ }
929
+ function effectiveBudget(plan, options) {
930
+ return options.maxTurns ?? plan.budget.maxTurns;
931
+ }
932
+ function hashPlan(plan) {
933
+ return sha256Text(stableStringify(plan));
934
+ }
935
+ function stableStringify(value) {
936
+ if (value === null || typeof value !== "object")
937
+ return JSON.stringify(value);
938
+ if (Array.isArray(value))
939
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
940
+ const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
941
+ return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`).join(",")}}`;
942
+ }
943
+ function normalizePath(path) {
944
+ const parts = path.replaceAll("\\", "/").split("/");
945
+ const normalized = [];
946
+ for (const part of parts) {
947
+ if (!part || part === ".")
948
+ continue;
949
+ if (part === "..") {
950
+ if (normalized.length === 0) {
951
+ normalized.push("..");
952
+ }
953
+ else if (normalized.at(-1) === "..") {
954
+ normalized.push("..");
955
+ }
956
+ else {
957
+ normalized.pop();
958
+ }
959
+ continue;
960
+ }
961
+ normalized.push(part);
962
+ }
963
+ return normalized.join("/");
964
+ }
965
+ function isAbsoluteOrTraversal(path) {
966
+ const raw = path.trim().replaceAll("\\", "/");
967
+ return raw.startsWith("/") || raw.startsWith("//") || /^[a-z]:\//i.test(raw) ||
968
+ raw.split("/").some((part) => part === "..");
969
+ }
970
+ function isProtectedPath(path) {
971
+ const normalized = normalizePath(path).toLowerCase();
972
+ return normalized === ".git" || normalized.startsWith(".git/") ||
973
+ normalized === "data/raw" || normalized.startsWith("data/raw/") ||
974
+ normalized.split("/").some((part) => part === "credentials" || part === "credential" || part === "secrets" || part === "secret");
975
+ }
976
+ function isOwnedPath(ownedPaths, candidate) {
977
+ const normalized = normalizePath(candidate);
978
+ return ownedPaths.some((owned) => {
979
+ const path = normalizePath(owned);
980
+ return normalized === path || normalized.startsWith(`${path}/`);
981
+ });
982
+ }
983
+ function isOwnedOrOutput(task, candidate) {
984
+ return isOwnedPath(task.ownedPaths, candidate) || isOwnedPath(task.outputs, candidate);
985
+ }
986
+ function pathsOverlap(left, right) {
987
+ const a = normalizePath(left);
988
+ const b = normalizePath(right);
989
+ return a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
990
+ }
991
+ //# sourceMappingURL=runner.js.map