@mcoda/codali 0.1.134 → 0.1.135-codali.4

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 (230) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/ORCHESTRATION.md +387 -0
  3. package/README.md +103 -1
  4. package/config/alternate-local.example.json +49 -0
  5. package/config/docdex-runtime.json +21 -0
  6. package/config/orchestration.schema.json +548 -0
  7. package/config/raijin-suku.example.json +172 -0
  8. package/config/terminal-long-scenarios.v1.json +70 -0
  9. package/dist/agents/RoleResolution.d.ts.map +1 -1
  10. package/dist/agents/RoleResolution.js +4 -4
  11. package/dist/api/CodaliApi.d.ts +16 -0
  12. package/dist/api/CodaliApi.d.ts.map +1 -1
  13. package/dist/api/CodaliApi.js +39 -10
  14. package/dist/cli/AskCommand.d.ts +3 -0
  15. package/dist/cli/AskCommand.d.ts.map +1 -1
  16. package/dist/cli/AskCommand.js +76 -13
  17. package/dist/cli/RunCommand.d.ts +4 -0
  18. package/dist/cli/RunCommand.d.ts.map +1 -1
  19. package/dist/cli/RunCommand.js +110 -24
  20. package/dist/cli/TerminalCommand.d.ts +35 -0
  21. package/dist/cli/TerminalCommand.d.ts.map +1 -0
  22. package/dist/cli/TerminalCommand.js +616 -0
  23. package/dist/cli/TerminalCredentials.d.ts +3 -0
  24. package/dist/cli/TerminalCredentials.d.ts.map +1 -0
  25. package/dist/cli/TerminalCredentials.js +89 -0
  26. package/dist/cli/TerminalInput.d.ts +24 -0
  27. package/dist/cli/TerminalInput.d.ts.map +1 -0
  28. package/dist/cli/TerminalInput.js +98 -0
  29. package/dist/cli/TerminalText.d.ts +10 -0
  30. package/dist/cli/TerminalText.d.ts.map +1 -0
  31. package/dist/cli/TerminalText.js +53 -0
  32. package/dist/cli.d.ts.map +1 -1
  33. package/dist/cli.js +33 -4
  34. package/dist/cognitive/ConfiguredPipeline.d.ts +34 -0
  35. package/dist/cognitive/ConfiguredPipeline.d.ts.map +1 -0
  36. package/dist/cognitive/ConfiguredPipeline.js +50 -0
  37. package/dist/cognitive/ValidationRunner.d.ts +4 -0
  38. package/dist/cognitive/ValidationRunner.d.ts.map +1 -1
  39. package/dist/cognitive/ValidationRunner.js +18 -41
  40. package/dist/config/NodeOrchestration.d.ts +21 -0
  41. package/dist/config/NodeOrchestration.d.ts.map +1 -0
  42. package/dist/config/NodeOrchestration.js +54 -0
  43. package/dist/config/OrchestrationConfig.d.ts +100 -0
  44. package/dist/config/OrchestrationConfig.d.ts.map +1 -0
  45. package/dist/config/OrchestrationConfig.js +245 -0
  46. package/dist/connectors/mcp/McpClient.d.ts +3 -1
  47. package/dist/connectors/mcp/McpClient.d.ts.map +1 -1
  48. package/dist/connectors/mcp/McpClient.js +3 -2
  49. package/dist/connectors/mcp/McpServerRegistry.d.ts +2 -2
  50. package/dist/connectors/mcp/McpServerRegistry.d.ts.map +1 -1
  51. package/dist/connectors/mcp/McpServerRegistry.js +39 -16
  52. package/dist/connectors/mcp/McpToolAdapter.js +2 -2
  53. package/dist/connectors/media/ConfiguredMediaTools.d.ts +6 -0
  54. package/dist/connectors/media/ConfiguredMediaTools.d.ts.map +1 -0
  55. package/dist/connectors/media/ConfiguredMediaTools.js +41 -0
  56. package/dist/connectors/media/ImageGenerationTool.js +3 -3
  57. package/dist/connectors/media/MediaArtifactReceipt.d.ts +5 -0
  58. package/dist/connectors/media/MediaArtifactReceipt.d.ts.map +1 -0
  59. package/dist/connectors/media/MediaArtifactReceipt.js +65 -0
  60. package/dist/connectors/media/MediaJobStore.d.ts +68 -0
  61. package/dist/connectors/media/MediaJobStore.d.ts.map +1 -0
  62. package/dist/connectors/media/MediaJobStore.js +199 -0
  63. package/dist/connectors/media/MediaJobWorker.d.ts +2 -0
  64. package/dist/connectors/media/MediaJobWorker.d.ts.map +1 -0
  65. package/dist/connectors/media/MediaJobWorker.js +65 -0
  66. package/dist/connectors/media/RemoteMediaJob.d.ts +6 -0
  67. package/dist/connectors/media/RemoteMediaJob.d.ts.map +1 -0
  68. package/dist/connectors/media/RemoteMediaJob.js +80 -0
  69. package/dist/docdex/DocdexClient.d.ts +18 -1
  70. package/dist/docdex/DocdexClient.d.ts.map +1 -1
  71. package/dist/docdex/DocdexClient.js +135 -19
  72. package/dist/docdex/ManagedDocdex.d.ts +29 -0
  73. package/dist/docdex/ManagedDocdex.d.ts.map +1 -0
  74. package/dist/docdex/ManagedDocdex.js +215 -0
  75. package/dist/docdex/ManagedRuntime.d.ts +28 -0
  76. package/dist/docdex/ManagedRuntime.d.ts.map +1 -0
  77. package/dist/docdex/ManagedRuntime.js +212 -0
  78. package/dist/eval/EvaluationProcess.d.ts +13 -0
  79. package/dist/eval/EvaluationProcess.d.ts.map +1 -0
  80. package/dist/eval/EvaluationProcess.js +76 -0
  81. package/dist/eval/LongWorkflowJourney.d.ts +34 -0
  82. package/dist/eval/LongWorkflowJourney.d.ts.map +1 -0
  83. package/dist/eval/LongWorkflowJourney.js +243 -0
  84. package/dist/eval/PlanningCorpus.d.ts +45 -0
  85. package/dist/eval/PlanningCorpus.d.ts.map +1 -0
  86. package/dist/eval/PlanningCorpus.js +189 -0
  87. package/dist/eval/PlanningCorpusRunner.d.ts +13 -0
  88. package/dist/eval/PlanningCorpusRunner.d.ts.map +1 -0
  89. package/dist/eval/PlanningCorpusRunner.js +89 -0
  90. package/dist/eval/TerminalCorpusRunner.d.ts +36 -0
  91. package/dist/eval/TerminalCorpusRunner.d.ts.map +1 -0
  92. package/dist/eval/TerminalCorpusRunner.js +222 -0
  93. package/dist/eval/TerminalTaskCorpus.d.ts +26 -0
  94. package/dist/eval/TerminalTaskCorpus.d.ts.map +1 -0
  95. package/dist/eval/TerminalTaskCorpus.js +105 -0
  96. package/dist/eval/TerminalWorkflowCorpus.d.ts +11 -0
  97. package/dist/eval/TerminalWorkflowCorpus.d.ts.map +1 -0
  98. package/dist/eval/TerminalWorkflowCorpus.js +116 -0
  99. package/dist/eval/fixtures/TerminalRecoveryJourney.d.ts +4 -0
  100. package/dist/eval/fixtures/TerminalRecoveryJourney.d.ts.map +1 -0
  101. package/dist/eval/fixtures/TerminalRecoveryJourney.js +132 -0
  102. package/dist/gateway/CodaliGateway.d.ts +2 -0
  103. package/dist/gateway/CodaliGateway.d.ts.map +1 -1
  104. package/dist/gateway/CodaliGateway.js +11 -1
  105. package/dist/gateway/ConfiguredGateway.d.ts +23 -0
  106. package/dist/gateway/ConfiguredGateway.d.ts.map +1 -0
  107. package/dist/gateway/ConfiguredGateway.js +44 -0
  108. package/dist/gateway/GatewayTracer.d.ts +1 -1
  109. package/dist/gateway/GatewayTracer.d.ts.map +1 -1
  110. package/dist/gateway/GatewayTracer.js +2 -27
  111. package/dist/index.d.ts +23 -0
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +12 -0
  114. package/dist/permissions/PermissionGate.d.ts +44 -0
  115. package/dist/permissions/PermissionGate.d.ts.map +1 -0
  116. package/dist/permissions/PermissionGate.js +97 -0
  117. package/dist/providers/ConfiguredEmbeddings.d.ts +43 -0
  118. package/dist/providers/ConfiguredEmbeddings.d.ts.map +1 -0
  119. package/dist/providers/ConfiguredEmbeddings.js +87 -0
  120. package/dist/providers/ConfiguredModelScope.d.ts +69 -0
  121. package/dist/providers/ConfiguredModelScope.d.ts.map +1 -0
  122. package/dist/providers/ConfiguredModelScope.js +113 -0
  123. package/dist/providers/ConfiguredProvider.d.ts +79 -0
  124. package/dist/providers/ConfiguredProvider.d.ts.map +1 -0
  125. package/dist/providers/ConfiguredProvider.js +238 -0
  126. package/dist/providers/ModelServiceError.d.ts +10 -0
  127. package/dist/providers/ModelServiceError.d.ts.map +1 -0
  128. package/dist/providers/ModelServiceError.js +45 -0
  129. package/dist/providers/MswarmWorkerProvider.d.ts.map +1 -1
  130. package/dist/providers/MswarmWorkerProvider.js +4 -0
  131. package/dist/providers/OpenAiCompatibleProvider.d.ts.map +1 -1
  132. package/dist/providers/OpenAiCompatibleProvider.js +91 -50
  133. package/dist/providers/ProviderTypes.d.ts +12 -0
  134. package/dist/providers/ProviderTypes.d.ts.map +1 -1
  135. package/dist/runtime/CodaliJobRuntime.d.ts +6 -0
  136. package/dist/runtime/CodaliJobRuntime.d.ts.map +1 -1
  137. package/dist/runtime/CodaliJobRuntime.js +36 -1
  138. package/dist/runtime/CodaliRuntime.d.ts +28 -0
  139. package/dist/runtime/CodaliRuntime.d.ts.map +1 -1
  140. package/dist/runtime/CodaliRuntime.js +69 -1
  141. package/dist/runtime/CompletionReport.d.ts +5 -0
  142. package/dist/runtime/CompletionReport.d.ts.map +1 -0
  143. package/dist/runtime/CompletionReport.js +12 -0
  144. package/dist/runtime/ProgressTracker.d.ts +17 -0
  145. package/dist/runtime/ProgressTracker.d.ts.map +1 -0
  146. package/dist/runtime/ProgressTracker.js +27 -0
  147. package/dist/runtime/RequestedTestEvidence.d.ts +12 -0
  148. package/dist/runtime/RequestedTestEvidence.d.ts.map +1 -0
  149. package/dist/runtime/RequestedTestEvidence.js +16 -0
  150. package/dist/runtime/RequiredRetrieval.d.ts +13 -0
  151. package/dist/runtime/RequiredRetrieval.d.ts.map +1 -0
  152. package/dist/runtime/RequiredRetrieval.js +25 -0
  153. package/dist/runtime/ReviewSource.d.ts +6 -0
  154. package/dist/runtime/ReviewSource.d.ts.map +1 -0
  155. package/dist/runtime/ReviewSource.js +38 -0
  156. package/dist/runtime/Runner.d.ts +7 -0
  157. package/dist/runtime/Runner.d.ts.map +1 -1
  158. package/dist/runtime/Runner.js +215 -171
  159. package/dist/runtime/SecretRedaction.d.ts +11 -0
  160. package/dist/runtime/SecretRedaction.d.ts.map +1 -0
  161. package/dist/runtime/SecretRedaction.js +85 -0
  162. package/dist/runtime/SessionController.d.ts +148 -0
  163. package/dist/runtime/SessionController.d.ts.map +1 -0
  164. package/dist/runtime/SessionController.js +1357 -0
  165. package/dist/runtime/StructuralEvidence.d.ts +22 -0
  166. package/dist/runtime/StructuralEvidence.d.ts.map +1 -0
  167. package/dist/runtime/StructuralEvidence.js +87 -0
  168. package/dist/runtime/TaskReviewContext.d.ts +34 -0
  169. package/dist/runtime/TaskReviewContext.d.ts.map +1 -0
  170. package/dist/runtime/TaskReviewContext.js +34 -0
  171. package/dist/runtime/ValidationOutput.d.ts +5 -0
  172. package/dist/runtime/ValidationOutput.d.ts.map +1 -0
  173. package/dist/runtime/ValidationOutput.js +32 -0
  174. package/dist/runtime/VerificationCommand.d.ts +5 -0
  175. package/dist/runtime/VerificationCommand.d.ts.map +1 -0
  176. package/dist/runtime/VerificationCommand.js +76 -0
  177. package/dist/runtime/WorkStatusPage.d.ts +15 -0
  178. package/dist/runtime/WorkStatusPage.d.ts.map +1 -0
  179. package/dist/runtime/WorkStatusPage.js +58 -0
  180. package/dist/runtime/WorkspaceLedger.d.ts +13 -0
  181. package/dist/runtime/WorkspaceLedger.d.ts.map +1 -0
  182. package/dist/runtime/WorkspaceLedger.js +145 -0
  183. package/dist/runtime/WorkspaceLock.d.ts.map +1 -1
  184. package/dist/runtime/WorkspaceLock.js +20 -17
  185. package/dist/runtime/WorkspaceSnapshot.d.ts +13 -0
  186. package/dist/runtime/WorkspaceSnapshot.d.ts.map +1 -0
  187. package/dist/runtime/WorkspaceSnapshot.js +88 -0
  188. package/dist/session/AtomicState.d.ts +13 -0
  189. package/dist/session/AtomicState.d.ts.map +1 -0
  190. package/dist/session/AtomicState.js +123 -0
  191. package/dist/session/SessionStoragePath.d.ts +15 -0
  192. package/dist/session/SessionStoragePath.d.ts.map +1 -0
  193. package/dist/session/SessionStoragePath.js +89 -0
  194. package/dist/session/SessionStore.d.ts +9 -0
  195. package/dist/session/SessionStore.d.ts.map +1 -1
  196. package/dist/session/SessionStore.js +181 -47
  197. package/dist/session/WorkState.d.ts +23 -0
  198. package/dist/session/WorkState.d.ts.map +1 -0
  199. package/dist/session/WorkState.js +367 -0
  200. package/dist/session/WorkTypes.d.ts +94 -0
  201. package/dist/session/WorkTypes.d.ts.map +1 -0
  202. package/dist/session/WorkTypes.js +1 -0
  203. package/dist/tools/ToolRegistry.d.ts +2 -0
  204. package/dist/tools/ToolRegistry.d.ts.map +1 -1
  205. package/dist/tools/ToolRegistry.js +12 -2
  206. package/dist/tools/ToolTypes.d.ts +16 -0
  207. package/dist/tools/ToolTypes.d.ts.map +1 -1
  208. package/dist/tools/docdex/DocdexTools.d.ts +2 -0
  209. package/dist/tools/docdex/DocdexTools.d.ts.map +1 -1
  210. package/dist/tools/docdex/DocdexTools.js +17 -2
  211. package/dist/tools/filesystem/FileTools.d.ts.map +1 -1
  212. package/dist/tools/filesystem/FileTools.js +30 -10
  213. package/dist/tools/shell/AsyncProcess.d.ts +19 -0
  214. package/dist/tools/shell/AsyncProcess.d.ts.map +1 -0
  215. package/dist/tools/shell/AsyncProcess.js +94 -0
  216. package/dist/tools/shell/DarwinRuntimeLibraries.d.ts +13 -0
  217. package/dist/tools/shell/DarwinRuntimeLibraries.d.ts.map +1 -0
  218. package/dist/tools/shell/DarwinRuntimeLibraries.js +130 -0
  219. package/dist/tools/shell/HostToolchain.d.ts +19 -0
  220. package/dist/tools/shell/HostToolchain.d.ts.map +1 -0
  221. package/dist/tools/shell/HostToolchain.js +266 -0
  222. package/dist/tools/shell/IsolatedExecutor.d.ts +15 -0
  223. package/dist/tools/shell/IsolatedExecutor.d.ts.map +1 -0
  224. package/dist/tools/shell/IsolatedExecutor.js +138 -0
  225. package/dist/tools/shell/ShellTool.d.ts.map +1 -1
  226. package/dist/tools/shell/ShellTool.js +9 -29
  227. package/dist/tools/shell/ShellWorkingDirectory.d.ts +3 -0
  228. package/dist/tools/shell/ShellWorkingDirectory.d.ts.map +1 -0
  229. package/dist/tools/shell/ShellWorkingDirectory.js +30 -0
  230. package/package.json +27 -24
@@ -0,0 +1,22 @@
1
+ export interface StructuralAnalysis {
2
+ kind: "structure" | "impact";
3
+ file: string;
4
+ hash: string;
5
+ valid: boolean;
6
+ }
7
+ export declare const isSourcePath: (file: string) => boolean;
8
+ export declare const workspaceRelativePath: (root: string, file: string) => string;
9
+ export declare const isWithinScope: (file: string, scope: string[]) => boolean;
10
+ /** Git ignores cannot hide existing source from an explicitly writable directory. */
11
+ export declare const writableInventory: (root: string, scopes: string[], maxEntries?: number) => Promise<Record<string, string>>;
12
+ export declare const analysisTarget: (tool: string, args: Record<string, unknown>) => {
13
+ kind: StructuralAnalysis["kind"];
14
+ file: string;
15
+ } | undefined;
16
+ /** A successful transport alone is not structural evidence (parsers can skip files). */
17
+ export declare const validAnalysisResult: (kind: StructuralAnalysis["kind"], data: unknown, file: string) => boolean;
18
+ export declare const missingStructuralEvidence: (file: string, hash: string, receipts: Iterable<{
19
+ status: string;
20
+ analysis?: StructuralAnalysis;
21
+ }>) => string[];
22
+ //# sourceMappingURL=StructuralEvidence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StructuralEvidence.d.ts","sourceRoot":"","sources":["../../src/runtime/StructuralEvidence.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,WAAW,GAAG,QAAQ,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,KAAG,OAA2I,CAAC;AAExL,eAAO,MAAM,qBAAqB,GAAI,MAAM,MAAM,EAAE,MAAM,MAAM,KAAG,MAIlE,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,EAAE,OAAO,MAAM,EAAE,KAAG,OAA+E,CAAC;AAE9I,qFAAqF;AACrF,eAAO,MAAM,iBAAiB,GAAU,MAAM,MAAM,EAAE,QAAQ,MAAM,EAAE,EAAE,mBAAmB,KAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAwB3H,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG;IAAE,IAAI,EAAE,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,SAIjI,CAAC;AAEF,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,GAAI,MAAM,kBAAkB,CAAC,MAAM,CAAC,EAAE,MAAM,OAAO,EAAE,MAAM,MAAM,KAAG,OAOnG,CAAC;AAEF,eAAO,MAAM,yBAAyB,GAAI,MAAM,MAAM,EAAE,MAAM,MAAM,EAAE,UAAU,QAAQ,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,kBAAkB,CAAA;CAAE,CAAC,KAAG,MAAM,EAQnJ,CAAC"}
@@ -0,0 +1,87 @@
1
+ import path from "node:path";
2
+ import { promises as fs } from "node:fs";
3
+ import { hashWorkspacePath } from "./WorkspaceSnapshot.js";
4
+ export const isSourcePath = (file) => /\.(?:[cm]?[jt]sx?|py|rs|go|java|kt|kts|c|cc|cpp|cxx|h|hpp|cs|rb|php|swift|sh|bash|lua|ex|exs|scala|sc|vue|svelte)$/i.test(file);
5
+ export const workspaceRelativePath = (root, file) => {
6
+ const relative = path.relative(root, path.resolve(root, file)).split(path.sep).join("/");
7
+ if (relative === ".." || relative.startsWith("../") || path.isAbsolute(relative))
8
+ throw new Error("Path escapes the workspace");
9
+ return relative || ".";
10
+ };
11
+ export const isWithinScope = (file, scope) => scope.some(p => p === "." || p === file || file.startsWith(`${p}/`));
12
+ /** Git ignores cannot hide existing source from an explicitly writable directory. */
13
+ export const writableInventory = async (root, scopes, maxEntries = 20000) => {
14
+ root = await fs.realpath(root);
15
+ const hashes = {};
16
+ const visited = new Set();
17
+ const walk = async (file) => {
18
+ if ([".git", ".mcoda"].some(dir => file === dir || file.startsWith(`${dir}/`)) || visited.has(file))
19
+ return;
20
+ visited.add(file);
21
+ if (visited.size > maxEntries)
22
+ throw new Error("Shell write inventory exceeds its bound; declare narrower writePaths");
23
+ const absolute = path.resolve(root, file);
24
+ let stat;
25
+ try {
26
+ stat = await fs.lstat(absolute);
27
+ }
28
+ catch (error) {
29
+ if (error.code === "ENOENT")
30
+ return;
31
+ throw error;
32
+ }
33
+ if (stat.isDirectory()) {
34
+ for (const entry of await fs.readdir(absolute))
35
+ await walk(workspaceRelativePath(root, path.join(absolute, entry)));
36
+ }
37
+ else {
38
+ if (stat.isFile() && stat.nlink > 1)
39
+ throw new Error(`Hard-linked writable file requires an independent copy: ${file}`);
40
+ hashes[file] = await hashWorkspacePath(root, file);
41
+ }
42
+ };
43
+ for (const scope of scopes) {
44
+ const relative = workspaceRelativePath(root, scope);
45
+ const requested = path.resolve(root, relative);
46
+ try {
47
+ if (await fs.realpath(requested) !== requested)
48
+ throw new Error("Shell write paths must be canonical and cannot be symlink aliases");
49
+ }
50
+ catch (error) {
51
+ if (error.code !== "ENOENT")
52
+ throw error;
53
+ }
54
+ await walk(relative);
55
+ }
56
+ return hashes;
57
+ };
58
+ export const analysisTarget = (tool, args) => {
59
+ if (["docdex_symbols", "docdex_ast"].includes(tool) && typeof args.path === "string")
60
+ return { kind: "structure", file: args.path };
61
+ if (tool === "docdex_impact_graph" && typeof args.file === "string")
62
+ return { kind: "impact", file: args.file };
63
+ return undefined;
64
+ };
65
+ /** A successful transport alone is not structural evidence (parsers can skip files). */
66
+ export const validAnalysisResult = (kind, data, file) => {
67
+ if (!data || typeof data !== "object" || Array.isArray(data))
68
+ return false;
69
+ const value = data;
70
+ if (value.error || value.isError)
71
+ return false;
72
+ if (kind === "impact")
73
+ return value.source === file && Array.isArray(value.edges) && Array.isArray(value.inbound) && Array.isArray(value.outbound);
74
+ const outcome = value.outcome;
75
+ return value.file === file && outcome?.status === "ok" && (Array.isArray(value.symbols) || Array.isArray(value.nodes));
76
+ };
77
+ export const missingStructuralEvidence = (file, hash, receipts) => {
78
+ if (hash === "missing" || !isSourcePath(file))
79
+ return [];
80
+ const present = new Set();
81
+ for (const receipt of receipts) {
82
+ const a = receipt.analysis;
83
+ if (receipt.status === "success" && a?.valid && a.file === file && a.hash === hash)
84
+ present.add(a.kind);
85
+ }
86
+ return ["structure", "impact"].filter(kind => !present.has(kind));
87
+ };
@@ -0,0 +1,34 @@
1
+ import type { WorkState, WorkTask } from "../session/WorkTypes.js";
2
+ /** Scope the review question without weakening the controller's global freshness checks. */
3
+ export declare function taskReviewContext(state: WorkState, task: WorkTask, input: {
4
+ changedPaths: string[];
5
+ observedPaths: string[];
6
+ contextPaths: string[];
7
+ currentHashes: Record<string, string>;
8
+ }): {
9
+ reviewScope: string;
10
+ goal: string | undefined;
11
+ goalReference: {
12
+ id: string;
13
+ revision: number;
14
+ planRevision: number;
15
+ };
16
+ requirements: import("../session/WorkTypes.js").Requirement[];
17
+ constraints: string[];
18
+ nonGoals: string[];
19
+ decisions: import("../session/WorkTypes.js").DecisionRecord[];
20
+ deferredRequirements: {
21
+ id: string;
22
+ owners: {
23
+ taskId: string;
24
+ status: import("../session/WorkTypes.js").WorkTaskStatus;
25
+ }[];
26
+ }[];
27
+ changedPaths: string[];
28
+ otherTaskChangedPaths: string[];
29
+ reviewPaths: string[];
30
+ sourceRoles: {
31
+ [k: string]: string;
32
+ };
33
+ };
34
+ //# sourceMappingURL=TaskReviewContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TaskReviewContext.d.ts","sourceRoot":"","sources":["../../src/runtime/TaskReviewContext.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAInE,4FAA4F;AAC5F,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE;IACzE,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACvC;;;;;;;;;;;;;;;;;;;;;;;;;EA8BA"}
@@ -0,0 +1,34 @@
1
+ import { isTestSourcePath } from "./RequestedTestEvidence.js";
2
+ import { isWithinScope } from "./StructuralEvidence.js";
3
+ /** Scope the review question without weakening the controller's global freshness checks. */
4
+ export function taskReviewContext(state, task, input) {
5
+ const requirements = state.requirements.filter(requirement => task.requirementIds.includes(requirement.id));
6
+ const wholeGoal = requirements.some(requirement => requirement.description === state.originalObjective);
7
+ const inScope = (file) => wholeGoal || isWithinScope(file, task.allowedPaths);
8
+ const changedPaths = input.changedPaths.filter(inScope);
9
+ const reviewPaths = [...new Set([
10
+ ...changedPaths,
11
+ ...Object.keys(input.currentHashes).filter(file => inScope(file) && isTestSourcePath(file)),
12
+ ...input.observedPaths.filter(inScope),
13
+ ...input.contextPaths,
14
+ ])];
15
+ const superseded = new Set(state.decisions.map(decision => decision.supersedes).filter(Boolean));
16
+ const decisions = state.decisions.filter(decision => !superseded.has(decision.id) && (wholeGoal || !decision.paths.length
17
+ || decision.paths.some(file => isWithinScope(file, task.allowedPaths)
18
+ || task.allowedPaths.some(scope => isWithinScope(scope, [file]))
19
+ || reviewPaths.some(observed => isWithinScope(observed, [file])))));
20
+ return {
21
+ reviewScope: wholeGoal ? "whole_goal" : "assigned_task",
22
+ goal: wholeGoal ? state.objective : undefined,
23
+ goalReference: { id: state.goalId, revision: state.goalRevision, planRevision: state.planRevision ?? 1 },
24
+ requirements, constraints: state.constraints, nonGoals: state.nonGoals, decisions,
25
+ deferredRequirements: wholeGoal ? [] : state.requirements.filter(requirement => !task.requirementIds.includes(requirement.id))
26
+ .map(requirement => ({ id: requirement.id, owners: state.tasks.filter(owner => owner.requirementIds.includes(requirement.id)
27
+ && !["cancelled", "superseded"].includes(owner.status)).map(owner => ({ taskId: owner.id, status: owner.status })) })),
28
+ changedPaths,
29
+ otherTaskChangedPaths: input.changedPaths.filter(file => !inScope(file)),
30
+ reviewPaths,
31
+ sourceRoles: Object.fromEntries(reviewPaths.map(file => [file, changedPaths.includes(file) ? "reviewed_change"
32
+ : inScope(file) ? "scope_context" : "read_only_context"])),
33
+ };
34
+ }
@@ -0,0 +1,5 @@
1
+ /** Bounded diagnostic context, never a substitute for the original receipt or
2
+ * its exit status. Preserve the failing region when passing checks fill the
3
+ * beginning of a long test log. */
4
+ export declare function validationOutputExcerpt(output: string, maximum?: number): string;
5
+ //# sourceMappingURL=ValidationOutput.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ValidationOutput.d.ts","sourceRoot":"","sources":["../../src/runtime/ValidationOutput.ts"],"names":[],"mappings":"AAAA;;mCAEmC;AACnC,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,SAAO,GAAG,MAAM,CAwB9E"}
@@ -0,0 +1,32 @@
1
+ /** Bounded diagnostic context, never a substitute for the original receipt or
2
+ * its exit status. Preserve the failing region when passing checks fill the
3
+ * beginning of a long test log. */
4
+ export function validationOutputExcerpt(output, maximum = 2400) {
5
+ if (!Number.isInteger(maximum) || maximum < 600 || maximum > 8000)
6
+ throw new RangeError("Invalid validation excerpt bound");
7
+ if (output.length <= maximum)
8
+ return output;
9
+ output = output.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "");
10
+ if (output.length <= maximum)
11
+ return output;
12
+ const marker = "\n[... output omitted from this excerpt ...]\n";
13
+ const strong = /^(?:\s*not ok\s+\d|\s*FAIL(?:ED)?\s|\s*=+\s*FAILURES\s*=+|\s*error\[E\d+\]|.*: error (?:TS\d+|[A-Z]\d+):?)/m;
14
+ const fallback = /^(?:\s*(?:AssertionError|TypeError|Error):|.*panicked at)/m;
15
+ const failure = strong.exec(output) ?? fallback.exec(output);
16
+ if (!failure) {
17
+ const available = maximum - marker.length;
18
+ const head = Math.floor(available * 0.55);
19
+ return output.slice(0, head) + marker + output.slice(-(available - head));
20
+ }
21
+ const headSize = Math.min(180, Math.floor(maximum * 0.15));
22
+ const tailSize = Math.min(400, Math.floor(maximum * 0.2));
23
+ const windowSize = maximum - headSize - tailSize - marker.length * 2;
24
+ const start = Math.max(0, failure.index - Math.min(80, Math.floor(windowSize * 0.15)));
25
+ const end = Math.min(output.length, start + windowSize);
26
+ // Regions cannot overlap. Keeping an unused few characters is preferable to
27
+ // duplicating log content or trimming the failure after assembly.
28
+ const head = output.slice(0, Math.min(headSize, start));
29
+ const tailStart = Math.max(end, output.length - tailSize);
30
+ const tail = output.slice(tailStart);
31
+ return head + (start > head.length ? marker : "") + output.slice(start, end) + (tailStart > end ? marker : "") + tail;
32
+ }
@@ -0,0 +1,5 @@
1
+ /** Budget eligibility for inspection only; never substitutes for test evidence. */
2
+ export declare const isVerificationInspectionCommand: (args: unknown) => boolean;
3
+ /** Command-shape eligibility only; success, fresh input hashes and review remain mandatory. */
4
+ export declare const isVerificationCommand: (args: unknown) => boolean;
5
+ //# sourceMappingURL=VerificationCommand.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VerificationCommand.d.ts","sourceRoot":"","sources":["../../src/runtime/VerificationCommand.ts"],"names":[],"mappings":"AAEA,mFAAmF;AACnF,eAAO,MAAM,+BAA+B,GAAI,MAAM,OAAO,KAAG,OAiB/D,CAAC;AAEF,+FAA+F;AAC/F,eAAO,MAAM,qBAAqB,GAAI,MAAM,OAAO,KAAG,OA6BrD,CAAC"}
@@ -0,0 +1,76 @@
1
+ import path from "node:path";
2
+ /** Budget eligibility for inspection only; never substitutes for test evidence. */
3
+ export const isVerificationInspectionCommand = (args) => {
4
+ if (!args || typeof args !== "object" || Array.isArray(args))
5
+ return false;
6
+ const value = args;
7
+ if (value.command !== "git" || !Array.isArray(value.args) || value.args.some(arg => typeof arg !== "string"))
8
+ return false;
9
+ if (value.writePaths !== undefined && (!Array.isArray(value.writePaths) || value.writePaths.length !== 0))
10
+ return false;
11
+ const argv = value.args;
12
+ const flags = argv[0] === "diff"
13
+ ? ["--no-ext-diff", "--no-textconv", "--no-color", "--stat", "--name-only", "--name-status", "--check", "--cached", "--staged"]
14
+ : argv[0] === "status" ? ["--short", "--porcelain", "--porcelain=v1", "--porcelain=v2", "--branch"] : undefined;
15
+ if (!flags)
16
+ return false;
17
+ let paths = false;
18
+ for (const arg of argv.slice(1)) {
19
+ if (paths) {
20
+ if (!arg)
21
+ return false;
22
+ }
23
+ else if (arg === "--")
24
+ paths = true;
25
+ else if (!flags.includes(arg))
26
+ return false;
27
+ }
28
+ return true;
29
+ };
30
+ /** Command-shape eligibility only; success, fresh input hashes and review remain mandatory. */
31
+ export const isVerificationCommand = (args) => {
32
+ if (!args || typeof args !== "object" || Array.isArray(args))
33
+ return false;
34
+ const value = args;
35
+ if (typeof value.command !== "string" || value.args !== undefined && (!Array.isArray(value.args) || value.args.some(arg => typeof arg !== "string")))
36
+ return false;
37
+ const command = path.basename(value.command);
38
+ const argv = (value.args ?? []);
39
+ if (argv.some(arg => /^(?:--help|-h|--version|--dry-run|--if-present|--collect-only|--collectonly|--listTests|--list-tests|--showConfig|--listFilesOnly)(?:=|$)/.test(arg)))
40
+ return false;
41
+ if (["npm", "pnpm", "yarn", "bun"].includes(command)) {
42
+ let offset = 0;
43
+ const directoryFlags = command === "npm" ? ["--prefix"] : command === "pnpm" ? ["--dir", "-C"] : ["--cwd"];
44
+ while (offset < argv.length) {
45
+ if (["--silent", "-s", "--quiet", "-q", "--no-progress"].includes(argv[offset])) {
46
+ offset++;
47
+ continue;
48
+ }
49
+ if (directoryFlags.includes(argv[offset])) {
50
+ if (!argv[offset + 1] || argv[offset + 1].startsWith("-"))
51
+ return false;
52
+ offset += 2;
53
+ continue;
54
+ }
55
+ if (directoryFlags.some(flag => argv[offset].startsWith(`${flag}=`) && argv[offset].length > flag.length + 1)) {
56
+ offset++;
57
+ continue;
58
+ }
59
+ break;
60
+ }
61
+ return argv[offset] === "test" || argv[offset] === "run" && /^(test|check|typecheck|lint)(:|$)/.test(argv[offset + 1] ?? "");
62
+ }
63
+ if (command === "node") {
64
+ let offset = 0;
65
+ while (["--no-warnings", "--enable-source-maps"].includes(argv[offset]))
66
+ offset++;
67
+ return argv[offset] === "--test" || /(?:^|\/)\S+\.(?:test|spec)\.[cm]?js$/.test(argv[offset] ?? "");
68
+ }
69
+ if (command === "pytest")
70
+ return !argv.includes("-V");
71
+ if (command === "tsc")
72
+ return !argv.includes("-v");
73
+ if (["cargo", "go", "make"].includes(command))
74
+ return argv[0] === "test";
75
+ return command === "python3" && argv[0] === "-m" && ["pytest", "unittest"].includes(argv[1]);
76
+ };
@@ -0,0 +1,15 @@
1
+ import type { WorkState } from "../session/WorkTypes.js";
2
+ export interface WorkStatusQuery {
3
+ offset?: number;
4
+ limit?: number;
5
+ taskIds?: string[];
6
+ requirementIds?: string[];
7
+ record?: "goal" | "task" | "requirement" | "decision" | "phase" | "constraints";
8
+ recordId?: string;
9
+ textOffset?: number;
10
+ textLimit?: number;
11
+ }
12
+ /** Inspection must remain available even when the whole goal cannot fit in a
13
+ * model context. Previews are explicit; record chunks retrieve the exact JSON. */
14
+ export declare function workStatusPage(state: WorkState, query: WorkStatusQuery): Record<string, unknown>;
15
+ //# sourceMappingURL=WorkStatusPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WorkStatusPage.d.ts","sourceRoot":"","sources":["../../src/runtime/WorkStatusPage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEzD,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,aAAa,GAAG,UAAU,GAAG,OAAO,GAAG,aAAa,CAAC;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;kFACkF;AAClF,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA8ChG"}
@@ -0,0 +1,58 @@
1
+ /** Inspection must remain available even when the whole goal cannot fit in a
2
+ * model context. Previews are explicit; record chunks retrieve the exact JSON. */
3
+ export function workStatusPage(state, query) {
4
+ const offset = query.offset ?? 0, limit = query.limit ?? 20;
5
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 50)
6
+ throw new Error("Invalid status page");
7
+ if (query.record) {
8
+ const record = query.record === "goal" ? { goalId: state.goalId, goalRevision: state.goalRevision, originalObjective: state.originalObjective, objective: state.objective }
9
+ : query.record === "constraints" ? { constraints: state.constraints, nonGoals: state.nonGoals }
10
+ : (() => {
11
+ const records = query.record === "task" ? state.tasks : query.record === "requirement" ? state.requirements : query.record === "decision" ? state.decisions : state.phases ?? [];
12
+ return query.recordId ? records.find(item => item.id === query.recordId) : records;
13
+ })();
14
+ if (!record)
15
+ throw new Error("Unknown status record; supply its exact recordId");
16
+ const encoded = JSON.stringify(record), start = query.textOffset ?? 0, size = query.textLimit ?? 6000;
17
+ if (!Number.isSafeInteger(start) || start < 0 || start > encoded.length || !Number.isSafeInteger(size) || size < 1 || size > 6000)
18
+ throw new Error("Invalid status record text range");
19
+ const content = encoded.slice(start, start + size);
20
+ return { record: query.record, recordId: query.recordId, encoding: "json", content, textOffset: start,
21
+ totalChars: encoded.length, nextTextOffset: start + content.length < encoded.length ? start + content.length : null };
22
+ }
23
+ const tasks = state.tasks.filter(task => !query.taskIds || query.taskIds.includes(task.id));
24
+ const candidates = tasks.slice(offset, offset + limit);
25
+ const ids = new Set(query.requirementIds ?? candidates.flatMap(task => task.requirementIds));
26
+ const requirements = state.requirements.filter(requirement => ids.has(requirement.id));
27
+ const view = {
28
+ goalId: state.goalId, goalRevision: state.goalRevision, planRevision: state.planRevision ?? 1, status: state.status,
29
+ originalObjective: state.originalObjective.slice(0, 1200), originalObjectiveTruncated: state.originalObjective.length > 1200,
30
+ objective: state.objective.slice(0, 1200), objectiveTruncated: state.objective.length > 1200,
31
+ tasks: [], requirements: [],
32
+ totalTasks: tasks.length, totalRequirements: state.requirements.length,
33
+ totalDecisions: state.decisions.length, totalPhases: state.phases?.length ?? 0,
34
+ totalConstraints: state.constraints.length, totalNonGoals: state.nonGoals.length,
35
+ nextOffset: null, omittedRequirements: 0,
36
+ continuation: "This is a bounded inspection page, not the complete contract. Use offset/limit or taskIds/requirementIds for records. Use record:goal|constraints|task|requirement|decision|phase, recordId and textOffset to retrieve exact JSON in chunks. Every canonical requirement remains mandatory.",
37
+ };
38
+ const preview = (item) => JSON.stringify(item).length <= 2500 ? item
39
+ : { id: item.id, status: item.status, title: typeof item.title === "string" ? item.title.slice(0, 240) : undefined,
40
+ description: typeof item.description === "string" ? item.description.slice(0, 600) : undefined, recordTruncated: true };
41
+ for (const task of candidates) {
42
+ view.tasks.push(preview({ ...task }));
43
+ if (JSON.stringify(view).length > 9000) {
44
+ view.tasks.pop();
45
+ break;
46
+ }
47
+ }
48
+ view.nextOffset = offset + view.tasks.length < tasks.length ? offset + view.tasks.length : null;
49
+ for (const requirement of requirements) {
50
+ view.requirements.push(preview({ ...requirement }));
51
+ if (JSON.stringify(view).length > 12000) {
52
+ view.requirements.pop();
53
+ break;
54
+ }
55
+ }
56
+ view.omittedRequirements = requirements.length - view.requirements.length;
57
+ return view;
58
+ }
@@ -0,0 +1,13 @@
1
+ import { type WorkspaceSnapshot } from "./WorkspaceSnapshot.js";
2
+ /** Private original contents separate the agent's diff from a dirty checkout. */
3
+ export declare class WorkspaceLedger {
4
+ readonly root: string;
5
+ readonly workspaceRoot: string;
6
+ constructor(root: string, workspaceRoot: string);
7
+ private get manifest();
8
+ private read;
9
+ private readWorkspaceContent;
10
+ capture(snapshot: WorkspaceSnapshot, scopes: string[], explicitPaths?: string[]): Promise<void>;
11
+ diff(files: string[], limit?: number): Promise<string>;
12
+ }
13
+ //# sourceMappingURL=WorkspaceLedger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WorkspaceLedger.d.ts","sourceRoot":"","sources":["../../src/runtime/WorkspaceLedger.ts"],"names":[],"mappings":"AAIA,OAAO,EAAoC,KAAK,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAOlG,iFAAiF;AACjF,qBAAa,eAAe;IACd,QAAQ,CAAC,IAAI,EAAE,MAAM;IAAE,QAAQ,CAAC,aAAa,EAAE,MAAM;gBAA5C,IAAI,EAAE,MAAM,EAAW,aAAa,EAAE,MAAM;IAGjE,OAAO,KAAK,QAAQ,GAA6D;YACnE,IAAI;YAUJ,oBAAoB;IAoB5B,OAAO,CAAC,QAAQ,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,aAAa,GAAE,MAAM,EAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA2BnG,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,KAAK,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC;CA+B7D"}
@@ -0,0 +1,145 @@
1
+ import { constants, promises as fs, realpathSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { execFile } from "node:child_process";
4
+ import { atomicWriteJson, withStateLock } from "../session/AtomicState.js";
5
+ import { hashWorkspacePath, workspaceHash } from "./WorkspaceSnapshot.js";
6
+ const MAX_FILE_BYTES = 8 * 1024 * 1024;
7
+ const inScope = (file, scopes) => scopes.some(scope => scope === "." || file === scope || file.startsWith(`${scope}/`));
8
+ /** Private original contents separate the agent's diff from a dirty checkout. */
9
+ export class WorkspaceLedger {
10
+ constructor(root, workspaceRoot) {
11
+ this.root = root;
12
+ this.workspaceRoot = workspaceRoot;
13
+ this.workspaceRoot = realpathSync.native(workspaceRoot);
14
+ }
15
+ get manifest() { return path.join(this.root, "ownership.json"); }
16
+ async read() {
17
+ try {
18
+ const value = JSON.parse(await fs.readFile(this.manifest, "utf8"));
19
+ if (value.schemaVersion !== 1 || !value.files)
20
+ throw new Error("Invalid workspace ownership ledger");
21
+ if (value.knownPaths !== undefined && (!Array.isArray(value.knownPaths) || value.knownPaths.some(file => typeof file !== "string" || path.isAbsolute(file) || file.split(/[\\/]/).includes(".."))))
22
+ throw new Error("Invalid workspace baseline inventory");
23
+ for (const [file, entry] of Object.entries(value.files))
24
+ if (path.isAbsolute(file) || file.split(/[\\/]/).includes("..") || !entry || typeof entry.hash !== "string" || entry.blob && !/^[a-f0-9]{64}$/.test(entry.blob))
25
+ throw new Error("Invalid workspace preimage reference");
26
+ return value;
27
+ }
28
+ catch (error) {
29
+ if (error.code === "ENOENT")
30
+ return { schemaVersion: 1, files: {} };
31
+ throw error;
32
+ }
33
+ }
34
+ async readWorkspaceContent(file, expectedHash) {
35
+ const target = path.resolve(this.workspaceRoot, file);
36
+ const parent = await fs.realpath(path.dirname(target));
37
+ if (!target.startsWith(`${this.workspaceRoot}${path.sep}`) || parent !== this.workspaceRoot && !parent.startsWith(`${this.workspaceRoot}${path.sep}`))
38
+ throw new Error("Workspace preimage path escapes the repository");
39
+ const handle = await fs.open(target, constants.O_RDONLY | constants.O_NOFOLLOW);
40
+ try {
41
+ const before = await handle.stat();
42
+ if (!before.isFile() || before.size > MAX_FILE_BYTES)
43
+ throw new Error("Workspace content exceeds the 8 MiB file limit");
44
+ const chunks = [];
45
+ let bytes = 0;
46
+ for await (const chunk of handle.createReadStream({ autoClose: false })) {
47
+ bytes += chunk.length;
48
+ if (bytes > MAX_FILE_BYTES)
49
+ throw new Error("Workspace content exceeds the 8 MiB file limit");
50
+ chunks.push(Buffer.from(chunk));
51
+ }
52
+ const after = await handle.stat();
53
+ const content = Buffer.concat(chunks);
54
+ if (before.size !== after.size || before.ctimeMs !== after.ctimeMs || workspaceHash(content) !== expectedHash)
55
+ throw new Error(`File changed while preparing the edit or diff: ${file}`);
56
+ return content;
57
+ }
58
+ finally {
59
+ await handle.close();
60
+ }
61
+ }
62
+ async capture(snapshot, scopes, explicitPaths = []) {
63
+ await withStateLock(this.manifest, async () => {
64
+ const record = await this.read();
65
+ record.knownPaths ?? (record.knownPaths = Object.keys(snapshot.hashes));
66
+ const candidates = new Set([...Object.keys(snapshot.hashes).filter(file => inScope(file, scopes)), ...explicitPaths]);
67
+ let bytes = Object.values(record.files).reduce((sum, entry) => sum + (entry.bytes ?? 0), 0);
68
+ for (const file of candidates) {
69
+ if (record.files[file])
70
+ continue;
71
+ const hash = snapshot.hashes[file] ?? await hashWorkspacePath(this.workspaceRoot, file);
72
+ const entry = { hash };
73
+ if (/^[a-f0-9]{64}$/.test(hash)) {
74
+ const stat = await fs.lstat(path.join(this.workspaceRoot, file));
75
+ if (stat.size > 8 * 1024 * 1024 || bytes + stat.size > 64 * 1024 * 1024)
76
+ throw new Error("Task preimages exceed the 64 MiB ledger or 8 MiB file limit; narrow the task scope");
77
+ const content = await this.readWorkspaceContent(file, hash);
78
+ if (workspaceHash(content) !== hash || await hashWorkspacePath(this.workspaceRoot, file) !== hash)
79
+ throw new Error(`File changed while preparing the edit: ${file}`);
80
+ const blob = path.join(this.root, "blobs", hash);
81
+ await fs.mkdir(path.dirname(blob), { recursive: true, mode: 0o700 });
82
+ try {
83
+ await fs.writeFile(blob, content, { flag: "wx", mode: 0o600 });
84
+ }
85
+ catch (error) {
86
+ if (error.code !== "EEXIST")
87
+ throw error;
88
+ }
89
+ entry.blob = hash;
90
+ entry.bytes = content.length;
91
+ bytes += content.length;
92
+ }
93
+ record.files[file] = entry;
94
+ }
95
+ await atomicWriteJson(this.manifest, record);
96
+ });
97
+ }
98
+ async diff(files, limit = 60000) {
99
+ const record = await this.read();
100
+ const sections = [];
101
+ let length = 0;
102
+ await fs.mkdir(this.root, { recursive: true, mode: 0o700 });
103
+ for (const file of files) {
104
+ if (length >= limit)
105
+ break;
106
+ const entry = record.files[file] ?? (record.knownPaths && !record.knownPaths.includes(file) ? { hash: "missing" } : undefined);
107
+ if (!entry)
108
+ throw new Error(`No saved preimage for ${file}; session ownership cannot be established`);
109
+ const currentHash = await hashWorkspacePath(this.workspaceRoot, file);
110
+ if (currentHash === entry.hash)
111
+ continue;
112
+ if ((!entry.blob && entry.hash !== "missing") || currentHash.startsWith("symlink:") || currentHash.startsWith("directory:")) {
113
+ const summary = `${file}: ${entry.hash} -> ${currentHash}\n`;
114
+ sections.push(summary);
115
+ length += summary.length;
116
+ continue;
117
+ }
118
+ if (currentHash !== "missing" && (await fs.lstat(path.join(this.workspaceRoot, file))).size > MAX_FILE_BYTES) {
119
+ const summary = `${file}: ${entry.hash} -> ${currentHash} (content diff omitted above 8 MiB; validate the artifact separately)\n`;
120
+ sections.push(summary);
121
+ length += summary.length;
122
+ continue;
123
+ }
124
+ const directory = await fs.mkdtemp(path.join(this.root, "diff-"));
125
+ try {
126
+ const before = entry.blob ? await fs.readFile(path.join(this.root, "blobs", entry.blob)) : Buffer.alloc(0);
127
+ const after = currentHash === "missing" ? Buffer.alloc(0) : await this.readWorkspaceContent(file, currentHash);
128
+ if (entry.blob && workspaceHash(before) !== entry.hash)
129
+ throw new Error("Workspace preimage checksum mismatch");
130
+ if (after.length && workspaceHash(after) !== currentHash)
131
+ throw new Error(`File changed during diff: ${file}`);
132
+ await fs.writeFile(path.join(directory, "before"), before, { mode: 0o600 });
133
+ await fs.writeFile(path.join(directory, "after"), after, { mode: 0o600 });
134
+ const diff = await new Promise((resolve, reject) => execFile("git", ["--no-pager", "diff", "--no-index", "--no-ext-diff", "--no-textconv", "--", "before", "after"], { cwd: directory, timeout: 10000, maxBuffer: 32 * 1024 * 1024, env: { PATH: process.env.PATH, LANG: "C", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null" } }, (error, stdout) => error && error.code !== 1 ? reject(error) : resolve(stdout)));
135
+ const section = `Session changes: ${JSON.stringify(file)}\n${diff}`.slice(0, limit - length);
136
+ sections.push(section);
137
+ length += section.length;
138
+ }
139
+ finally {
140
+ await fs.rm(directory, { recursive: true, force: true });
141
+ }
142
+ }
143
+ return sections.join("\n");
144
+ }
145
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"WorkspaceLock.d.ts","sourceRoot":"","sources":["../../src/runtime/WorkspaceLock.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,0BAA0B;IACzC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,KAAK,MAAM,CAAC;CACxD;AAED,qBAAa,aAAa;IAMtB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,QAAQ;IAPlB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiB;gBAGhC,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE,MAAM,EACb,QAAQ,GAAE,MAAuB;YAK7B,QAAQ;IAStB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,cAAc;YAcR,iBAAiB;YAuBjB,mBAAmB;IAQ3B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBxB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAU9B,sBAAsB,CAAC,OAAO,GAAE,0BAA+B,GAAG,MAAM,IAAI;CAmC7E"}
1
+ {"version":3,"file":"WorkspaceLock.d.ts","sourceRoot":"","sources":["../../src/runtime/WorkspaceLock.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,0BAA0B;IACzC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,KAAK,MAAM,CAAC;CACxD;AAED,qBAAa,aAAa;IAMtB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,QAAQ;IAPlB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiB;gBAGhC,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE,MAAM,EACb,QAAQ,GAAE,MAAuB;YAK7B,QAAQ;IAStB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,cAAc;YAcR,iBAAiB;YAuBjB,mBAAmB;IAQ3B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAqBxB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAU9B,sBAAsB,CAAC,OAAO,GAAE,0BAA+B,GAAG,MAAM,IAAI;CAmC7E"}
@@ -1,6 +1,7 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { withStateLock } from "../session/AtomicState.js";
4
5
  export class WorkspaceLock {
5
6
  constructor(workspaceRoot, runId, maxAgeMs = 60 * 60 * 1000) {
6
7
  this.workspaceRoot = workspaceRoot;
@@ -69,32 +70,34 @@ export class WorkspaceLock {
69
70
  return false;
70
71
  }
71
72
  async shouldClearExisting(info) {
72
- if (this.isStale(info))
73
- return true;
74
73
  const processAlive = this.isProcessAlive(info);
75
74
  if (processAlive === false)
76
75
  return true;
77
76
  if (processAlive === true)
78
77
  return false;
78
+ if (this.isStale(info) && this.isSameHost(info))
79
+ return this.hasTerminalRunLog(info.runId);
79
80
  return this.hasTerminalRunLog(info.runId);
80
81
  }
81
82
  async acquire() {
82
83
  await fs.mkdir(path.dirname(this.lockPath), { recursive: true });
83
- const existing = await this.readLock();
84
- if (existing && !(await this.shouldClearExisting(existing))) {
85
- throw new Error(`Workspace is locked by run ${existing.runId}`);
86
- }
87
- if (existing) {
88
- await fs.unlink(this.lockPath).catch(() => undefined);
89
- }
90
- const payload = {
91
- runId: this.runId,
92
- acquiredAt: Date.now(),
93
- pid: process.pid,
94
- hostname: this.hostname,
95
- };
96
- await fs.writeFile(this.lockPath, JSON.stringify(payload, null, 2), "utf8");
97
- this.acquired = true;
84
+ await withStateLock(`${this.lockPath}.transaction`, async () => {
85
+ const existing = await this.readLock();
86
+ if (existing && !(await this.shouldClearExisting(existing))) {
87
+ throw new Error(`Workspace is locked by run ${existing.runId}`);
88
+ }
89
+ if (existing) {
90
+ await fs.unlink(this.lockPath).catch(() => undefined);
91
+ }
92
+ const payload = {
93
+ runId: this.runId,
94
+ acquiredAt: Date.now(),
95
+ pid: process.pid,
96
+ hostname: this.hostname,
97
+ };
98
+ await fs.writeFile(this.lockPath, JSON.stringify(payload, null, 2), { encoding: "utf8", flag: "wx", mode: 0o600 });
99
+ this.acquired = true;
100
+ });
98
101
  }
99
102
  async release() {
100
103
  if (!this.acquired)