@tea-agent/loop-agent 0.44.0-next.12 → 0.44.0-next.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/CHANGELOG.md +27 -2
  2. package/README.md +12 -12
  3. package/dist/adapters/loop-agent.js +34 -0
  4. package/dist/build-stamp.json +3 -3
  5. package/dist/cli/command-definitions.js +5 -5
  6. package/dist/cli/program.js +1 -4
  7. package/dist/commands/init-legacy-audit.js +330 -0
  8. package/dist/commands/init-upgrade.js +644 -104
  9. package/dist/commands/init.js +686 -330
  10. package/dist/commands/operator-guide.js +99 -0
  11. package/dist/commands/operator.js +16 -3
  12. package/dist/executors/dag-pi-executor.js +11 -6
  13. package/dist/executors/pi-sdk-executor.js +3 -1
  14. package/dist/executors/pi-writer-tool-policy.js +60 -4
  15. package/dist/executors/playwright-cli-launcher.js +31 -45
  16. package/dist/executors/shell-write-guard.js +24 -67
  17. package/dist/governance/exec-plans.js +10 -1
  18. package/dist/governance/harness.js +2 -7
  19. package/dist/governance/manifest-types.js +28 -9
  20. package/dist/governance/project-capability-scanner.js +263 -0
  21. package/dist/governance/project-config.js +96 -0
  22. package/dist/governance/resource-catalog.js +151 -0
  23. package/dist/infrastructure/console/app-data.js +6 -2
  24. package/dist/shared/operator/capabilities.js +2 -2
  25. package/dist/shared/package-metadata.js +23 -7
  26. package/dist/shared/pi-context-pressure/extension.js +23 -0
  27. package/dist/shared/pi-context-pressure/index.js +1 -0
  28. package/dist/shared/pi-context-pressure/telemetry.js +4 -0
  29. package/dist/shared/pi-context-pressure/tool-result-pruner.js +98 -0
  30. package/dist/shared/update/npm-client.js +49 -1
  31. package/dist/task/config-types.js +4 -2
  32. package/dist/task/source-prepare/build-draft.js +1 -0
  33. package/dist/task/source-prepare/path-policy.js +17 -7
  34. package/dist/worker/cli.js +3 -5
  35. package/dist/worker/console/chat/browser-automation.js +455 -41
  36. package/dist/worker/console/chat/browser-content.js +74 -0
  37. package/dist/worker/console/chat/browser-diagnostics.js +250 -0
  38. package/dist/worker/console/chat/browser-evaluation.js +58 -0
  39. package/dist/worker/console/chat/browser-operation-queue.js +19 -0
  40. package/dist/worker/console/chat/browser-pick.js +113 -0
  41. package/dist/worker/console/chat/browser-routes.js +54 -5
  42. package/dist/worker/console/chat/browser-screenshot-result.js +47 -0
  43. package/dist/worker/console/chat/browser-storage.js +89 -0
  44. package/dist/worker/console/chat/chat-event-store.js +7 -6
  45. package/dist/worker/console/chat/context-insights.js +84 -19
  46. package/dist/worker/console/chat/pet/pet-affinity.js +169 -0
  47. package/dist/worker/console/chat/pet/pet-persist.js +109 -0
  48. package/dist/worker/console/chat/pet/pet-registry.js +142 -0
  49. package/dist/worker/console/chat/pet/pet-remarks.js +48 -0
  50. package/dist/worker/console/chat/pet/pet-routes.js +125 -0
  51. package/dist/worker/console/chat/pet/pet-service.js +262 -0
  52. package/dist/worker/console/chat/pet/pet-state.js +99 -0
  53. package/dist/worker/console/chat/pi-mode-loop-isolation.js +17 -2
  54. package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +125 -174
  55. package/dist/worker/console/chat/pi-runtime.js +8 -3
  56. package/dist/worker/console/chat/repo-browser.js +80 -2
  57. package/dist/worker/console/chat/repo-preview-kind.js +155 -0
  58. package/dist/worker/console/chat/routes.js +43 -7
  59. package/dist/worker/console/chat/scheduled-goal-request.js +14 -6
  60. package/dist/worker/console/chat/session-mode.js +1 -1
  61. package/dist/worker/console/chat/session-store.js +24 -5
  62. package/dist/worker/console/chat/tools.js +12 -0
  63. package/dist/worker/console/console-shutdown.js +34 -0
  64. package/dist/worker/console/server.js +2 -1
  65. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-r1NAPU_5.js → abnfDiagram-N423BO3Z-DASc69AM.js} +1 -1
  66. package/dist/worker/console/static/assets/{arc-dT5T4sVL.js → arc-h09p-LAc.js} +1 -1
  67. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP_cOq46.js → architectureDiagram-T3A2C74G-CZ3dtLkh.js} +1 -1
  68. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-sToEZxEf.js → blockDiagram-VBNYF7ZC-DMrRhMXG.js} +1 -1
  69. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DSxwwi2m.js → c4Diagram-5PPSVZJV-DQxisHV6.js} +1 -1
  70. package/dist/worker/console/static/assets/channel-D1zgKZfg.js +1 -0
  71. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B9fh0IkF.js → chunk-2GRJ4B5K-CefVN9zh.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CAUNl-A6.js → chunk-2Q5K7J3B-DmR1Koq2.js} +1 -1
  73. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BomeqbNX.js → chunk-5RXB4S5H-NlzrSW7L.js} +1 -1
  74. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BNAuTOLE.js → chunk-5VM5RSS4-DfelJv-0.js} +1 -1
  75. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DfzjRD5y.js → chunk-6Q2QTUOP-BK6RFy58.js} +1 -1
  76. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C3uE-yc2.js → chunk-GF5L2VYU-BAeh9lz4.js} +1 -1
  77. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-C3sQMMYr.js → chunk-JWPE2WC7-ChPS5WzG.js} +1 -1
  78. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D_AtEYQL.js → chunk-KBJHAD2P-BoVe5IM6.js} +1 -1
  79. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DW4TwhKv.js → chunk-RYQCIY6F-C4q_0Lw9.js} +1 -1
  80. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CTQDmJcp.js → chunk-XXDRQBXY-Bb9TvjeY.js} +1 -1
  81. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CXNNsY2M.js +1 -0
  82. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CXNNsY2M.js +1 -0
  83. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-iMqPuBsL.js → cose-bilkent-JH36ORCC-DmNnL8Ff.js} +1 -1
  84. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DktIuDWs.js → cynefin-VYW2F7L2-Contvx58.js} +1 -1
  85. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BgVnjyEs.js → cynefinDiagram-MW4NZA55-Cigq7cjk.js} +1 -1
  86. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CR25E4bu.js → dagre-VZM6K2ZE-dEbC0nxb.js} +1 -1
  87. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CPM2h5HY.js → diagram-7IWD3JNH-CzIztESV.js} +1 -1
  88. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DqF0x2g1.js → diagram-B4RE2ZJO-Bo14zbaY.js} +1 -1
  89. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-G8BYp2eb.js → diagram-LBJQPF4R-C4xUyil6.js} +1 -1
  90. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BbC4kT_D.js → diagram-Q27KOJAE-CaBY_uYa.js} +1 -1
  91. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CKZwZ-G5.js → diagram-UB23O5K3-ClUXUeiu.js} +1 -1
  92. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-kJDmheia.js → ebnfDiagram-BXEA7PRR-BbGCgjC6.js} +1 -1
  93. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DFTVHyxB.js → erDiagram-JOGREHBK-Dr-AcwSl.js} +1 -1
  94. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BqdN5_os.js → flowDiagram-UKHOOZJN-BG8Gcfyc.js} +1 -1
  95. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-SYDjVakh.js → ganttDiagram-PKOTCBZU-CFVaPs5r.js} +1 -1
  96. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js → gitGraphDiagram-DS77QQ5N-ChJ8pm-O.js} +1 -1
  97. package/dist/worker/console/static/assets/index-B_5GhVIO.js +488 -0
  98. package/dist/worker/console/static/assets/index-C7YMBluX.css +1 -0
  99. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DIeAmJ45.js → infoDiagram-6WML65LV-C5J9-uCZ.js} +1 -1
  100. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-oikqUAlo.js → ishikawaDiagram-WSZJBQD7-BcfjqzDv.js} +1 -1
  101. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-mKEF_xYT.js → journeyDiagram-NVQOT4AX-BzzCO-rD.js} +1 -1
  102. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BdWJHPnN.js → kanban-definition-27J2QSJJ-NCHUZ46h.js} +1 -1
  103. package/dist/worker/console/static/assets/{linear-DGRlieMH.js → linear-BKZlWCUU.js} +1 -1
  104. package/dist/worker/console/static/assets/{mermaid.core-4P9yPC9c.js → mermaid.core-DbTaQjFE.js} +5 -5
  105. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-5PTSkos6.js → mindmap-definition-FAOFIHXS-CroYl5xQ.js} +1 -1
  106. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DONIFpG5.js → pegDiagram-VL7TDLO6-DgMBx-9k.js} +1 -1
  107. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-D_Z4S70M.js → pieDiagram-7S7Q4E2Y-CyWDmbq1.js} +1 -1
  108. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CAsjh8iY.js → quadrantDiagram-CIZ2JOQS-BgGEOqSP.js} +1 -1
  109. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Cx4Dd6ac.js → railroadDiagram-AXF67PYL-BMZ7oeeV.js} +1 -1
  110. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-cQb7mwyH.js → requirementDiagram-LRYGKXZP-DxqyGhcc.js} +1 -1
  111. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cd-DhUdf.js → sankeyDiagram-W5VNT64P-DnSvNCG8.js} +1 -1
  112. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-AlKYRgIk.js → sequenceDiagram-SI44F4Z6-BiZh6f-R.js} +1 -1
  113. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CUy_ET4e.js → sizeCapture-X5ZJPWSS-BKXSI8WB.js} +1 -1
  114. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CTuVe6Sa.js → stateDiagram-OKZ733FA-CfSgoYFm.js} +1 -1
  115. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DgHi3vjb.js +1 -0
  116. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-ChFLJb-v.js → swimlanes-SLNWSIFB-CyGADcHe.js} +2 -2
  117. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bgvdt1Y0.js +8 -0
  118. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DZZeiAPr.js → timeline-definition-Z64GVDOM-HmPaSPOK.js} +1 -1
  119. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-t4O91cZU.js → vennDiagram-T6HMQDX7-VA2VBbez.js} +1 -1
  120. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CV5qGHZu.js → wardleyDiagram-T6FBY63Y-BxjiBPZa.js} +1 -1
  121. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DyXbytUI.js → xychartDiagram-ELKLHX3M--hUY-qFj.js} +1 -1
  122. package/dist/worker/console/static/index.html +2 -2
  123. package/dist/worker/console/static/pets/ember/pet.json +167 -0
  124. package/dist/worker/console/static/pets/ember/spritesheet.png +0 -0
  125. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +4 -3
  126. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +57 -9
  127. package/dist/worker/console/static-src/operator-chat/use-stat-dialog.js +95 -0
  128. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +6 -0
  129. package/dist/worker/console/workspace-context.js +15 -0
  130. package/dist/worker/console/workspace-initialization.js +25 -16
  131. package/dist/worker/delivery/package.js +4 -1
  132. package/dist/worker/materialize/harness-task-lineage.js +4 -2
  133. package/dist/worker/observe/static/console-theme.css +33 -29
  134. package/dist/worker/observe/static/operator-chrome.js +1 -42
  135. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +3 -1
  136. package/dist/workflows/dag/backend-test-layout.js +27 -7
  137. package/dist/workflows/dag/backend-test-markdown-workflow.js +3 -1
  138. package/dist/workflows/dag/backend-test-pytest-collection.js +4 -1
  139. package/dist/workflows/dag/final-verification.js +45 -10
  140. package/dist/workflows/dag/frontend-test-case-checklist.js +12 -3
  141. package/dist/workflows/dag/frontend-test-environment-probe.js +0 -2
  142. package/dist/workflows/dag/frontend-test-layout.js +41 -12
  143. package/dist/workflows/dag/hybrid/sources.js +35 -16
  144. package/dist/workflows/dag/hybrid/templates/backend-test.js +70 -55
  145. package/dist/workflows/dag/hybrid/templates/frontend-test.js +62 -41
  146. package/dist/workflows/dag/hybrid/templates/kg-bootstrap.js +6 -6
  147. package/dist/workflows/dag/hybrid/templates/knowledge-sync.js +5 -5
  148. package/dist/workflows/dag/skill-instructions.js +102 -32
  149. package/dist/workflows/dag/skill-snapshot.js +3 -0
  150. package/dist/workflows/dag/skills.js +7 -7
  151. package/dist/workflows/dag/test-artifact-namespace.js +73 -0
  152. package/dist/workflows/dag/validate.js +24 -5
  153. package/dist/workflows/dag/workspace-checkpoint.js +13 -1
  154. package/docs/architecture/runtime-boundaries.md +28 -22
  155. package/docs/init-surface.manifest.json +43 -295
  156. package/docs/templates/README.md +4 -4
  157. package/docs/templates/agent-dag.base.json +9 -9
  158. package/docs/templates/agent-dag.final-verification.json +6 -6
  159. package/docs/templates/agent-dag.supervised-implementation.json +12 -12
  160. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +3 -3
  161. package/docs/templates/backend-test-dag.json +708 -708
  162. package/docs/templates/backend-test-dag.review-cases.prompt.md +3 -3
  163. package/docs/templates/frontend-implementation-dag.json +2 -2
  164. package/docs/templates/frontend-test-case-checklist.md +2 -2
  165. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +7 -7
  166. package/docs/templates/frontend-test-dag.json +48 -39
  167. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +2 -2
  168. package/docs/templates/frontend-test-dag.retrospect.prompt.md +1 -1
  169. package/docs/templates/frontend-test-dag.review-cases.prompt.md +1 -1
  170. package/docs/templates/hybrid-dag.json +9 -9
  171. package/docs/templates/knowledge-sync-dag.json +8 -8
  172. package/package.json +12 -3
  173. package/resource-catalog.v1.json +1973 -0
  174. package/resources/agent-bridge/loop-agent/SKILL.md +40 -0
  175. package/resources/init-history/legacy-surface.v1.json +33 -0
  176. package/skills/fe-test-ui-scout/SKILL.md +4 -4
  177. package/skills/fe-test-ui-scout/references/ledger-schema.md +1 -1
  178. package/skills/init-update/SKILL.md +34 -0
  179. package/skills/init-update/agents/openai.yaml +6 -0
  180. package/skills/playwright-cli/SKILL.md +1 -1
  181. package/skills/playwright-cli-case-generator/SKILL.md +4 -4
  182. package/dist/worker/console/static/assets/channel-CyBY_yGk.js +0 -1
  183. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +0 -1
  184. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +0 -1
  185. package/dist/worker/console/static/assets/index-B28Onmfy.css +0 -1
  186. package/dist/worker/console/static/assets/index-CTMZsDmh.js +0 -486
  187. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +0 -1
  188. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +0 -8
@@ -0,0 +1,263 @@
1
+ import { createHash } from "node:crypto";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import { lstat, open, readdir, realpath, stat } from "node:fs/promises";
4
+ import path from "node:path";
5
+ export const PROJECT_CAPABILITY_SCANNER_VERSION = 1;
6
+ const MAX_INPUT_BYTES = 1024 * 1024;
7
+ const RISKY_COMMAND_RE = /(?:^|[^A-Za-z0-9])(deploy|publish|release|upload|push|destroy|apply|login|logout)(?:$|[^A-Za-z0-9])/i;
8
+ const SHELL_COMPOSITION_RE = /(?:&&|\|\||[|;<>`]|\$\(|\r|\n)/;
9
+ const SAFE_NODE_SCRIPT_RE = /^(?:(?:npx|pnpm exec|yarn exec)\s+)?(?:tsc|eslint|biome|prettier|vitest|jest|mocha|ava|vite|next|node --test)(?:\s|$)/i;
10
+ export function classifyVerificationCommand(command) {
11
+ const normalized = command.trim();
12
+ if (!normalized || RISKY_COMMAND_RE.test(normalized) || SHELL_COMPOSITION_RE.test(normalized))
13
+ return "unsafe";
14
+ return "safe";
15
+ }
16
+ function isContained(root, candidate) {
17
+ const relative = path.relative(root, candidate);
18
+ return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
19
+ }
20
+ async function readSafeInput(repoRoot, relativePath) {
21
+ const file = path.resolve(repoRoot, ...relativePath.split("/"));
22
+ if (!isContained(path.resolve(repoRoot), file))
23
+ return null;
24
+ try {
25
+ const [rootReal, fileReal, linkInfo] = await Promise.all([realpath(repoRoot), realpath(file), lstat(file)]);
26
+ if (!isContained(rootReal, fileReal) || !linkInfo.isFile() || linkInfo.isSymbolicLink() || linkInfo.nlink !== 1)
27
+ return null;
28
+ if (linkInfo.size > MAX_INPUT_BYTES)
29
+ return null;
30
+ const noFollow = "O_NOFOLLOW" in fsConstants ? fsConstants.O_NOFOLLOW : 0;
31
+ const handle = await open(file, fsConstants.O_RDONLY | noFollow);
32
+ let before;
33
+ let after;
34
+ let bytes;
35
+ try {
36
+ before = await handle.stat();
37
+ if (!before.isFile() || before.nlink !== 1 || before.size > MAX_INPUT_BYTES)
38
+ return null;
39
+ bytes = await handle.readFile();
40
+ after = await handle.stat();
41
+ }
42
+ finally {
43
+ await handle.close();
44
+ }
45
+ const [pathAfter, fileRealAfter] = await Promise.all([stat(file), realpath(file)]);
46
+ if (!isContained(rootReal, fileRealAfter))
47
+ return null;
48
+ if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs)
49
+ return null;
50
+ if (after.dev !== pathAfter.dev || after.ino !== pathAfter.ino || after.size !== pathAfter.size)
51
+ return null;
52
+ return {
53
+ path: relativePath,
54
+ bytes: bytes.byteLength,
55
+ sha256: createHash("sha256").update(bytes).digest("hex"),
56
+ content: bytes.toString("utf8"),
57
+ };
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ function isSafeNodeScript(name, scripts, visiting = new Set()) {
64
+ if (visiting.has(name))
65
+ return false;
66
+ const body = scripts[name];
67
+ if (typeof body !== "string" || classifyVerificationCommand(body) !== "safe")
68
+ return false;
69
+ if (SAFE_NODE_SCRIPT_RE.test(body.trim()))
70
+ return true;
71
+ const delegated = /^(?:npm|pnpm|yarn)\s+(?:run\s+)?([A-Za-z0-9:_-]+)$/.exec(body.trim());
72
+ if (!delegated)
73
+ return false;
74
+ visiting.add(name);
75
+ const safe = isSafeNodeScript(delegated[1], scripts, visiting);
76
+ visiting.delete(name);
77
+ return safe;
78
+ }
79
+ function parseJson(input) {
80
+ if (!input)
81
+ return null;
82
+ try {
83
+ const parsed = JSON.parse(input.content);
84
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ }
90
+ function nodePackageManager(pkg, evidence) {
91
+ if (typeof pkg.packageManager === "string") {
92
+ if (pkg.packageManager.startsWith("pnpm@"))
93
+ return "pnpm";
94
+ if (pkg.packageManager.startsWith("yarn@"))
95
+ return "yarn";
96
+ }
97
+ if (evidence.has("pnpm-lock.yaml"))
98
+ return "pnpm";
99
+ if (evidence.has("yarn.lock"))
100
+ return "yarn";
101
+ return "npm";
102
+ }
103
+ function nodeCommand(pm, script) {
104
+ if (script === "test")
105
+ return `${pm} test`;
106
+ return pm === "npm" ? `npm run ${script}` : `${pm} run ${script}`;
107
+ }
108
+ function workspacePatterns(pkg) {
109
+ const raw = Array.isArray(pkg.workspaces)
110
+ ? pkg.workspaces
111
+ : pkg.workspaces && typeof pkg.workspaces === "object" && Array.isArray(pkg.workspaces.packages)
112
+ ? pkg.workspaces.packages
113
+ : [];
114
+ return raw.filter((item) => typeof item === "string" && item.endsWith("/*") && !item.includes(".."));
115
+ }
116
+ async function discoverWorkspaceCandidates(repoRoot, patterns) {
117
+ const candidates = [];
118
+ for (const pattern of patterns) {
119
+ const parent = pattern.slice(0, -2).replaceAll("\\", "/");
120
+ const parentPath = path.resolve(repoRoot, ...parent.split("/"));
121
+ if (!isContained(path.resolve(repoRoot), parentPath))
122
+ continue;
123
+ let names;
124
+ try {
125
+ names = (await readdir(parentPath, { withFileTypes: true }))
126
+ .filter((entry) => entry.isDirectory() && !entry.isSymbolicLink())
127
+ .map((entry) => entry.name)
128
+ .sort();
129
+ }
130
+ catch {
131
+ continue;
132
+ }
133
+ for (const name of names) {
134
+ const relative = `${parent}/${name}`;
135
+ if (await readSafeInput(repoRoot, `${relative}/package.json`))
136
+ candidates.push(relative);
137
+ }
138
+ }
139
+ return [...new Set(candidates)].sort();
140
+ }
141
+ async function rootDotnetFiles(repoRoot) {
142
+ try {
143
+ return (await readdir(repoRoot, { withFileTypes: true }))
144
+ .filter((entry) => entry.isFile() && (/\.sln$/i.test(entry.name) || /\.csproj$/i.test(entry.name)))
145
+ .map((entry) => entry.name)
146
+ .sort();
147
+ }
148
+ catch {
149
+ return [];
150
+ }
151
+ }
152
+ export class ProjectCapabilityScanner {
153
+ async scan(repoRoot, options = {}) {
154
+ const root = path.resolve(repoRoot);
155
+ const evidence = new Map();
156
+ const add = async (relative) => {
157
+ if (evidence.has(relative))
158
+ return evidence.get(relative);
159
+ const value = await readSafeInput(root, relative);
160
+ if (value)
161
+ evidence.set(relative, value);
162
+ return value;
163
+ };
164
+ for (const name of [
165
+ "README.md", "package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock",
166
+ "pyproject.toml", "go.mod", "Cargo.toml", "pom.xml", "build.gradle", "build.gradle.kts",
167
+ "gradlew", "gradlew.bat", "Makefile",
168
+ ])
169
+ await add(name);
170
+ for (const name of await rootDotnetFiles(root))
171
+ await add(name);
172
+ const rootPkg = parseJson(evidence.get("package.json") ?? null);
173
+ const workspaceCandidates = rootPkg ? await discoverWorkspaceCandidates(root, workspacePatterns(rootPkg)) : [];
174
+ const requestedWorkspace = options.workspaceRoot?.replaceAll("\\", "/").replace(/^\.\//, "") ?? null;
175
+ const explicitRoot = requestedWorkspace === ".";
176
+ let selectedWorkspace = explicitRoot ? null : requestedWorkspace;
177
+ if (selectedWorkspace && !workspaceCandidates.includes(selectedWorkspace)) {
178
+ throw new Error(`workspaceRoot is not a discovered workspace: ${selectedWorkspace}`);
179
+ }
180
+ if (!explicitRoot && !selectedWorkspace && workspaceCandidates.length === 1)
181
+ selectedWorkspace = workspaceCandidates[0];
182
+ const ambiguousWorkspace = !explicitRoot && !selectedWorkspace && workspaceCandidates.length > 1;
183
+ if (selectedWorkspace)
184
+ await add(`${selectedWorkspace}/package.json`);
185
+ const commands = [];
186
+ const ecosystems = new Set();
187
+ const push = (ecosystem, id, command, evidencePaths, cwd = ".") => {
188
+ if (classifyVerificationCommand(command) !== "safe")
189
+ return;
190
+ ecosystems.add(ecosystem);
191
+ commands.push({ id, command, cwd, scope: cwd === "." ? ["**"] : [`${cwd}/**`], evidence: evidencePaths, confidence: "high", source: "scanner" });
192
+ };
193
+ const nodeManifestPath = selectedWorkspace ? `${selectedWorkspace}/package.json` : "package.json";
194
+ const nodePkg = parseJson(evidence.get(nodeManifestPath) ?? null);
195
+ if (nodePkg) {
196
+ ecosystems.add("node");
197
+ if (!ambiguousWorkspace) {
198
+ const pm = nodePackageManager(rootPkg ?? nodePkg, evidence);
199
+ for (const script of ["typecheck", "lint", "test", "build"]) {
200
+ const body = nodePkg.scripts?.[script];
201
+ if (typeof body !== "string" || !isSafeNodeScript(script, nodePkg.scripts ?? {}))
202
+ continue;
203
+ push("node", `node:${script}`, nodeCommand(pm, script), [nodeManifestPath], selectedWorkspace ?? ".");
204
+ }
205
+ }
206
+ }
207
+ if (evidence.has("pyproject.toml")) {
208
+ ecosystems.add("python");
209
+ if (/\[tool\.pytest\.ini_options\]/.test(evidence.get("pyproject.toml").content))
210
+ push("python", "python:test", "python -m pytest", ["pyproject.toml"]);
211
+ }
212
+ if (evidence.has("go.mod")) {
213
+ ecosystems.add("go");
214
+ push("go", "go:test", "go test ./...", ["go.mod"]);
215
+ }
216
+ if (evidence.has("Cargo.toml")) {
217
+ ecosystems.add("rust");
218
+ push("rust", "rust:test", "cargo test", ["Cargo.toml"]);
219
+ }
220
+ if (evidence.has("pom.xml")) {
221
+ ecosystems.add("maven");
222
+ push("maven", "maven:test", "mvn test", ["pom.xml"]);
223
+ }
224
+ const gradle = evidence.has("build.gradle.kts") ? "build.gradle.kts" : evidence.has("build.gradle") ? "build.gradle" : null;
225
+ if (gradle) {
226
+ ecosystems.add("gradle");
227
+ push("gradle", "gradle:test", process.platform === "win32" && evidence.has("gradlew.bat") ? "gradlew.bat test" : evidence.has("gradlew") ? "./gradlew test" : "gradle test", [gradle]);
228
+ }
229
+ if (evidence.has("Makefile")) {
230
+ ecosystems.add("make");
231
+ if (/^test\s*:/m.test(evidence.get("Makefile").content))
232
+ push("make", "make:test", "make test", ["Makefile"]);
233
+ }
234
+ const dotnet = [...evidence.keys()].filter((name) => /\.(?:sln|csproj)$/i.test(name));
235
+ if (dotnet.length > 0)
236
+ ecosystems.add("dotnet");
237
+ if (dotnet.length === 1)
238
+ push("dotnet", "dotnet:test", `dotnet test ${dotnet[0]}`, [dotnet[0]]);
239
+ const evidenceList = [...evidence.values()]
240
+ .map(({ content: _content, ...item }) => item)
241
+ .sort((a, b) => a.path.localeCompare(b.path));
242
+ const fingerprintHash = createHash("sha256");
243
+ fingerprintHash.update(`project-capability-scanner-v${PROJECT_CAPABILITY_SCANNER_VERSION}\n`);
244
+ for (const item of evidenceList)
245
+ fingerprintHash.update(`${item.path}\0${item.sha256}\n`);
246
+ fingerprintHash.update(`workspace\0${explicitRoot ? "." : selectedWorkspace ?? ""}\n`);
247
+ const status = ambiguousWorkspace ? "ambiguous" : commands.length > 0 ? "ready" : "unavailable";
248
+ return {
249
+ schemaVersion: 1,
250
+ scannerVersion: 1,
251
+ repoRoot: root,
252
+ workspace: { selected: explicitRoot ? "." : selectedWorkspace, candidates: workspaceCandidates },
253
+ ecosystems: [...ecosystems].sort(),
254
+ evidence: evidenceList,
255
+ fingerprint: fingerprintHash.digest("hex"),
256
+ verification: {
257
+ status,
258
+ commands: ambiguousWorkspace ? [] : commands,
259
+ requiredInput: ambiguousWorkspace ? ["workspaceRoot"] : commands.length === 0 ? ["verification"] : [],
260
+ },
261
+ };
262
+ }
263
+ }
@@ -0,0 +1,96 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { z } from "zod";
4
+ import { harnessManifestSchema, verificationCommandsSchema, workflowPolicySchema, workerPolicySchema, worktreeManifestConfigSchema, } from "./manifest-types.js";
5
+ function isSafeProjectPath(value) {
6
+ if (!value || path.isAbsolute(value))
7
+ return false;
8
+ return !value.replaceAll("\\", "/").split("/").some((part) => part === "" || part === "." || part === "..");
9
+ }
10
+ const nullablePathMapSchema = z.record(z.string(), z.string().refine(isSafeProjectPath, "path must be a safe project-relative path").nullable());
11
+ /** Shared, credential-free overrides that are safe to commit with the project. */
12
+ export const projectConfigSchema = z
13
+ .object({
14
+ $schema: z.string().optional(),
15
+ version: z.literal(1),
16
+ verification: verificationCommandsSchema.optional(),
17
+ entrypoints: nullablePathMapSchema.optional(),
18
+ artifacts: nullablePathMapSchema.optional(),
19
+ workflowPolicy: workflowPolicySchema.optional(),
20
+ workerPolicy: workerPolicySchema.optional(),
21
+ worktree: worktreeManifestConfigSchema.optional(),
22
+ })
23
+ .strict();
24
+ const EMPTY_VERIFICATION = { quick: [], standard: [], full: [] };
25
+ function appendUnique(left, right) {
26
+ return [...new Set([...left, ...right])];
27
+ }
28
+ function mergeVerification(tracked, local) {
29
+ const shared = tracked ?? EMPTY_VERIFICATION;
30
+ const machine = local ?? EMPTY_VERIFICATION;
31
+ const sources = [];
32
+ if ([...shared.quick, ...shared.standard, ...shared.full].length > 0)
33
+ sources.push("tracked");
34
+ if ([...machine.quick, ...machine.standard, ...machine.full].length > 0)
35
+ sources.push("local");
36
+ return {
37
+ quick: appendUnique(shared.quick, machine.quick),
38
+ standard: appendUnique(shared.standard, machine.standard),
39
+ full: appendUnique(shared.full, machine.full),
40
+ reproducibility: sources.includes("local") ? "local-only" : sources.includes("tracked") ? "tracked" : "inferred",
41
+ sources,
42
+ };
43
+ }
44
+ async function readOptionalJson(file) {
45
+ try {
46
+ return JSON.parse(await readFile(file, "utf8"));
47
+ }
48
+ catch (error) {
49
+ if (error.code === "ENOENT")
50
+ return undefined;
51
+ throw error;
52
+ }
53
+ }
54
+ /**
55
+ * Load package defaults, tracked project policy, then machine-local runtime
56
+ * settings. Local verification is additive by construction: it cannot remove
57
+ * or replace a command committed in project.json.
58
+ */
59
+ export async function loadNormalizedProjectConfig(repoRoot) {
60
+ const localPath = path.join(repoRoot, "harness.json");
61
+ const trackedPath = path.join(repoRoot, ".loop-agent", "project.json");
62
+ const [localRaw, trackedRaw] = await Promise.all([
63
+ readOptionalJson(localPath),
64
+ readOptionalJson(trackedPath),
65
+ ]);
66
+ if (localRaw === undefined)
67
+ throw Object.assign(new Error("harness.json not found"), { code: "HARNESS_NOT_FOUND" });
68
+ const local = harnessManifestSchema.parse(localRaw);
69
+ const tracked = trackedRaw === undefined ? undefined : projectConfigSchema.parse(trackedRaw);
70
+ const verification = mergeVerification(tracked?.verification, local.verification);
71
+ const localInput = localRaw;
72
+ const localFields = { ...local };
73
+ delete localFields.modelProfiles;
74
+ const manifest = harnessManifestSchema.parse({
75
+ ...localFields,
76
+ entrypoints: { ...(tracked?.entrypoints ?? {}), ...(local.entrypoints ?? {}) },
77
+ artifacts: { ...(tracked?.artifacts ?? {}), ...(local.artifacts ?? {}) },
78
+ workflowPolicy: Object.hasOwn(localInput, "workflowPolicy") ? local.workflowPolicy : tracked?.workflowPolicy ?? local.workflowPolicy,
79
+ workerPolicy: Object.hasOwn(localInput, "workerPolicy") ? local.workerPolicy : tracked?.workerPolicy ?? local.workerPolicy,
80
+ worktree: Object.hasOwn(localInput, "worktree") ? local.worktree : tracked?.worktree ?? local.worktree,
81
+ verification: {
82
+ quick: verification.quick,
83
+ standard: verification.standard,
84
+ full: verification.full,
85
+ },
86
+ });
87
+ return {
88
+ schemaVersion: 1,
89
+ manifest,
90
+ verification,
91
+ sources: {
92
+ tracked: tracked ? ".loop-agent/project.json" : null,
93
+ local: "harness.json",
94
+ },
95
+ };
96
+ }
@@ -0,0 +1,151 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, readFile, realpath, stat } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { z } from "zod";
6
+ import { findPackageRoot } from "../shared/package-metadata.js";
7
+ const SHA256_RE = /^[a-f0-9]{64}$/;
8
+ function isSafePackagePath(value) {
9
+ if (!value || path.isAbsolute(value) || value.includes("\\"))
10
+ return false;
11
+ const parts = value.split("/");
12
+ return parts.every((part) => part !== "" && part !== "." && part !== "..");
13
+ }
14
+ export const resourceCatalogEntrySchema = z
15
+ .object({
16
+ id: z.string().trim().min(1).regex(/^[A-Za-z0-9][A-Za-z0-9:._/-]*$/),
17
+ path: z.string().refine(isSafePackagePath, "path must stay within the package root"),
18
+ kind: z.enum(["document", "skill", "template", "schema", "prompt", "script", "other"]),
19
+ bytes: z.number().int().nonnegative(),
20
+ sha256: z.string().regex(SHA256_RE),
21
+ schemaVersion: z.number().int().positive(),
22
+ })
23
+ .strict();
24
+ export const resourceCatalogSchema = z
25
+ .object({
26
+ schemaVersion: z.literal(1),
27
+ entries: z.array(resourceCatalogEntrySchema),
28
+ })
29
+ .strict()
30
+ .superRefine((catalog, ctx) => {
31
+ const ids = new Set();
32
+ const paths = new Set();
33
+ for (const [index, entry] of catalog.entries.entries()) {
34
+ if (ids.has(entry.id))
35
+ ctx.addIssue({ code: "custom", path: ["entries", index, "id"], message: "duplicate resource id" });
36
+ if (paths.has(entry.path))
37
+ ctx.addIssue({ code: "custom", path: ["entries", index, "path"], message: "duplicate resource path" });
38
+ ids.add(entry.id);
39
+ paths.add(entry.path);
40
+ }
41
+ });
42
+ export const CONTROLLER_RESOURCE_IDS = {
43
+ loopAgentBridgeSkill: "agent-bridge:loop-agent",
44
+ };
45
+ export function builtinSkillResourceId(name) {
46
+ return `builtin-skill:${name}`;
47
+ }
48
+ export function builtinSkillReferenceResourceId(name, relativePath) {
49
+ return `builtin-skill-resource:${name}/${relativePath}`;
50
+ }
51
+ export class ResourceCatalogError extends Error {
52
+ code;
53
+ constructor(code, message) {
54
+ super(message);
55
+ this.code = code;
56
+ this.name = "ResourceCatalogError";
57
+ }
58
+ }
59
+ function sha256(bytes) {
60
+ return createHash("sha256").update(bytes).digest("hex");
61
+ }
62
+ async function assertContainedRegularFile(root, candidate) {
63
+ const [rootReal, candidateReal, linkInfo] = await Promise.all([
64
+ realpath(root),
65
+ realpath(candidate),
66
+ lstat(candidate),
67
+ ]);
68
+ const relative = path.relative(rootReal, candidateReal);
69
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
70
+ throw new ResourceCatalogError("RESOURCE_PATH_ESCAPE", `resource resolves outside package root: ${candidate}`);
71
+ }
72
+ if (!linkInfo.isFile() || linkInfo.isSymbolicLink() || linkInfo.nlink !== 1) {
73
+ throw new ResourceCatalogError("RESOURCE_NOT_REGULAR", `resource is not a regular file: ${candidate}`);
74
+ }
75
+ }
76
+ /** Lazy package resource provider: load the small catalog once, hash bytes only when used. */
77
+ export class ControllerContextProvider {
78
+ packageRoot;
79
+ identity;
80
+ entries;
81
+ constructor(packageRoot, catalog, catalogBytes) {
82
+ this.packageRoot = packageRoot;
83
+ this.entries = new Map(catalog.entries.map((entry) => [entry.id, entry]));
84
+ this.identity = { schemaVersion: 1, catalogDigest: sha256(catalogBytes) };
85
+ }
86
+ static async load(packageRoot, catalogName = "resource-catalog.v1.json") {
87
+ const catalogPath = path.join(packageRoot, catalogName);
88
+ await assertContainedRegularFile(packageRoot, catalogPath);
89
+ const bytes = await readFile(catalogPath);
90
+ let parsed;
91
+ try {
92
+ parsed = JSON.parse(bytes.toString("utf8"));
93
+ }
94
+ catch (error) {
95
+ throw new ResourceCatalogError("RESOURCE_CATALOG_INVALID", `invalid resource catalog JSON: ${String(error)}`);
96
+ }
97
+ const catalog = resourceCatalogSchema.safeParse(parsed);
98
+ if (!catalog.success) {
99
+ throw new ResourceCatalogError("RESOURCE_CATALOG_INVALID", `invalid resource catalog: ${catalog.error.message}`);
100
+ }
101
+ return new ControllerContextProvider(packageRoot, catalog.data, bytes);
102
+ }
103
+ list() {
104
+ return [...this.entries.values()].map((entry) => ({ ...entry }));
105
+ }
106
+ entry(id) {
107
+ const entry = this.entries.get(id);
108
+ return entry ? { ...entry } : undefined;
109
+ }
110
+ async read(id) {
111
+ const entry = this.entries.get(id);
112
+ if (!entry)
113
+ throw new ResourceCatalogError("RESOURCE_NOT_FOUND", `resource not found: ${id}`);
114
+ const file = path.join(this.packageRoot, ...entry.path.split("/"));
115
+ await assertContainedRegularFile(this.packageRoot, file);
116
+ const before = await stat(file);
117
+ const bytes = await readFile(file);
118
+ const after = await stat(file);
119
+ if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
120
+ throw new ResourceCatalogError("RESOURCE_CHANGED_DURING_READ", `resource changed while reading: ${id}`);
121
+ }
122
+ if (bytes.byteLength !== entry.bytes || sha256(bytes) !== entry.sha256) {
123
+ throw new ResourceCatalogError("RESOURCE_HASH_MISMATCH", `resource does not match catalog: ${id}`);
124
+ }
125
+ return bytes;
126
+ }
127
+ async readText(id) {
128
+ const entry = this.entries.get(id);
129
+ if (!entry)
130
+ throw new ResourceCatalogError("RESOURCE_NOT_FOUND", `resource not found: ${id}`);
131
+ const bytes = await this.read(id);
132
+ return {
133
+ id,
134
+ relativePath: entry.path,
135
+ absolutePath: path.join(this.packageRoot, ...entry.path.split("/")),
136
+ kind: entry.kind,
137
+ schemaVersion: entry.schemaVersion,
138
+ bytes: entry.bytes,
139
+ sha256: entry.sha256,
140
+ text: bytes.toString("utf8"),
141
+ };
142
+ }
143
+ }
144
+ export async function createControllerContextProvider(options = {}) {
145
+ const packageRoot = options.packageRoot
146
+ ?? findPackageRoot(path.dirname(fileURLToPath(import.meta.url)));
147
+ if (!packageRoot) {
148
+ throw new ResourceCatalogError("PACKAGE_ROOT_NOT_FOUND", "cannot locate the loop-agent package root for controller resources");
149
+ }
150
+ return ControllerContextProvider.load(packageRoot, options.catalogName ?? "resource-catalog.v1.json");
151
+ }
@@ -290,13 +290,17 @@ export async function removeStaged(filePath) {
290
290
  await rm(filePath, { force: true });
291
291
  }
292
292
  /** Synchronous helpers used by boot-time path checks. */
293
- export function writeSecureJsonSync(filePath, value) {
293
+ export function writeSecureJsonSync(filePath, value, options) {
294
294
  const dir = path.dirname(filePath);
295
295
  ensureSecureDir(dir);
296
296
  const resolved = assertSafeAppDataPath(dir, filePath);
297
297
  const tmp = `${resolved}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
298
+ const pretty = options?.pretty !== false;
299
+ const body = pretty
300
+ ? `${JSON.stringify(value, null, 2)}\n`
301
+ : `${JSON.stringify(value)}\n`;
298
302
  try {
299
- writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, {
303
+ writeFileSync(tmp, body, {
300
304
  encoding: "utf8",
301
305
  mode: 0o600,
302
306
  });
@@ -395,7 +395,7 @@ export function buildOperatorCapabilitiesDocument() {
395
395
  {
396
396
  action: "prepareScheduledTask", cli: "console scheduled task preparation", kind: "mutation",
397
397
  inputSchemaVersion: 1, resultSchemaVersion: 1, envelopeSchemaVersion: 1, requiredErrorCodes: ["INVALID_INPUT"],
398
- description: "Preferred entry for natural-language one-time scheduled tasks, including read-only inspection reports. Call this directly after understanding the goal, output scope, executable acceptance and time; the server uses the CURRENT project directory and creates the immutable PRD, managed Task and strict writeSet gate, binds THIS Chat automatically and returns a REAL planned goal/confirmation card. No pre-existing task, worktree or session ID is needed: never ask the user for these internal fields and do not manually create/edit them with bash/write/newTask/importPrd. Keep preparation concise: preserve the user goal without inventing extra requirements. Omit budget for ordinary tasks. Use minimal independent acceptance commands (AC-1, AC-2), preferably existing test/build scripts or test -s scheduled-report.md for a report; no compound shell/pipelines/substitution, language-specific grep checks or redundant scope assertions (the host enforces scope). Infer routine defaults and verification commands from the repository; ask only about genuinely ambiguous goal/scope/time. For read-only requests allow just an ordinary report file in the current project directory, e.g. scheduled-report.md, not .harness/. Use schedule.delayMinutes for relative time (counted after preparation finishes), or localAt + IANA timezone for an exact date/time. The current workspace must match committed HEAD; never auto-commit/stash/reset existing changes. The card exposes the exact time, bounded defaults and committed base before one human confirmation. Same request in this Chat is idempotent. Only a returned goalId with state=planned is a prepared reservation; textual drafts, Tasks or failed operations are NOT bookings. Never replace a requested schedule with immediate execution or sleep, never call runDag or self-confirm. On failure explain the returned stage, correct business inputs and retry this tool; do not mutate imported PRD/runtime artifacts. For a new distinct appointment, make its goal/time distinct.",
398
+ description: "Preferred entry for natural-language one-time scheduled tasks, including read-only inspection reports. Call this directly after understanding the goal, output scope, executable acceptance and time; the server uses the CURRENT project directory and creates the immutable PRD, managed Task and strict writeSet gate, binds THIS Chat automatically and returns a REAL planned goal/confirmation card. No pre-existing task, worktree or session ID is needed: never ask the user for these internal fields and do not manually create/edit them with bash/write/newTask/importPrd. Keep preparation concise: preserve the user goal without inventing extra requirements. Omit budget for ordinary tasks: the host freezes 5 goal attempts, 5 DAG runs and 4 supervisor calls, with a 15-minute latest-start grace and a 240-minute execution window, all shown before confirmation. maxTurns counts goal attempts, not model tool calls. Explicit budget or maxDurationMinutes is never raised. Use minimal independent acceptance commands (AC-1, AC-2), preferably existing test/build scripts or test -s scheduled-report.md for a report; no compound shell/pipelines/substitution, language-specific grep checks or redundant scope assertions (the host enforces scope). Infer routine defaults and verification commands from the repository; ask only about genuinely ambiguous goal/scope/time. For read-only requests allow just an ordinary report file in the current project directory, e.g. scheduled-report.md, not .harness/. Use schedule.delayMinutes for relative time (counted after preparation finishes), or localAt + IANA timezone for an exact date/time. The current workspace must match committed HEAD; never auto-commit/stash/reset existing changes. The card exposes the exact time, bounded defaults and committed base before one human confirmation. Same request in this Chat is idempotent. Only a returned goalId with state=planned is a prepared reservation; textual drafts, Tasks or failed operations are NOT bookings. Never replace a requested schedule with immediate execution or sleep, never call runDag or self-confirm. On failure explain the returned stage, correct business inputs and retry this tool; do not mutate imported PRD/runtime artifacts. For a new distinct appointment, make its goal/time distinct.",
399
399
  inputParams: [{ name: "request", type: "object", required: true, description: "Business goal, workflow, allowedPaths, acceptance and schedule; optional forbiddenPaths/budget. Runtime identity is server-owned." }],
400
400
  modelCallable: "always", humanConfirmation: "none",
401
401
  },
@@ -409,7 +409,7 @@ export function buildOperatorCapabilitiesDocument() {
409
409
  "requiredErrorCodes": [
410
410
  "INVALID_INPUT"
411
411
  ],
412
- "description": "Advanced entry ONLY for an already prepared Task. For a new natural-language reservation use prepareScheduledTask instead. Requires the current repository or its linked worktree at frozenBase and a managed Task already strict-prepared at its writeSet gate. booking must include managedTaskId, originalGoal, chatSessionRef, workflow, worktreeRef, frozenBase, allowedPaths, forbiddenPaths, acceptance {policy, criteria [{acId,description,command}]}, budget {maxTurns,maxDagRuns,maxSupervisorCalls}, time {executeAtUtc,latestStartUtc,stopAtUtc,expiresAt,timezone,allowedHours,misfirePolicy}. Resolve natural-language dates with the user timezone; ask about ambiguous time/AC/scope. No execution or authorization occurs. Show the complete scope/time/budget for one browser confirmation. Never call runDag for this reservation.",
412
+ "description": "Advanced entry ONLY for an already prepared Task. For a new natural-language reservation use prepareScheduledTask instead. Requires the current repository or its linked worktree at frozenBase and a managed Task already strict-prepared at its writeSet gate. booking must include managedTaskId, originalGoal, chatSessionRef, workflow, worktreeRef, frozenBase, allowedPaths, forbiddenPaths, acceptance {policy, criteria [{acId,description,command}]}, budget {maxTurns,maxDagRuns,maxSupervisorCalls}, time {executeAtUtc,latestStartUtc,stopAtUtc,expiresAt,timezone,allowedHours,misfirePolicy}. This entry never fills omitted budget; for implementation work supply at least maxTurns=5, maxDagRuns=5, maxSupervisorCalls=4. Resolve natural-language dates with the user timezone; ask about ambiguous time/AC/scope. No execution or authorization occurs. Show the complete scope/time/budget for one browser confirmation. Never call runDag for this reservation.",
413
413
  "inputParams": [
414
414
  {
415
415
  "name": "booking",
@@ -106,8 +106,9 @@ export function computeBinarySha256(filePath) {
106
106
  * Compute a portable, deterministic content fingerprint for the installed
107
107
  * package rooted at `packageRoot`.
108
108
  *
109
- * The fingerprint covers four scopes (package.json, bin/**, dist/**,
110
- * skills/**) and is independent of the absolute installation path.
109
+ * The fingerprint covers executable code plus package-owned Skills, templates,
110
+ * bridge resources, and their generated catalog. It is independent of the
111
+ * absolute installation path.
111
112
  *
112
113
  * Sorting uses code-point order (UTF-8 byte comparison), NOT localeCompare,
113
114
  * to ensure deterministic behaviour across locales.
@@ -118,17 +119,23 @@ export function computePackageFingerprint(packageRoot) {
118
119
  "bin/**",
119
120
  "dist/**",
120
121
  "skills/**",
122
+ "docs/templates/**",
123
+ "resources/**",
124
+ "resource-catalog.v1.json",
121
125
  ];
122
126
  // Collecting entries ---------------------------------------------------
123
127
  const entries = [];
124
128
  // 1) package.json (required single file)
125
129
  addRequiredFile(entries, path.join(packageRoot, "package.json"), "");
126
- // 2) bin/**, dist/**, skills/** via recursive walk
127
- for (const dir of ["bin", "dist", "skills"]) {
130
+ // 2) Package-owned executable and controller-resource trees.
131
+ for (const dir of ["bin", "dist", "skills", "docs/templates", "resources"]) {
128
132
  const absoluteDir = path.join(packageRoot, dir);
129
133
  collectTree(entries, absoluteDir, dir);
130
134
  }
131
- // 3) Deterministic sort by relative path (code-point / byte order)
135
+ // 3) The generated catalog binds logical resource ids to those bytes. It is
136
+ // optional only for source/test fixtures; published packages always ship it.
137
+ addOptionalFile(entries, path.join(packageRoot, "resource-catalog.v1.json"), "");
138
+ // 4) Deterministic sort by relative path (code-point / byte order)
132
139
  entries.sort((a, b) => {
133
140
  const aBuf = Buffer.from(a.relativePath, "utf-8");
134
141
  const bBuf = Buffer.from(b.relativePath, "utf-8");
@@ -141,14 +148,14 @@ export function computePackageFingerprint(packageRoot) {
141
148
  }
142
149
  return aBuf.length - bBuf.length;
143
150
  });
144
- // 4) Cumulative hash: hash of (<relativePath> + "\t" + <contentHash>) lines
151
+ // 5) Cumulative hash: hash of (<relativePath> + "\t" + <contentHash>) lines
145
152
  const cumulative = createHash("sha256");
146
153
  for (const entry of entries) {
147
154
  cumulative.update(`${entry.relativePath}\t${entry.contentHash}\n`);
148
155
  }
149
156
  return {
150
157
  algorithm: "sha256",
151
- scopeVersion: 1,
158
+ scopeVersion: 2,
152
159
  scope,
153
160
  value: `sha256:${cumulative.digest("hex")}`,
154
161
  fileCount: entries.length,
@@ -316,6 +323,15 @@ function addRequiredFile(entries, absolutePath, relativeDir) {
316
323
  : path.basename(absolutePath);
317
324
  entries.push({ relativePath, contentHash });
318
325
  }
326
+ function addOptionalFile(entries, absolutePath, relativeDir) {
327
+ try {
328
+ addRequiredFile(entries, absolutePath, relativeDir);
329
+ }
330
+ catch (error) {
331
+ if (!isNotFound(error))
332
+ throw error;
333
+ }
334
+ }
319
335
  function collectTree(entries, absoluteDir, relativeDir) {
320
336
  let stats;
321
337
  try {