@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.
- package/CHANGELOG.md +203 -0
- package/dist/core/agent-session.d.ts +72 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +437 -46
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/autonomy/envelope-enforcement.d.ts +5 -0
- package/dist/core/autonomy/envelope-enforcement.d.ts.map +1 -1
- package/dist/core/autonomy/envelope-enforcement.js +28 -4
- package/dist/core/autonomy/envelope-enforcement.js.map +1 -1
- package/dist/core/autonomy/path-scope.d.ts.map +1 -1
- package/dist/core/autonomy/path-scope.js +33 -2
- package/dist/core/autonomy/path-scope.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +15 -7
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/context/brain-curator.d.ts +10 -0
- package/dist/core/context/brain-curator.d.ts.map +1 -1
- package/dist/core/context/brain-curator.js +19 -2
- package/dist/core/context/brain-curator.js.map +1 -1
- package/dist/core/context-gc.d.ts +8 -3
- package/dist/core/context-gc.d.ts.map +1 -1
- package/dist/core/context-gc.js +5 -0
- package/dist/core/context-gc.js.map +1 -1
- package/dist/core/delegation/worker-actions.d.ts +0 -10
- package/dist/core/delegation/worker-actions.d.ts.map +1 -1
- package/dist/core/delegation/worker-actions.js +5 -7
- package/dist/core/delegation/worker-actions.js.map +1 -1
- package/dist/core/delegation/worker-result.d.ts +6 -0
- package/dist/core/delegation/worker-result.d.ts.map +1 -1
- package/dist/core/delegation/worker-result.js +10 -4
- package/dist/core/delegation/worker-result.js.map +1 -1
- package/dist/core/delegation/worker-runner.d.ts +3 -0
- package/dist/core/delegation/worker-runner.d.ts.map +1 -1
- package/dist/core/delegation/worker-runner.js +6 -1
- package/dist/core/delegation/worker-runner.js.map +1 -1
- package/dist/core/doctor.d.ts +66 -0
- package/dist/core/doctor.d.ts.map +1 -0
- package/dist/core/doctor.js +144 -0
- package/dist/core/doctor.js.map +1 -0
- package/dist/core/learning/learning-audit.d.ts +13 -1
- package/dist/core/learning/learning-audit.d.ts.map +1 -1
- package/dist/core/learning/learning-audit.js +22 -1
- package/dist/core/learning/learning-audit.js.map +1 -1
- package/dist/core/learning/learning-gate.d.ts +3 -0
- package/dist/core/learning/learning-gate.d.ts.map +1 -1
- package/dist/core/learning/learning-gate.js +13 -4
- package/dist/core/learning/learning-gate.js.map +1 -1
- package/dist/core/models/default-model-suggestions.d.ts +34 -0
- package/dist/core/models/default-model-suggestions.d.ts.map +1 -0
- package/dist/core/models/default-model-suggestions.js +58 -0
- package/dist/core/models/default-model-suggestions.js.map +1 -0
- package/dist/core/models/local-registration.d.ts +2 -0
- package/dist/core/models/local-registration.d.ts.map +1 -1
- package/dist/core/models/local-registration.js +2 -1
- package/dist/core/models/local-registration.js.map +1 -1
- package/dist/core/models/local-runtime.d.ts +76 -2
- package/dist/core/models/local-runtime.d.ts.map +1 -1
- package/dist/core/models/local-runtime.js +233 -21
- package/dist/core/models/local-runtime.js.map +1 -1
- package/dist/core/profile-resource-selection.d.ts +15 -3
- package/dist/core/profile-resource-selection.d.ts.map +1 -1
- package/dist/core/profile-resource-selection.js +19 -9
- package/dist/core/profile-resource-selection.js.map +1 -1
- package/dist/core/resource-loader.d.ts +20 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +42 -16
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/settings-manager.d.ts +30 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +65 -3
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts +14 -0
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +8 -0
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/tools/fff-search-backend.d.ts +34 -1
- package/dist/core/tools/fff-search-backend.d.ts.map +1 -1
- package/dist/core/tools/fff-search-backend.js +43 -4
- package/dist/core/tools/fff-search-backend.js.map +1 -1
- package/dist/core/tools/find.d.ts +16 -0
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +17 -3
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts +20 -0
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +15 -3
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +17 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/fitness-role-selector.d.ts +6 -2
- package/dist/modes/interactive/components/fitness-role-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/fitness-role-selector.js +55 -45
- package/dist/modes/interactive/components/fitness-role-selector.js.map +1 -1
- package/dist/modes/interactive/components/model-suggestion-selector.d.ts +13 -0
- package/dist/modes/interactive/components/model-suggestion-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/model-suggestion-selector.js +40 -0
- package/dist/modes/interactive/components/model-suggestion-selector.js.map +1 -0
- package/dist/modes/interactive/components/profile-resource-editor.d.ts +3 -0
- package/dist/modes/interactive/components/profile-resource-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/profile-resource-editor.js +16 -2
- package/dist/modes/interactive/components/profile-resource-editor.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +98 -0
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +8 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +57 -10
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/safe-write-stream.d.ts +8 -0
- package/dist/utils/safe-write-stream.d.ts.map +1 -1
- package/dist/utils/safe-write-stream.js +32 -0
- package/dist/utils/safe-write-stream.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +92 -2
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +172 -32
- package/dist/utils/tools-manager.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -36
- 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;
|
|
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
|
-
|
|
106
|
-
|
|
111
|
+
const scopedChangedFile = path.resolve(baseDir, changedFile);
|
|
112
|
+
for (const root of resolvedAllowed) {
|
|
107
113
|
const decision = checkPathScope({
|
|
108
114
|
root,
|
|
109
|
-
allowedPaths:
|
|
110
|
-
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
|