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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/CHANGELOG.md +27 -2
  2. package/README.md +12 -12
  3. package/dist/adapters/loop-agent.js +34 -0
  4. package/dist/build-stamp.json +3 -3
  5. package/dist/cli/command-definitions.js +5 -5
  6. package/dist/cli/program.js +1 -4
  7. package/dist/commands/init-legacy-audit.js +330 -0
  8. package/dist/commands/init-upgrade.js +644 -104
  9. package/dist/commands/init.js +686 -330
  10. package/dist/commands/operator-guide.js +99 -0
  11. package/dist/commands/operator.js +16 -3
  12. package/dist/executors/dag-pi-executor.js +11 -6
  13. package/dist/executors/pi-sdk-executor.js +3 -1
  14. package/dist/executors/pi-writer-tool-policy.js +60 -4
  15. package/dist/executors/playwright-cli-launcher.js +31 -45
  16. package/dist/executors/shell-write-guard.js +24 -67
  17. package/dist/governance/exec-plans.js +10 -1
  18. package/dist/governance/harness.js +2 -7
  19. package/dist/governance/manifest-types.js +28 -9
  20. package/dist/governance/project-capability-scanner.js +263 -0
  21. package/dist/governance/project-config.js +96 -0
  22. package/dist/governance/resource-catalog.js +151 -0
  23. package/dist/infrastructure/console/app-data.js +6 -2
  24. package/dist/shared/operator/capabilities.js +2 -2
  25. package/dist/shared/package-metadata.js +23 -7
  26. package/dist/shared/pi-context-pressure/extension.js +23 -0
  27. package/dist/shared/pi-context-pressure/index.js +1 -0
  28. package/dist/shared/pi-context-pressure/telemetry.js +4 -0
  29. package/dist/shared/pi-context-pressure/tool-result-pruner.js +98 -0
  30. package/dist/shared/update/npm-client.js +49 -1
  31. package/dist/task/config-types.js +4 -2
  32. package/dist/task/source-prepare/build-draft.js +1 -0
  33. package/dist/task/source-prepare/path-policy.js +17 -7
  34. package/dist/worker/cli.js +3 -5
  35. package/dist/worker/console/chat/browser-automation.js +455 -41
  36. package/dist/worker/console/chat/browser-content.js +74 -0
  37. package/dist/worker/console/chat/browser-diagnostics.js +250 -0
  38. package/dist/worker/console/chat/browser-evaluation.js +58 -0
  39. package/dist/worker/console/chat/browser-operation-queue.js +19 -0
  40. package/dist/worker/console/chat/browser-pick.js +113 -0
  41. package/dist/worker/console/chat/browser-routes.js +54 -5
  42. package/dist/worker/console/chat/browser-screenshot-result.js +47 -0
  43. package/dist/worker/console/chat/browser-storage.js +89 -0
  44. package/dist/worker/console/chat/chat-event-store.js +7 -6
  45. package/dist/worker/console/chat/context-insights.js +84 -19
  46. package/dist/worker/console/chat/pet/pet-affinity.js +169 -0
  47. package/dist/worker/console/chat/pet/pet-persist.js +109 -0
  48. package/dist/worker/console/chat/pet/pet-registry.js +142 -0
  49. package/dist/worker/console/chat/pet/pet-remarks.js +48 -0
  50. package/dist/worker/console/chat/pet/pet-routes.js +125 -0
  51. package/dist/worker/console/chat/pet/pet-service.js +262 -0
  52. package/dist/worker/console/chat/pet/pet-state.js +99 -0
  53. package/dist/worker/console/chat/pi-mode-loop-isolation.js +17 -2
  54. package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +125 -174
  55. package/dist/worker/console/chat/pi-runtime.js +8 -3
  56. package/dist/worker/console/chat/repo-browser.js +80 -2
  57. package/dist/worker/console/chat/repo-preview-kind.js +155 -0
  58. package/dist/worker/console/chat/routes.js +43 -7
  59. package/dist/worker/console/chat/scheduled-goal-request.js +14 -6
  60. package/dist/worker/console/chat/session-mode.js +1 -1
  61. package/dist/worker/console/chat/session-store.js +24 -5
  62. package/dist/worker/console/chat/tools.js +12 -0
  63. package/dist/worker/console/console-shutdown.js +34 -0
  64. package/dist/worker/console/server.js +2 -1
  65. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-r1NAPU_5.js → abnfDiagram-N423BO3Z-DASc69AM.js} +1 -1
  66. package/dist/worker/console/static/assets/{arc-dT5T4sVL.js → arc-h09p-LAc.js} +1 -1
  67. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP_cOq46.js → architectureDiagram-T3A2C74G-CZ3dtLkh.js} +1 -1
  68. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-sToEZxEf.js → blockDiagram-VBNYF7ZC-DMrRhMXG.js} +1 -1
  69. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DSxwwi2m.js → c4Diagram-5PPSVZJV-DQxisHV6.js} +1 -1
  70. package/dist/worker/console/static/assets/channel-D1zgKZfg.js +1 -0
  71. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B9fh0IkF.js → chunk-2GRJ4B5K-CefVN9zh.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CAUNl-A6.js → chunk-2Q5K7J3B-DmR1Koq2.js} +1 -1
  73. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BomeqbNX.js → chunk-5RXB4S5H-NlzrSW7L.js} +1 -1
  74. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BNAuTOLE.js → chunk-5VM5RSS4-DfelJv-0.js} +1 -1
  75. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DfzjRD5y.js → chunk-6Q2QTUOP-BK6RFy58.js} +1 -1
  76. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C3uE-yc2.js → chunk-GF5L2VYU-BAeh9lz4.js} +1 -1
  77. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-C3sQMMYr.js → chunk-JWPE2WC7-ChPS5WzG.js} +1 -1
  78. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D_AtEYQL.js → chunk-KBJHAD2P-BoVe5IM6.js} +1 -1
  79. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DW4TwhKv.js → chunk-RYQCIY6F-C4q_0Lw9.js} +1 -1
  80. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CTQDmJcp.js → chunk-XXDRQBXY-Bb9TvjeY.js} +1 -1
  81. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CXNNsY2M.js +1 -0
  82. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CXNNsY2M.js +1 -0
  83. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-iMqPuBsL.js → cose-bilkent-JH36ORCC-DmNnL8Ff.js} +1 -1
  84. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DktIuDWs.js → cynefin-VYW2F7L2-Contvx58.js} +1 -1
  85. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BgVnjyEs.js → cynefinDiagram-MW4NZA55-Cigq7cjk.js} +1 -1
  86. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CR25E4bu.js → dagre-VZM6K2ZE-dEbC0nxb.js} +1 -1
  87. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CPM2h5HY.js → diagram-7IWD3JNH-CzIztESV.js} +1 -1
  88. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DqF0x2g1.js → diagram-B4RE2ZJO-Bo14zbaY.js} +1 -1
  89. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-G8BYp2eb.js → diagram-LBJQPF4R-C4xUyil6.js} +1 -1
  90. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BbC4kT_D.js → diagram-Q27KOJAE-CaBY_uYa.js} +1 -1
  91. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CKZwZ-G5.js → diagram-UB23O5K3-ClUXUeiu.js} +1 -1
  92. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-kJDmheia.js → ebnfDiagram-BXEA7PRR-BbGCgjC6.js} +1 -1
  93. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DFTVHyxB.js → erDiagram-JOGREHBK-Dr-AcwSl.js} +1 -1
  94. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BqdN5_os.js → flowDiagram-UKHOOZJN-BG8Gcfyc.js} +1 -1
  95. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-SYDjVakh.js → ganttDiagram-PKOTCBZU-CFVaPs5r.js} +1 -1
  96. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js → gitGraphDiagram-DS77QQ5N-ChJ8pm-O.js} +1 -1
  97. package/dist/worker/console/static/assets/index-B_5GhVIO.js +488 -0
  98. package/dist/worker/console/static/assets/index-C7YMBluX.css +1 -0
  99. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DIeAmJ45.js → infoDiagram-6WML65LV-C5J9-uCZ.js} +1 -1
  100. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-oikqUAlo.js → ishikawaDiagram-WSZJBQD7-BcfjqzDv.js} +1 -1
  101. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-mKEF_xYT.js → journeyDiagram-NVQOT4AX-BzzCO-rD.js} +1 -1
  102. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BdWJHPnN.js → kanban-definition-27J2QSJJ-NCHUZ46h.js} +1 -1
  103. package/dist/worker/console/static/assets/{linear-DGRlieMH.js → linear-BKZlWCUU.js} +1 -1
  104. package/dist/worker/console/static/assets/{mermaid.core-4P9yPC9c.js → mermaid.core-DbTaQjFE.js} +5 -5
  105. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-5PTSkos6.js → mindmap-definition-FAOFIHXS-CroYl5xQ.js} +1 -1
  106. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DONIFpG5.js → pegDiagram-VL7TDLO6-DgMBx-9k.js} +1 -1
  107. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-D_Z4S70M.js → pieDiagram-7S7Q4E2Y-CyWDmbq1.js} +1 -1
  108. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CAsjh8iY.js → quadrantDiagram-CIZ2JOQS-BgGEOqSP.js} +1 -1
  109. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Cx4Dd6ac.js → railroadDiagram-AXF67PYL-BMZ7oeeV.js} +1 -1
  110. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-cQb7mwyH.js → requirementDiagram-LRYGKXZP-DxqyGhcc.js} +1 -1
  111. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cd-DhUdf.js → sankeyDiagram-W5VNT64P-DnSvNCG8.js} +1 -1
  112. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-AlKYRgIk.js → sequenceDiagram-SI44F4Z6-BiZh6f-R.js} +1 -1
  113. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CUy_ET4e.js → sizeCapture-X5ZJPWSS-BKXSI8WB.js} +1 -1
  114. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CTuVe6Sa.js → stateDiagram-OKZ733FA-CfSgoYFm.js} +1 -1
  115. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DgHi3vjb.js +1 -0
  116. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-ChFLJb-v.js → swimlanes-SLNWSIFB-CyGADcHe.js} +2 -2
  117. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bgvdt1Y0.js +8 -0
  118. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DZZeiAPr.js → timeline-definition-Z64GVDOM-HmPaSPOK.js} +1 -1
  119. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-t4O91cZU.js → vennDiagram-T6HMQDX7-VA2VBbez.js} +1 -1
  120. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CV5qGHZu.js → wardleyDiagram-T6FBY63Y-BxjiBPZa.js} +1 -1
  121. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DyXbytUI.js → xychartDiagram-ELKLHX3M--hUY-qFj.js} +1 -1
  122. package/dist/worker/console/static/index.html +2 -2
  123. package/dist/worker/console/static/pets/ember/pet.json +167 -0
  124. package/dist/worker/console/static/pets/ember/spritesheet.png +0 -0
  125. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +4 -3
  126. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +57 -9
  127. package/dist/worker/console/static-src/operator-chat/use-stat-dialog.js +95 -0
  128. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +6 -0
  129. package/dist/worker/console/workspace-context.js +15 -0
  130. package/dist/worker/console/workspace-initialization.js +25 -16
  131. package/dist/worker/delivery/package.js +4 -1
  132. package/dist/worker/materialize/harness-task-lineage.js +4 -2
  133. package/dist/worker/observe/static/console-theme.css +33 -29
  134. package/dist/worker/observe/static/operator-chrome.js +1 -42
  135. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +3 -1
  136. package/dist/workflows/dag/backend-test-layout.js +27 -7
  137. package/dist/workflows/dag/backend-test-markdown-workflow.js +3 -1
  138. package/dist/workflows/dag/backend-test-pytest-collection.js +4 -1
  139. package/dist/workflows/dag/final-verification.js +45 -10
  140. package/dist/workflows/dag/frontend-test-case-checklist.js +12 -3
  141. package/dist/workflows/dag/frontend-test-environment-probe.js +0 -2
  142. package/dist/workflows/dag/frontend-test-layout.js +41 -12
  143. package/dist/workflows/dag/hybrid/sources.js +35 -16
  144. package/dist/workflows/dag/hybrid/templates/backend-test.js +70 -55
  145. package/dist/workflows/dag/hybrid/templates/frontend-test.js +62 -41
  146. package/dist/workflows/dag/hybrid/templates/kg-bootstrap.js +6 -6
  147. package/dist/workflows/dag/hybrid/templates/knowledge-sync.js +5 -5
  148. package/dist/workflows/dag/skill-instructions.js +102 -32
  149. package/dist/workflows/dag/skill-snapshot.js +3 -0
  150. package/dist/workflows/dag/skills.js +7 -7
  151. package/dist/workflows/dag/test-artifact-namespace.js +73 -0
  152. package/dist/workflows/dag/validate.js +24 -5
  153. package/dist/workflows/dag/workspace-checkpoint.js +13 -1
  154. package/docs/architecture/runtime-boundaries.md +28 -22
  155. package/docs/init-surface.manifest.json +43 -295
  156. package/docs/templates/README.md +4 -4
  157. package/docs/templates/agent-dag.base.json +9 -9
  158. package/docs/templates/agent-dag.final-verification.json +6 -6
  159. package/docs/templates/agent-dag.supervised-implementation.json +12 -12
  160. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +3 -3
  161. package/docs/templates/backend-test-dag.json +708 -708
  162. package/docs/templates/backend-test-dag.review-cases.prompt.md +3 -3
  163. package/docs/templates/frontend-implementation-dag.json +2 -2
  164. package/docs/templates/frontend-test-case-checklist.md +2 -2
  165. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +7 -7
  166. package/docs/templates/frontend-test-dag.json +48 -39
  167. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +2 -2
  168. package/docs/templates/frontend-test-dag.retrospect.prompt.md +1 -1
  169. package/docs/templates/frontend-test-dag.review-cases.prompt.md +1 -1
  170. package/docs/templates/hybrid-dag.json +9 -9
  171. package/docs/templates/knowledge-sync-dag.json +8 -8
  172. package/package.json +12 -3
  173. package/resource-catalog.v1.json +1973 -0
  174. package/resources/agent-bridge/loop-agent/SKILL.md +40 -0
  175. package/resources/init-history/legacy-surface.v1.json +33 -0
  176. package/skills/fe-test-ui-scout/SKILL.md +4 -4
  177. package/skills/fe-test-ui-scout/references/ledger-schema.md +1 -1
  178. package/skills/init-update/SKILL.md +34 -0
  179. package/skills/init-update/agents/openai.yaml +6 -0
  180. package/skills/playwright-cli/SKILL.md +1 -1
  181. package/skills/playwright-cli-case-generator/SKILL.md +4 -4
  182. package/dist/worker/console/static/assets/channel-CyBY_yGk.js +0 -1
  183. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +0 -1
  184. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +0 -1
  185. package/dist/worker/console/static/assets/index-B28Onmfy.css +0 -1
  186. package/dist/worker/console/static/assets/index-CTMZsDmh.js +0 -486
  187. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +0 -1
  188. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +0 -8
@@ -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
- "const root='testcase/frontend/cases';",
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!=='testcase/frontend/cases/'+c.caseId+'.md')issues.push({ruleId:'case-path-mismatch',caseId:id,detail:casePath+' must equal testcase/frontend/cases/'+c.caseId+'.md'});",
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
- "const draft='testcase/frontend/cases/manifest.draft.json',file='testcase/frontend/cases/manifest.json',tmp=file+'.tmp';",
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
- " c.casePath='testcase/frontend/cases/'+c.caseId+'.md';",
142
- " c.evidenceDir='testcase/frontend/evidence/'+c.caseId+'/';",
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
- "const reports='testcase/frontend/reports'; fs.mkdirSync(reports,{recursive:true});",
162
+ `const reports='${layout.reportsDir}'; fs.mkdirSync(reports,{recursive:true});`,
162
163
  "const esc=v=>String(v??'').replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;').replaceAll('\\\"','&quot;');",
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: "Write testcase/frontend/rag/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
+ 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: "Write short testcase/frontend/rag/context.md and coverage-map.md with a model-resolved non-production baseUrl/baseUrlSource from task source config, environmentProbe=pending, and capability notes.",
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
- "Read task source, routes/components/API or Mock facts, and execution contract. Write only testcase/frontend/rag/context.md and coverage-map.md.",
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: "Parse the resolved absolute baseUrl from testcase/frontend/rag/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
+ 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
- "Read testcase/frontend/rag/standard-scenarios.v1.json and cover priority=must scenarios (or record GAP in coverage-map). Include ## 测试点 and ## 测试步骤 in each case.",
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
- "Read only testcase/frontend/rag/context.md, testcase/frontend/rag/coverage-map.md, and the draft case paths testcase/frontend/cases/FE-*.md, testcase/frontend/cases/index.md, and testcase/frontend/cases/manifest.draft.json. Write only those same draft paths. Do not write testcase/frontend/cases/manifest.json.",
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
- "- casePath MUST equal testcase/frontend/cases/<caseId>.md; evidenceDir MUST equal testcase/frontend/evidence/<caseId>/. Materialize will rewrite paths, but files must already use caseId filenames.",
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
- "Copy the resolved absolute baseUrl from testcase/frontend/rag/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
+ `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
- "Each case must be independently reproducible with fixture/reset, UI reset, snapshot-before-ref, evidence write point under testcase/frontend/evidence/<case-id>/. If the isolated environment is unavailable, require writing blocked evidence before any browser command.",
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: "This is the only permitted case revision. Read the first review findings and the RAG package. Revise only testcase/frontend/cases/FE-*.md, testcase/frontend/cases/index.md, and testcase/frontend/cases/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 testcase/frontend/cases/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
+ 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
- "testcase/frontend/cases/{{case.caseId}}.md",
415
- "testcase/frontend/rag/context.md",
416
- "testcase/frontend/rag/coverage-map.md",
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
- "1) Read the concrete baseUrl from testcase/frontend/rag/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
+ `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
- "const manifestPath='testcase/frontend/cases/manifest.json';",
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
- "for(const c of (manifest.cases||[])){const evidenceDir=(c.evidenceDir||('testcase/frontend/evidence/'+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||('testcase/frontend/cases/'+c.caseId+'.md'),evidenceDir,dimension:c.dimension||'core',acIds:c.acIds||[],rerunAttempt:attempt+1,reason:reason||'blocked'});}}",
449
- `fs.mkdirSync('testcase/frontend/evidence',{recursive:true});fs.writeFileSync('testcase/frontend/evidence/${candidateArtifact}',JSON.stringify({schemaVersion:1,cases},null,2)+'\\n');process.stdout.write(JSON.stringify({cases}));`,
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: ["testcase/frontend/evidence/**"],
459
- allowedPaths: [...casesWriteSet, "testcase/frontend/evidence/**"],
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
- "testcase/frontend/cases/{{case.caseId}}.md",
505
- "testcase/frontend/rag/context.md",
506
- "testcase/frontend/rag/coverage-map.md",
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
- "Read the concrete baseUrl from testcase/frontend/rag/context.md and start via playwright_cli command=open with args [--browser=chrome, <that-concrete-baseUrl>]. Passed requires open → find → cleanup receipts.",
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: ["testcase/frontend/cases/**", `${evidenceRoot}/**`],
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: ["testcase/frontend/reports/**"],
553
- allowedPaths: ["testcase/frontend/**"],
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: ["testcase/frontend/reports/**"],
600
- allowedPaths: ["testcase/frontend/**"],
600
+ writeSet: [`${layout.reportsDir}/**`],
601
+ allowedPaths: [`${layout.testRoot}/**`],
601
602
  forbiddenPaths: forbidden,
602
- outputContract: "Optional retrospective under testcase/frontend/reports/frontend-test-retrospect-<date>.md. Not required for pipeline acceptance.",
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
- "Pipeline acceptance for frontend-test is frontend-test-result-v1 plus the main report under testcase/frontend/reports/frontend-test-report.html; case pass rate and outcome=passed are quality signals; retrospect is opt-in (frontendTest.reports.retrospect).",
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
- const agentDir = input.env.PI_CODING_AGENT_DIR;
39
- if (agentDir) {
40
- candidates.push(normalizeCandidate(path.join(agentDir, "skills", input.name, "SKILL.md")), normalizeCandidate(path.join(agentDir, "skills", `${input.name}.md`)));
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
- const piSkillsDir = path.join(input.homeDir, ".pi", "agent", "skills");
43
- candidates.push(normalizeCandidate(path.join(piSkillsDir, input.name, "SKILL.md")), normalizeCandidate(path.join(piSkillsDir, `${input.name}.md`)));
44
- // Canonical target-project repo-local skill path.
45
- candidates.push(normalizeCandidate(path.join(input.cwd, ".agents", "skills", input.name, "SKILL.md")));
46
- // Source-repository compatibility: the loop-agent package itself still ships
47
- // bundled skills under `skills/`, and local source development may resolve them
48
- // before the package fallback is found.
49
- candidates.push(normalizeCandidate(path.join(input.cwd, "skills", input.name, "SKILL.md")));
50
- if (input.name === "loop-agent") {
51
- candidates.push(normalizeCandidate(path.join(input.cwd, "skill", "SKILL.md")));
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 content = await readFile(resolvedPath, "utf-8");
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
- `Path: ${resolvedPath}`,
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
- `Path: ${input.filePath}`,
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
- if (!SKILL_NAME_PATTERN.test(name)) {
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
- const searchedPaths = buildSkillCandidatePaths({
324
- name,
325
- cwd: options.cwd,
326
- env,
327
- homeDir,
328
- });
329
- const filePath = await findFirstExistingFile(searchedPaths);
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
- filePath,
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
- path: filePath,
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
- path: filePath,
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 && skills.includes("loop-agent")) {
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(),