@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.
- package/CHANGELOG.md +27 -2
- package/README.md +12 -12
- package/dist/adapters/loop-agent.js +34 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +5 -5
- package/dist/cli/program.js +1 -4
- package/dist/commands/init-legacy-audit.js +330 -0
- package/dist/commands/init-upgrade.js +644 -104
- package/dist/commands/init.js +686 -330
- package/dist/commands/operator-guide.js +99 -0
- package/dist/commands/operator.js +16 -3
- package/dist/executors/dag-pi-executor.js +11 -6
- package/dist/executors/pi-sdk-executor.js +3 -1
- package/dist/executors/pi-writer-tool-policy.js +60 -4
- package/dist/executors/playwright-cli-launcher.js +31 -45
- package/dist/executors/shell-write-guard.js +24 -67
- package/dist/governance/exec-plans.js +10 -1
- package/dist/governance/harness.js +2 -7
- package/dist/governance/manifest-types.js +28 -9
- package/dist/governance/project-capability-scanner.js +263 -0
- package/dist/governance/project-config.js +96 -0
- package/dist/governance/resource-catalog.js +151 -0
- package/dist/infrastructure/console/app-data.js +6 -2
- package/dist/shared/operator/capabilities.js +2 -2
- package/dist/shared/package-metadata.js +23 -7
- package/dist/shared/pi-context-pressure/extension.js +23 -0
- package/dist/shared/pi-context-pressure/index.js +1 -0
- package/dist/shared/pi-context-pressure/telemetry.js +4 -0
- package/dist/shared/pi-context-pressure/tool-result-pruner.js +98 -0
- package/dist/shared/update/npm-client.js +49 -1
- package/dist/task/config-types.js +4 -2
- package/dist/task/source-prepare/build-draft.js +1 -0
- package/dist/task/source-prepare/path-policy.js +17 -7
- package/dist/worker/cli.js +3 -5
- package/dist/worker/console/chat/browser-automation.js +455 -41
- package/dist/worker/console/chat/browser-content.js +74 -0
- package/dist/worker/console/chat/browser-diagnostics.js +250 -0
- package/dist/worker/console/chat/browser-evaluation.js +58 -0
- package/dist/worker/console/chat/browser-operation-queue.js +19 -0
- package/dist/worker/console/chat/browser-pick.js +113 -0
- package/dist/worker/console/chat/browser-routes.js +54 -5
- package/dist/worker/console/chat/browser-screenshot-result.js +47 -0
- package/dist/worker/console/chat/browser-storage.js +89 -0
- package/dist/worker/console/chat/chat-event-store.js +7 -6
- package/dist/worker/console/chat/context-insights.js +84 -19
- package/dist/worker/console/chat/pet/pet-affinity.js +169 -0
- package/dist/worker/console/chat/pet/pet-persist.js +109 -0
- package/dist/worker/console/chat/pet/pet-registry.js +142 -0
- package/dist/worker/console/chat/pet/pet-remarks.js +48 -0
- package/dist/worker/console/chat/pet/pet-routes.js +125 -0
- package/dist/worker/console/chat/pet/pet-service.js +262 -0
- package/dist/worker/console/chat/pet/pet-state.js +99 -0
- package/dist/worker/console/chat/pi-mode-loop-isolation.js +17 -2
- package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +125 -174
- package/dist/worker/console/chat/pi-runtime.js +8 -3
- package/dist/worker/console/chat/repo-browser.js +80 -2
- package/dist/worker/console/chat/repo-preview-kind.js +155 -0
- package/dist/worker/console/chat/routes.js +43 -7
- package/dist/worker/console/chat/scheduled-goal-request.js +14 -6
- package/dist/worker/console/chat/session-mode.js +1 -1
- package/dist/worker/console/chat/session-store.js +24 -5
- package/dist/worker/console/chat/tools.js +12 -0
- package/dist/worker/console/console-shutdown.js +34 -0
- package/dist/worker/console/server.js +2 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-r1NAPU_5.js → abnfDiagram-N423BO3Z-DASc69AM.js} +1 -1
- package/dist/worker/console/static/assets/{arc-dT5T4sVL.js → arc-h09p-LAc.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP_cOq46.js → architectureDiagram-T3A2C74G-CZ3dtLkh.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-sToEZxEf.js → blockDiagram-VBNYF7ZC-DMrRhMXG.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DSxwwi2m.js → c4Diagram-5PPSVZJV-DQxisHV6.js} +1 -1
- package/dist/worker/console/static/assets/channel-D1zgKZfg.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B9fh0IkF.js → chunk-2GRJ4B5K-CefVN9zh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CAUNl-A6.js → chunk-2Q5K7J3B-DmR1Koq2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BomeqbNX.js → chunk-5RXB4S5H-NlzrSW7L.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BNAuTOLE.js → chunk-5VM5RSS4-DfelJv-0.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DfzjRD5y.js → chunk-6Q2QTUOP-BK6RFy58.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C3uE-yc2.js → chunk-GF5L2VYU-BAeh9lz4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-C3sQMMYr.js → chunk-JWPE2WC7-ChPS5WzG.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D_AtEYQL.js → chunk-KBJHAD2P-BoVe5IM6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DW4TwhKv.js → chunk-RYQCIY6F-C4q_0Lw9.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CTQDmJcp.js → chunk-XXDRQBXY-Bb9TvjeY.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CXNNsY2M.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CXNNsY2M.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-iMqPuBsL.js → cose-bilkent-JH36ORCC-DmNnL8Ff.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DktIuDWs.js → cynefin-VYW2F7L2-Contvx58.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BgVnjyEs.js → cynefinDiagram-MW4NZA55-Cigq7cjk.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CR25E4bu.js → dagre-VZM6K2ZE-dEbC0nxb.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CPM2h5HY.js → diagram-7IWD3JNH-CzIztESV.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DqF0x2g1.js → diagram-B4RE2ZJO-Bo14zbaY.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-G8BYp2eb.js → diagram-LBJQPF4R-C4xUyil6.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BbC4kT_D.js → diagram-Q27KOJAE-CaBY_uYa.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CKZwZ-G5.js → diagram-UB23O5K3-ClUXUeiu.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-kJDmheia.js → ebnfDiagram-BXEA7PRR-BbGCgjC6.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DFTVHyxB.js → erDiagram-JOGREHBK-Dr-AcwSl.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BqdN5_os.js → flowDiagram-UKHOOZJN-BG8Gcfyc.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-SYDjVakh.js → ganttDiagram-PKOTCBZU-CFVaPs5r.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js → gitGraphDiagram-DS77QQ5N-ChJ8pm-O.js} +1 -1
- package/dist/worker/console/static/assets/index-B_5GhVIO.js +488 -0
- package/dist/worker/console/static/assets/index-C7YMBluX.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DIeAmJ45.js → infoDiagram-6WML65LV-C5J9-uCZ.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-oikqUAlo.js → ishikawaDiagram-WSZJBQD7-BcfjqzDv.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-mKEF_xYT.js → journeyDiagram-NVQOT4AX-BzzCO-rD.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BdWJHPnN.js → kanban-definition-27J2QSJJ-NCHUZ46h.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DGRlieMH.js → linear-BKZlWCUU.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-4P9yPC9c.js → mermaid.core-DbTaQjFE.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-5PTSkos6.js → mindmap-definition-FAOFIHXS-CroYl5xQ.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DONIFpG5.js → pegDiagram-VL7TDLO6-DgMBx-9k.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-D_Z4S70M.js → pieDiagram-7S7Q4E2Y-CyWDmbq1.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CAsjh8iY.js → quadrantDiagram-CIZ2JOQS-BgGEOqSP.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Cx4Dd6ac.js → railroadDiagram-AXF67PYL-BMZ7oeeV.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-cQb7mwyH.js → requirementDiagram-LRYGKXZP-DxqyGhcc.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cd-DhUdf.js → sankeyDiagram-W5VNT64P-DnSvNCG8.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-AlKYRgIk.js → sequenceDiagram-SI44F4Z6-BiZh6f-R.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CUy_ET4e.js → sizeCapture-X5ZJPWSS-BKXSI8WB.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CTuVe6Sa.js → stateDiagram-OKZ733FA-CfSgoYFm.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DgHi3vjb.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-ChFLJb-v.js → swimlanes-SLNWSIFB-CyGADcHe.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bgvdt1Y0.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DZZeiAPr.js → timeline-definition-Z64GVDOM-HmPaSPOK.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-t4O91cZU.js → vennDiagram-T6HMQDX7-VA2VBbez.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CV5qGHZu.js → wardleyDiagram-T6FBY63Y-BxjiBPZa.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DyXbytUI.js → xychartDiagram-ELKLHX3M--hUY-qFj.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static/pets/ember/pet.json +167 -0
- package/dist/worker/console/static/pets/ember/spritesheet.png +0 -0
- package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +4 -3
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +57 -9
- package/dist/worker/console/static-src/operator-chat/use-stat-dialog.js +95 -0
- package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +6 -0
- package/dist/worker/console/workspace-context.js +15 -0
- package/dist/worker/console/workspace-initialization.js +25 -16
- package/dist/worker/delivery/package.js +4 -1
- package/dist/worker/materialize/harness-task-lineage.js +4 -2
- package/dist/worker/observe/static/console-theme.css +33 -29
- package/dist/worker/observe/static/operator-chrome.js +1 -42
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +3 -1
- package/dist/workflows/dag/backend-test-layout.js +27 -7
- package/dist/workflows/dag/backend-test-markdown-workflow.js +3 -1
- package/dist/workflows/dag/backend-test-pytest-collection.js +4 -1
- package/dist/workflows/dag/final-verification.js +45 -10
- package/dist/workflows/dag/frontend-test-case-checklist.js +12 -3
- package/dist/workflows/dag/frontend-test-environment-probe.js +0 -2
- package/dist/workflows/dag/frontend-test-layout.js +41 -12
- package/dist/workflows/dag/hybrid/sources.js +35 -16
- package/dist/workflows/dag/hybrid/templates/backend-test.js +70 -55
- package/dist/workflows/dag/hybrid/templates/frontend-test.js +62 -41
- package/dist/workflows/dag/hybrid/templates/kg-bootstrap.js +6 -6
- package/dist/workflows/dag/hybrid/templates/knowledge-sync.js +5 -5
- package/dist/workflows/dag/skill-instructions.js +102 -32
- package/dist/workflows/dag/skill-snapshot.js +3 -0
- package/dist/workflows/dag/skills.js +7 -7
- package/dist/workflows/dag/test-artifact-namespace.js +73 -0
- package/dist/workflows/dag/validate.js +24 -5
- package/dist/workflows/dag/workspace-checkpoint.js +13 -1
- package/docs/architecture/runtime-boundaries.md +28 -22
- package/docs/init-surface.manifest.json +43 -295
- package/docs/templates/README.md +4 -4
- package/docs/templates/agent-dag.base.json +9 -9
- package/docs/templates/agent-dag.final-verification.json +6 -6
- package/docs/templates/agent-dag.supervised-implementation.json +12 -12
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +3 -3
- package/docs/templates/backend-test-dag.json +708 -708
- package/docs/templates/backend-test-dag.review-cases.prompt.md +3 -3
- package/docs/templates/frontend-implementation-dag.json +2 -2
- package/docs/templates/frontend-test-case-checklist.md +2 -2
- package/docs/templates/frontend-test-dag.generate-cases.prompt.md +7 -7
- package/docs/templates/frontend-test-dag.json +48 -39
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +2 -2
- package/docs/templates/frontend-test-dag.retrospect.prompt.md +1 -1
- package/docs/templates/frontend-test-dag.review-cases.prompt.md +1 -1
- package/docs/templates/hybrid-dag.json +9 -9
- package/docs/templates/knowledge-sync-dag.json +8 -8
- package/package.json +12 -3
- package/resource-catalog.v1.json +1973 -0
- package/resources/agent-bridge/loop-agent/SKILL.md +40 -0
- package/resources/init-history/legacy-surface.v1.json +33 -0
- package/skills/fe-test-ui-scout/SKILL.md +4 -4
- package/skills/fe-test-ui-scout/references/ledger-schema.md +1 -1
- package/skills/init-update/SKILL.md +34 -0
- package/skills/init-update/agents/openai.yaml +6 -0
- package/skills/playwright-cli/SKILL.md +1 -1
- package/skills/playwright-cli-case-generator/SKILL.md +4 -4
- package/dist/worker/console/static/assets/channel-CyBY_yGk.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +0 -1
- package/dist/worker/console/static/assets/index-B28Onmfy.css +0 -1
- package/dist/worker/console/static/assets/index-CTMZsDmh.js +0 -486
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +0 -1
- 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
|
|
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
|
-
:
|
|
103
|
-
: kind === "
|
|
104
|
-
:
|
|
105
|
-
: /
|
|
106
|
-
: /
|
|
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") ||
|
|
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
|
-
?
|
|
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", "--
|
|
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", "--
|
|
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(
|
|
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
|
-
? [
|
|
136
|
+
? [`${BACKEND_TEST_DEFAULT_TEST_ROOT}/**`, "docs/test-reports/**"]
|
|
135
137
|
: workflow === "frontend-test"
|
|
136
138
|
? [
|
|
137
|
-
`${options?.frontendTestRoot ??
|
|
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: #
|
|
45
|
-
--canvas-soft: #
|
|
46
|
-
--surface: #
|
|
47
|
-
--surface-muted: #
|
|
48
|
-
--ink: #
|
|
49
|
-
--surface-ink: #
|
|
50
|
-
--body: #
|
|
51
|
-
--muted: #
|
|
52
|
-
--muted-soft: #
|
|
53
|
-
--muted-ink: #
|
|
54
|
-
--hairline: #
|
|
55
|
-
--hairline-strong: #
|
|
56
|
-
--orange: #
|
|
57
|
-
--orange-active: #
|
|
58
|
-
--blue: #
|
|
59
|
-
--brand: #
|
|
60
|
-
--theme-action: #
|
|
61
|
-
--theme-action-hover: #
|
|
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: #
|
|
64
|
-
--amber: #
|
|
65
|
-
--warn: #
|
|
66
|
-
--red: #
|
|
67
|
-
--peach: #
|
|
68
|
-
--mint: #
|
|
69
|
-
--lavender: #
|
|
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(
|
|
72
|
-
--shadow-panel: 0 1px 2px rgb(
|
|
73
|
-
--shadow-soft: 0 8px 24px rgb(
|
|
74
|
-
--shadow-lg: 0 16px 40px rgb(
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 = "
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
32
|
-
|
|
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
|
+
}
|