@tea-agent/loop-agent 0.39.0-next.3 → 0.39.0-next.30

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 (223) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +267 -65
  3. package/README.md +11 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/dag/generate-task-dag.js +9 -0
  6. package/dist/application/task-lifecycle/advance.js +4 -0
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +16 -5
  9. package/dist/cli/program.js +16 -2
  10. package/dist/cli/update/notifier.js +2 -2
  11. package/dist/cli.js +2 -1
  12. package/dist/commands/dag-rerun.js +55 -1
  13. package/dist/commands/init-model-catalog.js +464 -0
  14. package/dist/commands/init-upgrade.js +265 -97
  15. package/dist/commands/init.js +428 -27
  16. package/dist/commands/inspect-next.js +8 -0
  17. package/dist/commands/task-advance.js +31 -0
  18. package/dist/executors/dag-pi-executor.js +118 -14
  19. package/dist/executors/pi-event-serializer.js +42 -1
  20. package/dist/executors/pi-executor.js +51 -0
  21. package/dist/executors/pi-extension-resolver.js +233 -0
  22. package/dist/executors/pi-playwright-cli-tool.js +8 -1
  23. package/dist/executors/pi-sdk-executor.js +187 -59
  24. package/dist/executors/shell-executor.js +533 -29
  25. package/dist/executors/shell-write-guard.js +7 -0
  26. package/dist/shared/dag-prompt-override.js +28 -0
  27. package/dist/shared/operator/capabilities.js +181 -27
  28. package/dist/shared/update/console-notifier.js +100 -0
  29. package/dist/{cli → shared}/update/npm-client.js +41 -8
  30. package/dist/{cli → shared}/update/state.js +41 -13
  31. package/dist/task/config-types.js +22 -0
  32. package/dist/task/contract/constants.js +1 -0
  33. package/dist/task/contract/diff.js +26 -0
  34. package/dist/task/contract/project.js +5 -0
  35. package/dist/task/contract/schema.js +6 -1
  36. package/dist/task/source-prepare/build-draft.js +15 -0
  37. package/dist/worker/cli.js +31 -1
  38. package/dist/worker/console/chat/deferred-turn.js +91 -0
  39. package/dist/worker/console/chat/mcp-inventory.js +221 -0
  40. package/dist/worker/console/chat/pi-runtime.js +165 -83
  41. package/dist/worker/console/chat/repo-walk.js +10 -1
  42. package/dist/worker/console/chat/routes.js +478 -55
  43. package/dist/worker/console/chat/semantic-activity.js +20 -8
  44. package/dist/worker/console/chat/session-store.js +146 -2
  45. package/dist/worker/console/chat/turn-process.js +17 -30
  46. package/dist/worker/console/harness-pi-model-matrix.js +137 -0
  47. package/dist/worker/console/operator-actions.js +244 -4
  48. package/dist/worker/console/operator-user-error.js +19 -0
  49. package/dist/worker/console/pi-readiness.js +12 -5
  50. package/dist/worker/console/recovery-cta.js +4 -4
  51. package/dist/worker/console/recovery-error-copy.js +4 -4
  52. package/dist/worker/console/routes.js +43 -2
  53. package/dist/worker/console/security.js +83 -10
  54. package/dist/worker/console/server.js +42 -6
  55. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DSvF6RXQ.js → abnfDiagram-N423BO3Z-CiGOV5eG.js} +1 -1
  56. package/dist/worker/console/static/assets/{arc-DuI4ogtJ.js → arc-Ds9Gyfd8.js} +1 -1
  57. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DdBbgD4y.js → architectureDiagram-T3A2C74G-C6V6uC98.js} +1 -1
  58. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-xQC2HELm.js → blockDiagram-VBNYF7ZC-DezF7Ol9.js} +1 -1
  59. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DPMSgwIZ.js → c4Diagram-5PPSVZJV-BCWk3gCT.js} +1 -1
  60. package/dist/worker/console/static/assets/channel-DMvu1nmi.js +1 -0
  61. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-WTEKTxin.js → chunk-2GRJ4B5K-DbYr-Cxy.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B6vZ0Pv_.js → chunk-2Q5K7J3B-XSlusW4G.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BrerNAix.js → chunk-5RXB4S5H-BtcKAj9i.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D_ch2aaI.js → chunk-5VM5RSS4-BF57lhXj.js} +1 -1
  65. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-VvfacNrq.js → chunk-6Q2QTUOP-DJIlR25o.js} +1 -1
  66. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-B-uGFIg2.js → chunk-GF5L2VYU-DklNc7dm.js} +1 -1
  67. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BlysCtod.js → chunk-JWPE2WC7-fZNtEdQZ.js} +1 -1
  68. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BjAUAfcT.js → chunk-KBJHAD2P-BBgLqV4h.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-BeZnBmb-.js → chunk-RYQCIY6F-Dl9e5n_J.js} +1 -1
  70. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Bbfe3UJG.js → chunk-XXDRQBXY-B0JK51NT.js} +1 -1
  71. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DVzM-02m.js +1 -0
  72. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DVzM-02m.js +1 -0
  73. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BKJQnYVM.js → cose-bilkent-JH36ORCC-Ch-ajv4O.js} +1 -1
  74. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-B1wtI3hU.js → cynefin-VYW2F7L2-xn_NxcfB.js} +1 -1
  75. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DCF6DBSu.js → cynefinDiagram-MW4NZA55-DRD0fyga.js} +1 -1
  76. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BJyNGRlP.js → dagre-VZM6K2ZE-COtnKp4B.js} +1 -1
  77. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B7j1-VsZ.js → diagram-7IWD3JNH-BQp_CAqG.js} +1 -1
  78. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BREJ140a.js → diagram-B4RE2ZJO-x0y-vNgq.js} +1 -1
  79. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDPHmHGH.js → diagram-LBJQPF4R-N2ezwL31.js} +1 -1
  80. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BtZLrez1.js → diagram-Q27KOJAE-CSaxRH6Q.js} +1 -1
  81. package/dist/worker/console/static/assets/{diagram-UB23O5K3-mzybu8oU.js → diagram-UB23O5K3-6Ro7Kpbd.js} +1 -1
  82. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Dk3I2uO6.js → ebnfDiagram-BXEA7PRR-CrIIQEAI.js} +1 -1
  83. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BT97u_tQ.js → erDiagram-JOGREHBK-DivKcq8g.js} +1 -1
  84. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMOtYzui.js → flowDiagram-UKHOOZJN-BXrfDo8y.js} +1 -1
  85. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiNydkuY.js → ganttDiagram-PKOTCBZU-LTTQmMRJ.js} +1 -1
  86. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Cwt6fLkl.js → gitGraphDiagram-DS77QQ5N-DP3o_5qd.js} +1 -1
  87. package/dist/worker/console/static/assets/index-C2jmYBfu.js +404 -0
  88. package/dist/worker/console/static/assets/index-C8rOIAJr.css +1 -0
  89. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-AsGIGKKB.js → infoDiagram-6WML65LV-wVfSY1WH.js} +1 -1
  90. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BL8ITZh3.js → ishikawaDiagram-WSZJBQD7-BRKfFL0I.js} +1 -1
  91. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-VPKAUX8M.js → journeyDiagram-NVQOT4AX-DEml3jLb.js} +1 -1
  92. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BL56hfGp.js → kanban-definition-27J2QSJJ-DTzBmOqN.js} +1 -1
  93. package/dist/worker/console/static/assets/{linear-BmSG3nIZ.js → linear-Cpy1d873.js} +1 -1
  94. package/dist/worker/console/static/assets/{mermaid.core-DWGK9kwP.js → mermaid.core-C1cSavOy.js} +5 -5
  95. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS--vvKLjo0.js → mindmap-definition-FAOFIHXS-BEBSKvYM.js} +1 -1
  96. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B41mxb2-.js → pegDiagram-VL7TDLO6-BrLs4Oac.js} +1 -1
  97. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DTlu77sw.js → pieDiagram-7S7Q4E2Y-B47AZcLa.js} +1 -1
  98. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Bk5A7alv.js → quadrantDiagram-CIZ2JOQS-s0dVeYR2.js} +1 -1
  99. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Csf9jzQy.js → railroadDiagram-AXF67PYL-lW8aJ7Q6.js} +1 -1
  100. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CeptoSNx.js → requirementDiagram-LRYGKXZP-BcHJPf6Q.js} +1 -1
  101. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-A-DLCLer.js → sankeyDiagram-W5VNT64P-DdegpUUf.js} +1 -1
  102. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-22VbPdOI.js → sequenceDiagram-SI44F4Z6-BxFWqNv8.js} +1 -1
  103. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWvAyBC9.js → sizeCapture-X5ZJPWSS-cOdUDPbR.js} +1 -1
  104. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-qICZ75yT.js → stateDiagram-OKZ733FA-DpxlYl8P.js} +1 -1
  105. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BwecGAkM.js +1 -0
  106. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-xi7lcMyp.js → swimlanes-SLNWSIFB-DQOfurE0.js} +2 -2
  107. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D5WW_LbN.js +8 -0
  108. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-Bu6Nl6O2.js → timeline-definition-Z64GVDOM-CIp3tC6V.js} +1 -1
  109. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BzMz4Bn9.js → vennDiagram-T6HMQDX7-C-adIPKn.js} +1 -1
  110. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CxaJnzc5.js → wardleyDiagram-T6FBY63Y-CiYrar9M.js} +1 -1
  111. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-5JK-omQK.js → xychartDiagram-ELKLHX3M-WHrdTV0-.js} +1 -1
  112. package/dist/worker/console/static/index.html +2 -2
  113. package/dist/worker/console/static-src/app/console-types.js +39 -0
  114. package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
  115. package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
  116. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +82 -22
  117. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +45 -0
  118. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  119. package/dist/worker/console/static-src/operator-chat/resource-diagnostics.js +204 -0
  120. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
  121. package/dist/worker/console/static-src/operator-chat/turn-submission.js +42 -0
  122. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +18 -0
  123. package/dist/worker/console/static-src/operator-chat/useChatStream.js +216 -10
  124. package/dist/worker/console/static-src/operator-chat/useChatThread.js +50 -22
  125. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +26 -1
  126. package/dist/worker/observe/node-input.js +74 -3
  127. package/dist/worker/observe/node-process.js +377 -0
  128. package/dist/worker/observe/routes.js +36 -3
  129. package/dist/worker/observe/shell-handler-keys.js +34 -0
  130. package/dist/worker/observe/static/api.js +66 -0
  131. package/dist/worker/observe/static/constants.js +8 -1
  132. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  133. package/dist/worker/observe/static/format.js +23 -0
  134. package/dist/worker/observe/static/markdown-render.js +20 -1
  135. package/dist/worker/observe/static/operator-chrome.js +4 -0
  136. package/dist/worker/observe/static/state.js +13 -2
  137. package/dist/worker/observe/static/styles.css +73 -0
  138. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  139. package/dist/worker/observe/static/views/dag.js +8 -1
  140. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  141. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +827 -53
  142. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  143. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  144. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  145. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  146. package/dist/workflows/dag/backend-test-layout.js +133 -0
  147. package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
  148. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  149. package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
  150. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  151. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  152. package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
  153. package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
  154. package/dist/workflows/dag/contract-output-registry.js +15 -0
  155. package/dist/workflows/dag/contract-validator-registrations.js +3 -1
  156. package/dist/workflows/dag/convergence/controller.js +141 -0
  157. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  158. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  159. package/dist/workflows/dag/failure-category.js +12 -0
  160. package/dist/workflows/dag/frontend-implementation-contract.js +493 -2
  161. package/dist/workflows/dag/frontend-plan-render.js +90 -0
  162. package/dist/workflows/dag/frontend-prewrite-gate.js +221 -31
  163. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  164. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  165. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  166. package/dist/workflows/dag/frontend-test-result-contract.js +10 -18
  167. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  168. package/dist/workflows/dag/init-hybrid.js +805 -113
  169. package/dist/workflows/dag/node-execution.js +194 -6
  170. package/dist/workflows/dag/prompt.js +60 -1
  171. package/dist/workflows/dag/rerun-feedback.js +212 -0
  172. package/dist/workflows/dag/rerun-plan.js +224 -27
  173. package/dist/workflows/dag/rerun-run.js +61 -6
  174. package/dist/workflows/dag/rerun-task.js +61 -2
  175. package/dist/workflows/dag/retry-policy.js +113 -0
  176. package/dist/workflows/dag/structured-output-repair.js +712 -0
  177. package/dist/workflows/dag/types.js +157 -1
  178. package/dist/workflows/dag/validate.js +41 -1
  179. package/docs/init-surface.manifest.json +5 -0
  180. package/docs/operations/README.md +1 -1
  181. package/docs/operations/local-development-environment.md +1 -5
  182. package/docs/skills/vetted-skill-registry.md +14 -0
  183. package/docs/templates/README.md +1 -1
  184. package/docs/templates/agent-dag.schema.json +44 -0
  185. package/docs/templates/backend-test-dag.json +60 -35
  186. package/docs/templates/frontend-implementation-contract.schema.json +9 -2
  187. package/docs/templates/frontend-test-dag.json +8 -10
  188. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  189. package/docs/templates/init-managed-agents.md +13 -3
  190. package/harness.json +3 -1
  191. package/package.json +12 -4
  192. package/scripts/next-info.mjs +356 -0
  193. package/scripts/next-publish-gate.mjs +238 -0
  194. package/scripts/release-source-binding.mjs +251 -0
  195. package/skills/analyze-product-requirements/SKILL.md +8 -5
  196. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  197. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  198. package/skills/analyze-product-requirements/references/example.md +50 -0
  199. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  200. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  201. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  202. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  203. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  204. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  205. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  206. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  207. package/skills/codebase-scout/SKILL.md +1 -1
  208. package/skills/frontend-design-review/references/review-checklist.md +2 -0
  209. package/skills/frontend-implementation/references/design-spec.md +16 -19
  210. package/skills/frontend-implementation/references/node-contracts.md +9 -6
  211. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  212. package/skills/improve-codebase-architecture/deepening.md +37 -0
  213. package/skills/improve-codebase-architecture/html-report.md +123 -0
  214. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  215. package/skills/improve-codebase-architecture/language.md +53 -0
  216. package/skills/loop-agent/references/command-reference.md +11 -4
  217. package/dist/worker/console/static/assets/channel-DLVz9TYW.js +0 -1
  218. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BeAuGcO9.js +0 -1
  219. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BeAuGcO9.js +0 -1
  220. package/dist/worker/console/static/assets/index-DQxiso1w.js +0 -325
  221. package/dist/worker/console/static/assets/index-_j0Hzo69.css +0 -1
  222. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DzKvfHTX.js +0 -1
  223. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Cxqr_qM1.js +0 -8
@@ -1,4 +1,4 @@
1
- import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
1
+ import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  export function updateStatePath(homeDir) {
4
4
  return path.join(homeDir, ".loop-agent", "update-state.json");
@@ -18,6 +18,7 @@ export async function readUpdateState(statePath) {
18
18
  latestVersion: stringOrUndefined(parsed.latestVersion),
19
19
  dismissedVersion: stringOrUndefined(parsed.dismissedVersion),
20
20
  retryAfter: stringOrUndefined(parsed.retryAfter),
21
+ lastPromptedLocalDate: stringOrUndefined(parsed.lastPromptedLocalDate),
21
22
  };
22
23
  }
23
24
  catch {
@@ -36,28 +37,55 @@ export async function acquireUpdateLock(lockPath, options = {}) {
36
37
  await mkdir(path.dirname(lockPath), { recursive: true });
37
38
  try {
38
39
  await mkdir(lockPath);
40
+ return await claimLock(lockPath, now);
41
+ }
42
+ catch {
43
+ if (await isFreshLock(lockPath, now, staleMs))
44
+ return undefined;
45
+ try {
46
+ await rm(lockPath, { recursive: true, force: true });
47
+ await mkdir(lockPath);
48
+ return await claimLock(lockPath, now);
49
+ }
50
+ catch {
51
+ return undefined;
52
+ }
53
+ }
54
+ }
55
+ async function claimLock(lockPath, now) {
56
+ try {
39
57
  await writeFile(path.join(lockPath, "owner.json"), JSON.stringify({ pid: process.pid, at: now().toISOString() }), "utf-8");
40
58
  return async () => {
41
59
  await rm(lockPath, { recursive: true, force: true });
42
60
  };
43
61
  }
62
+ catch {
63
+ await rm(lockPath, { recursive: true, force: true }).catch(() => undefined);
64
+ return undefined;
65
+ }
66
+ }
67
+ async function isFreshLock(lockPath, now, staleMs) {
68
+ try {
69
+ const ownerRaw = await readFile(path.join(lockPath, "owner.json"), "utf-8");
70
+ const owner = JSON.parse(ownerRaw);
71
+ if (typeof owner.at !== "string")
72
+ return false;
73
+ const at = Date.parse(owner.at);
74
+ if (!Number.isFinite(at))
75
+ return false;
76
+ return now().getTime() - at <= staleMs;
77
+ }
44
78
  catch {
45
79
  try {
46
- const ownerRaw = await readFile(path.join(lockPath, "owner.json"), "utf-8");
47
- const owner = JSON.parse(ownerRaw);
48
- if (owner.at && now().getTime() - Date.parse(owner.at) > staleMs) {
49
- await rm(lockPath, { recursive: true, force: true });
50
- await mkdir(lockPath);
51
- await writeFile(path.join(lockPath, "owner.json"), JSON.stringify({ pid: process.pid, at: now().toISOString() }), "utf-8");
52
- return async () => {
53
- await rm(lockPath, { recursive: true, force: true });
54
- };
55
- }
80
+ const info = await stat(lockPath);
81
+ const mtime = Number.isFinite(info.mtimeMs)
82
+ ? info.mtimeMs
83
+ : info.mtime.getTime();
84
+ return now().getTime() - mtime <= staleMs;
56
85
  }
57
86
  catch {
58
- return undefined;
87
+ return false;
59
88
  }
60
- return undefined;
61
89
  }
62
90
  }
63
91
  function emptyState() {
@@ -124,6 +124,26 @@ export const frontendTestConfigSchema = z.object({
124
124
  .strict()
125
125
  .optional(),
126
126
  });
127
+ /** Backend-test artifact layout and pipeline mode configuration (plan A/B/D). */
128
+ export const backendTestConfigSchema = z
129
+ .object({
130
+ /** Repository-relative test root. Default "testcase". */
131
+ testRoot: z.string().min(1).optional(),
132
+ /** Markdown subdirectory relative to testRoot. Default "md". */
133
+ markdownRel: z.string().min(1).optional(),
134
+ /** Pytest script subdirectory relative to testRoot. Default "." (testRoot itself). */
135
+ scriptRel: z.string().min(1).optional(),
136
+ /** User-provided shared setup document (plan B). Read-only bound source. */
137
+ sharedSetupDoc: z.string().min(1).optional(),
138
+ /** Pipeline mode: full 16-node chain (default) or gap-fill incremental DAG (plan D). */
139
+ mode: z.enum(["full", "gap-fill"]).optional(),
140
+ /** Required for mode=gap-fill: user missing-scenario document path. */
141
+ gapDoc: z.string().min(1).optional(),
142
+ /** gap-fill pytest execution scope: only affected items (default) or all eligible. */
143
+ executeScope: z.enum(["affected", "all"]).optional(),
144
+ })
145
+ .strict()
146
+ .optional();
127
147
  export const convergenceConfigSchema = z.object({
128
148
  enabled: z.boolean().optional().default(false),
129
149
  maxPasses: z.number().int().positive().optional().default(3),
@@ -196,6 +216,8 @@ const taskConfigObjectSchema = z.object({
196
216
  frontendTest: frontendTestConfigSchema.optional(),
197
217
  /** Frontend OpenSpec gate policy and contract-declared required read paths. */
198
218
  frontendOpenspec: frontendOpenspecConfigSchema.optional(),
219
+ /** Backend-test layout / shared-setup / gap-fill configuration. */
220
+ backendTest: backendTestConfigSchema,
199
221
  notes: z.string().optional().default(""),
200
222
  });
201
223
  export const taskConfigSchema = z.preprocess((raw) => {
@@ -17,6 +17,7 @@ export const MANAGED_TASK_CONFIG_FIELDS = [
17
17
  "forbiddenPaths",
18
18
  "hardConstraints",
19
19
  "verifyCommands",
20
+ "backendTest",
20
21
  ];
21
22
  export const CONTRACT_FILE_NAME = "contract.json";
22
23
  export const TRANSACTIONS_DIR_NAME = ".transactions";
@@ -1,3 +1,4 @@
1
+ import { canonicalizeJsonValue } from "./canonicalize.js";
1
2
  import { MANAGED_TASK_CONFIG_FIELDS } from "./constants.js";
2
3
  function maxRisk(a, b) {
3
4
  const order = ["routine", "elevated", "blocking"];
@@ -139,6 +140,31 @@ export function diffDraftAgainstCurrent(input) {
139
140
  path: "task.json#featureId",
140
141
  });
141
142
  }
143
+ // backendTest is managed whole-object: an absent draft value means
144
+ // "keep existing" and never diffs; declared values compare canonically.
145
+ if (input.draft.backendTest !== undefined) {
146
+ const before = cfg.backendTest ?? {};
147
+ const after = input.draft.backendTest;
148
+ if (canonicalizeJsonValue(before) !== canonicalizeJsonValue(after)) {
149
+ changedManagedFields.push("backendTest");
150
+ risks.push({
151
+ code: "BACKEND_TEST_CONFIG_CHANGED",
152
+ level: "routine",
153
+ message: "backendTest configuration changed",
154
+ path: "task.json#backendTest",
155
+ });
156
+ const afterKeys = new Set(Object.keys(after));
157
+ const droppedKeys = Object.keys(before).filter((key) => !afterKeys.has(key));
158
+ if (droppedKeys.length > 0) {
159
+ risks.push({
160
+ code: "BACKEND_TEST_CONFIG_WEAKENED",
161
+ level: "elevated",
162
+ message: `drop backendTest keys: ${droppedKeys.sort().join(", ")}`,
163
+ path: "task.json#backendTest",
164
+ });
165
+ }
166
+ }
167
+ }
142
168
  }
143
169
  else {
144
170
  for (const field of MANAGED_TASK_CONFIG_FIELDS) {
@@ -174,6 +174,11 @@ export function mergeManagedTaskConfigFields(existing, draft) {
174
174
  if (draft.featureId !== undefined) {
175
175
  next.featureId = draft.featureId;
176
176
  }
177
+ // backendTest is whole-object replace when the draft declares it (`{}` is a
178
+ // legal explicit clear back to the default layout); absent keeps existing.
179
+ if (draft.backendTest !== undefined) {
180
+ next.backendTest = draft.backendTest;
181
+ }
177
182
  return next;
178
183
  }
179
184
  /**
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { taskKindSchema, taskVerifyCommandSchema } from "../config-types.js";
2
+ import { backendTestConfigSchema, taskKindSchema, taskVerifyCommandSchema, } from "../config-types.js";
3
3
  import { TASK_CONFIG_SCHEMA_VERSION, TASK_CONTRACT_CANONICALIZER_VERSION, TASK_CONTRACT_DRAFT_SCHEMA_VERSION, TASK_CONTRACT_PROJECTION_VERSION, TASK_CONTRACT_REF_SCHEMA_VERSION, } from "./constants.js";
4
4
  const sha256Schema = z
5
5
  .string()
@@ -62,6 +62,11 @@ export const taskContractDraftSchema = z
62
62
  requirement: taskContractDraftRequirementSchema,
63
63
  constraints: taskContractDraftConstraintsSchema,
64
64
  verification: taskContractDraftVerificationSchema,
65
+ /**
66
+ * Managed backend-test layout / pipeline configuration projected into
67
+ * task.json#backendTest. Optional: absent means "keep existing".
68
+ */
69
+ backendTest: backendTestConfigSchema,
65
70
  references: z.array(taskContractDraftReferenceSchema).optional(),
66
71
  sourceResolutions: z
67
72
  .array(taskContractDraftSourceResolutionSchema)
@@ -22,6 +22,18 @@ function resolveTaskKindExplicit(flags, baseDraft) {
22
22
  return flags.taskKindExplicit;
23
23
  return baseDraft?.taskKindExplicit ?? false;
24
24
  }
25
+ /**
26
+ * Resolve managed backend-test layout config.
27
+ * Precedence: explicit CLI flags > --from-draft base > existing task.json
28
+ * (the last tier self-heals tasks whose backendTest was hand-written).
29
+ */
30
+ function resolveBackendTest(flags, baseDraft, existing) {
31
+ if (flags.backendTest !== undefined)
32
+ return flags.backendTest;
33
+ if (baseDraft?.backendTest !== undefined)
34
+ return baseDraft.backendTest;
35
+ return existing?.backendTest ?? undefined;
36
+ }
25
37
  function resolveTitle(taskId, flags, facts, baseDraft, existing) {
26
38
  return (trimNonEmpty(flags.title) ??
27
39
  trimNonEmpty(baseDraft?.title) ??
@@ -205,6 +217,9 @@ export function buildPrepareDraft(input) {
205
217
  };
206
218
  if (featureId)
207
219
  draft.featureId = featureId;
220
+ const backendTest = resolveBackendTest(flags, baseDraft, existingTaskConfig);
221
+ if (backendTest !== undefined)
222
+ draft.backendTest = backendTest;
208
223
  if (references && references.length > 0) {
209
224
  draft.references = references.map((ref) => ({
210
225
  role: ref.role,
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFile } from "node:fs/promises";
3
+ import os from "node:os";
3
4
  import path from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { Command } from "commander";
7
+ import { createNpmUpdateClient } from "../shared/update/npm-client.js";
8
+ import { runConsoleDailyUpdateCheck } from "../shared/update/console-notifier.js";
6
9
  import { readPackageVersion } from "../shared/package-metadata.js";
7
10
  import YAML from "yaml";
8
11
  import { LoopAgentClient } from "./loop-agent/loop-agent-client.js";
@@ -838,10 +841,33 @@ function parseOptionalTimeoutMs(raw) {
838
841
  return value;
839
842
  }
840
843
  /** Console HTTP entry for bare `agent-worker console` (listen first, then optional browser open). */
841
- async function runConsoleServe(options) {
844
+ export async function runConsoleServe(options) {
842
845
  const { createConsoleServer } = await import("./console/server.js");
843
846
  const { maybeOpenConsoleBrowser } = await import("./console/open-browser.js");
844
847
  try {
848
+ try {
849
+ const packageRoot = path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))));
850
+ const currentVersion = readPackageVersion(packageRoot) ?? "0.0.0";
851
+ await runConsoleDailyUpdateCheck({
852
+ currentVersion,
853
+ packageRoot,
854
+ homeDir: os.homedir(),
855
+ cwd: options.repoRoot,
856
+ env: process.env,
857
+ stdinIsTTY: process.stdin.isTTY === true,
858
+ stderrIsTTY: process.stderr.isTTY === true,
859
+ client: createNpmUpdateClient({
860
+ currentVersion,
861
+ packageRoot,
862
+ cwd: options.repoRoot,
863
+ env: process.env,
864
+ }),
865
+ });
866
+ }
867
+ catch (error) {
868
+ const detail = error instanceof Error ? error.message : String(error);
869
+ process.stderr.write(`loop-agent 更新检查已跳过:${detail}\n`);
870
+ }
845
871
  const server = await createConsoleServer({
846
872
  repoRoot: options.repoRoot,
847
873
  host: options.host,
@@ -857,6 +883,10 @@ async function runConsoleServe(options) {
857
883
  noOpen: options.noOpen === true,
858
884
  serverUrl: server.url,
859
885
  });
886
+ if (options.holdUntilSignal === false) {
887
+ await server.close();
888
+ return;
889
+ }
860
890
  await new Promise((resolve) => {
861
891
  const shutdown = () => resolve();
862
892
  process.once("SIGINT", shutdown);
@@ -0,0 +1,91 @@
1
+ import { createHash } from "node:crypto";
2
+ const IMAGE_MIME_TYPES = new Set([
3
+ "image/png",
4
+ "image/jpeg",
5
+ "image/gif",
6
+ "image/webp",
7
+ ]);
8
+ export function hashDeferredTurnPayload(input) {
9
+ return createHash("sha256")
10
+ .update(JSON.stringify({
11
+ text: input.text,
12
+ images: input.images.map(({ type, mimeType, data }) => ({
13
+ type,
14
+ mimeType,
15
+ data,
16
+ })),
17
+ }))
18
+ .digest("hex");
19
+ }
20
+ export function parseDeferredTurn(value) {
21
+ if (!value || typeof value !== "object")
22
+ return undefined;
23
+ const item = value;
24
+ if (item.schemaVersion !== 1 ||
25
+ !Number.isSafeInteger(item.revision) ||
26
+ (item.revision ?? 0) < 1 ||
27
+ typeof item.clientRequestId !== "string" ||
28
+ !item.clientRequestId.trim() ||
29
+ typeof item.payloadHash !== "string" ||
30
+ !/^[a-f0-9]{64}$/.test(item.payloadHash) ||
31
+ typeof item.blockedByTurnId !== "string" ||
32
+ !item.blockedByTurnId.trim() ||
33
+ typeof item.text !== "string" ||
34
+ !Array.isArray(item.images) ||
35
+ !["queued", "dispatching", "accepted", "failed"].includes(item.state) ||
36
+ typeof item.createdAt !== "string" ||
37
+ typeof item.updatedAt !== "string") {
38
+ return undefined;
39
+ }
40
+ const images = [];
41
+ for (const image of item.images) {
42
+ if (!image || typeof image !== "object")
43
+ return undefined;
44
+ const candidate = image;
45
+ if (typeof candidate.name !== "string" ||
46
+ candidate.type !== "image" ||
47
+ typeof candidate.mimeType !== "string" ||
48
+ !IMAGE_MIME_TYPES.has(candidate.mimeType) ||
49
+ typeof candidate.data !== "string") {
50
+ return undefined;
51
+ }
52
+ images.push(candidate);
53
+ }
54
+ return {
55
+ schemaVersion: 1,
56
+ revision: item.revision,
57
+ clientRequestId: item.clientRequestId.trim(),
58
+ payloadHash: item.payloadHash,
59
+ blockedByTurnId: item.blockedByTurnId.trim(),
60
+ text: item.text,
61
+ images,
62
+ state: item.state,
63
+ createdAt: item.createdAt,
64
+ updatedAt: item.updatedAt,
65
+ ...(typeof item.acceptedTurnId === "string" && item.acceptedTurnId
66
+ ? { acceptedTurnId: item.acceptedTurnId }
67
+ : {}),
68
+ ...(item.lastError &&
69
+ typeof item.lastError.code === "string" &&
70
+ typeof item.lastError.message === "string"
71
+ ? { lastError: { code: item.lastError.code, message: item.lastError.message } }
72
+ : {}),
73
+ };
74
+ }
75
+ export function projectDeferredTurnSummary(deferredTurn) {
76
+ if (!deferredTurn)
77
+ return null;
78
+ return {
79
+ revision: deferredTurn.revision,
80
+ clientRequestId: deferredTurn.clientRequestId,
81
+ blockedByTurnId: deferredTurn.blockedByTurnId,
82
+ state: deferredTurn.state,
83
+ updatedAt: deferredTurn.updatedAt,
84
+ textPreview: deferredTurn.text.replace(/\s+/g, " ").trim().slice(0, 120),
85
+ imageCount: deferredTurn.images.length,
86
+ ...(deferredTurn.acceptedTurnId
87
+ ? { acceptedTurnId: deferredTurn.acceptedTurnId }
88
+ : {}),
89
+ ...(deferredTurn.lastError ? { lastError: deferredTurn.lastError } : {}),
90
+ };
91
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Read-only MCP inventory for Operator Chat.
3
+ *
4
+ * Pi has no built-in MCP catalog. Console projects two real facts:
5
+ * 1. mcp.json server specs (configured, may or may not have registered tools)
6
+ * 2. tools already on the live session that look like MCP registrations
7
+ *
8
+ * Never fabricate servers or tools. Missing files stay empty.
9
+ */
10
+ import { readFile } from "node:fs/promises";
11
+ import path from "node:path";
12
+ const MCP_CONFIG_NAMES = ["mcp.json"];
13
+ export function isMcpToolName(name) {
14
+ const lower = name.trim().toLowerCase();
15
+ return lower === "mcp" || lower.startsWith("mcp_") || lower.startsWith("mcp-");
16
+ }
17
+ export function inferMcpServerName(toolName, knownServers = []) {
18
+ const name = toolName.trim();
19
+ if (/^mcp$/i.test(name))
20
+ return undefined;
21
+ const rest = name.replace(/^mcp[_-]/i, "");
22
+ if (!rest || rest === name || knownServers.length === 0)
23
+ return undefined;
24
+ const ranked = [...knownServers].sort((left, right) => right.length - left.length);
25
+ const lower = rest.toLowerCase();
26
+ return ranked.find((server) => {
27
+ const key = server.toLowerCase();
28
+ return lower === key || lower.startsWith(`${key}_`) || lower.startsWith(`${key}-`);
29
+ });
30
+ }
31
+ export function looksLikeMcpTool(tool) {
32
+ const name = typeof tool.name === "string" ? tool.name : "";
33
+ if (!name)
34
+ return false;
35
+ if (isMcpToolName(name))
36
+ return true;
37
+ const source = `${tool.sourceInfo?.source ?? ""} ${tool.sourceInfo?.path ?? ""}`.toLowerCase();
38
+ return source.includes("mcp");
39
+ }
40
+ function asRecord(value) {
41
+ return value && typeof value === "object" && !Array.isArray(value)
42
+ ? value
43
+ : undefined;
44
+ }
45
+ function readEnabled(entry) {
46
+ return entry.enabled !== false;
47
+ }
48
+ function readTransport(entry) {
49
+ if (typeof entry.transport === "string" && entry.transport.trim()) {
50
+ return entry.transport.trim();
51
+ }
52
+ if (typeof entry.type === "string" && entry.type.trim()) {
53
+ return entry.type.trim();
54
+ }
55
+ if (typeof entry.url === "string")
56
+ return "http";
57
+ if (Array.isArray(entry.command) || typeof entry.command === "string") {
58
+ return "stdio";
59
+ }
60
+ return undefined;
61
+ }
62
+ function readCommand(entry) {
63
+ if (typeof entry.command === "string" && entry.command.trim()) {
64
+ const args = Array.isArray(entry.args)
65
+ ? entry.args.filter((item) => typeof item === "string").join(" ")
66
+ : "";
67
+ return args ? `${entry.command} ${args}` : entry.command;
68
+ }
69
+ if (Array.isArray(entry.command)) {
70
+ return entry.command
71
+ .filter((item) => typeof item === "string")
72
+ .join(" ")
73
+ .trim();
74
+ }
75
+ return undefined;
76
+ }
77
+ function extractServerMap(parsed) {
78
+ const root = asRecord(parsed);
79
+ if (!root)
80
+ return {};
81
+ const mcpServers = asRecord(root.mcpServers);
82
+ if (mcpServers)
83
+ return mcpServers;
84
+ const mcp = asRecord(root.mcp);
85
+ if (mcp)
86
+ return mcp;
87
+ const servers = asRecord(root.servers);
88
+ if (servers)
89
+ return servers;
90
+ return {};
91
+ }
92
+ export function parseMcpConfig(raw, scope, configPath) {
93
+ let parsed;
94
+ try {
95
+ parsed = JSON.parse(raw);
96
+ }
97
+ catch {
98
+ return [];
99
+ }
100
+ const servers = [];
101
+ for (const [name, value] of Object.entries(extractServerMap(parsed))) {
102
+ if (!name.trim())
103
+ continue;
104
+ const entry = asRecord(value) ?? {};
105
+ const command = readCommand(entry);
106
+ const url = typeof entry.url === "string" ? entry.url.trim() : undefined;
107
+ servers.push({
108
+ name: name.trim(),
109
+ scope,
110
+ enabled: readEnabled(entry),
111
+ ...(readTransport(entry) ? { transport: readTransport(entry) } : {}),
112
+ ...(command ? { command } : {}),
113
+ ...(url ? { url } : {}),
114
+ configPath,
115
+ });
116
+ }
117
+ return servers;
118
+ }
119
+ export function projectSessionMcpTools(tools, activeToolNames = [], knownServers = []) {
120
+ const active = new Set(activeToolNames.map((name) => name.toLowerCase()));
121
+ const seen = new Set();
122
+ const projected = [];
123
+ for (const tool of tools) {
124
+ if (!looksLikeMcpTool(tool) || typeof tool.name !== "string")
125
+ continue;
126
+ const name = tool.name.trim();
127
+ if (!name || seen.has(name))
128
+ continue;
129
+ seen.add(name);
130
+ const server = inferMcpServerName(name, knownServers);
131
+ projected.push({
132
+ name,
133
+ description: typeof tool.description === "string" ? tool.description : "",
134
+ ...(server ? { server } : {}),
135
+ source: tool.sourceInfo?.source ?? "",
136
+ scope: tool.sourceInfo?.scope ?? "session",
137
+ ...(tool.sourceInfo?.path ? { path: tool.sourceInfo.path } : {}),
138
+ active: active.has(name.toLowerCase()),
139
+ });
140
+ }
141
+ return projected;
142
+ }
143
+ export function assembleMcpInventory(servers, tools) {
144
+ const byServer = new Map();
145
+ const unassigned = [];
146
+ for (const tool of tools) {
147
+ const key = tool.server?.toLowerCase();
148
+ if (!key) {
149
+ unassigned.push(tool);
150
+ continue;
151
+ }
152
+ const list = byServer.get(key) ?? [];
153
+ list.push(tool);
154
+ byServer.set(key, list);
155
+ }
156
+ const views = servers.map((server) => {
157
+ const matched = byServer.get(server.name.toLowerCase()) ?? [];
158
+ byServer.delete(server.name.toLowerCase());
159
+ const status = !server.enabled
160
+ ? "disabled"
161
+ : matched.length > 0
162
+ ? "loaded"
163
+ : "configured";
164
+ return {
165
+ name: server.name,
166
+ scope: server.scope,
167
+ status,
168
+ enabled: server.enabled,
169
+ ...(server.transport ? { transport: server.transport } : {}),
170
+ ...(server.command ? { command: server.command } : {}),
171
+ ...(server.url ? { url: server.url } : {}),
172
+ configPath: server.configPath,
173
+ tools: matched,
174
+ };
175
+ });
176
+ for (const [name, matched] of byServer) {
177
+ views.push({
178
+ name,
179
+ scope: "session",
180
+ status: "loaded",
181
+ enabled: true,
182
+ tools: matched,
183
+ });
184
+ }
185
+ return { servers: views, unassignedTools: unassigned };
186
+ }
187
+ async function readConfigFile(filePath) {
188
+ try {
189
+ return await readFile(filePath, "utf8");
190
+ }
191
+ catch {
192
+ return undefined;
193
+ }
194
+ }
195
+ export async function loadMcpServerSpecs(options) {
196
+ const candidates = [
197
+ { path: path.join(options.agentDir, ...MCP_CONFIG_NAMES), scope: "user" },
198
+ { path: path.join(options.cwd, ".pi", ...MCP_CONFIG_NAMES), scope: "project" },
199
+ { path: path.join(options.cwd, ".mcp.json"), scope: "project" },
200
+ ];
201
+ const servers = [];
202
+ const seen = new Set();
203
+ for (const candidate of candidates) {
204
+ const raw = await readConfigFile(candidate.path);
205
+ if (!raw)
206
+ continue;
207
+ for (const server of parseMcpConfig(raw, candidate.scope, candidate.path)) {
208
+ const key = `${server.scope}:${server.name.toLowerCase()}`;
209
+ if (seen.has(key))
210
+ continue;
211
+ seen.add(key);
212
+ servers.push(server);
213
+ }
214
+ }
215
+ return servers;
216
+ }
217
+ export async function readMcpInventory(options) {
218
+ const servers = await loadMcpServerSpecs(options);
219
+ const tools = projectSessionMcpTools(options.tools ?? [], options.activeToolNames ?? [], servers.map((server) => server.name));
220
+ return assembleMcpInventory(servers, tools);
221
+ }