@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,95 @@
1
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
2
+ /** Viewport margin the placement clamp keeps (DSH PANEL_MARGIN parity). */
3
+ const PANEL_MARGIN = 12;
4
+ /** Distance between the trigger's top edge and the panel's bottom (DSH gap). */
5
+ const PANEL_GAP = 8;
6
+ /**
7
+ * Unplaced portal panel: hidden but laid out so the clamp measures real
8
+ * dimensions (DSH MEASURE_STYLE parity).
9
+ */
10
+ export const MEASURE_STYLE = { visibility: "hidden", left: 0, top: 0 };
11
+ /**
12
+ * Trigger-anchored, viewport-clamped fixed placement for a portaled panel
13
+ * (DSH useAnchoredPosition / useStatDialog parity): the panel floats above
14
+ * the trigger with PANEL_GAP and is clamped to PANEL_MARGIN inside the
15
+ * viewport, so a trigger near a window edge cannot push it off-screen.
16
+ * Re-measures on open, resize, and scroll (capture) while open.
17
+ */
18
+ export function useAnchoredPosition(props) {
19
+ const { open, triggerRef, panelRef } = props;
20
+ const [pos, setPos] = useState(null);
21
+ const measure = () => {
22
+ const trigger = triggerRef.current;
23
+ const panel = panelRef.current;
24
+ if (!trigger || !panel)
25
+ return;
26
+ const triggerRect = trigger.getBoundingClientRect();
27
+ const panelRect = panel.getBoundingClientRect();
28
+ const vw = window.innerWidth;
29
+ const vh = window.innerHeight;
30
+ // Preferred side: above the trigger (composer pills sit at the bottom).
31
+ let top = triggerRect.top - panelRect.height - PANEL_GAP;
32
+ if (top < PANEL_MARGIN) {
33
+ top = Math.min(triggerRect.bottom + PANEL_GAP, vh - panelRect.height - PANEL_MARGIN);
34
+ }
35
+ top = Math.max(PANEL_MARGIN, Math.min(top, vh - panelRect.height - PANEL_MARGIN));
36
+ // Horizontal: align the panel's left edge with the trigger, clamp right.
37
+ let left = triggerRect.left;
38
+ if (left + panelRect.width + PANEL_MARGIN > vw) {
39
+ left = vw - panelRect.width - PANEL_MARGIN;
40
+ }
41
+ left = Math.max(PANEL_MARGIN, left);
42
+ setPos({ position: "fixed", top: `${Math.round(top)}px`, left: `${Math.round(left)}px` });
43
+ };
44
+ useLayoutEffect(() => {
45
+ if (!open) {
46
+ setPos(null);
47
+ return;
48
+ }
49
+ measure();
50
+ window.addEventListener("resize", measure);
51
+ window.addEventListener("scroll", measure, true);
52
+ return () => {
53
+ window.removeEventListener("resize", measure);
54
+ window.removeEventListener("scroll", measure, true);
55
+ };
56
+ // eslint-disable-next-line react-hooks/exhaustive-deps
57
+ }, [open]);
58
+ return pos;
59
+ }
60
+ /**
61
+ * One trigger-anchored dialog seat: open state, viewport-clamped placement,
62
+ * outside-pointerdown close (portaled panel counts as inside), Escape close
63
+ * (DSH useStatDialog parity).
64
+ */
65
+ export function useStatDialog() {
66
+ const [open, setOpen] = useState(false);
67
+ const rootRef = useRef(null);
68
+ const panelRef = useRef(null);
69
+ const pos = useAnchoredPosition({ open, triggerRef: rootRef, panelRef });
70
+ useEffect(() => {
71
+ if (!open)
72
+ return;
73
+ const onPointerDown = (event) => {
74
+ const target = event.target;
75
+ if (!target)
76
+ return;
77
+ if (rootRef.current?.contains(target) ||
78
+ panelRef.current?.contains(target)) {
79
+ return;
80
+ }
81
+ setOpen(false);
82
+ };
83
+ const onKeyDown = (event) => {
84
+ if (event.key === "Escape")
85
+ setOpen(false);
86
+ };
87
+ document.addEventListener("pointerdown", onPointerDown);
88
+ document.addEventListener("keydown", onKeyDown);
89
+ return () => {
90
+ document.removeEventListener("pointerdown", onPointerDown);
91
+ document.removeEventListener("keydown", onKeyDown);
92
+ };
93
+ }, [open]);
94
+ return { open, setOpen, rootRef, panelRef, pos };
95
+ }
@@ -16,6 +16,12 @@ export function repoHtmlPreviewUrl(origin, workspaceId, file) {
16
16
  return `${base}${REPO_HTML_PREVIEW_ROUTE_PREFIX}${encodeURIComponent(id)}/${segments.map(encodeURIComponent).join("/")}`;
17
17
  }
18
18
  function withHtmlPreviewUrl(preview, origin, workspaceId) {
19
+ if (preview.kind === "image") {
20
+ // Images load their bytes from the same workspace-scoped route that
21
+ // serves HTML relative assets; the JSON snapshot itself stays empty.
22
+ const previewUrl = repoHtmlPreviewUrl(origin, workspaceId, preview.path);
23
+ return previewUrl ? { ...preview, previewUrl } : preview;
24
+ }
19
25
  const language = preview.language.trim().toLowerCase();
20
26
  if (language !== "html" && language !== "htm")
21
27
  return preview;
@@ -41,6 +41,8 @@ import { TodoListRegistry } from "./chat/todos.js";
41
41
  import { GoalRegistry } from "./chat/goals.js";
42
42
  import { GoalRoundDriver } from "./chat/goal-round-driver.js";
43
43
  import { TerminalSessionRegistry } from "./chat/terminal-sessions.js";
44
+ import { PetService } from "./chat/pet/pet-service.js";
45
+ import { loadPetRegistry } from "./chat/pet/pet-registry.js";
44
46
  import { ScmService } from "./chat/scm-service.js";
45
47
  import { BrowserAutomationService } from "./chat/browser-automation.js";
46
48
  import { driveGoalRound, recoverDeferredTurns } from "./chat/routes.js";
@@ -306,6 +308,7 @@ export async function createWorkspaceContext(record, deps) {
306
308
  profileDir: appData.browserProfile,
307
309
  downloadsDir: appData.browserDownloads,
308
310
  screenshotsDir: appData.browserScreenshots,
311
+ workspaceId: record.workspaceId,
309
312
  });
310
313
  const chatRuntime = new ConsolePiRuntime({
311
314
  cwd: repoRoot,
@@ -323,6 +326,11 @@ export async function createWorkspaceContext(record, deps) {
323
326
  });
324
327
  let chatRouteDeps;
325
328
  const goalRoundDriver = new GoalRoundDriver((sessionId) => driveGoalRound(chatRouteDeps, sessionId));
329
+ // Operator pet (dsh-pet port): one service per workspace context. The
330
+ // registry reads pet manifests from the console static pets dir (shipped
331
+ // with the SPA build); persistence lands under the workspace app-data dir.
332
+ const petRegistry = loadPetRegistry(path.join(deps.staticDir, "pets"));
333
+ const petService = new PetService(chatEvents, petRegistry, appData.root);
326
334
  chatRouteDeps = {
327
335
  runtime: chatRuntime,
328
336
  store: chatStore,
@@ -334,6 +342,7 @@ export async function createWorkspaceContext(record, deps) {
334
342
  sddDataAlignment,
335
343
  goalRoundDriver,
336
344
  operationLinker: chatOperationLinker,
345
+ pet: petService,
337
346
  terminals: terminalRegistry,
338
347
  scm: scmService,
339
348
  browserAutomation,
@@ -386,6 +395,12 @@ export async function createWorkspaceContext(record, deps) {
386
395
  return;
387
396
  disposed = true;
388
397
  scheduledGoalHost.stop();
398
+ try {
399
+ petService.dispose();
400
+ }
401
+ catch {
402
+ // best-effort
403
+ }
389
404
  try {
390
405
  goalRoundDriver.dispose();
391
406
  }
@@ -70,7 +70,7 @@ export class WorkspaceInitializationService {
70
70
  }
71
71
  const surface = await this.jsonFile(".harness/init-surface.json");
72
72
  const harness = await this.jsonFile("harness.json");
73
- if (surface && (surface.schemaVersion !== 1 || !surface.files || typeof surface.files !== "object" || Array.isArray(surface.files))) {
73
+ if (surface && (![1, 2].includes(Number(surface.schemaVersion)) || !surface.files || typeof surface.files !== "object" || Array.isArray(surface.files))) {
74
74
  throw new Error("初始化记录不完整,请检查 .harness/init-surface.json");
75
75
  }
76
76
  if (!surface && !harness) {
@@ -83,10 +83,15 @@ export class WorkspaceInitializationService {
83
83
  }
84
84
  return snapshot;
85
85
  }
86
- const report = await this.command(["init", "check-update", "--json"], "check");
86
+ const report = await this.command(["init", "check-update", "--legacy-audit", "--json"], "check");
87
87
  if (!["recorded", "inferred-baseline", "missing"].includes(String(report.surfaceState)) || typeof report.ok !== "boolean") {
88
88
  throw new Error("初始化检查返回了未知状态");
89
89
  }
90
+ const coreState = String(report.coreState);
91
+ const migrationState = String(report.migrationState);
92
+ if (!["ready", "blocked", "error"].includes(coreState) || !["clean", "retained", "review-required", "in-progress", "failed"].includes(migrationState)) {
93
+ throw new Error("初始化检查缺少核心或迁移状态");
94
+ }
90
95
  const actions = entries(report.deterministicActions);
91
96
  const merges = entries(report.modelMergeTasks);
92
97
  const decisions = entries(report.humanDecisions);
@@ -98,17 +103,20 @@ export class WorkspaceInitializationService {
98
103
  const detail = typeof item.reason === "string" ? item.reason : undefined;
99
104
  const baseline = item.type === "bootstrap-surface";
100
105
  const itemKind = baseline ? "confirmation" : kind;
106
+ const legacyCandidate = typeof item.candidateId === "string";
101
107
  const reason = baseline ? "缺少可靠的初始化记录,不能直接判断哪些内容可覆盖。"
102
- : kind === "safe-update" ? "此项可由程序安全更新,保留项目自定义内容。"
103
- : kind === "merge" ? "现有内容与新版初始化模板不同,需要保留项目配置后合并。"
104
- : /not a directory/.test(detail ?? "") ? "这里需要一个目录,但当前路径被文件占用。"
105
- : /removed.*local changes/.test(detail ?? "") ? "新版不再使用此文件,但它包含本地修改,需要决定保留还是删除。"
106
- : /target path.*already exists/.test(detail ?? "") ? "迁移目标已有不同内容,需要确认两份文件如何保留。"
107
- : "此项无法安全自动处理,请先核对检查详情,再确定处理方式。";
108
+ : legacyCandidate ? "这是历史初始化候选,但当前证据不足以自动删除。"
109
+ : kind === "safe-update" ? "此项可由程序安全更新,保留项目自定义内容。"
110
+ : kind === "merge" ? "现有内容与新版初始化模板不同,需要保留项目配置后合并。"
111
+ : /not a directory/.test(detail ?? "") ? "这里需要一个目录,但当前路径被文件占用。"
112
+ : /removed.*local changes/.test(detail ?? "") ? "新版不再使用此文件,但它包含本地修改,需要决定保留还是删除。"
113
+ : /target path.*already exists/.test(detail ?? "") ? "迁移目标已有不同内容,需要确认两份文件如何保留。"
114
+ : "此项无法安全自动处理,请先核对检查详情,再确定处理方式。";
108
115
  pendingItems.push({ path: item.path, kind: itemKind, reason,
109
116
  nextStep: itemKind === "safe-update" ? "点击更新按钮即可处理。"
110
117
  : itemKind === "merge" ? "可将下方处理说明交给智能体合并;只有需求冲突时才需你确认。"
111
- : "将处理说明交给智能体检查;需要取舍时确认方案,处理后重新检查。",
118
+ : legacyCandidate ? "在对话页手动调用 /skill:init-update;Skill 会解释证据并在需要时询问,不会直接删除文件。"
119
+ : "将处理说明交给智能体检查;需要取舍时确认方案,处理后重新检查。",
112
120
  ...(detail ? { detail } : {}) });
113
121
  }
114
122
  }
@@ -118,19 +126,20 @@ export class WorkspaceInitializationService {
118
126
  reason: "当前模型配置或认证尚未就绪,更新文件不能解决此问题。",
119
127
  nextStep: "检查模型定义及提供商认证,保留仍可用的模型配置,完成后重新检查。" });
120
128
  const canUpdate = pendingItems.some(item => item.kind === "safe-update");
121
- const needsAttention = pendingItems.some(item => item.kind !== "safe-update") || !report.ok || report.surfaceState !== "recorded" && !canUpdate;
129
+ const needsAttention = pendingItems.some(item => item.kind !== "safe-update") || coreState !== "ready" || migrationState === "review-required" || migrationState === "failed" || report.surfaceState !== "recorded" && !canUpdate;
122
130
  if (needsAttention || canUpdate) {
123
131
  const safeCount = pendingItems.filter(item => item.kind === "safe-update").length;
132
+ const coreReadyPrefix = coreState === "ready" ? "核心初始化已就绪;" : "";
124
133
  const message = canUpdate
125
- ? `有 ${safeCount} 项可在此更新。${needsAttention ? "其余待处理项的原因和下一步见下方。" : "更新会保留项目自定义内容。"}`
126
- : "暂无可直接更新的内容。请按下方原因处理,完成后重新检查。";
134
+ ? `${coreReadyPrefix}有 ${safeCount} 项可在此安全更新。${needsAttention ? "其余旧初始化待处理项见下方。" : "更新会保留项目自定义内容。"}`
135
+ : `${coreReadyPrefix}暂无可直接更新的内容。请按下方原因处理,完成后重新检查。`;
127
136
  if (!pendingItems.length)
128
137
  pendingItems.push({ path: "初始化配置", kind: "confirmation",
129
138
  reason: "检查尚未通过,但没有返回可自动更新的项目。",
130
139
  nextStep: "复制处理说明交给智能体检查初始化记录,完成后重新检查。" });
131
- return { ...this.snapshot(needsAttention ? "needs-attention" : "update-available", message, [...new Set(pendingItems.map(item => item.path))]), canUpdate, pendingItems };
140
+ return { ...this.snapshot(needsAttention ? "needs-attention" : "update-available", message, [...new Set(pendingItems.map(item => item.path))]), canUpdate, pendingItems, coreState: coreState, migrationState: migrationState };
132
141
  }
133
- return this.snapshot("ready", "工作区配置已就绪");
142
+ return { ...this.snapshot("ready", migrationState === "retained" ? "核心初始化已就绪;旧初始化内容已按决定保留。" : "工作区配置已就绪"), coreState: coreState, migrationState: migrationState };
134
143
  }
135
144
  catch (error) {
136
145
  return this.snapshot("error", error instanceof Error ? error.message : "初始化状态检查失败");
@@ -182,10 +191,10 @@ export class WorkspaceInitializationService {
182
191
  if (before.state === "uninitialized") {
183
192
  if (!model || !before.models?.some(item => item.reference === model))
184
193
  throw new Error("请选择一个可用的初始模型;可在模型设置中完成认证后重新检查");
185
- await this.command(["init", "--profile", "full", "--merge", "--client-recovery", "project", "--model", model, "--json"], "apply");
194
+ await this.command(["init", "--merge", "--model", model, "--json"], "apply");
186
195
  }
187
196
  else if (before.canUpdate) {
188
- await this.command(["init", "update", "--apply-safe", "--client-recovery", "project", "--json"], "apply");
197
+ await this.command(["init", "update", "--apply-safe", "--legacy-audit", "--json"], "apply");
189
198
  }
190
199
  else if (before.state !== "ready")
191
200
  throw new Error("没有可安全更新的内容;请查看待处理项的原因和下一步。");
@@ -48,12 +48,15 @@ const controllerIdentityEvidenceSchema = z.object({
48
48
  reportedVersion: z.string().min(1).optional(),
49
49
  packageFingerprint: z.object({
50
50
  algorithm: z.literal("sha256"),
51
- scopeVersion: z.literal(1),
51
+ scopeVersion: z.literal(2),
52
52
  scope: z.tuple([
53
53
  z.literal("package.json"),
54
54
  z.literal("bin/**"),
55
55
  z.literal("dist/**"),
56
56
  z.literal("skills/**"),
57
+ z.literal("docs/templates/**"),
58
+ z.literal("resources/**"),
59
+ z.literal("resource-catalog.v1.json"),
57
60
  ]),
58
61
  value: z.string().regex(/^sha256:[a-f0-9]{64}$/),
59
62
  fileCount: z.number().int().positive(),
@@ -5,6 +5,8 @@ import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
5
5
  import { resolveLoopAgentProfile } from "../profile-mapping.js";
6
6
  import { mapRiskLevelToComplexity } from "../task-spec/complexity-mapping.js";
7
7
  import { resolveWorkflow, } from "../task-spec/workflow-routing.js";
8
+ import { BACKEND_TEST_DEFAULT_TEST_ROOT } from "../../workflows/dag/backend-test-layout.js";
9
+ import { FRONTEND_TEST_DEFAULT_TEST_ROOT } from "../../workflows/dag/frontend-test-layout.js";
8
10
  export const HARNESS_TASK_LINEAGE_MATERIALIZATION_VERSION = 1;
9
11
  export const MAX_HARNESS_TASK_ID_LENGTH = 96;
10
12
  export function slugifyIdentity(value) {
@@ -131,10 +133,10 @@ export async function resolveRevisionDigestsForTaskSpec(input) {
131
133
  /** Union TaskSpec allowed_paths with workflow-owned write roots. */
132
134
  export function expandAllowedPathsForWorkflow(workflow, allowedPaths, options) {
133
135
  const extras = workflow === "backend-test"
134
- ? ["testcase/**", "docs/test-reports/**"]
136
+ ? [`${BACKEND_TEST_DEFAULT_TEST_ROOT}/**`, "docs/test-reports/**"]
135
137
  : workflow === "frontend-test"
136
138
  ? [
137
- `${options?.frontendTestRoot ?? "testcase/frontend"}/**`,
139
+ `${options?.frontendTestRoot ?? FRONTEND_TEST_DEFAULT_TEST_ROOT}/**`,
138
140
  "docs/test-reports/**",
139
141
  ]
140
142
  : [];
@@ -41,37 +41,37 @@
41
41
  :root[data-console-theme="daylight"] body[data-shell="observe-console"],
42
42
  :root[data-console-theme="daylight"] .inspect-workspace-root {
43
43
  color-scheme: light;
44
- --canvas: #eef0f2;
45
- --canvas-soft: #f3f4f5;
46
- --surface: #f9fafb;
47
- --surface-muted: #e5e9ed;
48
- --ink: #303b47;
49
- --surface-ink: #303b47;
50
- --body: #4b5867;
51
- --muted: #5e6a78;
52
- --muted-soft: #697583;
53
- --muted-ink: #5e6a78;
54
- --hairline: #dce1e6;
55
- --hairline-strong: #b5bfc9;
56
- --orange: #466581;
57
- --orange-active: #365570;
58
- --blue: #466581;
59
- --brand: #706387;
60
- --theme-action: #536f88;
61
- --theme-action-hover: #466079;
44
+ --canvas: #f5f6f7;
45
+ --canvas-soft: #f9fafb;
46
+ --surface: #ffffff;
47
+ --surface-muted: #f1f3f5;
48
+ --ink: #0f1115;
49
+ --surface-ink: #0f1115;
50
+ --body: #61666b;
51
+ --muted: #5c636a;
52
+ --muted-soft: #8b9198;
53
+ --muted-ink: #5c636a;
54
+ --hairline: #ebeef2;
55
+ --hairline-strong: #d9dee6;
56
+ --orange: #4868b2;
57
+ --orange-active: #4176e6;
58
+ --blue: #4868b2;
59
+ --brand: #4176e6;
60
+ --theme-action: #4176e6;
61
+ --theme-action-hover: #679efe;
62
62
  --theme-on-action: #ffffff;
63
- --green: #3e735d;
64
- --amber: #85632d;
65
- --warn: #85632d;
66
- --red: #a04f5c;
67
- --peach: #dfe7ee;
68
- --mint: #e0eae4;
69
- --lavender: #e7e3ed;
63
+ --green: #15803d;
64
+ --amber: #b45309;
65
+ --warn: #b45309;
66
+ --red: #dc2626;
67
+ --peach: #edf3fe;
68
+ --mint: #e6faed;
69
+ --lavender: #e7edf7;
70
70
  --gold: #927644;
71
- --shadow-sm: 0 1px 2px rgb(35 45 58 / 4%);
72
- --shadow-panel: 0 1px 2px rgb(35 45 58 / 3%), 0 6px 18px rgb(35 45 58 / 3%);
73
- --shadow-soft: 0 8px 24px rgb(35 45 58 / 6%);
74
- --shadow-lg: 0 16px 40px rgb(35 45 58 / 9%);
71
+ --shadow-sm: 0 1px 2px rgb(15 17 21 / 4%);
72
+ --shadow-panel: 0 1px 2px rgb(15 17 21 / 3%), 0 6px 18px rgb(15 17 21 / 3%);
73
+ --shadow-soft: 0 8px 24px rgb(15 17 21 / 6%);
74
+ --shadow-lg: 0 16px 40px rgb(15 17 21 / 9%);
75
75
  }
76
76
 
77
77
  /* Only the two added themes override component literals; original warm paper
@@ -172,3 +172,7 @@
172
172
  .hljs-addition { color: var(--green); background: var(--mint); }
173
173
  .hljs-deletion { color: var(--red); background: color-mix(in srgb, var(--red) 12%, var(--surface)); }
174
174
  }
175
+
176
+ :root[data-console-theme="daylight"] {
177
+ .oc-bubble-user { background: var(--peach); }
178
+ }
@@ -12,8 +12,7 @@
12
12
  */
13
13
 
14
14
  import { withWorkspaceQuery } from "./router.js";
15
- import { CONSOLE_THEMES, applyStoredConsoleTheme, setConsoleTheme } from "./console-theme.js";
16
- export { CONSOLE_THEMES } from "./console-theme.js";
15
+ import { applyStoredConsoleTheme } from "./console-theme.js";
17
16
 
18
17
  export const OPERATE_TABS = Object.freeze([
19
18
  { id: "tasks", label: "任务", href: "/#/operate/tasks" },
@@ -711,46 +710,6 @@ export function mountOperatorChrome(root, config = {}) {
711
710
 
712
711
  global.appendChild(el("span", "och-spacer"));
713
712
 
714
- const themePicker = el("div", "och-theme");
715
- const trigger = el("button", "och-theme-trigger");
716
- trigger.type = "button";
717
- trigger.title = "主题";
718
- trigger.setAttribute("aria-label", "主题");
719
- trigger.appendChild(makeIcon("theme"));
720
- trigger.setAttribute("aria-expanded", "false");
721
- const menu = el("div", "och-theme-menu");
722
- menu.id = `${popoverId}-theme`;
723
- menu.popover = "auto";
724
- menu.setAttribute("role", "group");
725
- menu.setAttribute("aria-label", "选择主题");
726
- trigger.setAttribute("aria-controls", menu.id);
727
- trigger.popoverTargetElement = menu;
728
- menu.addEventListener("beforetoggle", (event) => {
729
- if (event.newState !== "open") return;
730
- const rect = trigger.getBoundingClientRect();
731
- menu.style.top = `${rect.bottom + 6}px`;
732
- menu.style.left = `${Math.max(8, Math.min(rect.right - 112, window.innerWidth - 120))}px`;
733
- });
734
- menu.addEventListener("toggle", (event) => {
735
- trigger.setAttribute("aria-expanded", String(event.newState === "open"));
736
- });
737
- for (const theme of CONSOLE_THEMES) {
738
- const option = el("button", "och-theme-option", theme.label);
739
- option.type = "button";
740
- option.dataset.theme = theme.id;
741
- option.setAttribute("aria-pressed", String(theme.id === (document.documentElement.dataset.consoleTheme ?? "default")));
742
- option.addEventListener("click", () => {
743
- setConsoleTheme(theme.id);
744
- for (const item of menu.children) {
745
- item.setAttribute("aria-pressed", String(item.dataset.theme === theme.id));
746
- }
747
- menu.hidePopover();
748
- trigger.focus();
749
- });
750
- menu.appendChild(option);
751
- }
752
- themePicker.append(trigger, menu);
753
- global.appendChild(themePicker);
754
713
 
755
714
  if (model.statusVisible && model.freshness) {
756
715
  const fresh = el("span", "och-fresh");
@@ -666,7 +666,9 @@ function scenarioTypes(body) {
666
666
  }
667
667
  function declaredScripts(body) {
668
668
  const automation = sectionBody(body, CASE_SECTION_ALIASES.automation);
669
- return orderedUnique([...automation.matchAll(/testcase\/[A-Za-z0-9_./-]*test_[A-Za-z0-9_.-]*\.py/gi)]
669
+ // Accept both the canonical `.harness/testcase/` root and the legacy
670
+ // `testcase/` root so Markdown written by older runs still declares.
671
+ return orderedUnique([...automation.matchAll(/(?:\.harness\/)?testcase\/[A-Za-z0-9_./-]*test_[A-Za-z0-9_.-]*\.py/gi)]
670
672
  .map((match) => match[0].replaceAll("\\", "/")));
671
673
  }
672
674
  function automationLabelBody(body, labels) {
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { canonicalTestArtifactRoot } from "./test-artifact-namespace.js";
2
3
  /**
3
4
  * Backend-test artifact layout (plan A: configurable test asset paths).
4
5
  *
@@ -8,13 +9,15 @@ import path from "node:path";
8
9
  * writers, gates, analyzers and the executor can never disagree about where
9
10
  * backend-test assets live.
10
11
  *
11
- * Default layout keeps the historical frozen contract:
12
- * testcase/md/README.md
13
- * testcase/md/<stem>.md
14
- * testcase/test_<stem>.py
15
- * pytest target: testcase/
12
+ * Default layout (since 2026-09-17) lives under the shared runtime namespace:
13
+ * .harness/testcase/md/README.md
14
+ * .harness/testcase/md/<stem>.md
15
+ * .harness/testcase/test_<stem>.py
16
+ * pytest target: .harness/testcase/
16
17
  */
17
- export const BACKEND_TEST_DEFAULT_TEST_ROOT = "testcase";
18
+ export const BACKEND_TEST_DEFAULT_TEST_ROOT = canonicalTestArtifactRoot("backend-test");
19
+ /** Historical pre-2026-09-17 default root, kept for reference/diagnostics. */
20
+ export const BACKEND_TEST_LEGACY_DEFAULT_TEST_ROOT = "testcase";
18
21
  export const BACKEND_TEST_DEFAULT_MARKDOWN_REL = "md";
19
22
  export const BACKEND_TEST_DEFAULT_SCRIPT_REL = ".";
20
23
  /** Repository roots that must never be configured as backend-test test roots. */
@@ -54,6 +57,9 @@ function normalizeRelPosix(value, label) {
54
57
  if (segments.some((segment) => segment === ".." || segment === ".")) {
55
58
  throw new Error(`backendTest.${label} must not contain "." or ".." segments: ${value}`);
56
59
  }
60
+ if (segments.some((segment) => segment.startsWith("."))) {
61
+ throw new Error(`backendTest.${label} must not target hidden directories: ${value}`);
62
+ }
57
63
  if (segments.some((segment) => !/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(segment))) {
58
64
  throw new Error(`backendTest.${label} contains unsafe path segments: ${value}`);
59
65
  }
@@ -77,6 +83,10 @@ function normalizeOptionalRel(value, fallback, label) {
77
83
  return normalizeRelPosix(value, label);
78
84
  }
79
85
  function assertTestRootAllowed(testRoot) {
86
+ // D3: the canonical harness test-artifact root is the ONLY hidden-root
87
+ // exception. Every other hidden or forbidden top segment stays rejected.
88
+ if (testRoot === BACKEND_TEST_DEFAULT_TEST_ROOT)
89
+ return;
80
90
  const topSegment = testRoot.split("/")[0];
81
91
  if (FORBIDDEN_TEST_ROOT_PREFIXES.some((forbidden) => topSegment === forbidden || topSegment.startsWith(`${forbidden}.`))) {
82
92
  throw new Error(`backendTest.testRoot must not target production/governance trees: ${testRoot}`);
@@ -85,6 +95,16 @@ function assertTestRootAllowed(testRoot) {
85
95
  throw new Error(`backendTest.testRoot must not target hidden directories: ${testRoot}`);
86
96
  }
87
97
  }
98
+ function resolveTestRootInput(raw, canonical) {
99
+ // D3: only the EXACT canonical harness test-artifact root may be a hidden
100
+ // path. Compare after trimming + stripping trailing slashes so
101
+ // `.harness/testcase/` matches, but every other hidden spelling falls
102
+ // through to the fail-closed normalizer below.
103
+ const trimmed = raw.trim().replace(/\/+$/, "");
104
+ if (trimmed === canonical)
105
+ return canonical;
106
+ return normalizeRelPosix(raw, "testRoot");
107
+ }
88
108
  /**
89
109
  * Resolve the frozen backend-test artifact layout from task config.
90
110
  * Throws on unsafe or ambiguous configuration (fail-closed).
@@ -92,7 +112,7 @@ function assertTestRootAllowed(testRoot) {
92
112
  export function resolveBackendTestLayout(config) {
93
113
  const testRoot = config?.testRoot === undefined || config?.testRoot === null
94
114
  ? BACKEND_TEST_DEFAULT_TEST_ROOT
95
- : normalizeRelPosix(config.testRoot, "testRoot");
115
+ : resolveTestRootInput(config.testRoot, BACKEND_TEST_DEFAULT_TEST_ROOT);
96
116
  assertTestRootAllowed(testRoot);
97
117
  const markdownRel = config?.markdownRel === undefined || config?.markdownRel === null
98
118
  ? BACKEND_TEST_DEFAULT_MARKDOWN_REL
@@ -787,7 +787,9 @@ function extractMappedPytestScripts(testCaseBody) {
787
787
  const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
788
788
  if (!automation.trim())
789
789
  return [];
790
- return unique([...automation.matchAll(/`?(testcase\/[A-Za-z0-9_./-]*test_[A-Za-z0-9_.-]*\.py)`?/gi)]
790
+ // Accept both the canonical `.harness/testcase/` root and the legacy
791
+ // `testcase/` root so Markdown written by older runs still maps.
792
+ return unique([...automation.matchAll(/`?((?:\.harness\/)?testcase\/[A-Za-z0-9_./-]*test_[A-Za-z0-9_.-]*\.py)`?/gi)]
791
793
  .map((match) => match[1].replaceAll("\\", "/"))
792
794
  .filter((value) => !value.includes("..")));
793
795
  }
@@ -422,7 +422,10 @@ function classifyCollectionFailure(output, evidence) {
422
422
  const blockedPatterns = [
423
423
  [/No module named ['\"](?!testcase(?:\.|['\"]))/i, "missing-third-party-module"],
424
424
  [/pytest(?:_|-)plugin|pluggy|unknown hook|unrecognized arguments/i, "pytest-plugin-or-config"],
425
- [/(?:apps|src|server|api)\/[A-Za-z0-9_./-]+\.py/i, "production-module-import"],
425
+ // Windows Python installs live under ...\WindowsApps\...; after slash
426
+ // normalization that path suffix must not read as an `apps/…py`
427
+ // production import (pytest >= 9 includes the importlib bootstrap frame).
428
+ [/(?<!Windows)(?:apps|src|server|api)\/[A-Za-z0-9_./-]+\.py/i, "production-module-import"],
426
429
  [/permission denied|access is denied|timed? out|not recognized as an internal|no such file or directory.*python/i, "environment-or-spawn"],
427
430
  ];
428
431
  for (const [pattern, kind] of blockedPatterns) {
@@ -3,7 +3,8 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { DEFAULT_DAG_EXECUTOR_MODELS } from './types.js';
5
5
  import { parseDagSpec } from './types.js';
6
- import { loadHarnessManifest } from '../../governance/harness.js';
6
+ import { ProjectCapabilityScanner } from '../../governance/project-capability-scanner.js';
7
+ import { loadNormalizedProjectConfig } from '../../governance/project-config.js';
7
8
  import { loadTaskConfig } from '../../task/runtime.js';
8
9
  import { resolveCliPath } from '../../shared/path-refs.js';
9
10
  const DEFAULT_OUTPUT_DIR = os.tmpdir();
@@ -22,14 +23,13 @@ export async function initFinalVerificationDagFromTask(repoRoot, taskId, options
22
23
  };
23
24
  }
24
25
  export async function buildFinalVerificationDagFromTask(repoRoot, taskId, options = {}) {
25
- const [taskConfig, manifest] = await Promise.all([
26
- loadTaskConfig(repoRoot, taskId),
27
- loadHarnessManifest(repoRoot),
28
- ]);
26
+ const taskConfig = await loadTaskConfig(repoRoot, taskId);
29
27
  const taskGlob = options.taskGlob ?? taskId;
30
28
  const closeoutPath = options.closeoutPath ?? path.join(os.tmpdir(), `${taskId}-closeout-report.md`);
31
- const checkRepo = manifest.scripts.checkRepo ?? 'scripts/check-repo.sh';
32
- const finalVerifyCommands = [`HARNESS_ALLOW_ACTIVE_DAG_RUNS=1 bash ${checkRepo}`];
29
+ const finalVerifyCommands = await resolveFinalVerificationCommands({
30
+ repoRoot,
31
+ taskCommands: taskConfig.verifyCommands.map((entry) => entry.command),
32
+ });
33
33
  const hasChangesetSignal = taskDeclaresReleaseImpact(taskConfig);
34
34
  const tasks = [
35
35
  shellTask({
@@ -142,12 +142,12 @@ export async function buildFinalVerificationDagFromTask(repoRoot, taskId, option
142
142
  executor: 'shell',
143
143
  contextProfile: 'slim',
144
144
  writePolicy: 'read-only',
145
- skills: ['ai-engineering-context'],
145
+ skills: ['builtin:ai-engineering-context'],
146
146
  },
147
147
  executorModels: DEFAULT_DAG_EXECUTOR_MODELS,
148
148
  skillsByRole: {
149
- verifier: ['verification-before-completion'],
150
- closeout: ['loop-agent', 'verification-before-completion'],
149
+ verifier: ['builtin:verification-before-completion'],
150
+ closeout: ['builtin:loop-agent', 'builtin:verification-before-completion'],
151
151
  },
152
152
  tasks,
153
153
  });
@@ -178,3 +178,38 @@ function taskDeclaresReleaseImpact(taskConfig) {
178
178
  ].join('\n');
179
179
  return /changeset|release|publish|version/i.test(haystack);
180
180
  }
181
+ export async function resolveFinalVerificationCommandSelection(input) {
182
+ if (input.taskCommands.length > 0) {
183
+ return {
184
+ source: 'task-contract',
185
+ commands: [...new Set(input.taskCommands)],
186
+ };
187
+ }
188
+ const verification = (await loadNormalizedProjectConfig(input.repoRoot)).verification;
189
+ const configured = verification.full.length > 0
190
+ ? verification.full
191
+ : verification.standard.length > 0
192
+ ? verification.standard
193
+ : verification.quick;
194
+ if (configured.length > 0) {
195
+ return {
196
+ source: 'project-config',
197
+ commands: [...new Set(configured)],
198
+ };
199
+ }
200
+ const scan = await new ProjectCapabilityScanner().scan(input.repoRoot);
201
+ const selected = scan.verification.commands.find((candidate) => candidate.id.endsWith(':test'))
202
+ ?? scan.verification.commands.find((candidate) => candidate.id.endsWith(':typecheck'))
203
+ ?? scan.verification.commands.find((candidate) => candidate.id.endsWith(':lint'))
204
+ ?? scan.verification.commands[0];
205
+ if (scan.verification.status !== 'ready' || !selected) {
206
+ throw new Error(`final verification is ${scan.verification.status}; add Task Contract verification or .loop-agent/project.json verification`);
207
+ }
208
+ return {
209
+ source: 'capability-scanner',
210
+ commands: [selected.command],
211
+ };
212
+ }
213
+ async function resolveFinalVerificationCommands(input) {
214
+ return (await resolveFinalVerificationCommandSelection(input)).commands;
215
+ }