@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
|
@@ -5,6 +5,7 @@ import { assertValidDagSpec } from "../../validate.js";
|
|
|
5
5
|
import { DEFAULT_DAG_EXECUTOR_MODELS, DEFAULT_DAG_OUTPUT_LANGUAGE, parseDagSpec } from "../../types.js";
|
|
6
6
|
import { pathMatchesPattern } from "../../../../shared/git-progress.js";
|
|
7
7
|
import { applyFrontendTestLayoutToText, resolveFrontendTestLayout } from "../../frontend-test-layout.js";
|
|
8
|
+
import { isHarnessNamespacePath } from "../../test-artifact-namespace.js";
|
|
8
9
|
import { buildFrontendTestOutcomeGateShellSnippet } from "../../frontend-test-result-contract.js";
|
|
9
10
|
import { GENERATED_DAG_RUNTIME_CONTRACT, HYBRID_DEFAULTS, STANDARD_GLOBAL_CONSTRAINTS, buildDagSourceBinding, buildSourceContextBlock, extractObjective, extractSuccessCriteria, resolveDagVerifyStrategy } from "../sources.js";
|
|
10
11
|
import { applyDefaultReadOnlyRetryPolicy, commonForbiddenPaths, commonReadOnlyPaths, stampGeneratedArtifactBindings, } from "./shared.js";
|
|
@@ -62,7 +63,7 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
62
63
|
const maxCasesPerBatchLiteral = String(config.maxCasesPerBatch);
|
|
63
64
|
const checklistScript = [
|
|
64
65
|
"const fs=require('fs'),path=require('path');",
|
|
65
|
-
|
|
66
|
+
`const root='${layout.casesDir}';`,
|
|
66
67
|
"const draft=path.join(root,'manifest.draft.json');",
|
|
67
68
|
"const final=path.join(root,'manifest.json');",
|
|
68
69
|
"const manifestPath=fs.existsSync(draft)?draft:(fs.existsSync(final)?final:null);",
|
|
@@ -84,8 +85,8 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
84
85
|
" if(typeof c.caseId==='string'&&/^AC-/i.test(c.caseId))issues.push({ruleId:'case-id-is-ac',caseId:id,detail:'do not use acceptance id as caseId; put AC-FE-* only in acIds'});",
|
|
85
86
|
" const casePath=typeof c.casePath==='string'?c.casePath:null;",
|
|
86
87
|
" if(!casePath||!fs.existsSync(casePath)){issues.push({ruleId:'case-file-missing',caseId:id,detail:String(casePath)});continue;}",
|
|
87
|
-
" if(typeof c.caseId==='string'&&casePath!=='
|
|
88
|
-
" const body=fs.readFileSync(casePath,'utf8');",
|
|
88
|
+
" if(typeof c.caseId==='string'&&casePath!=='${layout.casesDir}/'+c.caseId+'.md')issues.push({ruleId:'case-path-mismatch',caseId:id,detail:casePath+' must equal ${layout.casesDir}/'+c.caseId+'.md'});",
|
|
89
|
+
" const body=fs.readFileSync(casePath,'utf8').replace(/\\bplaywright_cli\\b/g,'playwright-cli');",
|
|
89
90
|
" if(!openRe.test(body))issues.push({ruleId:'open-prefix',caseId:id,detail:'missing playwright-cli open --browser=chrome <absolute-url>; playwright-cli is strongly recommended for browser execution'});",
|
|
90
91
|
,
|
|
91
92
|
" const m=body.match(/playwright-cli\\s+open\\s+--browser=chrome\\s+(https?:\\/\\/\\S+)/i);",
|
|
@@ -117,7 +118,7 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
117
118
|
JSON.stringify([
|
|
118
119
|
"const fs=require('fs'),path=require('path');",
|
|
119
120
|
"function fail(ruleId,detail){ const msg=JSON.stringify({ruleId:ruleId,detail:String(detail||'')}); console.error('frontend-test manifest blocked: '+msg); throw new Error('frontend-test manifest blocked: '+ruleId+(detail?(': '+detail):'')); }",
|
|
120
|
-
|
|
121
|
+
`const draft='${layout.casesDir}/manifest.draft.json',file='${layout.casesDir}/manifest.json',tmp=file+'.tmp';`,
|
|
121
122
|
"if(!fs.existsSync(draft)) fail('draft-missing','missing '+draft);",
|
|
122
123
|
"let manifest; try{manifest=JSON.parse(fs.readFileSync(draft,'utf8'));}catch(e){fail('draft-invalid-json',e&&e.message||e);}",
|
|
123
124
|
"if(manifest.schemaVersion!==1) fail('draft-schema','schemaVersion must be 1');",
|
|
@@ -138,8 +139,8 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
138
139
|
" if(typeof c.dimension!=='string'||!dims.has(c.dimension)) fail('invalid-dimension',String(c.dimension));",
|
|
139
140
|
" if(!Array.isArray(c.acIds)||c.acIds.length===0||c.acIds.some(a=>typeof a!=='string'||!a.trim())) fail('ac-mapping','invalid acIds for '+c.caseId);",
|
|
140
141
|
" for(const ac of c.acIds){ if(!acIdRe.test(ac)) fail('ac-id-shape','acIds entry must be AC-* acceptance id, not caseId: '+ac); if(truncatedAcRe.test(ac)||(declaredAc.size===0&&truncatedUndeclaredAcRe.test(ac))) fail('ac-id-truncated','acIds entry is a truncated acceptance id (missing feature prefix or sequence number): '+ac); if(declaredAc.size>0&&!declaredAc.has(ac)) fail('unknown-ac',ac+' not in sourceBinding; repair generator input or AC list'); }",
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
` c.casePath='${layout.casesDir}/'+c.caseId+'.md';`,
|
|
143
|
+
` c.evidenceDir='${layout.evidenceDir}/'+c.caseId+'/';`,
|
|
143
144
|
" for(const k of ['casePath','evidenceDir']){ const v=c[k]; if(typeof v!=='string'||path.isAbsolute(v)||v.includes('..')) fail('unsafe-path',k+': '+String(v)); }",
|
|
144
145
|
" if(!fs.existsSync(c.casePath)) fail('case-file-missing','missing case file '+c.casePath+' (filename must equal caseId.md)');",
|
|
145
146
|
" if(seenCasePath.has(c.casePath)) fail('duplicate-case-path',c.casePath); seenCasePath.add(c.casePath);",
|
|
@@ -158,7 +159,7 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
158
159
|
"const runDir=process.env.HARNESS_DAG_RUN_DIR; if(!runDir)throw new Error('missing HARNESS_DAG_RUN_DIR');",
|
|
159
160
|
"const resultPath=path.join(runDir,'contracts','frontend-test-result.json'); if(!fs.existsSync(resultPath))throw new Error('missing '+resultPath);",
|
|
160
161
|
"const r=JSON.parse(fs.readFileSync(resultPath,'utf8')); const findings=Array.isArray(r.advisoryFindings)?r.advisoryFindings:[];",
|
|
161
|
-
|
|
162
|
+
`const reports='${layout.reportsDir}'; fs.mkdirSync(reports,{recursive:true});`,
|
|
162
163
|
"const esc=v=>String(v??'').replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('\\\"','"');",
|
|
163
164
|
"const labels={cases:'用例总数',passed:'通过',failed:'失败',blocked:'阻塞'}; const totals=r.totals||{}; const missing=r.acceptanceCoverage&&Array.isArray(r.acceptanceCoverage.missing)?r.acceptanceCoverage.missing:[];",
|
|
164
165
|
"const md=['# 前端测试质量建议报告','','> Advisory:以下 finding 用于改进测试资产和证据质量,不阻塞后续流程。','','## 执行摘要','',...Object.entries(labels).map(([k,v])=>'- '+v+': '+Number(totals[k]||0)),'- 执行结果: '+String(r.outcome||'unknown'),'- 缺失 AC: '+(missing.join(', ')||'无'),'','## 建议项','',...(findings.length?findings.map(f=>'- ['+f.ruleId+']'+(f.caseId?' '+f.caseId:'')+': '+f.detail):['- 未发现建议项。']),''];",
|
|
@@ -197,7 +198,7 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
197
198
|
writeSet: ragWriteSet,
|
|
198
199
|
allowedPaths: [...ragWriteSet],
|
|
199
200
|
forbiddenPaths: forbidden,
|
|
200
|
-
outputContract:
|
|
201
|
+
outputContract: `Write ${layout.ragDir}/standard-scenarios.v1.json for generate-time standard scenario coverage. Copy docs/templates or harness.json governanceRoot templates (including ai_workspace/loop-agent/templates) when present; otherwise write the minimal STD-FE-SMOKE-ENTRY fallback.`,
|
|
201
202
|
subtask_prompt: "Prepare frontend-test package: materialize standard-scenarios.v1.json into the RAG package from docs/templates, governanceRoot/templates, or the init-projected ai_workspace/loop-agent/templates path.",
|
|
202
203
|
shell: {
|
|
203
204
|
commands: [],
|
|
@@ -218,10 +219,10 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
218
219
|
writeSet: ragWriteSet,
|
|
219
220
|
allowedPaths: [...commonReadOnlyPaths(sources), ...ragWriteSet],
|
|
220
221
|
forbiddenPaths: forbidden,
|
|
221
|
-
outputContract:
|
|
222
|
+
outputContract: `Write short ${layout.ragDir}/context.md and coverage-map.md with a model-resolved non-production baseUrl/baseUrlSource from task source config, environmentProbe=pending, and capability notes.`,
|
|
222
223
|
subtask_prompt: [
|
|
223
224
|
"Build the frontend test RAG package (keep it short).",
|
|
224
|
-
|
|
225
|
+
`Read task source, routes/components/API or Mock facts, and execution contract. Write only ${layout.ragDir}/context.md and coverage-map.md.`,
|
|
225
226
|
"Prefer fixed fields: baseUrl, baseUrlSource, environmentProbe, AC table, capability matrix (backend real/mock, pagination data, HTTP observation, error injection), risks, forbidden hosts. Do not paste large implementation dumps.",
|
|
226
227
|
"Base URL resolution (required): read the bound task source and config/environment references. Prefer TARGET_URL/targetUrl as the concrete tested entry, then BASE_URL/baseUrl or FRONTEND_BASE_URL/frontendUrl, then LOGIN_URL/loginUrl; accept common casing and underscore/kebab/space variants. Choose one absolute non-production http(s) URL, never API-only or production hosts. If no usable URL exists, use http://localhost:5173. Write `baseUrl: <url>`, `baseUrlSource: <bound source path>|default-localhost-5173`, and `environmentProbe: pending`. Include exact start prefix: playwright-cli open --browser=chrome <resolved-base-url> with the concrete selected URL.",
|
|
227
228
|
buildSourceContextBlock(sources),
|
|
@@ -238,7 +239,7 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
238
239
|
allowedPaths: [...ragWriteSet],
|
|
239
240
|
forbiddenPaths: forbidden,
|
|
240
241
|
outputContract: "Fail-closed environment preflight: absolute non-production baseUrl + curl HTTP reachability; writes environmentProbe facts; unreachable => blockedReason frontend-base-url-unreachable with errorClass (connection-refused / dns-unresolved / connect-timeout / http-N / curl-exit-N). Node ERROR so generate/map do not run. Does not start the app.",
|
|
241
|
-
subtask_prompt:
|
|
242
|
+
subtask_prompt: `Parse the resolved absolute baseUrl from ${layout.ragDir}/context.md, reject production/non-http(s)/credential/query/fragment URLs, then probe it with curl HEAD and GET fallback (connect/max-time; no auth/cookie). 2xx/3xx => reachable and continue. Connection refused (curl 7) records errorClass=connection-refused and tells the operator to start the local app (scripts/serve.sh or npm start) then rerun from this node. 4xx/5xx/DNS/timeout/TLS => blockedReason frontend-base-url-unreachable with a distinct errorClass. Missing curl => blockedReason curl-unavailable. Do not start the app. Fixture/reset remain soft guidance.`,
|
|
242
243
|
shell: {
|
|
243
244
|
commands: [],
|
|
244
245
|
frontendTestEnvironmentProbe: {},
|
|
@@ -261,22 +262,22 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
261
262
|
outputContract: "Write executable Markdown frontend cases, index.md, and manifest.draft.json schemaVersion 1 only; the exclusive shell materializer promotes the validated draft to manifest.json. Do not write manifest.json or test source code.",
|
|
262
263
|
subtask_prompt: [
|
|
263
264
|
"Use skill playwright-cli-case-generator.",
|
|
264
|
-
|
|
265
|
+
`Read ${layout.ragDir}/standard-scenarios.v1.json and cover priority=must scenarios (or record GAP in coverage-map). Include ## 测试点 and ## 测试步骤 in each case.`,
|
|
265
266
|
...(uiAnchorsLedgerInstruction ? [uiAnchorsLedgerInstruction] : []),
|
|
266
|
-
|
|
267
|
+
`Read only ${layout.ragDir}/context.md, ${layout.ragDir}/coverage-map.md, and the draft case paths ${layout.casesDir}/FE-*.md, ${layout.casesDir}/index.md, and ${layout.casesDir}/manifest.draft.json. Write only those same draft paths. Do not write ${layout.casesDir}/manifest.json.`,
|
|
267
268
|
"Generate Markdown cases, index.md and manifest.draft.json (schemaVersion 1; cases[] with caseId, casePath, dimension, acIds, evidenceDir).",
|
|
268
269
|
"HARD ID CONTRACT (do not confuse these):",
|
|
269
270
|
"- caseId / filename MUST be FE-<FEATURE>-<NNN>-<dimension> (example FE-LOGIN-001-core). NEVER use AC-FE-* as caseId or filename.",
|
|
270
271
|
"- acIds MUST list acceptance criteria only: AC-FE-* / AC-* from the declared task list (example AC-FE-001). NEVER put FE-* case ids into acIds.",
|
|
271
|
-
|
|
272
|
+
`- casePath MUST equal ${layout.casesDir}/<caseId>.md; evidenceDir MUST equal ${layout.evidenceDir}/<caseId>/. Materialize will rewrite paths, but files must already use caseId filenames.`,
|
|
272
273
|
`Declared acceptance ids for this task (use only these in acIds when non-empty): ${declaredAcIds.length > 0 ? declaredAcIds.join(", ") : "(none extracted - still use AC-* shape, never FE-* case ids)"}.`,
|
|
273
274
|
"dimensions: core|boundary|flow|backend only.",
|
|
274
275
|
"Prefer a small smoke suite (default max roughly 4-8 cases unless task frontendTest.maxCasesPerBatch is higher). Never invent unavailable API fields or credentials. Do not create pytest or Playwright source.",
|
|
275
276
|
"HARD playwright-cli-only: every browser step must use repo skill playwright-cli declared commands only. Forbidden: bare `playwright`, `npx playwright`, `playwright test`, `@playwright/test`, Node Playwright API, or generating Playwright/Pytest source. No fallback when playwright-cli is unavailable - case must instruct blocked evidence playwright-cli-unavailable.",
|
|
276
|
-
|
|
277
|
+
`Copy the resolved absolute baseUrl from ${layout.ragDir}/context.md after the environment probe has marked it reachable. Every browser start command must be exactly: playwright-cli open --browser=chrome <resolved-base-url-from-context.md> with that concrete URL. Never leave a base-url placeholder. Use default browser session only; never write -s=<case-id>.`,
|
|
277
278
|
"HARD dynamic refs: executable playwright-cli lines must never contain an angle-bracket token such as <fresh-ref> or descriptive <...> placeholder. Use only shell-safe documentation placeholders `eX`, `eY`, ...; each means the real `eNN` ref parsed from the immediately preceding latest `snapshot`. Write a fresh snapshot before every element reference. eX/eY are never literal structured-tool arguments; a later snapshot invalidates prior refs, so never reuse stale refs.",
|
|
278
279
|
"HARD file-output argv: use canonical `--filename` only. Screenshot uses `playwright-cli screenshot --filename final.png` (a real ref may precede the flag); PDF uses `playwright-cli pdf --filename final.pdf`; snapshot without filename is response-only and a snapshot file uses `playwright-cli snapshot --filename snapshot.txt`. Never generate `playwright-cli screenshot <path>`, use `--path`, `--output`, or `--file`, or pass an output path as a positional target.",
|
|
279
|
-
|
|
280
|
+
`Each case must be independently reproducible with fixture/reset, UI reset, snapshot-before-ref, evidence write point under ${layout.evidenceDir}/<case-id>/. If the isolated environment is unavailable, require writing blocked evidence before any browser command.`,
|
|
280
281
|
"U/D data ownership (required for modify/delete): (1) Only mutate data whose ownership is proven by current login identity + observable UI/API owner fields - never by name/guessed id/list order alone. (2) If current user has no data, create tagged cleanable data in current-user context, then U/D, then cleanup+verify. (3) Else only task-authorized Mock, labeled as Mock (not real backend proof). (4) If ownership unverifiable and create/Mock unavailable: write blocked with blockedReason current-user-data-unavailable | data-ownership-unverifiable | safe-test-data-setup-unavailable - do not risk cross-user data. (5) Never touch other users, shared fixtures, production, or non-cleanable data.",
|
|
281
282
|
].join("\n\n"),
|
|
282
283
|
},
|
|
@@ -307,7 +308,7 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
307
308
|
allowedPaths: [...ragWriteSet, ...caseDraftWriteSet],
|
|
308
309
|
forbiddenPaths: forbidden,
|
|
309
310
|
outputContract: "Apply the one permitted frontend case revision to FE-*.md, index.md, and manifest.draft.json only; the exclusive shell materializer remains the sole writer of manifest.json. No browser execution or evidence writes.",
|
|
310
|
-
subtask_prompt:
|
|
311
|
+
subtask_prompt: `This is the only permitted case revision. Read the first review findings and the RAG package. Revise only ${layout.casesDir}/FE-*.md, ${layout.casesDir}/index.md, and ${layout.casesDir}/manifest.draft.json; preserve traceable AC mappings. Preserve the dynamic-ref contract: executable playwright-cli lines use only eX/eY-style shell-safe documentation placeholders, never <...>; each placeholder is resolved from the immediately preceding latest snapshot and stale refs are not reused. Do not write ${layout.casesDir}/manifest.json, execute a browser, or write evidence. HARD: Never delete case files; only edit in place or add missing cases. Preserve the full planned suite, index.md, and manifest.draft.json.`,
|
|
311
312
|
}, {
|
|
312
313
|
id: "review-frontend-cases-final-pi",
|
|
313
314
|
depends_on: ["revise-frontend-cases-pi"],
|
|
@@ -411,9 +412,9 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
411
412
|
writePolicy: "exclusive",
|
|
412
413
|
writeGuardPolicy: "tools-only",
|
|
413
414
|
allowedPaths: [
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
415
|
+
`${layout.casesDir}/{{case.caseId}}.md`,
|
|
416
|
+
`${layout.ragDir}/context.md`,
|
|
417
|
+
`${layout.ragDir}/coverage-map.md`,
|
|
417
418
|
`${evidenceRoot}/{{case.caseId}}/**`,
|
|
418
419
|
],
|
|
419
420
|
forbiddenPaths: forbidden,
|
|
@@ -422,7 +423,7 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
422
423
|
subtaskPromptTemplate: [
|
|
423
424
|
"Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session; do not use /new). playwright-cli-only: never bare Playwright CLI/API/test runner; no fallback.",
|
|
424
425
|
"Use the structured playwright_cli tool for every browser action. Do not request or search for bash. Do not execute raw shell commands. Translate each playwright-cli line in the case Markdown into one playwright_cli tool call ({command, args?, timeoutSeconds?}).",
|
|
425
|
-
|
|
426
|
+
`1) Read the concrete baseUrl from ${layout.ragDir}/context.md; it has already passed the environment shell safety/reachability gate. Start browser ONLY via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>] (default session only; no -s=). 2) Dynamic refs: eX/eY in case Markdown are documentation placeholders, never tool args. Immediately before every structured playwright_cli call that references an element, parse the current real eNN from the immediately preceding latest snapshot and pass only that real eNN; never send literal \`eX\`/\`eY\`. A new snapshot invalidates prior refs, so never reuse stale refs. File outputs are canonical: screenshot args [--filename, final.png] (or [e5, --filename, final.png] for a real target), PDF args [--filename, final.pdf], and snapshot writes a file only with [--filename, snapshot.txt]; snapshot without filename is response-only. Never use --path, --output, --file, or any output path as a positional target. Follow case steps with snapshot before element refs using only playwright_cli. A passed case requires this same child receipt order: successful open → successful find → controller post-execution cleanup. Only successful find is a meaningful assertion; snapshot, goto, screenshot, request/console, click/fill and other ordinary interactions cannot establish passed authority. 3) Only when preflight or playwright_cli tool explicitly fails may you write blocked evidence (blockedReason playwright-cli-unavailable | frontend-base-url-unreachable); never invent CLI-unavailable solely because bash is absent. Unique-control early stop: when an AC names a specific control that is absent from the first post-navigation snapshot after reaching the required state (and no setup-required gate appeared), do ONE find with the case's literal as evidence; if it returns 0 matches, record status=blocked with blockedReason unique-control-unavailable citing that single find - do NOT re-explore with alternative literals, repeated snapshots, navigation detours, or rerun loops. 4) For U/D: enforce current-user ownership / create-or-mock-or-blocked; never mutate other users' data.`,
|
|
426
427
|
"Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId={{case.caseId}}, status passed|failed|blocked, evidencePaths (relative under evidenceDir). blocked needs non-empty blockedReason. After writing, self-check the same contract; if self-check fails, rewrite both files as status=blocked blockedReason=invalid-evidence-shape (never leave missing/malformed evidence).",
|
|
427
428
|
"Business failed/blocked is a recorded result, not a node failure. Close browser via playwright_cli command=close. Return compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}.",
|
|
428
429
|
].join("\n\n"),
|
|
@@ -442,11 +443,11 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
442
443
|
: `rerun-candidates-round${round}.json`;
|
|
443
444
|
const selectCommand = [
|
|
444
445
|
"const fs=require('fs'),path=require('path');",
|
|
445
|
-
|
|
446
|
+
`const manifestPath='${layout.casesDir}/manifest.json';`,
|
|
446
447
|
"if(!fs.existsSync(manifestPath)){process.stdout.write(JSON.stringify({cases:[]}));process.exit(0);}",
|
|
447
448
|
"const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));const cases=[];",
|
|
448
|
-
|
|
449
|
-
`fs.mkdirSync('
|
|
449
|
+
`for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('${layout.evidenceDir}/'+c.caseId+'/')).replace(/\\/+$/,'')+'/';const resultPath=path.join(evidenceDir,'case-result.json');const execPath=path.join(evidenceDir,'execution.md');let reason=null;let attempt=0;let missing=false;if(!fs.existsSync(resultPath)){missing=true;reason='missing-result-files';}else{try{const r=JSON.parse(fs.readFileSync(resultPath,'utf8'));attempt=Number(r.rerunAttempt||0)||0;if(r.status==='blocked')reason='blocked';if(r.status==='failed')reason='failed-retry';if(!r.status){missing=true;reason='missing-result-files';}}catch(_){missing=true;reason='missing-result-files';}}if(!fs.existsSync(execPath)&&reason!=='blocked'){missing=true;reason=reason||'missing-result-files';}const should=(reason==='blocked'||reason==='failed-retry'||missing)&&attempt<` + String(round) + `;if(should){cases.push({caseId:c.caseId,casePath:c.casePath||('${layout.casesDir}/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}`,
|
|
450
|
+
`fs.mkdirSync('${layout.evidenceDir}',{recursive:true});fs.writeFileSync('${layout.evidenceDir}/${candidateArtifact}',JSON.stringify({schemaVersion:1,cases},null,2)+'\\n');process.stdout.write(JSON.stringify({cases}));`,
|
|
450
451
|
].join("");
|
|
451
452
|
tasks.push({
|
|
452
453
|
id: selectorId,
|
|
@@ -455,8 +456,8 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
455
456
|
executor: "shell",
|
|
456
457
|
complexity: "LOW",
|
|
457
458
|
writePolicy: "exclusive",
|
|
458
|
-
writeSet: [
|
|
459
|
-
allowedPaths: [...casesWriteSet,
|
|
459
|
+
writeSet: [`${evidenceRoot}/**`],
|
|
460
|
+
allowedPaths: [...casesWriteSet, `${evidenceRoot}/**`],
|
|
460
461
|
forbiddenPaths: forbidden,
|
|
461
462
|
outputContract: `Stdout JSON {cases} for blocked or missing-result-file cases eligible for rerun round ${round}/${maxRerunAttempts}.`,
|
|
462
463
|
subtask_prompt: "Select frontend-test cases eligible for this bounded rerun round.",
|
|
@@ -501,9 +502,9 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
501
502
|
writePolicy: "exclusive",
|
|
502
503
|
writeGuardPolicy: "tools-only",
|
|
503
504
|
allowedPaths: [
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
505
|
+
`${layout.casesDir}/{{case.caseId}}.md`,
|
|
506
|
+
`${layout.ragDir}/context.md`,
|
|
507
|
+
`${layout.ragDir}/coverage-map.md`,
|
|
507
508
|
`${evidenceRoot}/{{case.caseId}}/**`,
|
|
508
509
|
],
|
|
509
510
|
forbiddenPaths: forbidden,
|
|
@@ -512,7 +513,7 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
512
513
|
subtaskPromptTemplate: [
|
|
513
514
|
"RERUN attempt {{case.rerunAttempt}} for {{case.caseId}} (reason={{case.reason}}). Rewrite the authoritative execution.md and case-result.json; its final status replaces the earlier case result.",
|
|
514
515
|
"Primary job: EXECUTE case {{case.caseId}} from {{case.casePath}} with skill playwright-cli (fresh Pi session). Use structured playwright_cli only; headless open.",
|
|
515
|
-
|
|
516
|
+
`Read the concrete baseUrl from ${layout.ragDir}/context.md and start via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>]. Passed requires open → find → cleanup receipts.`,
|
|
516
517
|
"Unique-control early stop: if the first post-navigation snapshot lacks the control an AC names (and no setup-required gate appeared), one find returning 0 matches is sufficient evidence - write status=blocked blockedReason unique-control-unavailable instead of exploratory retries.",
|
|
517
518
|
"Always write {{case.evidenceDir}}execution.md and {{case.evidenceDir}}case-result.json with caseId, status, evidencePaths, rerunAttempt={{case.rerunAttempt}}. Write fixed sections `### 执行摘要` and `### 实际执行步骤` to execution.md when available.",
|
|
518
519
|
].join("\n\n"),
|
|
@@ -531,7 +532,7 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
531
532
|
complexity: "LOW",
|
|
532
533
|
writePolicy: "exclusive",
|
|
533
534
|
writeSet: [`${evidenceRoot}/**`],
|
|
534
|
-
allowedPaths: [
|
|
535
|
+
allowedPaths: [`${layout.casesDir}/**`, `${evidenceRoot}/**`],
|
|
535
536
|
forbiddenPaths: forbidden,
|
|
536
537
|
outputContract: "Evidence validation (advisory missing/malformed does not fail the node; hard-fail only path escape) then hash-bound frontend-test-result-v1. Node success means result-v1 was written, not that all cases passed.",
|
|
537
538
|
subtask_prompt: "Validate case evidence then materialize authoritative frontend-test-result-v1. Missing/malformed case evidence is advisory; only unsafe evidence paths hard-fail. Do not use Pi prose as input.",
|
|
@@ -549,8 +550,8 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
549
550
|
executor: "shell",
|
|
550
551
|
complexity: "LOW",
|
|
551
552
|
writePolicy: "exclusive",
|
|
552
|
-
writeSet: [
|
|
553
|
-
allowedPaths: [
|
|
553
|
+
writeSet: [`${layout.reportsDir}/**`],
|
|
554
|
+
allowedPaths: [`${layout.testRoot}/**`],
|
|
554
555
|
forbiddenPaths: forbidden,
|
|
555
556
|
outputContract: "Deterministic L-5 (optional) plus main frontend-test-report.md/html from frontend-test-result-v1. Pipeline acceptance = result-v1 + main HTML.",
|
|
556
557
|
subtask_prompt: "Render operational reports from frontend-test-result-v1 only. Do not invent coverage. Main HTML is required; L-5 follows frontendTest.reports.l5.",
|
|
@@ -596,10 +597,10 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
596
597
|
complexity: "MED",
|
|
597
598
|
writePolicy: "exclusive",
|
|
598
599
|
writeGuardPolicy: "tools-only",
|
|
599
|
-
writeSet: [
|
|
600
|
-
allowedPaths: [
|
|
600
|
+
writeSet: [`${layout.reportsDir}/**`],
|
|
601
|
+
allowedPaths: [`${layout.testRoot}/**`],
|
|
601
602
|
forbiddenPaths: forbidden,
|
|
602
|
-
outputContract:
|
|
603
|
+
outputContract: `Optional retrospective under ${layout.reportsDir}/frontend-test-retrospect-<date>.md. Not required for pipeline acceptance.`,
|
|
603
604
|
subtask_prompt: "Write optional frontend-test retrospective after result + reports. Do not recompute L-5. Pipeline success does not require this file.",
|
|
604
605
|
});
|
|
605
606
|
}
|
|
@@ -617,7 +618,7 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
617
618
|
"Environment preflight must parse the retrieve-authored context.md baseUrl, reject unsafe/non-production violations, and curl-probe it before generate; unreachable or curl-unavailable ends preflight as blocked (frontend-base-url-unreachable|curl-unavailable) so generate/map do not run.",
|
|
618
619
|
"U/D cases must prove current-user data ownership or create cleanable current-user data or authorized Mock; otherwise blocked (current-user-data-unavailable|data-ownership-unverifiable|safe-test-data-setup-unavailable) without cross-user mutation.",
|
|
619
620
|
"Token settings are post-case stop thresholds, never a hard provider token cap. Unstarted cases after a threshold are blocked: token-budget-exhausted.",
|
|
620
|
-
|
|
621
|
+
`Pipeline acceptance for frontend-test is frontend-test-result-v1 plus the main report under ${layout.reportsDir}/frontend-test-report.html; case pass rate and outcome=passed are quality signals; retrospect is opt-in (frontendTest.reports.retrospect).`,
|
|
621
622
|
blockingReview
|
|
622
623
|
? "frontendTest.reviewMode=blocking: a frontend case review must emit VERDICT: pass before checklist/manifest materialization; request-revision blocks browser execution."
|
|
623
624
|
: "frontendTest.reviewMode is off|advisory by default: mechanical checklist-shell gates materialize/execute; LLM review is not a hard browser gate.",
|
|
@@ -639,23 +640,43 @@ export function buildFrontendTestHybridDag(sources) {
|
|
|
639
640
|
},
|
|
640
641
|
skillsByRole: {
|
|
641
642
|
planner: [],
|
|
642
|
-
scout: ["playwright-cli"],
|
|
643
|
-
implementer: ["playwright-cli"],
|
|
643
|
+
scout: ["builtin:playwright-cli"],
|
|
644
|
+
implementer: ["builtin:playwright-cli"],
|
|
644
645
|
reviewer: [],
|
|
645
|
-
verifier: ["playwright-cli"],
|
|
646
|
-
closeout: ["verification-before-completion"],
|
|
646
|
+
verifier: ["builtin:playwright-cli"],
|
|
647
|
+
closeout: ["builtin:verification-before-completion"],
|
|
647
648
|
},
|
|
648
649
|
executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
|
|
649
650
|
verifyStrategy: resolveDagVerifyStrategy(sources.taskConfig),
|
|
650
651
|
tasks,
|
|
651
652
|
};
|
|
652
653
|
applyFrontendTestLayoutToDagSpec(spec, layout);
|
|
654
|
+
applyFrontendTestHarnessRootGuard(spec, layout);
|
|
653
655
|
applyDefaultReadOnlyRetryPolicy(spec);
|
|
654
656
|
stampGeneratedArtifactBindings(spec);
|
|
655
657
|
parseDagSpec(spec);
|
|
656
658
|
assertValidDagSpec(spec);
|
|
657
659
|
return spec;
|
|
658
660
|
}
|
|
661
|
+
/**
|
|
662
|
+
* D4: when the frozen frontend-test root lives under `.harness/` the whole tree
|
|
663
|
+
* is Git-ignored, so Git-status write guards would see empty diffs for real
|
|
664
|
+
* writes. Flip every file-writing node (and the dynamic browser child) from
|
|
665
|
+
* tools-only to the bounded filesystem snapshot channel instead.
|
|
666
|
+
*/
|
|
667
|
+
export function applyFrontendTestHarnessRootGuard(spec, layout) {
|
|
668
|
+
if (!isHarnessNamespacePath(layout.testRoot))
|
|
669
|
+
return;
|
|
670
|
+
for (const task of spec.tasks) {
|
|
671
|
+
if (task.writeGuardPolicy === "tools-only") {
|
|
672
|
+
task.writeGuardPolicy = "filesystem-only";
|
|
673
|
+
}
|
|
674
|
+
const child = task.dynamicExpansion?.childTask;
|
|
675
|
+
if (child?.writeGuardPolicy === "tools-only") {
|
|
676
|
+
child.writeGuardPolicy = "filesystem-only";
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
659
680
|
export function applyFrontendTestLayoutToDagSpec(spec, layout) {
|
|
660
681
|
if (layout.isDefault) {
|
|
661
682
|
spec.frontendTestLayout = layout;
|
|
@@ -8,12 +8,12 @@ export const KG_BOOTSTRAP_DEFAULTS = {
|
|
|
8
8
|
writePolicy: "read-only",
|
|
9
9
|
};
|
|
10
10
|
export const KG_BOOTSTRAP_SKILLS_BY_ROLE = {
|
|
11
|
-
planner: ["loop-agent"],
|
|
12
|
-
scout: ["codebase-scout"],
|
|
13
|
-
implementer: ["verification-before-completion"],
|
|
14
|
-
reviewer: ["requesting-code-review"],
|
|
15
|
-
verifier: ["verification-before-completion", "systematic-debugging"],
|
|
16
|
-
closeout: ["loop-agent", "verification-before-completion"],
|
|
11
|
+
planner: ["builtin:loop-agent"],
|
|
12
|
+
scout: ["builtin:codebase-scout"],
|
|
13
|
+
implementer: ["builtin:verification-before-completion"],
|
|
14
|
+
reviewer: ["builtin:requesting-code-review"],
|
|
15
|
+
verifier: ["builtin:verification-before-completion", "builtin:systematic-debugging"],
|
|
16
|
+
closeout: ["builtin:loop-agent", "builtin:verification-before-completion"],
|
|
17
17
|
};
|
|
18
18
|
export function buildKgBootstrapInlineNodeScript(lines) {
|
|
19
19
|
return ["node", "-e", JSON.stringify(lines.join(""))].join(" ");
|
|
@@ -7,12 +7,12 @@ export const KNOWLEDGE_SYNC_DEFAULTS = {
|
|
|
7
7
|
writePolicy: "read-only",
|
|
8
8
|
};
|
|
9
9
|
export const KNOWLEDGE_SYNC_SKILLS_BY_ROLE = {
|
|
10
|
-
planner: ["loop-agent"],
|
|
10
|
+
planner: ["builtin:loop-agent"],
|
|
11
11
|
scout: [],
|
|
12
|
-
implementer: ["verification-before-completion"],
|
|
13
|
-
reviewer: ["requesting-code-review"],
|
|
14
|
-
verifier: ["verification-before-completion", "systematic-debugging"],
|
|
15
|
-
closeout: ["loop-agent", "verification-before-completion"],
|
|
12
|
+
implementer: ["builtin:verification-before-completion"],
|
|
13
|
+
reviewer: ["builtin:requesting-code-review"],
|
|
14
|
+
verifier: ["builtin:verification-before-completion", "builtin:systematic-debugging"],
|
|
15
|
+
closeout: ["builtin:loop-agent", "builtin:verification-before-completion"],
|
|
16
16
|
};
|
|
17
17
|
export function buildMultiPerspectiveReviewAggregateScript(fromNodeIds, label) {
|
|
18
18
|
if (fromNodeIds.length === 0) {
|
|
@@ -3,9 +3,20 @@ import { access, readFile } from "node:fs/promises";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { builtinSkillReferenceResourceId, builtinSkillResourceId, createControllerContextProvider, } from "../../governance/resource-catalog.js";
|
|
6
7
|
export const DEFAULT_SKILL_INSTRUCTION_MAX_CHARS = 3_000;
|
|
7
8
|
export const DEFAULT_SKILL_INSTRUCTION_TOTAL_MAX_CHARS = 12_000;
|
|
8
9
|
const SKILL_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
10
|
+
const SKILL_REFERENCE_PATTERN = /^(?:(builtin|project|user):)?([A-Za-z0-9][A-Za-z0-9_-]*)$/;
|
|
11
|
+
function parseSkillReference(reference) {
|
|
12
|
+
const match = SKILL_REFERENCE_PATTERN.exec(reference);
|
|
13
|
+
if (!match)
|
|
14
|
+
return undefined;
|
|
15
|
+
return {
|
|
16
|
+
...(match[1] ? { source: match[1] } : {}),
|
|
17
|
+
name: match[2],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
9
20
|
async function fileExists(filePath) {
|
|
10
21
|
try {
|
|
11
22
|
await access(filePath);
|
|
@@ -35,22 +46,26 @@ function bundledSkillCandidatePaths(name) {
|
|
|
35
46
|
}
|
|
36
47
|
function buildSkillCandidatePaths(input) {
|
|
37
48
|
const candidates = [];
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
if (!input.source || input.source === "user") {
|
|
50
|
+
const agentDir = input.env.PI_CODING_AGENT_DIR;
|
|
51
|
+
if (agentDir) {
|
|
52
|
+
candidates.push(normalizeCandidate(path.join(agentDir, "skills", input.name, "SKILL.md")), normalizeCandidate(path.join(agentDir, "skills", `${input.name}.md`)));
|
|
53
|
+
}
|
|
54
|
+
const piSkillsDir = path.join(input.homeDir, ".pi", "agent", "skills");
|
|
55
|
+
candidates.push(normalizeCandidate(path.join(piSkillsDir, input.name, "SKILL.md")), normalizeCandidate(path.join(piSkillsDir, `${input.name}.md`)));
|
|
41
56
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
if (!input.source || input.source === "project") {
|
|
58
|
+
// Canonical target-project repo-local skill path.
|
|
59
|
+
candidates.push(normalizeCandidate(path.join(input.cwd, ".agents", "skills", input.name, "SKILL.md")));
|
|
60
|
+
// Legacy project-local Skill path. Explicit builtin references never consult it.
|
|
61
|
+
candidates.push(normalizeCandidate(path.join(input.cwd, "skills", input.name, "SKILL.md")));
|
|
62
|
+
if (input.name === "loop-agent") {
|
|
63
|
+
candidates.push(normalizeCandidate(path.join(input.cwd, "skill", "SKILL.md")));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!input.source || input.source === "builtin") {
|
|
67
|
+
candidates.push(...bundledSkillCandidatePaths(input.name));
|
|
52
68
|
}
|
|
53
|
-
candidates.push(...bundledSkillCandidatePaths(input.name));
|
|
54
69
|
return Array.from(new Set(candidates));
|
|
55
70
|
}
|
|
56
71
|
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
@@ -207,7 +222,7 @@ async function resolveSkillReferences(input) {
|
|
|
207
222
|
});
|
|
208
223
|
continue;
|
|
209
224
|
}
|
|
210
|
-
if (!(await fileExists(resolvedPath))) {
|
|
225
|
+
if (!input.packageSkill && !(await fileExists(resolvedPath))) {
|
|
211
226
|
unresolvedReferences.push({
|
|
212
227
|
name,
|
|
213
228
|
path: refPath,
|
|
@@ -216,7 +231,13 @@ async function resolveSkillReferences(input) {
|
|
|
216
231
|
continue;
|
|
217
232
|
}
|
|
218
233
|
try {
|
|
219
|
-
const
|
|
234
|
+
const packageResource = input.packageSkill
|
|
235
|
+
? await input.packageSkill.provider.readText(builtinSkillReferenceResourceId(input.packageSkill.name, refPath))
|
|
236
|
+
: undefined;
|
|
237
|
+
const content = packageResource?.text ?? await readFile(resolvedPath, "utf-8");
|
|
238
|
+
if (packageResource && path.resolve(packageResource.absolutePath) !== resolvedPath) {
|
|
239
|
+
throw new Error(`catalog path mismatch for ${refPath}`);
|
|
240
|
+
}
|
|
220
241
|
const contentSha256 = sha256(content);
|
|
221
242
|
const bytes = Buffer.byteLength(content, "utf-8");
|
|
222
243
|
const budget = Math.max(0, Math.min(declaration.maxChars ?? input.perReferenceMaxChars, remainingTotal));
|
|
@@ -224,7 +245,7 @@ async function resolveSkillReferences(input) {
|
|
|
224
245
|
input.resolvedReferencePaths.add(resolvedPath);
|
|
225
246
|
references.push({
|
|
226
247
|
name,
|
|
227
|
-
path: resolvedPath,
|
|
248
|
+
path: packageResource?.id ?? resolvedPath,
|
|
228
249
|
sha256: contentSha256,
|
|
229
250
|
bytes,
|
|
230
251
|
...(truncated ? { truncated: true } : {}),
|
|
@@ -232,7 +253,7 @@ async function resolveSkillReferences(input) {
|
|
|
232
253
|
remainingTotal = Math.max(0, remainingTotal - Math.min(content.length, budget));
|
|
233
254
|
sections.push([
|
|
234
255
|
`Reference: ${name}`,
|
|
235
|
-
|
|
256
|
+
`${packageResource ? "Resource" : "Path"}: ${packageResource?.id ?? resolvedPath}`,
|
|
236
257
|
`Sha256: ${contentSha256}`,
|
|
237
258
|
excerpt,
|
|
238
259
|
].join("\n"));
|
|
@@ -263,7 +284,7 @@ function buildResolvedPromptText(input) {
|
|
|
263
284
|
const lines = [
|
|
264
285
|
`Skill: ${input.name}`,
|
|
265
286
|
"Resolution: resolved",
|
|
266
|
-
|
|
287
|
+
`${input.locationKind}: ${input.location}`,
|
|
267
288
|
`Sha256: ${input.contentSha256}`,
|
|
268
289
|
"Injection mode: bounded-inline",
|
|
269
290
|
"Instructions:",
|
|
@@ -308,29 +329,62 @@ export async function resolveDagSkillInstructions(skills, options) {
|
|
|
308
329
|
let remainingTotal = options.totalMaxChars ?? DEFAULT_SKILL_INSTRUCTION_TOTAL_MAX_CHARS;
|
|
309
330
|
const resolved = [];
|
|
310
331
|
const resolvedReferencePaths = new Set();
|
|
332
|
+
let packageProvider;
|
|
311
333
|
for (const name of skills) {
|
|
312
|
-
|
|
334
|
+
const reference = parseSkillReference(name);
|
|
335
|
+
if (!reference || !SKILL_NAME_PATTERN.test(reference.name)) {
|
|
313
336
|
const error = `invalid skill name: ${name}`;
|
|
314
337
|
resolved.push({
|
|
315
338
|
name,
|
|
316
339
|
resolution: "error",
|
|
340
|
+
sourceKind: "auto",
|
|
317
341
|
error,
|
|
318
342
|
injectionMode: "name-only",
|
|
319
343
|
promptText: buildErrorPromptText({ name, error }),
|
|
320
344
|
});
|
|
321
345
|
continue;
|
|
322
346
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
347
|
+
let packageContent;
|
|
348
|
+
let filePath;
|
|
349
|
+
let searchedPaths = [];
|
|
350
|
+
if (reference.source === "builtin") {
|
|
351
|
+
const logicalResourceId = builtinSkillResourceId(reference.name);
|
|
352
|
+
try {
|
|
353
|
+
packageProvider ??= await createControllerContextProvider();
|
|
354
|
+
const resource = await packageProvider.readText(logicalResourceId);
|
|
355
|
+
filePath = resource.absolutePath;
|
|
356
|
+
packageContent = resource.text;
|
|
357
|
+
searchedPaths = [resource.absolutePath];
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
361
|
+
resolved.push({
|
|
362
|
+
name,
|
|
363
|
+
resolution: "error",
|
|
364
|
+
sourceKind: "builtin",
|
|
365
|
+
logicalResourceId,
|
|
366
|
+
error: message,
|
|
367
|
+
injectionMode: "name-only",
|
|
368
|
+
promptText: buildErrorPromptText({ name, error: message }),
|
|
369
|
+
});
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
searchedPaths = buildSkillCandidatePaths({
|
|
375
|
+
name: reference.name,
|
|
376
|
+
...(reference.source ? { source: reference.source } : {}),
|
|
377
|
+
cwd: options.cwd,
|
|
378
|
+
env,
|
|
379
|
+
homeDir,
|
|
380
|
+
});
|
|
381
|
+
filePath = await findFirstExistingFile(searchedPaths);
|
|
382
|
+
}
|
|
330
383
|
if (!filePath) {
|
|
331
384
|
resolved.push({
|
|
332
385
|
name,
|
|
333
386
|
resolution: "missing",
|
|
387
|
+
sourceKind: reference.source ?? "auto",
|
|
334
388
|
searchedPaths,
|
|
335
389
|
injectionMode: "name-only",
|
|
336
390
|
promptText: buildMissingPromptText({ name, searchedPaths }),
|
|
@@ -338,7 +392,7 @@ export async function resolveDagSkillInstructions(skills, options) {
|
|
|
338
392
|
continue;
|
|
339
393
|
}
|
|
340
394
|
try {
|
|
341
|
-
const content = await readFile(filePath, "utf-8");
|
|
395
|
+
const content = packageContent ?? await readFile(filePath, "utf-8");
|
|
342
396
|
const { frontmatter, body } = splitSkillFrontmatter(content);
|
|
343
397
|
const instructionContent = frontmatter ? body : content;
|
|
344
398
|
const referenceDeclarations = frontmatter
|
|
@@ -354,11 +408,17 @@ export async function resolveDagSkillInstructions(skills, options) {
|
|
|
354
408
|
remainingTotal,
|
|
355
409
|
perReferenceMaxChars: perSkillMaxChars,
|
|
356
410
|
resolvedReferencePaths,
|
|
411
|
+
...(reference.source === "builtin" && packageProvider
|
|
412
|
+
? { packageSkill: { name: reference.name, provider: packageProvider } }
|
|
413
|
+
: {}),
|
|
357
414
|
});
|
|
358
415
|
remainingTotal = afterReferences;
|
|
359
416
|
const { promptText, truncated } = buildResolvedPromptText({
|
|
360
417
|
name,
|
|
361
|
-
|
|
418
|
+
location: reference.source === "builtin"
|
|
419
|
+
? builtinSkillResourceId(reference.name)
|
|
420
|
+
: filePath,
|
|
421
|
+
locationKind: reference.source === "builtin" ? "Resource" : "Path",
|
|
362
422
|
content: instructionContent,
|
|
363
423
|
contentSha256,
|
|
364
424
|
bytes,
|
|
@@ -368,7 +428,13 @@ export async function resolveDagSkillInstructions(skills, options) {
|
|
|
368
428
|
resolved.push({
|
|
369
429
|
name,
|
|
370
430
|
resolution: "resolved",
|
|
371
|
-
|
|
431
|
+
sourceKind: reference.source ?? "auto",
|
|
432
|
+
...(reference.source === "builtin" && packageProvider
|
|
433
|
+
? {
|
|
434
|
+
logicalResourceId: builtinSkillResourceId(reference.name),
|
|
435
|
+
catalogDigest: packageProvider.identity.catalogDigest,
|
|
436
|
+
}
|
|
437
|
+
: { path: filePath }),
|
|
372
438
|
sha256: contentSha256,
|
|
373
439
|
bytes,
|
|
374
440
|
truncated,
|
|
@@ -385,14 +451,18 @@ export async function resolveDagSkillInstructions(skills, options) {
|
|
|
385
451
|
resolved.push({
|
|
386
452
|
name,
|
|
387
453
|
resolution: "error",
|
|
388
|
-
|
|
454
|
+
sourceKind: reference.source ?? "auto",
|
|
455
|
+
...(reference.source === "builtin"
|
|
456
|
+
? { logicalResourceId: builtinSkillResourceId(reference.name) }
|
|
457
|
+
: { path: filePath }),
|
|
389
458
|
error: message,
|
|
390
459
|
injectionMode: "name-only",
|
|
391
460
|
promptText: buildErrorPromptText({ name, error: message }),
|
|
392
461
|
});
|
|
393
462
|
}
|
|
394
463
|
}
|
|
395
|
-
if (options.includeLearnedPatterns
|
|
464
|
+
if (options.includeLearnedPatterns
|
|
465
|
+
&& skills.some((skill) => skill === "loop-agent" || skill === "builtin:loop-agent")) {
|
|
396
466
|
const learned = await resolveLearnedPatternInstructions({
|
|
397
467
|
cwd: options.cwd,
|
|
398
468
|
maxChars: Math.max(0, Math.min(perSkillMaxChars, remainingTotal)),
|
|
@@ -32,6 +32,9 @@ const unresolvedReferenceSchema = z.object({
|
|
|
32
32
|
const resolvedInstructionSchema = z.object({
|
|
33
33
|
name: z.string().min(1),
|
|
34
34
|
resolution: z.enum(["resolved", "missing", "error"]),
|
|
35
|
+
sourceKind: z.enum(["builtin", "project", "user", "auto"]).optional(),
|
|
36
|
+
logicalResourceId: z.string().min(1).optional(),
|
|
37
|
+
catalogDigest: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
35
38
|
path: z.string().min(1).optional(),
|
|
36
39
|
sha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
37
40
|
bytes: z.number().int().nonnegative().optional(),
|