@caupulican/pi-adaptative 0.80.99 → 0.80.102

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 (129) hide show
  1. package/CHANGELOG.md +203 -0
  2. package/dist/core/agent-session.d.ts +72 -0
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +437 -46
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/autonomy/envelope-enforcement.d.ts +5 -0
  7. package/dist/core/autonomy/envelope-enforcement.d.ts.map +1 -1
  8. package/dist/core/autonomy/envelope-enforcement.js +28 -4
  9. package/dist/core/autonomy/envelope-enforcement.js.map +1 -1
  10. package/dist/core/autonomy/path-scope.d.ts.map +1 -1
  11. package/dist/core/autonomy/path-scope.js +33 -2
  12. package/dist/core/autonomy/path-scope.js.map +1 -1
  13. package/dist/core/bash-executor.d.ts.map +1 -1
  14. package/dist/core/bash-executor.js +15 -7
  15. package/dist/core/bash-executor.js.map +1 -1
  16. package/dist/core/context/brain-curator.d.ts +10 -0
  17. package/dist/core/context/brain-curator.d.ts.map +1 -1
  18. package/dist/core/context/brain-curator.js +19 -2
  19. package/dist/core/context/brain-curator.js.map +1 -1
  20. package/dist/core/context-gc.d.ts +8 -3
  21. package/dist/core/context-gc.d.ts.map +1 -1
  22. package/dist/core/context-gc.js +5 -0
  23. package/dist/core/context-gc.js.map +1 -1
  24. package/dist/core/delegation/worker-actions.d.ts +0 -10
  25. package/dist/core/delegation/worker-actions.d.ts.map +1 -1
  26. package/dist/core/delegation/worker-actions.js +5 -7
  27. package/dist/core/delegation/worker-actions.js.map +1 -1
  28. package/dist/core/delegation/worker-result.d.ts +6 -0
  29. package/dist/core/delegation/worker-result.d.ts.map +1 -1
  30. package/dist/core/delegation/worker-result.js +10 -4
  31. package/dist/core/delegation/worker-result.js.map +1 -1
  32. package/dist/core/delegation/worker-runner.d.ts +3 -0
  33. package/dist/core/delegation/worker-runner.d.ts.map +1 -1
  34. package/dist/core/delegation/worker-runner.js +6 -1
  35. package/dist/core/delegation/worker-runner.js.map +1 -1
  36. package/dist/core/doctor.d.ts +66 -0
  37. package/dist/core/doctor.d.ts.map +1 -0
  38. package/dist/core/doctor.js +144 -0
  39. package/dist/core/doctor.js.map +1 -0
  40. package/dist/core/learning/learning-audit.d.ts +13 -1
  41. package/dist/core/learning/learning-audit.d.ts.map +1 -1
  42. package/dist/core/learning/learning-audit.js +22 -1
  43. package/dist/core/learning/learning-audit.js.map +1 -1
  44. package/dist/core/learning/learning-gate.d.ts +3 -0
  45. package/dist/core/learning/learning-gate.d.ts.map +1 -1
  46. package/dist/core/learning/learning-gate.js +13 -4
  47. package/dist/core/learning/learning-gate.js.map +1 -1
  48. package/dist/core/models/default-model-suggestions.d.ts +34 -0
  49. package/dist/core/models/default-model-suggestions.d.ts.map +1 -0
  50. package/dist/core/models/default-model-suggestions.js +58 -0
  51. package/dist/core/models/default-model-suggestions.js.map +1 -0
  52. package/dist/core/models/local-registration.d.ts +2 -0
  53. package/dist/core/models/local-registration.d.ts.map +1 -1
  54. package/dist/core/models/local-registration.js +2 -1
  55. package/dist/core/models/local-registration.js.map +1 -1
  56. package/dist/core/models/local-runtime.d.ts +76 -2
  57. package/dist/core/models/local-runtime.d.ts.map +1 -1
  58. package/dist/core/models/local-runtime.js +233 -21
  59. package/dist/core/models/local-runtime.js.map +1 -1
  60. package/dist/core/profile-resource-selection.d.ts +15 -3
  61. package/dist/core/profile-resource-selection.d.ts.map +1 -1
  62. package/dist/core/profile-resource-selection.js +19 -9
  63. package/dist/core/profile-resource-selection.js.map +1 -1
  64. package/dist/core/resource-loader.d.ts +20 -0
  65. package/dist/core/resource-loader.d.ts.map +1 -1
  66. package/dist/core/resource-loader.js +42 -16
  67. package/dist/core/resource-loader.js.map +1 -1
  68. package/dist/core/settings-manager.d.ts +30 -0
  69. package/dist/core/settings-manager.d.ts.map +1 -1
  70. package/dist/core/settings-manager.js +65 -3
  71. package/dist/core/settings-manager.js.map +1 -1
  72. package/dist/core/skills.d.ts +14 -0
  73. package/dist/core/skills.d.ts.map +1 -1
  74. package/dist/core/skills.js +8 -0
  75. package/dist/core/skills.js.map +1 -1
  76. package/dist/core/slash-commands.d.ts.map +1 -1
  77. package/dist/core/slash-commands.js +1 -1
  78. package/dist/core/slash-commands.js.map +1 -1
  79. package/dist/core/tools/fff-search-backend.d.ts +34 -1
  80. package/dist/core/tools/fff-search-backend.d.ts.map +1 -1
  81. package/dist/core/tools/fff-search-backend.js +43 -4
  82. package/dist/core/tools/fff-search-backend.js.map +1 -1
  83. package/dist/core/tools/find.d.ts +16 -0
  84. package/dist/core/tools/find.d.ts.map +1 -1
  85. package/dist/core/tools/find.js +17 -3
  86. package/dist/core/tools/find.js.map +1 -1
  87. package/dist/core/tools/grep.d.ts +20 -0
  88. package/dist/core/tools/grep.d.ts.map +1 -1
  89. package/dist/core/tools/grep.js +15 -3
  90. package/dist/core/tools/grep.js.map +1 -1
  91. package/dist/main.d.ts.map +1 -1
  92. package/dist/main.js +17 -0
  93. package/dist/main.js.map +1 -1
  94. package/dist/modes/interactive/components/fitness-role-selector.d.ts +6 -2
  95. package/dist/modes/interactive/components/fitness-role-selector.d.ts.map +1 -1
  96. package/dist/modes/interactive/components/fitness-role-selector.js +55 -45
  97. package/dist/modes/interactive/components/fitness-role-selector.js.map +1 -1
  98. package/dist/modes/interactive/components/model-suggestion-selector.d.ts +13 -0
  99. package/dist/modes/interactive/components/model-suggestion-selector.d.ts.map +1 -0
  100. package/dist/modes/interactive/components/model-suggestion-selector.js +40 -0
  101. package/dist/modes/interactive/components/model-suggestion-selector.js.map +1 -0
  102. package/dist/modes/interactive/components/profile-resource-editor.d.ts +3 -0
  103. package/dist/modes/interactive/components/profile-resource-editor.d.ts.map +1 -1
  104. package/dist/modes/interactive/components/profile-resource-editor.js +16 -2
  105. package/dist/modes/interactive/components/profile-resource-editor.js.map +1 -1
  106. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  107. package/dist/modes/interactive/components/settings-selector.js +98 -0
  108. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  109. package/dist/modes/interactive/interactive-mode.d.ts +8 -1
  110. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  111. package/dist/modes/interactive/interactive-mode.js +57 -10
  112. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  113. package/dist/utils/safe-write-stream.d.ts +8 -0
  114. package/dist/utils/safe-write-stream.d.ts.map +1 -1
  115. package/dist/utils/safe-write-stream.js +32 -0
  116. package/dist/utils/safe-write-stream.js.map +1 -1
  117. package/dist/utils/tools-manager.d.ts +92 -2
  118. package/dist/utils/tools-manager.d.ts.map +1 -1
  119. package/dist/utils/tools-manager.js +172 -32
  120. package/dist/utils/tools-manager.js.map +1 -1
  121. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  122. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  123. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  124. package/examples/extensions/sandbox/package-lock.json +2 -2
  125. package/examples/extensions/sandbox/package.json +1 -1
  126. package/examples/extensions/with-deps/package-lock.json +2 -2
  127. package/examples/extensions/with-deps/package.json +1 -1
  128. package/npm-shrinkwrap.json +12 -36
  129. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"worker-result.d.ts","sourceRoot":"","sources":["../../../src/core/delegation/worker-result.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAIzF,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAO9E;AAQD,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CA0BpE;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAWlE;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,GAAG,WAAW,CA6GxG","sourcesContent":["import path from \"node:path\";\nimport type { GateOutcome, WorkerRequest, WorkerResult } from \"../autonomy/contracts.ts\";\nimport { checkPathScope } from \"../autonomy/path-scope.ts\";\nimport { cloneEvidenceBundleForStorage, isEvidenceBundle } from \"../research/evidence-bundle.ts\";\n\nexport function cloneWorkerResultForStorage(result: WorkerResult): WorkerResult {\n\treturn {\n\t\t...result,\n\t\tchangedFiles: [...result.changedFiles],\n\t\tblockers: result.blockers ? [...result.blockers] : undefined,\n\t\tevidence: result.evidence ? cloneEvidenceBundleForStorage(result.evidence) : undefined,\n\t};\n}\n\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) return false;\n\tconst prototype = Object.getPrototypeOf(value);\n\treturn prototype === Object.prototype || prototype === null;\n}\n\nexport function isWorkerResult(value: unknown): value is WorkerResult {\n\tif (!isPlainRecord(value)) return false;\n\tconst obj = value as Record<string, unknown>;\n\n\tif (typeof obj.requestId !== \"string\") return false;\n\tif (typeof obj.status !== \"string\" || ![\"completed\", \"blocked\", \"failed\", \"cancelled\"].includes(obj.status)) {\n\t\treturn false;\n\t}\n\tif (typeof obj.summary !== \"string\") return false;\n\n\tif (!Array.isArray(obj.changedFiles) || !obj.changedFiles.every((f) => typeof f === \"string\")) {\n\t\treturn false;\n\t}\n\n\tif (obj.blockers !== undefined) {\n\t\tif (!Array.isArray(obj.blockers) || !obj.blockers.every((b) => typeof b === \"string\")) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tif (obj.usageReportId !== undefined && typeof obj.usageReportId !== \"string\") return false;\n\tif (obj.createdAt !== undefined && typeof obj.createdAt !== \"string\") return false;\n\n\tif (obj.evidence !== undefined && !isEvidenceBundle(obj.evidence)) return false;\n\n\treturn true;\n}\n\nexport function requiresParentReview(result: WorkerResult): boolean {\n\tif (result.status !== \"completed\") {\n\t\treturn true;\n\t}\n\tif (result.blockers && result.blockers.length > 0) {\n\t\treturn true;\n\t}\n\tif (result.changedFiles.length > 0) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nexport function validateWorkerResult(args: { request: WorkerRequest; result: WorkerResult }): GateOutcome {\n\tconst { request, result } = args;\n\n\tif (result.requestId !== request.id) {\n\t\treturn {\n\t\t\toutcome: \"block\",\n\t\t\tgate: \"worker_result\",\n\t\t\treasonCode: \"request_id_mismatch\",\n\t\t\tmessage: `Result requestId '${result.requestId}' does not match request id '${request.id}'.`,\n\t\t};\n\t}\n\n\tif (result.status !== \"completed\") {\n\t\treturn {\n\t\t\toutcome: \"block\",\n\t\t\tgate: \"worker_result\",\n\t\t\treasonCode: \"worker_not_completed\",\n\t\t\tmessage: `Worker finished with status '${result.status}'.`,\n\t\t\tdetails: result.blockers && result.blockers.length > 0 ? { blockers: [...result.blockers] } : undefined,\n\t\t};\n\t}\n\n\tif (!result.usageReportId) {\n\t\treturn {\n\t\t\toutcome: \"block\",\n\t\t\tgate: \"worker_result\",\n\t\t\treasonCode: \"missing_usage_report\",\n\t\t\tmessage: \"Completed worker result is missing usageReportId.\",\n\t\t};\n\t}\n\n\tif (result.blockers && result.blockers.length > 0) {\n\t\treturn {\n\t\t\toutcome: \"ask-user\",\n\t\t\tgate: \"worker_result\",\n\t\t\treasonCode: \"parent_review_required\",\n\t\t\tmessage: \"Completed worker result includes blockers and requires parent review.\",\n\t\t\tdetails: { blockers: [...result.blockers] },\n\t\t};\n\t}\n\n\tif (result.changedFiles.length > 0) {\n\t\tif (!request.envelope.allowedPaths || request.envelope.allowedPaths.length === 0) {\n\t\t\treturn {\n\t\t\t\toutcome: \"block\",\n\t\t\t\tgate: \"worker_result\",\n\t\t\t\treasonCode: \"missing_path_scope\",\n\t\t\t\tmessage: \"Worker changed files but no allowedPaths are configured in the envelope.\",\n\t\t\t};\n\t\t}\n\n\t\tfor (const changedFile of result.changedFiles) {\n\t\t\tlet isInsideAny = false;\n\t\t\tlet isDenied = false;\n\n\t\t\tfor (const root of request.envelope.allowedPaths) {\n\t\t\t\tconst scopedChangedFile = path.isAbsolute(changedFile) ? changedFile : path.resolve(root, changedFile);\n\t\t\t\tconst decision = checkPathScope(\n\t\t\t\t\t{\n\t\t\t\t\t\troot,\n\t\t\t\t\t\tallowedPaths: request.envelope.allowedPaths,\n\t\t\t\t\t\tdeniedPaths: request.envelope.deniedPaths,\n\t\t\t\t\t},\n\t\t\t\t\tscopedChangedFile,\n\t\t\t\t);\n\n\t\t\t\tif (decision.kind === \"denied\") {\n\t\t\t\t\tisDenied = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (decision.kind === \"inside\") {\n\t\t\t\t\tisInsideAny = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (isDenied) {\n\t\t\t\treturn {\n\t\t\t\t\toutcome: \"block\",\n\t\t\t\t\tgate: \"worker_result\",\n\t\t\t\t\treasonCode: \"changed_file_denied\",\n\t\t\t\t\tmessage: `Worker changed file '${changedFile}' which matches a denied path.`,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!isInsideAny) {\n\t\t\t\treturn {\n\t\t\t\t\toutcome: \"block\",\n\t\t\t\t\tgate: \"worker_result\",\n\t\t\t\t\treasonCode: \"changed_file_outside_scope\",\n\t\t\t\t\tmessage: `Worker changed file '${changedFile}' outside allowed scope.`,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Files are inside scope, but worker output is untrusted\n\t\treturn {\n\t\t\toutcome: \"ask-user\",\n\t\t\tgate: \"worker_result\",\n\t\t\treasonCode: \"parent_review_required\",\n\t\t\tmessage: \"Worker changed files require parent review.\",\n\t\t};\n\t}\n\n\treturn {\n\t\toutcome: \"allow\",\n\t\tgate: \"worker_result\",\n\t\treasonCode: \"allowed\",\n\t\tmessage: \"Worker result is read-only and allowed.\",\n\t};\n}\n"]}
1
+ {"version":3,"file":"worker-result.d.ts","sourceRoot":"","sources":["../../../src/core/delegation/worker-result.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAIzF,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAO9E;AAQD,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CA0BpE;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAWlE;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IAC1C,OAAO,EAAE,aAAa,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;IACrB;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACb,GAAG,WAAW,CAmHd","sourcesContent":["import path from \"node:path\";\nimport type { GateOutcome, WorkerRequest, WorkerResult } from \"../autonomy/contracts.ts\";\nimport { checkPathScope } from \"../autonomy/path-scope.ts\";\nimport { cloneEvidenceBundleForStorage, isEvidenceBundle } from \"../research/evidence-bundle.ts\";\n\nexport function cloneWorkerResultForStorage(result: WorkerResult): WorkerResult {\n\treturn {\n\t\t...result,\n\t\tchangedFiles: [...result.changedFiles],\n\t\tblockers: result.blockers ? [...result.blockers] : undefined,\n\t\tevidence: result.evidence ? cloneEvidenceBundleForStorage(result.evidence) : undefined,\n\t};\n}\n\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) return false;\n\tconst prototype = Object.getPrototypeOf(value);\n\treturn prototype === Object.prototype || prototype === null;\n}\n\nexport function isWorkerResult(value: unknown): value is WorkerResult {\n\tif (!isPlainRecord(value)) return false;\n\tconst obj = value as Record<string, unknown>;\n\n\tif (typeof obj.requestId !== \"string\") return false;\n\tif (typeof obj.status !== \"string\" || ![\"completed\", \"blocked\", \"failed\", \"cancelled\"].includes(obj.status)) {\n\t\treturn false;\n\t}\n\tif (typeof obj.summary !== \"string\") return false;\n\n\tif (!Array.isArray(obj.changedFiles) || !obj.changedFiles.every((f) => typeof f === \"string\")) {\n\t\treturn false;\n\t}\n\n\tif (obj.blockers !== undefined) {\n\t\tif (!Array.isArray(obj.blockers) || !obj.blockers.every((b) => typeof b === \"string\")) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tif (obj.usageReportId !== undefined && typeof obj.usageReportId !== \"string\") return false;\n\tif (obj.createdAt !== undefined && typeof obj.createdAt !== \"string\") return false;\n\n\tif (obj.evidence !== undefined && !isEvidenceBundle(obj.evidence)) return false;\n\n\treturn true;\n}\n\nexport function requiresParentReview(result: WorkerResult): boolean {\n\tif (result.status !== \"completed\") {\n\t\treturn true;\n\t}\n\tif (result.blockers && result.blockers.length > 0) {\n\t\treturn true;\n\t}\n\tif (result.changedFiles.length > 0) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nexport function validateWorkerResult(args: {\n\trequest: WorkerRequest;\n\tresult: WorkerResult;\n\t/**\n\t * Baseline for relative paths — BOTH the runner's cwd-relative `changedFiles` and the\n\t * envelope's possibly-relative path scopes resolve against this. Defaults to process.cwd()\n\t * for callers whose session cwd is the process cwd.\n\t */\n\tcwd?: string;\n}): GateOutcome {\n\tconst { request, result } = args;\n\tconst baseDir = args.cwd ?? process.cwd();\n\n\tif (result.requestId !== request.id) {\n\t\treturn {\n\t\t\toutcome: \"block\",\n\t\t\tgate: \"worker_result\",\n\t\t\treasonCode: \"request_id_mismatch\",\n\t\t\tmessage: `Result requestId '${result.requestId}' does not match request id '${request.id}'.`,\n\t\t};\n\t}\n\n\tif (result.status !== \"completed\") {\n\t\treturn {\n\t\t\toutcome: \"block\",\n\t\t\tgate: \"worker_result\",\n\t\t\treasonCode: \"worker_not_completed\",\n\t\t\tmessage: `Worker finished with status '${result.status}'.`,\n\t\t\tdetails: result.blockers && result.blockers.length > 0 ? { blockers: [...result.blockers] } : undefined,\n\t\t};\n\t}\n\n\tif (!result.usageReportId) {\n\t\treturn {\n\t\t\toutcome: \"block\",\n\t\t\tgate: \"worker_result\",\n\t\t\treasonCode: \"missing_usage_report\",\n\t\t\tmessage: \"Completed worker result is missing usageReportId.\",\n\t\t};\n\t}\n\n\tif (result.blockers && result.blockers.length > 0) {\n\t\treturn {\n\t\t\toutcome: \"ask-user\",\n\t\t\tgate: \"worker_result\",\n\t\t\treasonCode: \"parent_review_required\",\n\t\t\tmessage: \"Completed worker result includes blockers and requires parent review.\",\n\t\t\tdetails: { blockers: [...result.blockers] },\n\t\t};\n\t}\n\n\tif (result.changedFiles.length > 0) {\n\t\tif (!request.envelope.allowedPaths || request.envelope.allowedPaths.length === 0) {\n\t\t\treturn {\n\t\t\t\toutcome: \"block\",\n\t\t\t\tgate: \"worker_result\",\n\t\t\t\treasonCode: \"missing_path_scope\",\n\t\t\t\tmessage: \"Worker changed files but no allowedPaths are configured in the envelope.\",\n\t\t\t};\n\t\t}\n\n\t\t// The runner reports changed files relative to the session cwd — resolve file and scope\n\t\t// roots against the SAME baseline. Resolving per-root double-prefixed nested names and\n\t\t// let a denied subtree slip past the deny rule.\n\t\tconst resolvedAllowed = request.envelope.allowedPaths.map((p) => path.resolve(baseDir, p));\n\t\tconst resolvedDenied = request.envelope.deniedPaths?.map((p) => path.resolve(baseDir, p));\n\t\tfor (const changedFile of result.changedFiles) {\n\t\t\tlet isInsideAny = false;\n\t\t\tlet isDenied = false;\n\n\t\t\tconst scopedChangedFile = path.resolve(baseDir, changedFile);\n\t\t\tfor (const root of resolvedAllowed) {\n\t\t\t\tconst decision = checkPathScope(\n\t\t\t\t\t{\n\t\t\t\t\t\troot,\n\t\t\t\t\t\tallowedPaths: resolvedAllowed,\n\t\t\t\t\t\tdeniedPaths: resolvedDenied,\n\t\t\t\t\t},\n\t\t\t\t\tscopedChangedFile,\n\t\t\t\t);\n\n\t\t\t\tif (decision.kind === \"denied\") {\n\t\t\t\t\tisDenied = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (decision.kind === \"inside\") {\n\t\t\t\t\tisInsideAny = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (isDenied) {\n\t\t\t\treturn {\n\t\t\t\t\toutcome: \"block\",\n\t\t\t\t\tgate: \"worker_result\",\n\t\t\t\t\treasonCode: \"changed_file_denied\",\n\t\t\t\t\tmessage: `Worker changed file '${changedFile}' which matches a denied path.`,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!isInsideAny) {\n\t\t\t\treturn {\n\t\t\t\t\toutcome: \"block\",\n\t\t\t\t\tgate: \"worker_result\",\n\t\t\t\t\treasonCode: \"changed_file_outside_scope\",\n\t\t\t\t\tmessage: `Worker changed file '${changedFile}' outside allowed scope.`,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Files are inside scope, but worker output is untrusted\n\t\treturn {\n\t\t\toutcome: \"ask-user\",\n\t\t\tgate: \"worker_result\",\n\t\t\treasonCode: \"parent_review_required\",\n\t\t\tmessage: \"Worker changed files require parent review.\",\n\t\t};\n\t}\n\n\treturn {\n\t\toutcome: \"allow\",\n\t\tgate: \"worker_result\",\n\t\treasonCode: \"allowed\",\n\t\tmessage: \"Worker result is read-only and allowed.\",\n\t};\n}\n"]}
@@ -56,6 +56,7 @@ export function requiresParentReview(result) {
56
56
  }
57
57
  export function validateWorkerResult(args) {
58
58
  const { request, result } = args;
59
+ const baseDir = args.cwd ?? process.cwd();
59
60
  if (result.requestId !== request.id) {
60
61
  return {
61
62
  outcome: "block",
@@ -99,15 +100,20 @@ export function validateWorkerResult(args) {
99
100
  message: "Worker changed files but no allowedPaths are configured in the envelope.",
100
101
  };
101
102
  }
103
+ // The runner reports changed files relative to the session cwd — resolve file and scope
104
+ // roots against the SAME baseline. Resolving per-root double-prefixed nested names and
105
+ // let a denied subtree slip past the deny rule.
106
+ const resolvedAllowed = request.envelope.allowedPaths.map((p) => path.resolve(baseDir, p));
107
+ const resolvedDenied = request.envelope.deniedPaths?.map((p) => path.resolve(baseDir, p));
102
108
  for (const changedFile of result.changedFiles) {
103
109
  let isInsideAny = false;
104
110
  let isDenied = false;
105
- for (const root of request.envelope.allowedPaths) {
106
- const scopedChangedFile = path.isAbsolute(changedFile) ? changedFile : path.resolve(root, changedFile);
111
+ const scopedChangedFile = path.resolve(baseDir, changedFile);
112
+ for (const root of resolvedAllowed) {
107
113
  const decision = checkPathScope({
108
114
  root,
109
- allowedPaths: request.envelope.allowedPaths,
110
- deniedPaths: request.envelope.deniedPaths,
115
+ allowedPaths: resolvedAllowed,
116
+ deniedPaths: resolvedDenied,
111
117
  }, scopedChangedFile);
112
118
  if (decision.kind === "denied") {
113
119
  isDenied = true;
@@ -1 +1 @@
1
- {"version":3,"file":"worker-result.js","sourceRoot":"","sources":["../../../src/core/delegation/worker-result.ts"],"names":[],"mappings":"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","sourcesContent":["import path from \"node:path\";\nimport type { GateOutcome, WorkerRequest, WorkerResult } from \"../autonomy/contracts.ts\";\nimport { checkPathScope } from \"../autonomy/path-scope.ts\";\nimport { cloneEvidenceBundleForStorage, isEvidenceBundle } from \"../research/evidence-bundle.ts\";\n\nexport function cloneWorkerResultForStorage(result: WorkerResult): WorkerResult {\n\treturn {\n\t\t...result,\n\t\tchangedFiles: [...result.changedFiles],\n\t\tblockers: result.blockers ? [...result.blockers] : undefined,\n\t\tevidence: result.evidence ? cloneEvidenceBundleForStorage(result.evidence) : undefined,\n\t};\n}\n\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) return false;\n\tconst prototype = Object.getPrototypeOf(value);\n\treturn prototype === Object.prototype || prototype === null;\n}\n\nexport function isWorkerResult(value: unknown): value is WorkerResult {\n\tif (!isPlainRecord(value)) return false;\n\tconst obj = value as Record<string, unknown>;\n\n\tif (typeof obj.requestId !== \"string\") return false;\n\tif (typeof obj.status !== \"string\" || ![\"completed\", \"blocked\", \"failed\", \"cancelled\"].includes(obj.status)) {\n\t\treturn false;\n\t}\n\tif (typeof obj.summary !== \"string\") return false;\n\n\tif (!Array.isArray(obj.changedFiles) || !obj.changedFiles.every((f) => typeof f === \"string\")) {\n\t\treturn false;\n\t}\n\n\tif (obj.blockers !== undefined) {\n\t\tif (!Array.isArray(obj.blockers) || !obj.blockers.every((b) => typeof b === \"string\")) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tif (obj.usageReportId !== undefined && typeof obj.usageReportId !== \"string\") return false;\n\tif (obj.createdAt !== undefined && typeof obj.createdAt !== \"string\") return false;\n\n\tif (obj.evidence !== undefined && !isEvidenceBundle(obj.evidence)) return false;\n\n\treturn true;\n}\n\nexport function requiresParentReview(result: WorkerResult): boolean {\n\tif (result.status !== \"completed\") {\n\t\treturn true;\n\t}\n\tif (result.blockers && result.blockers.length > 0) {\n\t\treturn true;\n\t}\n\tif (result.changedFiles.length > 0) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nexport function validateWorkerResult(args: { request: WorkerRequest; result: WorkerResult }): GateOutcome {\n\tconst { request, result } = args;\n\n\tif (result.requestId !== request.id) {\n\t\treturn {\n\t\t\toutcome: \"block\",\n\t\t\tgate: \"worker_result\",\n\t\t\treasonCode: \"request_id_mismatch\",\n\t\t\tmessage: `Result requestId '${result.requestId}' does not match request id '${request.id}'.`,\n\t\t};\n\t}\n\n\tif (result.status !== \"completed\") {\n\t\treturn {\n\t\t\toutcome: \"block\",\n\t\t\tgate: \"worker_result\",\n\t\t\treasonCode: \"worker_not_completed\",\n\t\t\tmessage: `Worker finished with status '${result.status}'.`,\n\t\t\tdetails: result.blockers && result.blockers.length > 0 ? { blockers: [...result.blockers] } : undefined,\n\t\t};\n\t}\n\n\tif (!result.usageReportId) {\n\t\treturn {\n\t\t\toutcome: \"block\",\n\t\t\tgate: \"worker_result\",\n\t\t\treasonCode: \"missing_usage_report\",\n\t\t\tmessage: \"Completed worker result is missing usageReportId.\",\n\t\t};\n\t}\n\n\tif (result.blockers && result.blockers.length > 0) {\n\t\treturn {\n\t\t\toutcome: \"ask-user\",\n\t\t\tgate: \"worker_result\",\n\t\t\treasonCode: \"parent_review_required\",\n\t\t\tmessage: \"Completed worker result includes blockers and requires parent review.\",\n\t\t\tdetails: { blockers: [...result.blockers] },\n\t\t};\n\t}\n\n\tif (result.changedFiles.length > 0) {\n\t\tif (!request.envelope.allowedPaths || request.envelope.allowedPaths.length === 0) {\n\t\t\treturn {\n\t\t\t\toutcome: \"block\",\n\t\t\t\tgate: \"worker_result\",\n\t\t\t\treasonCode: \"missing_path_scope\",\n\t\t\t\tmessage: \"Worker changed files but no allowedPaths are configured in the envelope.\",\n\t\t\t};\n\t\t}\n\n\t\tfor (const changedFile of result.changedFiles) {\n\t\t\tlet isInsideAny = false;\n\t\t\tlet isDenied = false;\n\n\t\t\tfor (const root of request.envelope.allowedPaths) {\n\t\t\t\tconst scopedChangedFile = path.isAbsolute(changedFile) ? changedFile : path.resolve(root, changedFile);\n\t\t\t\tconst decision = checkPathScope(\n\t\t\t\t\t{\n\t\t\t\t\t\troot,\n\t\t\t\t\t\tallowedPaths: request.envelope.allowedPaths,\n\t\t\t\t\t\tdeniedPaths: request.envelope.deniedPaths,\n\t\t\t\t\t},\n\t\t\t\t\tscopedChangedFile,\n\t\t\t\t);\n\n\t\t\t\tif (decision.kind === \"denied\") {\n\t\t\t\t\tisDenied = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (decision.kind === \"inside\") {\n\t\t\t\t\tisInsideAny = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (isDenied) {\n\t\t\t\treturn {\n\t\t\t\t\toutcome: \"block\",\n\t\t\t\t\tgate: \"worker_result\",\n\t\t\t\t\treasonCode: \"changed_file_denied\",\n\t\t\t\t\tmessage: `Worker changed file '${changedFile}' which matches a denied path.`,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!isInsideAny) {\n\t\t\t\treturn {\n\t\t\t\t\toutcome: \"block\",\n\t\t\t\t\tgate: \"worker_result\",\n\t\t\t\t\treasonCode: \"changed_file_outside_scope\",\n\t\t\t\t\tmessage: `Worker changed file '${changedFile}' outside allowed scope.`,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Files are inside scope, but worker output is untrusted\n\t\treturn {\n\t\t\toutcome: \"ask-user\",\n\t\t\tgate: \"worker_result\",\n\t\t\treasonCode: \"parent_review_required\",\n\t\t\tmessage: \"Worker changed files require parent review.\",\n\t\t};\n\t}\n\n\treturn {\n\t\toutcome: \"allow\",\n\t\tgate: \"worker_result\",\n\t\treasonCode: \"allowed\",\n\t\tmessage: \"Worker result is read-only and allowed.\",\n\t};\n}\n"]}
1
+ {"version":3,"file":"worker-result.js","sourceRoot":"","sources":["../../../src/core/delegation/worker-result.ts"],"names":[],"mappings":"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","sourcesContent":["import path from \"node:path\";\nimport type { GateOutcome, WorkerRequest, WorkerResult } from \"../autonomy/contracts.ts\";\nimport { checkPathScope } from \"../autonomy/path-scope.ts\";\nimport { cloneEvidenceBundleForStorage, isEvidenceBundle } from \"../research/evidence-bundle.ts\";\n\nexport function cloneWorkerResultForStorage(result: WorkerResult): WorkerResult {\n\treturn {\n\t\t...result,\n\t\tchangedFiles: [...result.changedFiles],\n\t\tblockers: result.blockers ? [...result.blockers] : undefined,\n\t\tevidence: result.evidence ? cloneEvidenceBundleForStorage(result.evidence) : undefined,\n\t};\n}\n\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) return false;\n\tconst prototype = Object.getPrototypeOf(value);\n\treturn prototype === Object.prototype || prototype === null;\n}\n\nexport function isWorkerResult(value: unknown): value is WorkerResult {\n\tif (!isPlainRecord(value)) return false;\n\tconst obj = value as Record<string, unknown>;\n\n\tif (typeof obj.requestId !== \"string\") return false;\n\tif (typeof obj.status !== \"string\" || ![\"completed\", \"blocked\", \"failed\", \"cancelled\"].includes(obj.status)) {\n\t\treturn false;\n\t}\n\tif (typeof obj.summary !== \"string\") return false;\n\n\tif (!Array.isArray(obj.changedFiles) || !obj.changedFiles.every((f) => typeof f === \"string\")) {\n\t\treturn false;\n\t}\n\n\tif (obj.blockers !== undefined) {\n\t\tif (!Array.isArray(obj.blockers) || !obj.blockers.every((b) => typeof b === \"string\")) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tif (obj.usageReportId !== undefined && typeof obj.usageReportId !== \"string\") return false;\n\tif (obj.createdAt !== undefined && typeof obj.createdAt !== \"string\") return false;\n\n\tif (obj.evidence !== undefined && !isEvidenceBundle(obj.evidence)) return false;\n\n\treturn true;\n}\n\nexport function requiresParentReview(result: WorkerResult): boolean {\n\tif (result.status !== \"completed\") {\n\t\treturn true;\n\t}\n\tif (result.blockers && result.blockers.length > 0) {\n\t\treturn true;\n\t}\n\tif (result.changedFiles.length > 0) {\n\t\treturn true;\n\t}\n\treturn false;\n}\n\nexport function validateWorkerResult(args: {\n\trequest: WorkerRequest;\n\tresult: WorkerResult;\n\t/**\n\t * Baseline for relative paths — BOTH the runner's cwd-relative `changedFiles` and the\n\t * envelope's possibly-relative path scopes resolve against this. Defaults to process.cwd()\n\t * for callers whose session cwd is the process cwd.\n\t */\n\tcwd?: string;\n}): GateOutcome {\n\tconst { request, result } = args;\n\tconst baseDir = args.cwd ?? process.cwd();\n\n\tif (result.requestId !== request.id) {\n\t\treturn {\n\t\t\toutcome: \"block\",\n\t\t\tgate: \"worker_result\",\n\t\t\treasonCode: \"request_id_mismatch\",\n\t\t\tmessage: `Result requestId '${result.requestId}' does not match request id '${request.id}'.`,\n\t\t};\n\t}\n\n\tif (result.status !== \"completed\") {\n\t\treturn {\n\t\t\toutcome: \"block\",\n\t\t\tgate: \"worker_result\",\n\t\t\treasonCode: \"worker_not_completed\",\n\t\t\tmessage: `Worker finished with status '${result.status}'.`,\n\t\t\tdetails: result.blockers && result.blockers.length > 0 ? { blockers: [...result.blockers] } : undefined,\n\t\t};\n\t}\n\n\tif (!result.usageReportId) {\n\t\treturn {\n\t\t\toutcome: \"block\",\n\t\t\tgate: \"worker_result\",\n\t\t\treasonCode: \"missing_usage_report\",\n\t\t\tmessage: \"Completed worker result is missing usageReportId.\",\n\t\t};\n\t}\n\n\tif (result.blockers && result.blockers.length > 0) {\n\t\treturn {\n\t\t\toutcome: \"ask-user\",\n\t\t\tgate: \"worker_result\",\n\t\t\treasonCode: \"parent_review_required\",\n\t\t\tmessage: \"Completed worker result includes blockers and requires parent review.\",\n\t\t\tdetails: { blockers: [...result.blockers] },\n\t\t};\n\t}\n\n\tif (result.changedFiles.length > 0) {\n\t\tif (!request.envelope.allowedPaths || request.envelope.allowedPaths.length === 0) {\n\t\t\treturn {\n\t\t\t\toutcome: \"block\",\n\t\t\t\tgate: \"worker_result\",\n\t\t\t\treasonCode: \"missing_path_scope\",\n\t\t\t\tmessage: \"Worker changed files but no allowedPaths are configured in the envelope.\",\n\t\t\t};\n\t\t}\n\n\t\t// The runner reports changed files relative to the session cwd — resolve file and scope\n\t\t// roots against the SAME baseline. Resolving per-root double-prefixed nested names and\n\t\t// let a denied subtree slip past the deny rule.\n\t\tconst resolvedAllowed = request.envelope.allowedPaths.map((p) => path.resolve(baseDir, p));\n\t\tconst resolvedDenied = request.envelope.deniedPaths?.map((p) => path.resolve(baseDir, p));\n\t\tfor (const changedFile of result.changedFiles) {\n\t\t\tlet isInsideAny = false;\n\t\t\tlet isDenied = false;\n\n\t\t\tconst scopedChangedFile = path.resolve(baseDir, changedFile);\n\t\t\tfor (const root of resolvedAllowed) {\n\t\t\t\tconst decision = checkPathScope(\n\t\t\t\t\t{\n\t\t\t\t\t\troot,\n\t\t\t\t\t\tallowedPaths: resolvedAllowed,\n\t\t\t\t\t\tdeniedPaths: resolvedDenied,\n\t\t\t\t\t},\n\t\t\t\t\tscopedChangedFile,\n\t\t\t\t);\n\n\t\t\t\tif (decision.kind === \"denied\") {\n\t\t\t\t\tisDenied = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tif (decision.kind === \"inside\") {\n\t\t\t\t\tisInsideAny = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (isDenied) {\n\t\t\t\treturn {\n\t\t\t\t\toutcome: \"block\",\n\t\t\t\t\tgate: \"worker_result\",\n\t\t\t\t\treasonCode: \"changed_file_denied\",\n\t\t\t\t\tmessage: `Worker changed file '${changedFile}' which matches a denied path.`,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (!isInsideAny) {\n\t\t\t\treturn {\n\t\t\t\t\toutcome: \"block\",\n\t\t\t\t\tgate: \"worker_result\",\n\t\t\t\t\treasonCode: \"changed_file_outside_scope\",\n\t\t\t\t\tmessage: `Worker changed file '${changedFile}' outside allowed scope.`,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Files are inside scope, but worker output is untrusted\n\t\treturn {\n\t\t\toutcome: \"ask-user\",\n\t\t\tgate: \"worker_result\",\n\t\t\treasonCode: \"parent_review_required\",\n\t\t\tmessage: \"Worker changed files require parent review.\",\n\t\t};\n\t}\n\n\treturn {\n\t\toutcome: \"allow\",\n\t\tgate: \"worker_result\",\n\t\treasonCode: \"allowed\",\n\t\tmessage: \"Worker result is read-only and allowed.\",\n\t};\n}\n"]}
@@ -41,6 +41,9 @@ export interface WorkerRunnerOptions {
41
41
  * runner applies the worker's structured actions through the envelope path scope; refusals
42
42
  * and failures become blockers, never silent drops. */
43
43
  applyActions?: (actions: readonly WorkerAction[]) => AppliedActionsReport;
44
+ /** Session cwd — the baseline for relative changed-file and envelope paths in parent
45
+ * validation. Defaults to process.cwd(). */
46
+ cwd?: string;
44
47
  }
45
48
  export interface WorkerRunOutcome {
46
49
  result: WorkerResult;
@@ -1 +1 @@
1
- {"version":3,"file":"worker-runner.d.ts","sourceRoot":"","sources":["../../../src/core/delegation/worker-runner.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAwB,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC/G,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAEtE,OAAO,EAAE,KAAK,oBAAoB,EAAsB,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGvG;;;;;;;GAOG;AAEH,wEAAwE;AACxE,eAAO,MAAM,yBAAyB,QAO1B,CAAC;AAEb;2FAC2F;AAC3F,eAAO,MAAM,+BAA+B,QAQhC,CAAC;AAEb,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IACnC,OAAO,EAAE,aAAa,CAAC;IACvB,qFAAqF;IACrF,MAAM,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,cAAc,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,CAAC,IAAI,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAClH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB;;2DAEuD;IACvD,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,KAAK,oBAAoB,CAAC;CAC1E;AAED,MAAM,WAAW,gBAAgB;IAChC,MAAM,EAAE,YAAY,CAAC;IACrB,8FAA8F;IAC9F,UAAU,EAAE,WAAW,CAAC;IACxB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,kBAAkB,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAEpE;AAED,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,WAAW,GAAG,SAAS,CAAC;IAChC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1D,OAAO,EAAE,YAAY,EAAE,CAAC;CACxB;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CA0C9E;AAgDD,wBAAsB,SAAS,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA4GvF","sourcesContent":["import { runBoundedCompletion } from \"../autonomy/bounded-completion.ts\";\nimport type { EvidenceRef, Finding, GateOutcome, WorkerRequest, WorkerResult } from \"../autonomy/contracts.ts\";\nimport type { LaneTerminalStatus } from \"../autonomy/lane-tracker.ts\";\nimport { createEvidenceBundle } from \"../research/evidence-bundle.ts\";\nimport { type AppliedActionsReport, parseWorkerActions, type WorkerAction } from \"./worker-actions.ts\";\nimport { validateWorkerResult } from \"./worker-result.ts\";\n\n/**\n * Pure orchestration for one bounded scout-worker delegation: bounded isolated completion ->\n * parse -> `WorkerResult` -> parent validation via {@link validateWorkerResult}.\n *\n * Slice scope: scout (read-only) workers only — the completion receives text prompts, no tools, so\n * `changedFiles` is always empty. Code-writing workers stay out until a real execution envelope\n * enforces path scope at tool level. Worker output is untrusted until the parent verifies it.\n */\n\n/** Static across calls so callers can use `cacheRetention: \"short\"`. */\nexport const WORKER_LANE_SYSTEM_PROMPT = [\n\t\"You are a bounded read-only scout worker delegated one task by a coding agent.\",\n\t\"You cannot run tools or change files; produce your best analysis of the delegated task.\",\n\t\"Respond with STRICT JSON only - no prose, no markdown fences:\",\n\t'{\"summary\":\"<what you concluded>\",\"status\":\"completed\"|\"blocked\",\"blockers\":[\"<why you are stuck>\"],\"findings\":[{\"summary\":\"<one concrete finding>\",\"confidence\":<0..1>}]}',\n\t'Use status \"blocked\" with blockers only when the task cannot be answered from the provided context.',\n\t\"Never invent file paths, APIs, or facts.\",\n].join(\"\\n\");\n\n/** Write-capable variant (G2): same contract plus a structured actions array — the model never\n * touches the filesystem; the runner applies actions through the envelope's path scope. */\nexport const WORKER_WRITE_LANE_SYSTEM_PROMPT = [\n\t\"You are a bounded code-writing worker delegated one task by a coding agent.\",\n\t\"You cannot run tools; you CHANGE FILES only by listing actions the runner applies for you.\",\n\t\"Respond with STRICT JSON only - no prose, no markdown fences:\",\n\t'{\"summary\":\"<what you did>\",\"status\":\"completed\"|\"blocked\",\"blockers\":[],\"findings\":[{\"summary\":\"<finding>\",\"confidence\":<0..1>}],\"actions\":[{\"op\":\"write\",\"path\":\"<relative path>\",\"content\":\"<full file content>\"},{\"op\":\"edit\",\"path\":\"<relative path>\",\"old\":\"<exact text>\",\"new\":\"<replacement>\"}]}',\n\t\"Only touch paths inside your delegated scope. Keep edits minimal and exact.\",\n\t'Use status \"blocked\" with blockers when the task cannot be done from the provided context.',\n\t\"Never invent file paths, APIs, or facts.\",\n].join(\"\\n\");\n\nexport interface WorkerCompletion {\n\ttext: string;\n\tcostUsd: number;\n\tstopReason: string;\n}\n\nexport interface WorkerRunnerOptions {\n\trequest: WorkerRequest;\n\t/** Budget for this delegation; a post-hoc breach marks the lane budget_exhausted. */\n\tmaxUsd: number;\n\t/** Wall-clock budget in milliseconds; 0 disables. */\n\tmaxWallClockMs: number;\n\t/**\n\t * Pre-allocated spawned-usage report id. Always stamped on the result so parent validation can\n\t * enforce the cost-visibility invariant (a completed result without a usage report is blocked).\n\t */\n\tusageReportId: string;\n\tcomplete: (args: { systemPrompt: string; userPrompt: string; signal?: AbortSignal }) => Promise<WorkerCompletion>;\n\tsignal?: AbortSignal;\n\tnow?: () => string;\n\t/** Enables the WRITE lane: only honored when the request envelope grants \"write_files\". The\n\t * runner applies the worker's structured actions through the envelope path scope; refusals\n\t * and failures become blockers, never silent drops. */\n\tapplyActions?: (actions: readonly WorkerAction[]) => AppliedActionsReport;\n}\n\nexport interface WorkerRunOutcome {\n\tresult: WorkerResult;\n\t/** Parent-review verdict from {@link validateWorkerResult}; worker output stays untrusted. */\n\tacceptance: GateOutcome;\n\taccepted: boolean;\n\tlaneStatus: LaneTerminalStatus;\n\treasonCode: string;\n\tcostUsd: number;\n}\n\nexport function buildWorkerUserPrompt(request: WorkerRequest): string {\n\treturn `Delegated task: ${request.instructions}`;\n}\n\nexport interface ParsedWorkerOutput {\n\tsummary: string;\n\tstatus: \"completed\" | \"blocked\";\n\tblockers: string[];\n\tfindings: Array<{ summary: string; confidence?: number }>;\n\tactions: WorkerAction[];\n}\n\nexport function parseWorkerOutput(text: string): ParsedWorkerOutput | undefined {\n\tconst trimmed = text.trim();\n\tconst candidates: string[] = [trimmed];\n\tconst fenced = /```(?:json)?\\s*([\\s\\S]*?)```/.exec(trimmed);\n\tif (fenced?.[1]) candidates.push(fenced[1].trim());\n\tconst start = trimmed.indexOf(\"{\");\n\tconst end = trimmed.lastIndexOf(\"}\");\n\tif (start >= 0 && end > start) candidates.push(trimmed.slice(start, end + 1));\n\n\tfor (const candidate of candidates) {\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(candidate);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) continue;\n\t\tconst record = parsed as Record<string, unknown>;\n\t\tconst summary = record.summary;\n\t\tif (typeof summary !== \"string\" || summary.trim().length === 0) continue;\n\n\t\tconst status = record.status === \"blocked\" ? \"blocked\" : \"completed\";\n\t\tconst blockers = Array.isArray(record.blockers)\n\t\t\t? record.blockers.filter((blocker): blocker is string => typeof blocker === \"string\" && blocker.length > 0)\n\t\t\t: [];\n\t\tconst findings: Array<{ summary: string; confidence?: number }> = [];\n\t\tif (Array.isArray(record.findings)) {\n\t\t\tfor (const item of record.findings) {\n\t\t\t\tif (!item || typeof item !== \"object\" || Array.isArray(item)) continue;\n\t\t\t\tconst findingSummary = (item as { summary?: unknown }).summary;\n\t\t\t\tif (typeof findingSummary !== \"string\" || findingSummary.trim().length === 0) continue;\n\t\t\t\tconst confidenceRaw = (item as { confidence?: unknown }).confidence;\n\t\t\t\tconst confidence =\n\t\t\t\t\ttypeof confidenceRaw === \"number\" && Number.isFinite(confidenceRaw)\n\t\t\t\t\t\t? Math.min(Math.max(confidenceRaw, 0), 1)\n\t\t\t\t\t\t: undefined;\n\t\t\t\tfindings.push({ summary: findingSummary.trim(), confidence });\n\t\t\t}\n\t\t}\n\t\treturn { summary: summary.trim(), status, blockers, findings, actions: parseWorkerActions(record.actions) };\n\t}\n\treturn undefined;\n}\n\nfunction buildWorkerEvidence(request: WorkerRequest, findings: ParsedWorkerOutput[\"findings\"]) {\n\tif (findings.length === 0) return undefined;\n\tconst instructionsRef: EvidenceRef = {\n\t\tid: \"src-instructions\",\n\t\tkind: \"user\",\n\t\ttitle: \"Delegated task instructions\",\n\t\ttrusted: true,\n\t\texcerpt: request.instructions.slice(0, 2000),\n\t};\n\tconst synthesisRef: EvidenceRef = {\n\t\tid: \"src-worker\",\n\t\tkind: \"tool\",\n\t\ttitle: \"Scout-worker synthesis\",\n\t\ttrusted: false,\n\t};\n\tconst bundleFindings: Finding[] = findings.map((finding, index) => ({\n\t\tid: `finding-${index + 1}`,\n\t\tsummary: finding.summary,\n\t\tevidenceIds: [synthesisRef.id],\n\t\t...(finding.confidence !== undefined ? { confidence: finding.confidence } : {}),\n\t}));\n\treturn createEvidenceBundle({\n\t\tquery: `worker:${request.id}`,\n\t\tsources: [instructionsRef, synthesisRef],\n\t\tfindings: bundleFindings,\n\t});\n}\n\nfunction finishOutcome(args: {\n\trequest: WorkerRequest;\n\tresult: WorkerResult;\n\tlaneStatus: LaneTerminalStatus;\n\treasonCode: string;\n\tcostUsd: number;\n}): WorkerRunOutcome {\n\tconst acceptance = validateWorkerResult({ request: args.request, result: args.result });\n\treturn {\n\t\tresult: args.result,\n\t\tacceptance,\n\t\taccepted: acceptance.outcome === \"allow\",\n\t\tlaneStatus: args.laneStatus,\n\t\treasonCode: args.reasonCode,\n\t\tcostUsd: args.costUsd,\n\t};\n}\n\nexport async function runWorker(options: WorkerRunnerOptions): Promise<WorkerRunOutcome> {\n\tconst now = options.now ?? (() => new Date().toISOString());\n\tconst baseResult = {\n\t\trequestId: options.request.id,\n\t\tchangedFiles: [] as string[],\n\t\tusageReportId: options.usageReportId,\n\t\tcreatedAt: now(),\n\t};\n\n\t// The WRITE lane requires BOTH the envelope grant and a caller-supplied applier — either\n\t// alone keeps the read-only scout contract byte-for-byte.\n\tconst writeCapable =\n\t\toptions.request.envelope.capabilities.includes(\"write_files\") && options.applyActions !== undefined;\n\n\tconst bounded = await runBoundedCompletion({\n\t\tmaxWallClockMs: options.maxWallClockMs,\n\t\tsignal: options.signal,\n\t\texecute: (signal) =>\n\t\t\toptions.complete({\n\t\t\t\tsystemPrompt: writeCapable ? WORKER_WRITE_LANE_SYSTEM_PROMPT : WORKER_LANE_SYSTEM_PROMPT,\n\t\t\t\tuserPrompt: buildWorkerUserPrompt(options.request),\n\t\t\t\tsignal,\n\t\t\t}),\n\t});\n\tconst costUsd = bounded.completion?.costUsd ?? 0;\n\n\tif (bounded.failure) {\n\t\tconst cancelled = bounded.failure.status === \"canceled\" || bounded.failure.status === \"timeout\";\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tresult: {\n\t\t\t\t...baseResult,\n\t\t\t\tstatus: cancelled ? \"cancelled\" : \"failed\",\n\t\t\t\tsummary: `Worker did not complete: ${bounded.failure.reasonCode}`,\n\t\t\t},\n\t\t\tlaneStatus: bounded.failure.status,\n\t\t\treasonCode: bounded.failure.reasonCode,\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst completion = bounded.completion;\n\tif (!completion || completion.stopReason === \"error\" || completion.stopReason === \"aborted\") {\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tresult: { ...baseResult, status: \"failed\", summary: \"Worker model call failed.\" },\n\t\t\tlaneStatus: \"failed\",\n\t\t\treasonCode: \"model_error\",\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst parsed = parseWorkerOutput(completion.text);\n\tif (!parsed) {\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tresult: { ...baseResult, status: \"failed\", summary: \"Worker output was not valid structured JSON.\" },\n\t\t\tlaneStatus: \"failed\",\n\t\t\treasonCode: \"unparseable_output\",\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst evidence = buildWorkerEvidence(options.request, parsed.findings);\n\tlet changedFiles: string[] = [];\n\tconst actionBlockers: string[] = [];\n\tif (writeCapable && parsed.status !== \"blocked\" && parsed.actions.length > 0 && options.applyActions) {\n\t\t// Runner-side application through the envelope path scope: refusals and failures are\n\t\t// surfaced as blockers so a partially-applied change can never look like clean success.\n\t\tconst applied = options.applyActions(parsed.actions);\n\t\tchangedFiles = applied.changedFiles;\n\t\tfor (const refusal of applied.refused) {\n\t\t\tactionBlockers.push(`action refused (${refusal.path}): ${refusal.reason}`);\n\t\t}\n\t\tfor (const failure of applied.failed) {\n\t\t\tactionBlockers.push(`action failed (${failure.path}): ${failure.reason}`);\n\t\t}\n\t} else if (!writeCapable && parsed.actions.length > 0) {\n\t\tactionBlockers.push(\"worker emitted file actions without a write_files envelope grant; nothing was applied\");\n\t}\n\tconst allBlockers = [...parsed.blockers, ...actionBlockers];\n\tconst result: WorkerResult = {\n\t\t...baseResult,\n\t\tchangedFiles,\n\t\tstatus: parsed.status === \"blocked\" || allBlockers.length > 0 ? \"blocked\" : \"completed\",\n\t\tsummary: parsed.summary,\n\t\t...(allBlockers.length > 0 ? { blockers: allBlockers } : {}),\n\t\t...(evidence ? { evidence } : {}),\n\t};\n\n\tif (result.status === \"blocked\") {\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tresult,\n\t\t\tlaneStatus: \"failed\",\n\t\t\treasonCode: \"worker_blocked\",\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst overBudget = options.maxUsd > 0 && costUsd > options.maxUsd;\n\treturn finishOutcome({\n\t\trequest: options.request,\n\t\tresult,\n\t\tlaneStatus: overBudget ? \"budget_exhausted\" : \"succeeded\",\n\t\treasonCode: overBudget ? \"cost_budget_exceeded\" : \"worker_completed\",\n\t\tcostUsd,\n\t});\n}\n"]}
1
+ {"version":3,"file":"worker-runner.d.ts","sourceRoot":"","sources":["../../../src/core/delegation/worker-runner.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAwB,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC/G,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAEtE,OAAO,EAAE,KAAK,oBAAoB,EAAsB,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGvG;;;;;;;GAOG;AAEH,wEAAwE;AACxE,eAAO,MAAM,yBAAyB,QAO1B,CAAC;AAEb;2FAC2F;AAC3F,eAAO,MAAM,+BAA+B,QAQhC,CAAC;AAEb,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IACnC,OAAO,EAAE,aAAa,CAAC;IACvB,qFAAqF;IACrF,MAAM,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,cAAc,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,CAAC,IAAI,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAClH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB;;2DAEuD;IACvD,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,KAAK,oBAAoB,CAAC;IAC1E;gDAC4C;IAC5C,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAChC,MAAM,EAAE,YAAY,CAAC;IACrB,8FAA8F;IAC9F,UAAU,EAAE,WAAW,CAAC;IACxB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,kBAAkB,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAEpE;AAED,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,WAAW,GAAG,SAAS,CAAC;IAChC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1D,OAAO,EAAE,YAAY,EAAE,CAAC;CACxB;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CA0C9E;AAiDD,wBAAsB,SAAS,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAiHvF","sourcesContent":["import { runBoundedCompletion } from \"../autonomy/bounded-completion.ts\";\nimport type { EvidenceRef, Finding, GateOutcome, WorkerRequest, WorkerResult } from \"../autonomy/contracts.ts\";\nimport type { LaneTerminalStatus } from \"../autonomy/lane-tracker.ts\";\nimport { createEvidenceBundle } from \"../research/evidence-bundle.ts\";\nimport { type AppliedActionsReport, parseWorkerActions, type WorkerAction } from \"./worker-actions.ts\";\nimport { validateWorkerResult } from \"./worker-result.ts\";\n\n/**\n * Pure orchestration for one bounded scout-worker delegation: bounded isolated completion ->\n * parse -> `WorkerResult` -> parent validation via {@link validateWorkerResult}.\n *\n * Slice scope: scout (read-only) workers only — the completion receives text prompts, no tools, so\n * `changedFiles` is always empty. Code-writing workers stay out until a real execution envelope\n * enforces path scope at tool level. Worker output is untrusted until the parent verifies it.\n */\n\n/** Static across calls so callers can use `cacheRetention: \"short\"`. */\nexport const WORKER_LANE_SYSTEM_PROMPT = [\n\t\"You are a bounded read-only scout worker delegated one task by a coding agent.\",\n\t\"You cannot run tools or change files; produce your best analysis of the delegated task.\",\n\t\"Respond with STRICT JSON only - no prose, no markdown fences:\",\n\t'{\"summary\":\"<what you concluded>\",\"status\":\"completed\"|\"blocked\",\"blockers\":[\"<why you are stuck>\"],\"findings\":[{\"summary\":\"<one concrete finding>\",\"confidence\":<0..1>}]}',\n\t'Use status \"blocked\" with blockers only when the task cannot be answered from the provided context.',\n\t\"Never invent file paths, APIs, or facts.\",\n].join(\"\\n\");\n\n/** Write-capable variant (G2): same contract plus a structured actions array — the model never\n * touches the filesystem; the runner applies actions through the envelope's path scope. */\nexport const WORKER_WRITE_LANE_SYSTEM_PROMPT = [\n\t\"You are a bounded code-writing worker delegated one task by a coding agent.\",\n\t\"You cannot run tools; you CHANGE FILES only by listing actions the runner applies for you.\",\n\t\"Respond with STRICT JSON only - no prose, no markdown fences:\",\n\t'{\"summary\":\"<what you did>\",\"status\":\"completed\"|\"blocked\",\"blockers\":[],\"findings\":[{\"summary\":\"<finding>\",\"confidence\":<0..1>}],\"actions\":[{\"op\":\"write\",\"path\":\"<relative path>\",\"content\":\"<full file content>\"},{\"op\":\"edit\",\"path\":\"<relative path>\",\"old\":\"<exact text>\",\"new\":\"<replacement>\"}]}',\n\t\"Only touch paths inside your delegated scope. Keep edits minimal and exact.\",\n\t'Use status \"blocked\" with blockers when the task cannot be done from the provided context.',\n\t\"Never invent file paths, APIs, or facts.\",\n].join(\"\\n\");\n\nexport interface WorkerCompletion {\n\ttext: string;\n\tcostUsd: number;\n\tstopReason: string;\n}\n\nexport interface WorkerRunnerOptions {\n\trequest: WorkerRequest;\n\t/** Budget for this delegation; a post-hoc breach marks the lane budget_exhausted. */\n\tmaxUsd: number;\n\t/** Wall-clock budget in milliseconds; 0 disables. */\n\tmaxWallClockMs: number;\n\t/**\n\t * Pre-allocated spawned-usage report id. Always stamped on the result so parent validation can\n\t * enforce the cost-visibility invariant (a completed result without a usage report is blocked).\n\t */\n\tusageReportId: string;\n\tcomplete: (args: { systemPrompt: string; userPrompt: string; signal?: AbortSignal }) => Promise<WorkerCompletion>;\n\tsignal?: AbortSignal;\n\tnow?: () => string;\n\t/** Enables the WRITE lane: only honored when the request envelope grants \"write_files\". The\n\t * runner applies the worker's structured actions through the envelope path scope; refusals\n\t * and failures become blockers, never silent drops. */\n\tapplyActions?: (actions: readonly WorkerAction[]) => AppliedActionsReport;\n\t/** Session cwd — the baseline for relative changed-file and envelope paths in parent\n\t * validation. Defaults to process.cwd(). */\n\tcwd?: string;\n}\n\nexport interface WorkerRunOutcome {\n\tresult: WorkerResult;\n\t/** Parent-review verdict from {@link validateWorkerResult}; worker output stays untrusted. */\n\tacceptance: GateOutcome;\n\taccepted: boolean;\n\tlaneStatus: LaneTerminalStatus;\n\treasonCode: string;\n\tcostUsd: number;\n}\n\nexport function buildWorkerUserPrompt(request: WorkerRequest): string {\n\treturn `Delegated task: ${request.instructions}`;\n}\n\nexport interface ParsedWorkerOutput {\n\tsummary: string;\n\tstatus: \"completed\" | \"blocked\";\n\tblockers: string[];\n\tfindings: Array<{ summary: string; confidence?: number }>;\n\tactions: WorkerAction[];\n}\n\nexport function parseWorkerOutput(text: string): ParsedWorkerOutput | undefined {\n\tconst trimmed = text.trim();\n\tconst candidates: string[] = [trimmed];\n\tconst fenced = /```(?:json)?\\s*([\\s\\S]*?)```/.exec(trimmed);\n\tif (fenced?.[1]) candidates.push(fenced[1].trim());\n\tconst start = trimmed.indexOf(\"{\");\n\tconst end = trimmed.lastIndexOf(\"}\");\n\tif (start >= 0 && end > start) candidates.push(trimmed.slice(start, end + 1));\n\n\tfor (const candidate of candidates) {\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(candidate);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) continue;\n\t\tconst record = parsed as Record<string, unknown>;\n\t\tconst summary = record.summary;\n\t\tif (typeof summary !== \"string\" || summary.trim().length === 0) continue;\n\n\t\tconst status = record.status === \"blocked\" ? \"blocked\" : \"completed\";\n\t\tconst blockers = Array.isArray(record.blockers)\n\t\t\t? record.blockers.filter((blocker): blocker is string => typeof blocker === \"string\" && blocker.length > 0)\n\t\t\t: [];\n\t\tconst findings: Array<{ summary: string; confidence?: number }> = [];\n\t\tif (Array.isArray(record.findings)) {\n\t\t\tfor (const item of record.findings) {\n\t\t\t\tif (!item || typeof item !== \"object\" || Array.isArray(item)) continue;\n\t\t\t\tconst findingSummary = (item as { summary?: unknown }).summary;\n\t\t\t\tif (typeof findingSummary !== \"string\" || findingSummary.trim().length === 0) continue;\n\t\t\t\tconst confidenceRaw = (item as { confidence?: unknown }).confidence;\n\t\t\t\tconst confidence =\n\t\t\t\t\ttypeof confidenceRaw === \"number\" && Number.isFinite(confidenceRaw)\n\t\t\t\t\t\t? Math.min(Math.max(confidenceRaw, 0), 1)\n\t\t\t\t\t\t: undefined;\n\t\t\t\tfindings.push({ summary: findingSummary.trim(), confidence });\n\t\t\t}\n\t\t}\n\t\treturn { summary: summary.trim(), status, blockers, findings, actions: parseWorkerActions(record.actions) };\n\t}\n\treturn undefined;\n}\n\nfunction buildWorkerEvidence(request: WorkerRequest, findings: ParsedWorkerOutput[\"findings\"]) {\n\tif (findings.length === 0) return undefined;\n\tconst instructionsRef: EvidenceRef = {\n\t\tid: \"src-instructions\",\n\t\tkind: \"user\",\n\t\ttitle: \"Delegated task instructions\",\n\t\ttrusted: true,\n\t\texcerpt: request.instructions.slice(0, 2000),\n\t};\n\tconst synthesisRef: EvidenceRef = {\n\t\tid: \"src-worker\",\n\t\tkind: \"tool\",\n\t\ttitle: \"Scout-worker synthesis\",\n\t\ttrusted: false,\n\t};\n\tconst bundleFindings: Finding[] = findings.map((finding, index) => ({\n\t\tid: `finding-${index + 1}`,\n\t\tsummary: finding.summary,\n\t\tevidenceIds: [synthesisRef.id],\n\t\t...(finding.confidence !== undefined ? { confidence: finding.confidence } : {}),\n\t}));\n\treturn createEvidenceBundle({\n\t\tquery: `worker:${request.id}`,\n\t\tsources: [instructionsRef, synthesisRef],\n\t\tfindings: bundleFindings,\n\t});\n}\n\nfunction finishOutcome(args: {\n\trequest: WorkerRequest;\n\tresult: WorkerResult;\n\tlaneStatus: LaneTerminalStatus;\n\treasonCode: string;\n\tcostUsd: number;\n\tcwd?: string;\n}): WorkerRunOutcome {\n\tconst acceptance = validateWorkerResult({ request: args.request, result: args.result, cwd: args.cwd });\n\treturn {\n\t\tresult: args.result,\n\t\tacceptance,\n\t\taccepted: acceptance.outcome === \"allow\",\n\t\tlaneStatus: args.laneStatus,\n\t\treasonCode: args.reasonCode,\n\t\tcostUsd: args.costUsd,\n\t};\n}\n\nexport async function runWorker(options: WorkerRunnerOptions): Promise<WorkerRunOutcome> {\n\tconst now = options.now ?? (() => new Date().toISOString());\n\tconst baseResult = {\n\t\trequestId: options.request.id,\n\t\tchangedFiles: [] as string[],\n\t\tusageReportId: options.usageReportId,\n\t\tcreatedAt: now(),\n\t};\n\n\t// The WRITE lane requires BOTH the envelope grant and a caller-supplied applier — either\n\t// alone keeps the read-only scout contract byte-for-byte.\n\tconst writeCapable =\n\t\toptions.request.envelope.capabilities.includes(\"write_files\") && options.applyActions !== undefined;\n\n\tconst bounded = await runBoundedCompletion({\n\t\tmaxWallClockMs: options.maxWallClockMs,\n\t\tsignal: options.signal,\n\t\texecute: (signal) =>\n\t\t\toptions.complete({\n\t\t\t\tsystemPrompt: writeCapable ? WORKER_WRITE_LANE_SYSTEM_PROMPT : WORKER_LANE_SYSTEM_PROMPT,\n\t\t\t\tuserPrompt: buildWorkerUserPrompt(options.request),\n\t\t\t\tsignal,\n\t\t\t}),\n\t});\n\tconst costUsd = bounded.completion?.costUsd ?? 0;\n\n\tif (bounded.failure) {\n\t\tconst cancelled = bounded.failure.status === \"canceled\" || bounded.failure.status === \"timeout\";\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tcwd: options.cwd,\n\t\t\tresult: {\n\t\t\t\t...baseResult,\n\t\t\t\tstatus: cancelled ? \"cancelled\" : \"failed\",\n\t\t\t\tsummary: `Worker did not complete: ${bounded.failure.reasonCode}`,\n\t\t\t},\n\t\t\tlaneStatus: bounded.failure.status,\n\t\t\treasonCode: bounded.failure.reasonCode,\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst completion = bounded.completion;\n\tif (!completion || completion.stopReason === \"error\" || completion.stopReason === \"aborted\") {\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tcwd: options.cwd,\n\t\t\tresult: { ...baseResult, status: \"failed\", summary: \"Worker model call failed.\" },\n\t\t\tlaneStatus: \"failed\",\n\t\t\treasonCode: \"model_error\",\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst parsed = parseWorkerOutput(completion.text);\n\tif (!parsed) {\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tcwd: options.cwd,\n\t\t\tresult: { ...baseResult, status: \"failed\", summary: \"Worker output was not valid structured JSON.\" },\n\t\t\tlaneStatus: \"failed\",\n\t\t\treasonCode: \"unparseable_output\",\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst evidence = buildWorkerEvidence(options.request, parsed.findings);\n\tlet changedFiles: string[] = [];\n\tconst actionBlockers: string[] = [];\n\tif (writeCapable && parsed.status !== \"blocked\" && parsed.actions.length > 0 && options.applyActions) {\n\t\t// Runner-side application through the envelope path scope: refusals and failures are\n\t\t// surfaced as blockers so a partially-applied change can never look like clean success.\n\t\tconst applied = options.applyActions(parsed.actions);\n\t\tchangedFiles = applied.changedFiles;\n\t\tfor (const refusal of applied.refused) {\n\t\t\tactionBlockers.push(`action refused (${refusal.path}): ${refusal.reason}`);\n\t\t}\n\t\tfor (const failure of applied.failed) {\n\t\t\tactionBlockers.push(`action failed (${failure.path}): ${failure.reason}`);\n\t\t}\n\t} else if (!writeCapable && parsed.actions.length > 0) {\n\t\tactionBlockers.push(\"worker emitted file actions without a write_files envelope grant; nothing was applied\");\n\t}\n\tconst allBlockers = [...parsed.blockers, ...actionBlockers];\n\tconst result: WorkerResult = {\n\t\t...baseResult,\n\t\tchangedFiles,\n\t\tstatus: parsed.status === \"blocked\" || allBlockers.length > 0 ? \"blocked\" : \"completed\",\n\t\tsummary: parsed.summary,\n\t\t...(allBlockers.length > 0 ? { blockers: allBlockers } : {}),\n\t\t...(evidence ? { evidence } : {}),\n\t};\n\n\tif (result.status === \"blocked\") {\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tcwd: options.cwd,\n\t\t\tresult,\n\t\t\tlaneStatus: \"failed\",\n\t\t\treasonCode: \"worker_blocked\",\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst overBudget = options.maxUsd > 0 && costUsd > options.maxUsd;\n\treturn finishOutcome({\n\t\trequest: options.request,\n\t\tcwd: options.cwd,\n\t\tresult,\n\t\tlaneStatus: overBudget ? \"budget_exhausted\" : \"succeeded\",\n\t\treasonCode: overBudget ? \"cost_budget_exceeded\" : \"worker_completed\",\n\t\tcostUsd,\n\t});\n}\n"]}
@@ -109,7 +109,7 @@ function buildWorkerEvidence(request, findings) {
109
109
  });
110
110
  }
111
111
  function finishOutcome(args) {
112
- const acceptance = validateWorkerResult({ request: args.request, result: args.result });
112
+ const acceptance = validateWorkerResult({ request: args.request, result: args.result, cwd: args.cwd });
113
113
  return {
114
114
  result: args.result,
115
115
  acceptance,
@@ -144,6 +144,7 @@ export async function runWorker(options) {
144
144
  const cancelled = bounded.failure.status === "canceled" || bounded.failure.status === "timeout";
145
145
  return finishOutcome({
146
146
  request: options.request,
147
+ cwd: options.cwd,
147
148
  result: {
148
149
  ...baseResult,
149
150
  status: cancelled ? "cancelled" : "failed",
@@ -158,6 +159,7 @@ export async function runWorker(options) {
158
159
  if (!completion || completion.stopReason === "error" || completion.stopReason === "aborted") {
159
160
  return finishOutcome({
160
161
  request: options.request,
162
+ cwd: options.cwd,
161
163
  result: { ...baseResult, status: "failed", summary: "Worker model call failed." },
162
164
  laneStatus: "failed",
163
165
  reasonCode: "model_error",
@@ -168,6 +170,7 @@ export async function runWorker(options) {
168
170
  if (!parsed) {
169
171
  return finishOutcome({
170
172
  request: options.request,
173
+ cwd: options.cwd,
171
174
  result: { ...baseResult, status: "failed", summary: "Worker output was not valid structured JSON." },
172
175
  laneStatus: "failed",
173
176
  reasonCode: "unparseable_output",
@@ -204,6 +207,7 @@ export async function runWorker(options) {
204
207
  if (result.status === "blocked") {
205
208
  return finishOutcome({
206
209
  request: options.request,
210
+ cwd: options.cwd,
207
211
  result,
208
212
  laneStatus: "failed",
209
213
  reasonCode: "worker_blocked",
@@ -213,6 +217,7 @@ export async function runWorker(options) {
213
217
  const overBudget = options.maxUsd > 0 && costUsd > options.maxUsd;
214
218
  return finishOutcome({
215
219
  request: options.request,
220
+ cwd: options.cwd,
216
221
  result,
217
222
  laneStatus: overBudget ? "budget_exhausted" : "succeeded",
218
223
  reasonCode: overBudget ? "cost_budget_exceeded" : "worker_completed",
@@ -1 +1 @@
1
- {"version":3,"file":"worker-runner.js","sourceRoot":"","sources":["../../../src/core/delegation/worker-runner.ts"],"names":[],"mappings":"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","sourcesContent":["import { runBoundedCompletion } from \"../autonomy/bounded-completion.ts\";\nimport type { EvidenceRef, Finding, GateOutcome, WorkerRequest, WorkerResult } from \"../autonomy/contracts.ts\";\nimport type { LaneTerminalStatus } from \"../autonomy/lane-tracker.ts\";\nimport { createEvidenceBundle } from \"../research/evidence-bundle.ts\";\nimport { type AppliedActionsReport, parseWorkerActions, type WorkerAction } from \"./worker-actions.ts\";\nimport { validateWorkerResult } from \"./worker-result.ts\";\n\n/**\n * Pure orchestration for one bounded scout-worker delegation: bounded isolated completion ->\n * parse -> `WorkerResult` -> parent validation via {@link validateWorkerResult}.\n *\n * Slice scope: scout (read-only) workers only — the completion receives text prompts, no tools, so\n * `changedFiles` is always empty. Code-writing workers stay out until a real execution envelope\n * enforces path scope at tool level. Worker output is untrusted until the parent verifies it.\n */\n\n/** Static across calls so callers can use `cacheRetention: \"short\"`. */\nexport const WORKER_LANE_SYSTEM_PROMPT = [\n\t\"You are a bounded read-only scout worker delegated one task by a coding agent.\",\n\t\"You cannot run tools or change files; produce your best analysis of the delegated task.\",\n\t\"Respond with STRICT JSON only - no prose, no markdown fences:\",\n\t'{\"summary\":\"<what you concluded>\",\"status\":\"completed\"|\"blocked\",\"blockers\":[\"<why you are stuck>\"],\"findings\":[{\"summary\":\"<one concrete finding>\",\"confidence\":<0..1>}]}',\n\t'Use status \"blocked\" with blockers only when the task cannot be answered from the provided context.',\n\t\"Never invent file paths, APIs, or facts.\",\n].join(\"\\n\");\n\n/** Write-capable variant (G2): same contract plus a structured actions array — the model never\n * touches the filesystem; the runner applies actions through the envelope's path scope. */\nexport const WORKER_WRITE_LANE_SYSTEM_PROMPT = [\n\t\"You are a bounded code-writing worker delegated one task by a coding agent.\",\n\t\"You cannot run tools; you CHANGE FILES only by listing actions the runner applies for you.\",\n\t\"Respond with STRICT JSON only - no prose, no markdown fences:\",\n\t'{\"summary\":\"<what you did>\",\"status\":\"completed\"|\"blocked\",\"blockers\":[],\"findings\":[{\"summary\":\"<finding>\",\"confidence\":<0..1>}],\"actions\":[{\"op\":\"write\",\"path\":\"<relative path>\",\"content\":\"<full file content>\"},{\"op\":\"edit\",\"path\":\"<relative path>\",\"old\":\"<exact text>\",\"new\":\"<replacement>\"}]}',\n\t\"Only touch paths inside your delegated scope. Keep edits minimal and exact.\",\n\t'Use status \"blocked\" with blockers when the task cannot be done from the provided context.',\n\t\"Never invent file paths, APIs, or facts.\",\n].join(\"\\n\");\n\nexport interface WorkerCompletion {\n\ttext: string;\n\tcostUsd: number;\n\tstopReason: string;\n}\n\nexport interface WorkerRunnerOptions {\n\trequest: WorkerRequest;\n\t/** Budget for this delegation; a post-hoc breach marks the lane budget_exhausted. */\n\tmaxUsd: number;\n\t/** Wall-clock budget in milliseconds; 0 disables. */\n\tmaxWallClockMs: number;\n\t/**\n\t * Pre-allocated spawned-usage report id. Always stamped on the result so parent validation can\n\t * enforce the cost-visibility invariant (a completed result without a usage report is blocked).\n\t */\n\tusageReportId: string;\n\tcomplete: (args: { systemPrompt: string; userPrompt: string; signal?: AbortSignal }) => Promise<WorkerCompletion>;\n\tsignal?: AbortSignal;\n\tnow?: () => string;\n\t/** Enables the WRITE lane: only honored when the request envelope grants \"write_files\". The\n\t * runner applies the worker's structured actions through the envelope path scope; refusals\n\t * and failures become blockers, never silent drops. */\n\tapplyActions?: (actions: readonly WorkerAction[]) => AppliedActionsReport;\n}\n\nexport interface WorkerRunOutcome {\n\tresult: WorkerResult;\n\t/** Parent-review verdict from {@link validateWorkerResult}; worker output stays untrusted. */\n\tacceptance: GateOutcome;\n\taccepted: boolean;\n\tlaneStatus: LaneTerminalStatus;\n\treasonCode: string;\n\tcostUsd: number;\n}\n\nexport function buildWorkerUserPrompt(request: WorkerRequest): string {\n\treturn `Delegated task: ${request.instructions}`;\n}\n\nexport interface ParsedWorkerOutput {\n\tsummary: string;\n\tstatus: \"completed\" | \"blocked\";\n\tblockers: string[];\n\tfindings: Array<{ summary: string; confidence?: number }>;\n\tactions: WorkerAction[];\n}\n\nexport function parseWorkerOutput(text: string): ParsedWorkerOutput | undefined {\n\tconst trimmed = text.trim();\n\tconst candidates: string[] = [trimmed];\n\tconst fenced = /```(?:json)?\\s*([\\s\\S]*?)```/.exec(trimmed);\n\tif (fenced?.[1]) candidates.push(fenced[1].trim());\n\tconst start = trimmed.indexOf(\"{\");\n\tconst end = trimmed.lastIndexOf(\"}\");\n\tif (start >= 0 && end > start) candidates.push(trimmed.slice(start, end + 1));\n\n\tfor (const candidate of candidates) {\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(candidate);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) continue;\n\t\tconst record = parsed as Record<string, unknown>;\n\t\tconst summary = record.summary;\n\t\tif (typeof summary !== \"string\" || summary.trim().length === 0) continue;\n\n\t\tconst status = record.status === \"blocked\" ? \"blocked\" : \"completed\";\n\t\tconst blockers = Array.isArray(record.blockers)\n\t\t\t? record.blockers.filter((blocker): blocker is string => typeof blocker === \"string\" && blocker.length > 0)\n\t\t\t: [];\n\t\tconst findings: Array<{ summary: string; confidence?: number }> = [];\n\t\tif (Array.isArray(record.findings)) {\n\t\t\tfor (const item of record.findings) {\n\t\t\t\tif (!item || typeof item !== \"object\" || Array.isArray(item)) continue;\n\t\t\t\tconst findingSummary = (item as { summary?: unknown }).summary;\n\t\t\t\tif (typeof findingSummary !== \"string\" || findingSummary.trim().length === 0) continue;\n\t\t\t\tconst confidenceRaw = (item as { confidence?: unknown }).confidence;\n\t\t\t\tconst confidence =\n\t\t\t\t\ttypeof confidenceRaw === \"number\" && Number.isFinite(confidenceRaw)\n\t\t\t\t\t\t? Math.min(Math.max(confidenceRaw, 0), 1)\n\t\t\t\t\t\t: undefined;\n\t\t\t\tfindings.push({ summary: findingSummary.trim(), confidence });\n\t\t\t}\n\t\t}\n\t\treturn { summary: summary.trim(), status, blockers, findings, actions: parseWorkerActions(record.actions) };\n\t}\n\treturn undefined;\n}\n\nfunction buildWorkerEvidence(request: WorkerRequest, findings: ParsedWorkerOutput[\"findings\"]) {\n\tif (findings.length === 0) return undefined;\n\tconst instructionsRef: EvidenceRef = {\n\t\tid: \"src-instructions\",\n\t\tkind: \"user\",\n\t\ttitle: \"Delegated task instructions\",\n\t\ttrusted: true,\n\t\texcerpt: request.instructions.slice(0, 2000),\n\t};\n\tconst synthesisRef: EvidenceRef = {\n\t\tid: \"src-worker\",\n\t\tkind: \"tool\",\n\t\ttitle: \"Scout-worker synthesis\",\n\t\ttrusted: false,\n\t};\n\tconst bundleFindings: Finding[] = findings.map((finding, index) => ({\n\t\tid: `finding-${index + 1}`,\n\t\tsummary: finding.summary,\n\t\tevidenceIds: [synthesisRef.id],\n\t\t...(finding.confidence !== undefined ? { confidence: finding.confidence } : {}),\n\t}));\n\treturn createEvidenceBundle({\n\t\tquery: `worker:${request.id}`,\n\t\tsources: [instructionsRef, synthesisRef],\n\t\tfindings: bundleFindings,\n\t});\n}\n\nfunction finishOutcome(args: {\n\trequest: WorkerRequest;\n\tresult: WorkerResult;\n\tlaneStatus: LaneTerminalStatus;\n\treasonCode: string;\n\tcostUsd: number;\n}): WorkerRunOutcome {\n\tconst acceptance = validateWorkerResult({ request: args.request, result: args.result });\n\treturn {\n\t\tresult: args.result,\n\t\tacceptance,\n\t\taccepted: acceptance.outcome === \"allow\",\n\t\tlaneStatus: args.laneStatus,\n\t\treasonCode: args.reasonCode,\n\t\tcostUsd: args.costUsd,\n\t};\n}\n\nexport async function runWorker(options: WorkerRunnerOptions): Promise<WorkerRunOutcome> {\n\tconst now = options.now ?? (() => new Date().toISOString());\n\tconst baseResult = {\n\t\trequestId: options.request.id,\n\t\tchangedFiles: [] as string[],\n\t\tusageReportId: options.usageReportId,\n\t\tcreatedAt: now(),\n\t};\n\n\t// The WRITE lane requires BOTH the envelope grant and a caller-supplied applier — either\n\t// alone keeps the read-only scout contract byte-for-byte.\n\tconst writeCapable =\n\t\toptions.request.envelope.capabilities.includes(\"write_files\") && options.applyActions !== undefined;\n\n\tconst bounded = await runBoundedCompletion({\n\t\tmaxWallClockMs: options.maxWallClockMs,\n\t\tsignal: options.signal,\n\t\texecute: (signal) =>\n\t\t\toptions.complete({\n\t\t\t\tsystemPrompt: writeCapable ? WORKER_WRITE_LANE_SYSTEM_PROMPT : WORKER_LANE_SYSTEM_PROMPT,\n\t\t\t\tuserPrompt: buildWorkerUserPrompt(options.request),\n\t\t\t\tsignal,\n\t\t\t}),\n\t});\n\tconst costUsd = bounded.completion?.costUsd ?? 0;\n\n\tif (bounded.failure) {\n\t\tconst cancelled = bounded.failure.status === \"canceled\" || bounded.failure.status === \"timeout\";\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tresult: {\n\t\t\t\t...baseResult,\n\t\t\t\tstatus: cancelled ? \"cancelled\" : \"failed\",\n\t\t\t\tsummary: `Worker did not complete: ${bounded.failure.reasonCode}`,\n\t\t\t},\n\t\t\tlaneStatus: bounded.failure.status,\n\t\t\treasonCode: bounded.failure.reasonCode,\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst completion = bounded.completion;\n\tif (!completion || completion.stopReason === \"error\" || completion.stopReason === \"aborted\") {\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tresult: { ...baseResult, status: \"failed\", summary: \"Worker model call failed.\" },\n\t\t\tlaneStatus: \"failed\",\n\t\t\treasonCode: \"model_error\",\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst parsed = parseWorkerOutput(completion.text);\n\tif (!parsed) {\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tresult: { ...baseResult, status: \"failed\", summary: \"Worker output was not valid structured JSON.\" },\n\t\t\tlaneStatus: \"failed\",\n\t\t\treasonCode: \"unparseable_output\",\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst evidence = buildWorkerEvidence(options.request, parsed.findings);\n\tlet changedFiles: string[] = [];\n\tconst actionBlockers: string[] = [];\n\tif (writeCapable && parsed.status !== \"blocked\" && parsed.actions.length > 0 && options.applyActions) {\n\t\t// Runner-side application through the envelope path scope: refusals and failures are\n\t\t// surfaced as blockers so a partially-applied change can never look like clean success.\n\t\tconst applied = options.applyActions(parsed.actions);\n\t\tchangedFiles = applied.changedFiles;\n\t\tfor (const refusal of applied.refused) {\n\t\t\tactionBlockers.push(`action refused (${refusal.path}): ${refusal.reason}`);\n\t\t}\n\t\tfor (const failure of applied.failed) {\n\t\t\tactionBlockers.push(`action failed (${failure.path}): ${failure.reason}`);\n\t\t}\n\t} else if (!writeCapable && parsed.actions.length > 0) {\n\t\tactionBlockers.push(\"worker emitted file actions without a write_files envelope grant; nothing was applied\");\n\t}\n\tconst allBlockers = [...parsed.blockers, ...actionBlockers];\n\tconst result: WorkerResult = {\n\t\t...baseResult,\n\t\tchangedFiles,\n\t\tstatus: parsed.status === \"blocked\" || allBlockers.length > 0 ? \"blocked\" : \"completed\",\n\t\tsummary: parsed.summary,\n\t\t...(allBlockers.length > 0 ? { blockers: allBlockers } : {}),\n\t\t...(evidence ? { evidence } : {}),\n\t};\n\n\tif (result.status === \"blocked\") {\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tresult,\n\t\t\tlaneStatus: \"failed\",\n\t\t\treasonCode: \"worker_blocked\",\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst overBudget = options.maxUsd > 0 && costUsd > options.maxUsd;\n\treturn finishOutcome({\n\t\trequest: options.request,\n\t\tresult,\n\t\tlaneStatus: overBudget ? \"budget_exhausted\" : \"succeeded\",\n\t\treasonCode: overBudget ? \"cost_budget_exceeded\" : \"worker_completed\",\n\t\tcostUsd,\n\t});\n}\n"]}
1
+ {"version":3,"file":"worker-runner.js","sourceRoot":"","sources":["../../../src/core/delegation/worker-runner.ts"],"names":[],"mappings":"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","sourcesContent":["import { runBoundedCompletion } from \"../autonomy/bounded-completion.ts\";\nimport type { EvidenceRef, Finding, GateOutcome, WorkerRequest, WorkerResult } from \"../autonomy/contracts.ts\";\nimport type { LaneTerminalStatus } from \"../autonomy/lane-tracker.ts\";\nimport { createEvidenceBundle } from \"../research/evidence-bundle.ts\";\nimport { type AppliedActionsReport, parseWorkerActions, type WorkerAction } from \"./worker-actions.ts\";\nimport { validateWorkerResult } from \"./worker-result.ts\";\n\n/**\n * Pure orchestration for one bounded scout-worker delegation: bounded isolated completion ->\n * parse -> `WorkerResult` -> parent validation via {@link validateWorkerResult}.\n *\n * Slice scope: scout (read-only) workers only — the completion receives text prompts, no tools, so\n * `changedFiles` is always empty. Code-writing workers stay out until a real execution envelope\n * enforces path scope at tool level. Worker output is untrusted until the parent verifies it.\n */\n\n/** Static across calls so callers can use `cacheRetention: \"short\"`. */\nexport const WORKER_LANE_SYSTEM_PROMPT = [\n\t\"You are a bounded read-only scout worker delegated one task by a coding agent.\",\n\t\"You cannot run tools or change files; produce your best analysis of the delegated task.\",\n\t\"Respond with STRICT JSON only - no prose, no markdown fences:\",\n\t'{\"summary\":\"<what you concluded>\",\"status\":\"completed\"|\"blocked\",\"blockers\":[\"<why you are stuck>\"],\"findings\":[{\"summary\":\"<one concrete finding>\",\"confidence\":<0..1>}]}',\n\t'Use status \"blocked\" with blockers only when the task cannot be answered from the provided context.',\n\t\"Never invent file paths, APIs, or facts.\",\n].join(\"\\n\");\n\n/** Write-capable variant (G2): same contract plus a structured actions array — the model never\n * touches the filesystem; the runner applies actions through the envelope's path scope. */\nexport const WORKER_WRITE_LANE_SYSTEM_PROMPT = [\n\t\"You are a bounded code-writing worker delegated one task by a coding agent.\",\n\t\"You cannot run tools; you CHANGE FILES only by listing actions the runner applies for you.\",\n\t\"Respond with STRICT JSON only - no prose, no markdown fences:\",\n\t'{\"summary\":\"<what you did>\",\"status\":\"completed\"|\"blocked\",\"blockers\":[],\"findings\":[{\"summary\":\"<finding>\",\"confidence\":<0..1>}],\"actions\":[{\"op\":\"write\",\"path\":\"<relative path>\",\"content\":\"<full file content>\"},{\"op\":\"edit\",\"path\":\"<relative path>\",\"old\":\"<exact text>\",\"new\":\"<replacement>\"}]}',\n\t\"Only touch paths inside your delegated scope. Keep edits minimal and exact.\",\n\t'Use status \"blocked\" with blockers when the task cannot be done from the provided context.',\n\t\"Never invent file paths, APIs, or facts.\",\n].join(\"\\n\");\n\nexport interface WorkerCompletion {\n\ttext: string;\n\tcostUsd: number;\n\tstopReason: string;\n}\n\nexport interface WorkerRunnerOptions {\n\trequest: WorkerRequest;\n\t/** Budget for this delegation; a post-hoc breach marks the lane budget_exhausted. */\n\tmaxUsd: number;\n\t/** Wall-clock budget in milliseconds; 0 disables. */\n\tmaxWallClockMs: number;\n\t/**\n\t * Pre-allocated spawned-usage report id. Always stamped on the result so parent validation can\n\t * enforce the cost-visibility invariant (a completed result without a usage report is blocked).\n\t */\n\tusageReportId: string;\n\tcomplete: (args: { systemPrompt: string; userPrompt: string; signal?: AbortSignal }) => Promise<WorkerCompletion>;\n\tsignal?: AbortSignal;\n\tnow?: () => string;\n\t/** Enables the WRITE lane: only honored when the request envelope grants \"write_files\". The\n\t * runner applies the worker's structured actions through the envelope path scope; refusals\n\t * and failures become blockers, never silent drops. */\n\tapplyActions?: (actions: readonly WorkerAction[]) => AppliedActionsReport;\n\t/** Session cwd — the baseline for relative changed-file and envelope paths in parent\n\t * validation. Defaults to process.cwd(). */\n\tcwd?: string;\n}\n\nexport interface WorkerRunOutcome {\n\tresult: WorkerResult;\n\t/** Parent-review verdict from {@link validateWorkerResult}; worker output stays untrusted. */\n\tacceptance: GateOutcome;\n\taccepted: boolean;\n\tlaneStatus: LaneTerminalStatus;\n\treasonCode: string;\n\tcostUsd: number;\n}\n\nexport function buildWorkerUserPrompt(request: WorkerRequest): string {\n\treturn `Delegated task: ${request.instructions}`;\n}\n\nexport interface ParsedWorkerOutput {\n\tsummary: string;\n\tstatus: \"completed\" | \"blocked\";\n\tblockers: string[];\n\tfindings: Array<{ summary: string; confidence?: number }>;\n\tactions: WorkerAction[];\n}\n\nexport function parseWorkerOutput(text: string): ParsedWorkerOutput | undefined {\n\tconst trimmed = text.trim();\n\tconst candidates: string[] = [trimmed];\n\tconst fenced = /```(?:json)?\\s*([\\s\\S]*?)```/.exec(trimmed);\n\tif (fenced?.[1]) candidates.push(fenced[1].trim());\n\tconst start = trimmed.indexOf(\"{\");\n\tconst end = trimmed.lastIndexOf(\"}\");\n\tif (start >= 0 && end > start) candidates.push(trimmed.slice(start, end + 1));\n\n\tfor (const candidate of candidates) {\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(candidate);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) continue;\n\t\tconst record = parsed as Record<string, unknown>;\n\t\tconst summary = record.summary;\n\t\tif (typeof summary !== \"string\" || summary.trim().length === 0) continue;\n\n\t\tconst status = record.status === \"blocked\" ? \"blocked\" : \"completed\";\n\t\tconst blockers = Array.isArray(record.blockers)\n\t\t\t? record.blockers.filter((blocker): blocker is string => typeof blocker === \"string\" && blocker.length > 0)\n\t\t\t: [];\n\t\tconst findings: Array<{ summary: string; confidence?: number }> = [];\n\t\tif (Array.isArray(record.findings)) {\n\t\t\tfor (const item of record.findings) {\n\t\t\t\tif (!item || typeof item !== \"object\" || Array.isArray(item)) continue;\n\t\t\t\tconst findingSummary = (item as { summary?: unknown }).summary;\n\t\t\t\tif (typeof findingSummary !== \"string\" || findingSummary.trim().length === 0) continue;\n\t\t\t\tconst confidenceRaw = (item as { confidence?: unknown }).confidence;\n\t\t\t\tconst confidence =\n\t\t\t\t\ttypeof confidenceRaw === \"number\" && Number.isFinite(confidenceRaw)\n\t\t\t\t\t\t? Math.min(Math.max(confidenceRaw, 0), 1)\n\t\t\t\t\t\t: undefined;\n\t\t\t\tfindings.push({ summary: findingSummary.trim(), confidence });\n\t\t\t}\n\t\t}\n\t\treturn { summary: summary.trim(), status, blockers, findings, actions: parseWorkerActions(record.actions) };\n\t}\n\treturn undefined;\n}\n\nfunction buildWorkerEvidence(request: WorkerRequest, findings: ParsedWorkerOutput[\"findings\"]) {\n\tif (findings.length === 0) return undefined;\n\tconst instructionsRef: EvidenceRef = {\n\t\tid: \"src-instructions\",\n\t\tkind: \"user\",\n\t\ttitle: \"Delegated task instructions\",\n\t\ttrusted: true,\n\t\texcerpt: request.instructions.slice(0, 2000),\n\t};\n\tconst synthesisRef: EvidenceRef = {\n\t\tid: \"src-worker\",\n\t\tkind: \"tool\",\n\t\ttitle: \"Scout-worker synthesis\",\n\t\ttrusted: false,\n\t};\n\tconst bundleFindings: Finding[] = findings.map((finding, index) => ({\n\t\tid: `finding-${index + 1}`,\n\t\tsummary: finding.summary,\n\t\tevidenceIds: [synthesisRef.id],\n\t\t...(finding.confidence !== undefined ? { confidence: finding.confidence } : {}),\n\t}));\n\treturn createEvidenceBundle({\n\t\tquery: `worker:${request.id}`,\n\t\tsources: [instructionsRef, synthesisRef],\n\t\tfindings: bundleFindings,\n\t});\n}\n\nfunction finishOutcome(args: {\n\trequest: WorkerRequest;\n\tresult: WorkerResult;\n\tlaneStatus: LaneTerminalStatus;\n\treasonCode: string;\n\tcostUsd: number;\n\tcwd?: string;\n}): WorkerRunOutcome {\n\tconst acceptance = validateWorkerResult({ request: args.request, result: args.result, cwd: args.cwd });\n\treturn {\n\t\tresult: args.result,\n\t\tacceptance,\n\t\taccepted: acceptance.outcome === \"allow\",\n\t\tlaneStatus: args.laneStatus,\n\t\treasonCode: args.reasonCode,\n\t\tcostUsd: args.costUsd,\n\t};\n}\n\nexport async function runWorker(options: WorkerRunnerOptions): Promise<WorkerRunOutcome> {\n\tconst now = options.now ?? (() => new Date().toISOString());\n\tconst baseResult = {\n\t\trequestId: options.request.id,\n\t\tchangedFiles: [] as string[],\n\t\tusageReportId: options.usageReportId,\n\t\tcreatedAt: now(),\n\t};\n\n\t// The WRITE lane requires BOTH the envelope grant and a caller-supplied applier — either\n\t// alone keeps the read-only scout contract byte-for-byte.\n\tconst writeCapable =\n\t\toptions.request.envelope.capabilities.includes(\"write_files\") && options.applyActions !== undefined;\n\n\tconst bounded = await runBoundedCompletion({\n\t\tmaxWallClockMs: options.maxWallClockMs,\n\t\tsignal: options.signal,\n\t\texecute: (signal) =>\n\t\t\toptions.complete({\n\t\t\t\tsystemPrompt: writeCapable ? WORKER_WRITE_LANE_SYSTEM_PROMPT : WORKER_LANE_SYSTEM_PROMPT,\n\t\t\t\tuserPrompt: buildWorkerUserPrompt(options.request),\n\t\t\t\tsignal,\n\t\t\t}),\n\t});\n\tconst costUsd = bounded.completion?.costUsd ?? 0;\n\n\tif (bounded.failure) {\n\t\tconst cancelled = bounded.failure.status === \"canceled\" || bounded.failure.status === \"timeout\";\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tcwd: options.cwd,\n\t\t\tresult: {\n\t\t\t\t...baseResult,\n\t\t\t\tstatus: cancelled ? \"cancelled\" : \"failed\",\n\t\t\t\tsummary: `Worker did not complete: ${bounded.failure.reasonCode}`,\n\t\t\t},\n\t\t\tlaneStatus: bounded.failure.status,\n\t\t\treasonCode: bounded.failure.reasonCode,\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst completion = bounded.completion;\n\tif (!completion || completion.stopReason === \"error\" || completion.stopReason === \"aborted\") {\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tcwd: options.cwd,\n\t\t\tresult: { ...baseResult, status: \"failed\", summary: \"Worker model call failed.\" },\n\t\t\tlaneStatus: \"failed\",\n\t\t\treasonCode: \"model_error\",\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst parsed = parseWorkerOutput(completion.text);\n\tif (!parsed) {\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tcwd: options.cwd,\n\t\t\tresult: { ...baseResult, status: \"failed\", summary: \"Worker output was not valid structured JSON.\" },\n\t\t\tlaneStatus: \"failed\",\n\t\t\treasonCode: \"unparseable_output\",\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst evidence = buildWorkerEvidence(options.request, parsed.findings);\n\tlet changedFiles: string[] = [];\n\tconst actionBlockers: string[] = [];\n\tif (writeCapable && parsed.status !== \"blocked\" && parsed.actions.length > 0 && options.applyActions) {\n\t\t// Runner-side application through the envelope path scope: refusals and failures are\n\t\t// surfaced as blockers so a partially-applied change can never look like clean success.\n\t\tconst applied = options.applyActions(parsed.actions);\n\t\tchangedFiles = applied.changedFiles;\n\t\tfor (const refusal of applied.refused) {\n\t\t\tactionBlockers.push(`action refused (${refusal.path}): ${refusal.reason}`);\n\t\t}\n\t\tfor (const failure of applied.failed) {\n\t\t\tactionBlockers.push(`action failed (${failure.path}): ${failure.reason}`);\n\t\t}\n\t} else if (!writeCapable && parsed.actions.length > 0) {\n\t\tactionBlockers.push(\"worker emitted file actions without a write_files envelope grant; nothing was applied\");\n\t}\n\tconst allBlockers = [...parsed.blockers, ...actionBlockers];\n\tconst result: WorkerResult = {\n\t\t...baseResult,\n\t\tchangedFiles,\n\t\tstatus: parsed.status === \"blocked\" || allBlockers.length > 0 ? \"blocked\" : \"completed\",\n\t\tsummary: parsed.summary,\n\t\t...(allBlockers.length > 0 ? { blockers: allBlockers } : {}),\n\t\t...(evidence ? { evidence } : {}),\n\t};\n\n\tif (result.status === \"blocked\") {\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tcwd: options.cwd,\n\t\t\tresult,\n\t\t\tlaneStatus: \"failed\",\n\t\t\treasonCode: \"worker_blocked\",\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst overBudget = options.maxUsd > 0 && costUsd > options.maxUsd;\n\treturn finishOutcome({\n\t\trequest: options.request,\n\t\tcwd: options.cwd,\n\t\tresult,\n\t\tlaneStatus: overBudget ? \"budget_exhausted\" : \"succeeded\",\n\t\treasonCode: overBudget ? \"cost_budget_exceeded\" : \"worker_completed\",\n\t\tcostUsd,\n\t});\n}\n"]}
@@ -0,0 +1,66 @@
1
+ import { type FffInstallOutcome, type SystemToolStatus } from "../utils/tools-manager.ts";
2
+ import { OllamaRuntime } from "./models/local-runtime.ts";
3
+ /**
4
+ * Environment doctor: verifies required tooling and installs what it safely
5
+ * can, instead of leaving provisioning entirely to lazy first-use (the exact
6
+ * gap behind "ran `pi-adaptative update` on another machine and fff-node
7
+ * never got installed" -- see fff-lazy-install.test.ts/fff-search-tools.test.ts
8
+ * for the underlying lazy-install fix; this module is the proactive half).
9
+ *
10
+ * Two tool kinds, two different postures:
11
+ * - "managed": pi already owns the install path (fff-node into
12
+ * ~/.pi/agent/bin via tools-manager.ts's ensureFffNodePackage). The doctor
13
+ * actually attempts the install when missing.
14
+ * - "system": pi does not own the install (ripgrep, ollama, python). GUIDE
15
+ * MODE only -- exact manual steps are reported, never executed, never
16
+ * `curl | sh`, never `sudo`. This matches the harness's deliberate
17
+ * no-silent-installer stance for tooling outside its own managed bin dir.
18
+ */
19
+ export type DoctorToolKind = "managed" | "system";
20
+ export interface DoctorCheck {
21
+ /** Stable identifier, e.g. "fff-node", "ripgrep", "ollama", "python". */
22
+ id: string;
23
+ /** Human-readable label for the report. */
24
+ label: string;
25
+ kind: DoctorToolKind;
26
+ present: boolean;
27
+ /** Version/path/status summary, present or not. */
28
+ detail?: string;
29
+ /** Only set for a "managed" tool: whether an install was just attempted. */
30
+ installAttempted?: boolean;
31
+ /** Only set for a "system" tool that's missing: exact manual steps, never executed. */
32
+ guide?: string[];
33
+ }
34
+ export interface DoctorReport {
35
+ checks: DoctorCheck[];
36
+ }
37
+ /** Minimal slice of OllamaRuntime the doctor needs -- lets tests inject a fake without touching the real runtime. */
38
+ export type DoctorOllamaRuntime = Pick<OllamaRuntime, "detect" | "installGuide">;
39
+ export interface DoctorDeps {
40
+ loadAvailableFffNodePackage: () => unknown | undefined;
41
+ ensureFffNodePackage: (silent?: boolean) => Promise<unknown | undefined>;
42
+ getLastFffInstallOutcome: () => FffInstallOutcome | undefined;
43
+ getRgPath: () => string | null;
44
+ detectPython: () => SystemToolStatus;
45
+ /** Best-effort `<command> --version` probe; undefined if it can't be run. Used only to enrich a status line, never for presence detection. */
46
+ probeVersion: (command: string, versionArgs?: readonly string[]) => string | undefined;
47
+ ollamaRuntime: DoctorOllamaRuntime;
48
+ }
49
+ export interface RunDoctorOptions {
50
+ /**
51
+ * Whether the fff-node managed install (if one is attempted) stays quiet.
52
+ * Default true, matching runUpdatePreflight's existing background
53
+ * behavior. The interactive `doctor` command passes `false` so a
54
+ * multi-second install doesn't read as a silent hang.
55
+ */
56
+ silent?: boolean;
57
+ }
58
+ export declare function runDoctor(deps?: DoctorDeps, options?: RunDoctorOptions): Promise<DoctorReport>;
59
+ export declare function formatDoctorReport(report: DoctorReport): string;
60
+ /**
61
+ * Best-effort preflight meant to be called right after `pi-adaptative update`
62
+ * succeeds. Must never fail the update itself: any error here is swallowed
63
+ * and reported as a skipped check, not surfaced as an update failure.
64
+ */
65
+ export declare function runUpdatePreflight(): Promise<void>;
66
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/core/doctor.ts"],"names":[],"mappings":"AAEA,OAAO,EAGN,KAAK,iBAAiB,EAKtB,KAAK,gBAAgB,EACrB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE1D;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,QAAQ,CAAC;AAElD,MAAM,WAAW,WAAW;IAC3B,yEAAyE;IACzE,EAAE,EAAE,MAAM,CAAC;IACX,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,uFAAuF;IACvF,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC5B,MAAM,EAAE,WAAW,EAAE,CAAC;CACtB;AAED,qHAAqH;AACrH,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,cAAc,CAAC,CAAC;AAEjF,MAAM,WAAW,UAAU;IAC1B,2BAA2B,EAAE,MAAM,OAAO,GAAG,SAAS,CAAC;IACvD,oBAAoB,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IACzE,wBAAwB,EAAE,MAAM,iBAAiB,GAAG,SAAS,CAAC;IAC9D,SAAS,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAC/B,YAAY,EAAE,MAAM,gBAAgB,CAAC;IACrC,8IAA8I;IAC9I,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,KAAK,MAAM,GAAG,SAAS,CAAC;IACvF,aAAa,EAAE,mBAAmB,CAAC;CACnC;AAED,MAAM,WAAW,gBAAgB;IAChC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAsHD,wBAAsB,SAAS,CAC9B,IAAI,GAAE,UAA2B,EACjC,OAAO,GAAE,gBAAqB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAIvB;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAW/D;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAOxD","sourcesContent":["import chalk from \"chalk\";\nimport { getAgentDir } from \"../config.ts\";\nimport {\n\tdetectPython,\n\tensureFffNodePackage,\n\ttype FffInstallOutcome,\n\tgetLastFffInstallOutcome,\n\tgetToolPath,\n\tloadAvailableFffNodePackage,\n\tprobeVersion,\n\ttype SystemToolStatus,\n} from \"../utils/tools-manager.ts\";\nimport { OllamaRuntime } from \"./models/local-runtime.ts\";\n\n/**\n * Environment doctor: verifies required tooling and installs what it safely\n * can, instead of leaving provisioning entirely to lazy first-use (the exact\n * gap behind \"ran `pi-adaptative update` on another machine and fff-node\n * never got installed\" -- see fff-lazy-install.test.ts/fff-search-tools.test.ts\n * for the underlying lazy-install fix; this module is the proactive half).\n *\n * Two tool kinds, two different postures:\n * - \"managed\": pi already owns the install path (fff-node into\n * ~/.pi/agent/bin via tools-manager.ts's ensureFffNodePackage). The doctor\n * actually attempts the install when missing.\n * - \"system\": pi does not own the install (ripgrep, ollama, python). GUIDE\n * MODE only -- exact manual steps are reported, never executed, never\n * `curl | sh`, never `sudo`. This matches the harness's deliberate\n * no-silent-installer stance for tooling outside its own managed bin dir.\n */\n\nexport type DoctorToolKind = \"managed\" | \"system\";\n\nexport interface DoctorCheck {\n\t/** Stable identifier, e.g. \"fff-node\", \"ripgrep\", \"ollama\", \"python\". */\n\tid: string;\n\t/** Human-readable label for the report. */\n\tlabel: string;\n\tkind: DoctorToolKind;\n\tpresent: boolean;\n\t/** Version/path/status summary, present or not. */\n\tdetail?: string;\n\t/** Only set for a \"managed\" tool: whether an install was just attempted. */\n\tinstallAttempted?: boolean;\n\t/** Only set for a \"system\" tool that's missing: exact manual steps, never executed. */\n\tguide?: string[];\n}\n\nexport interface DoctorReport {\n\tchecks: DoctorCheck[];\n}\n\n/** Minimal slice of OllamaRuntime the doctor needs -- lets tests inject a fake without touching the real runtime. */\nexport type DoctorOllamaRuntime = Pick<OllamaRuntime, \"detect\" | \"installGuide\">;\n\nexport interface DoctorDeps {\n\tloadAvailableFffNodePackage: () => unknown | undefined;\n\tensureFffNodePackage: (silent?: boolean) => Promise<unknown | undefined>;\n\tgetLastFffInstallOutcome: () => FffInstallOutcome | undefined;\n\tgetRgPath: () => string | null;\n\tdetectPython: () => SystemToolStatus;\n\t/** Best-effort `<command> --version` probe; undefined if it can't be run. Used only to enrich a status line, never for presence detection. */\n\tprobeVersion: (command: string, versionArgs?: readonly string[]) => string | undefined;\n\tollamaRuntime: DoctorOllamaRuntime;\n}\n\nexport interface RunDoctorOptions {\n\t/**\n\t * Whether the fff-node managed install (if one is attempted) stays quiet.\n\t * Default true, matching runUpdatePreflight's existing background\n\t * behavior. The interactive `doctor` command passes `false` so a\n\t * multi-second install doesn't read as a silent hang.\n\t */\n\tsilent?: boolean;\n}\n\nconst realDoctorDeps: DoctorDeps = {\n\tloadAvailableFffNodePackage,\n\tensureFffNodePackage,\n\tgetLastFffInstallOutcome,\n\tgetRgPath: () => getToolPath(\"rg\"),\n\tdetectPython: () => detectPython(),\n\tprobeVersion,\n\tollamaRuntime: new OllamaRuntime({ agentDir: getAgentDir() }),\n};\n\nconst RIPGREP_GUIDE = [\n\t\"ripgrep (rg) was not found. Pi never runs installers itself -- manual steps (user-level, no sudo):\",\n\t\" - macOS (Homebrew): brew install ripgrep\",\n\t\" - Debian/Ubuntu: apt install ripgrep\",\n\t\" - Arch: pacman -S ripgrep\",\n\t\" - Or download a release: https://github.com/BurntSushi/ripgrep/releases\",\n];\n\nconst PYTHON_GUIDE = [\n\t\"python3 was not found. Pi never runs installers itself -- manual steps (user-level, no sudo):\",\n\t\" - macOS (Homebrew): brew install python3\",\n\t\" - Debian/Ubuntu: apt install python3\",\n\t\" - Arch: pacman -S python\",\n\t\" - Or download from https://www.python.org/downloads/\",\n];\n\nfunction describeFffOutcome(outcome: FffInstallOutcome | undefined): string {\n\tswitch (outcome?.status) {\n\t\tcase \"already-available\":\n\t\t\treturn \"already available\";\n\t\tcase \"installed\":\n\t\t\treturn \"installed just now\";\n\t\tcase \"offline\":\n\t\t\treturn \"offline mode enabled, skipped\";\n\t\tcase \"unsupported-platform\":\n\t\t\treturn \"no native build for this platform\";\n\t\tcase \"install-failed\":\n\t\t\treturn `install failed: ${outcome.reason}`;\n\t\tdefault:\n\t\t\treturn \"unavailable\";\n\t}\n}\n\n/** First line only -- a compiled-features/build-flags tail (e.g. rg --version) is noise in a one-line status report. */\nfunction firstLine(text: string | undefined): string | undefined {\n\treturn text?.split(\"\\n\")[0];\n}\n\n/** MANAGED tool: pi owns this install path, so the doctor actually attempts it when missing. */\nasync function checkFffNode(deps: DoctorDeps, silent: boolean): Promise<DoctorCheck> {\n\t// Delegate entirely to ensureFffNodePackage rather than re-deriving \"is it\n\t// already there\" here: that function (and its offline/unsupported-platform/\n\t// cooldown handling, see tools-manager.ts) is the single source of truth for\n\t// whether an install should even be attempted -- the doctor must not\n\t// duplicate or second-guess that logic, only report its outcome honestly.\n\tconst installed = await deps.ensureFffNodePackage(silent);\n\tconst outcome = deps.getLastFffInstallOutcome();\n\treturn {\n\t\tid: \"fff-node\",\n\t\tlabel: \"FFF native search (@ff-labs/fff-node)\",\n\t\tkind: \"managed\",\n\t\tpresent: Boolean(installed),\n\t\tinstallAttempted: outcome !== undefined && outcome.status !== \"already-available\",\n\t\tdetail: describeFffOutcome(outcome),\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor. */\nfunction checkRipgrep(deps: DoctorDeps): DoctorCheck {\n\tconst path = deps.getRgPath();\n\tif (!path) {\n\t\treturn { id: \"ripgrep\", label: \"ripgrep (rg)\", kind: \"system\", present: false, guide: RIPGREP_GUIDE };\n\t}\n\tconst version = firstLine(deps.probeVersion(path));\n\treturn {\n\t\tid: \"ripgrep\",\n\t\tlabel: \"ripgrep (rg)\",\n\t\tkind: \"system\",\n\t\tpresent: true,\n\t\tdetail: version ? `${path} (${version})` : path,\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor (OllamaRuntime.installGuide() is itself already guide-mode-only). */\nasync function checkOllama(deps: DoctorDeps): Promise<DoctorCheck> {\n\tconst status = await deps.ollamaRuntime.detect();\n\tif (!status.binaryPath) {\n\t\treturn {\n\t\t\tid: \"ollama\",\n\t\t\tlabel: \"Ollama (local model runtime)\",\n\t\t\tkind: \"system\",\n\t\t\tpresent: false,\n\t\t\tguide: deps.ollamaRuntime.installGuide(),\n\t\t};\n\t}\n\tconst version = firstLine(deps.probeVersion(status.binaryPath));\n\tconst versionSuffix = version ? `, ${version}` : \"\";\n\treturn {\n\t\tid: \"ollama\",\n\t\tlabel: \"Ollama (local model runtime)\",\n\t\tkind: \"system\",\n\t\tpresent: true,\n\t\tdetail: `binary: ${status.binaryPath} [${status.binarySource}]${versionSuffix}; server: ${status.serverUp ? \"up\" : \"down\"} at ${status.serverUrl}`,\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor. */\nfunction checkPython(deps: DoctorDeps): DoctorCheck {\n\tconst status = deps.detectPython();\n\tif (!status.present) {\n\t\treturn { id: \"python\", label: \"Python (python3)\", kind: \"system\", present: false, guide: PYTHON_GUIDE };\n\t}\n\tconst detail = status.version ? `${status.command} (${status.version})` : status.command;\n\treturn { id: \"python\", label: \"Python (python3)\", kind: \"system\", present: true, detail };\n}\n\nexport async function runDoctor(\n\tdeps: DoctorDeps = realDoctorDeps,\n\toptions: RunDoctorOptions = {},\n): Promise<DoctorReport> {\n\tconst silent = options.silent ?? true;\n\tconst [fffNode, ollama] = await Promise.all([checkFffNode(deps, silent), checkOllama(deps)]);\n\treturn { checks: [fffNode, checkRipgrep(deps), ollama, checkPython(deps)] };\n}\n\nexport function formatDoctorReport(report: DoctorReport): string {\n\tconst lines: string[] = [];\n\tfor (const check of report.checks) {\n\t\tconst status = check.present ? chalk.green(\"[OK]\") : chalk.yellow(\"[MISSING]\");\n\t\tconst detail = check.detail ? ` -- ${check.detail}` : \"\";\n\t\tlines.push(`${status} ${check.label}${detail}`);\n\t\tif (!check.present && check.guide) {\n\t\t\tfor (const guideLine of check.guide) lines.push(` ${guideLine}`);\n\t\t}\n\t}\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Best-effort preflight meant to be called right after `pi-adaptative update`\n * succeeds. Must never fail the update itself: any error here is swallowed\n * and reported as a skipped check, not surfaced as an update failure.\n */\nexport async function runUpdatePreflight(): Promise<void> {\n\ttry {\n\t\tconst report = await runDoctor();\n\t\tconsole.log(`\\n${formatDoctorReport(report)}\\n`);\n\t} catch (error) {\n\t\tconsole.log(chalk.dim(`(environment check skipped: ${error instanceof Error ? error.message : String(error)})`));\n\t}\n}\n"]}
@@ -0,0 +1,144 @@
1
+ import chalk from "chalk";
2
+ import { getAgentDir } from "../config.js";
3
+ import { detectPython, ensureFffNodePackage, getLastFffInstallOutcome, getToolPath, loadAvailableFffNodePackage, probeVersion, } from "../utils/tools-manager.js";
4
+ import { OllamaRuntime } from "./models/local-runtime.js";
5
+ const realDoctorDeps = {
6
+ loadAvailableFffNodePackage,
7
+ ensureFffNodePackage,
8
+ getLastFffInstallOutcome,
9
+ getRgPath: () => getToolPath("rg"),
10
+ detectPython: () => detectPython(),
11
+ probeVersion,
12
+ ollamaRuntime: new OllamaRuntime({ agentDir: getAgentDir() }),
13
+ };
14
+ const RIPGREP_GUIDE = [
15
+ "ripgrep (rg) was not found. Pi never runs installers itself -- manual steps (user-level, no sudo):",
16
+ " - macOS (Homebrew): brew install ripgrep",
17
+ " - Debian/Ubuntu: apt install ripgrep",
18
+ " - Arch: pacman -S ripgrep",
19
+ " - Or download a release: https://github.com/BurntSushi/ripgrep/releases",
20
+ ];
21
+ const PYTHON_GUIDE = [
22
+ "python3 was not found. Pi never runs installers itself -- manual steps (user-level, no sudo):",
23
+ " - macOS (Homebrew): brew install python3",
24
+ " - Debian/Ubuntu: apt install python3",
25
+ " - Arch: pacman -S python",
26
+ " - Or download from https://www.python.org/downloads/",
27
+ ];
28
+ function describeFffOutcome(outcome) {
29
+ switch (outcome?.status) {
30
+ case "already-available":
31
+ return "already available";
32
+ case "installed":
33
+ return "installed just now";
34
+ case "offline":
35
+ return "offline mode enabled, skipped";
36
+ case "unsupported-platform":
37
+ return "no native build for this platform";
38
+ case "install-failed":
39
+ return `install failed: ${outcome.reason}`;
40
+ default:
41
+ return "unavailable";
42
+ }
43
+ }
44
+ /** First line only -- a compiled-features/build-flags tail (e.g. rg --version) is noise in a one-line status report. */
45
+ function firstLine(text) {
46
+ return text?.split("\n")[0];
47
+ }
48
+ /** MANAGED tool: pi owns this install path, so the doctor actually attempts it when missing. */
49
+ async function checkFffNode(deps, silent) {
50
+ // Delegate entirely to ensureFffNodePackage rather than re-deriving "is it
51
+ // already there" here: that function (and its offline/unsupported-platform/
52
+ // cooldown handling, see tools-manager.ts) is the single source of truth for
53
+ // whether an install should even be attempted -- the doctor must not
54
+ // duplicate or second-guess that logic, only report its outcome honestly.
55
+ const installed = await deps.ensureFffNodePackage(silent);
56
+ const outcome = deps.getLastFffInstallOutcome();
57
+ return {
58
+ id: "fff-node",
59
+ label: "FFF native search (@ff-labs/fff-node)",
60
+ kind: "managed",
61
+ present: Boolean(installed),
62
+ installAttempted: outcome !== undefined && outcome.status !== "already-available",
63
+ detail: describeFffOutcome(outcome),
64
+ };
65
+ }
66
+ /** SYSTEM tool: guide mode only, never installed by the doctor. */
67
+ function checkRipgrep(deps) {
68
+ const path = deps.getRgPath();
69
+ if (!path) {
70
+ return { id: "ripgrep", label: "ripgrep (rg)", kind: "system", present: false, guide: RIPGREP_GUIDE };
71
+ }
72
+ const version = firstLine(deps.probeVersion(path));
73
+ return {
74
+ id: "ripgrep",
75
+ label: "ripgrep (rg)",
76
+ kind: "system",
77
+ present: true,
78
+ detail: version ? `${path} (${version})` : path,
79
+ };
80
+ }
81
+ /** SYSTEM tool: guide mode only, never installed by the doctor (OllamaRuntime.installGuide() is itself already guide-mode-only). */
82
+ async function checkOllama(deps) {
83
+ const status = await deps.ollamaRuntime.detect();
84
+ if (!status.binaryPath) {
85
+ return {
86
+ id: "ollama",
87
+ label: "Ollama (local model runtime)",
88
+ kind: "system",
89
+ present: false,
90
+ guide: deps.ollamaRuntime.installGuide(),
91
+ };
92
+ }
93
+ const version = firstLine(deps.probeVersion(status.binaryPath));
94
+ const versionSuffix = version ? `, ${version}` : "";
95
+ return {
96
+ id: "ollama",
97
+ label: "Ollama (local model runtime)",
98
+ kind: "system",
99
+ present: true,
100
+ detail: `binary: ${status.binaryPath} [${status.binarySource}]${versionSuffix}; server: ${status.serverUp ? "up" : "down"} at ${status.serverUrl}`,
101
+ };
102
+ }
103
+ /** SYSTEM tool: guide mode only, never installed by the doctor. */
104
+ function checkPython(deps) {
105
+ const status = deps.detectPython();
106
+ if (!status.present) {
107
+ return { id: "python", label: "Python (python3)", kind: "system", present: false, guide: PYTHON_GUIDE };
108
+ }
109
+ const detail = status.version ? `${status.command} (${status.version})` : status.command;
110
+ return { id: "python", label: "Python (python3)", kind: "system", present: true, detail };
111
+ }
112
+ export async function runDoctor(deps = realDoctorDeps, options = {}) {
113
+ const silent = options.silent ?? true;
114
+ const [fffNode, ollama] = await Promise.all([checkFffNode(deps, silent), checkOllama(deps)]);
115
+ return { checks: [fffNode, checkRipgrep(deps), ollama, checkPython(deps)] };
116
+ }
117
+ export function formatDoctorReport(report) {
118
+ const lines = [];
119
+ for (const check of report.checks) {
120
+ const status = check.present ? chalk.green("[OK]") : chalk.yellow("[MISSING]");
121
+ const detail = check.detail ? ` -- ${check.detail}` : "";
122
+ lines.push(`${status} ${check.label}${detail}`);
123
+ if (!check.present && check.guide) {
124
+ for (const guideLine of check.guide)
125
+ lines.push(` ${guideLine}`);
126
+ }
127
+ }
128
+ return lines.join("\n");
129
+ }
130
+ /**
131
+ * Best-effort preflight meant to be called right after `pi-adaptative update`
132
+ * succeeds. Must never fail the update itself: any error here is swallowed
133
+ * and reported as a skipped check, not surfaced as an update failure.
134
+ */
135
+ export async function runUpdatePreflight() {
136
+ try {
137
+ const report = await runDoctor();
138
+ console.log(`\n${formatDoctorReport(report)}\n`);
139
+ }
140
+ catch (error) {
141
+ console.log(chalk.dim(`(environment check skipped: ${error instanceof Error ? error.message : String(error)})`));
142
+ }
143
+ }
144
+ //# sourceMappingURL=doctor.js.map