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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/CHANGELOG.md +27 -2
  2. package/README.md +12 -12
  3. package/dist/adapters/loop-agent.js +34 -0
  4. package/dist/build-stamp.json +3 -3
  5. package/dist/cli/command-definitions.js +5 -5
  6. package/dist/cli/program.js +1 -4
  7. package/dist/commands/init-legacy-audit.js +330 -0
  8. package/dist/commands/init-upgrade.js +644 -104
  9. package/dist/commands/init.js +686 -330
  10. package/dist/commands/operator-guide.js +99 -0
  11. package/dist/commands/operator.js +16 -3
  12. package/dist/executors/dag-pi-executor.js +11 -6
  13. package/dist/executors/pi-sdk-executor.js +3 -1
  14. package/dist/executors/pi-writer-tool-policy.js +60 -4
  15. package/dist/executors/playwright-cli-launcher.js +31 -45
  16. package/dist/executors/shell-write-guard.js +24 -67
  17. package/dist/governance/exec-plans.js +10 -1
  18. package/dist/governance/harness.js +2 -7
  19. package/dist/governance/manifest-types.js +28 -9
  20. package/dist/governance/project-capability-scanner.js +263 -0
  21. package/dist/governance/project-config.js +96 -0
  22. package/dist/governance/resource-catalog.js +151 -0
  23. package/dist/infrastructure/console/app-data.js +6 -2
  24. package/dist/shared/operator/capabilities.js +2 -2
  25. package/dist/shared/package-metadata.js +23 -7
  26. package/dist/shared/pi-context-pressure/extension.js +23 -0
  27. package/dist/shared/pi-context-pressure/index.js +1 -0
  28. package/dist/shared/pi-context-pressure/telemetry.js +4 -0
  29. package/dist/shared/pi-context-pressure/tool-result-pruner.js +98 -0
  30. package/dist/shared/update/npm-client.js +49 -1
  31. package/dist/task/config-types.js +4 -2
  32. package/dist/task/source-prepare/build-draft.js +1 -0
  33. package/dist/task/source-prepare/path-policy.js +17 -7
  34. package/dist/worker/cli.js +3 -5
  35. package/dist/worker/console/chat/browser-automation.js +455 -41
  36. package/dist/worker/console/chat/browser-content.js +74 -0
  37. package/dist/worker/console/chat/browser-diagnostics.js +250 -0
  38. package/dist/worker/console/chat/browser-evaluation.js +58 -0
  39. package/dist/worker/console/chat/browser-operation-queue.js +19 -0
  40. package/dist/worker/console/chat/browser-pick.js +113 -0
  41. package/dist/worker/console/chat/browser-routes.js +54 -5
  42. package/dist/worker/console/chat/browser-screenshot-result.js +47 -0
  43. package/dist/worker/console/chat/browser-storage.js +89 -0
  44. package/dist/worker/console/chat/chat-event-store.js +7 -6
  45. package/dist/worker/console/chat/context-insights.js +84 -19
  46. package/dist/worker/console/chat/pet/pet-affinity.js +169 -0
  47. package/dist/worker/console/chat/pet/pet-persist.js +109 -0
  48. package/dist/worker/console/chat/pet/pet-registry.js +142 -0
  49. package/dist/worker/console/chat/pet/pet-remarks.js +48 -0
  50. package/dist/worker/console/chat/pet/pet-routes.js +125 -0
  51. package/dist/worker/console/chat/pet/pet-service.js +262 -0
  52. package/dist/worker/console/chat/pet/pet-state.js +99 -0
  53. package/dist/worker/console/chat/pi-mode-loop-isolation.js +17 -2
  54. package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +125 -174
  55. package/dist/worker/console/chat/pi-runtime.js +8 -3
  56. package/dist/worker/console/chat/repo-browser.js +80 -2
  57. package/dist/worker/console/chat/repo-preview-kind.js +155 -0
  58. package/dist/worker/console/chat/routes.js +43 -7
  59. package/dist/worker/console/chat/scheduled-goal-request.js +14 -6
  60. package/dist/worker/console/chat/session-mode.js +1 -1
  61. package/dist/worker/console/chat/session-store.js +24 -5
  62. package/dist/worker/console/chat/tools.js +12 -0
  63. package/dist/worker/console/console-shutdown.js +34 -0
  64. package/dist/worker/console/server.js +2 -1
  65. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-r1NAPU_5.js → abnfDiagram-N423BO3Z-DASc69AM.js} +1 -1
  66. package/dist/worker/console/static/assets/{arc-dT5T4sVL.js → arc-h09p-LAc.js} +1 -1
  67. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP_cOq46.js → architectureDiagram-T3A2C74G-CZ3dtLkh.js} +1 -1
  68. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-sToEZxEf.js → blockDiagram-VBNYF7ZC-DMrRhMXG.js} +1 -1
  69. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DSxwwi2m.js → c4Diagram-5PPSVZJV-DQxisHV6.js} +1 -1
  70. package/dist/worker/console/static/assets/channel-D1zgKZfg.js +1 -0
  71. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B9fh0IkF.js → chunk-2GRJ4B5K-CefVN9zh.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CAUNl-A6.js → chunk-2Q5K7J3B-DmR1Koq2.js} +1 -1
  73. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-BomeqbNX.js → chunk-5RXB4S5H-NlzrSW7L.js} +1 -1
  74. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BNAuTOLE.js → chunk-5VM5RSS4-DfelJv-0.js} +1 -1
  75. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DfzjRD5y.js → chunk-6Q2QTUOP-BK6RFy58.js} +1 -1
  76. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C3uE-yc2.js → chunk-GF5L2VYU-BAeh9lz4.js} +1 -1
  77. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-C3sQMMYr.js → chunk-JWPE2WC7-ChPS5WzG.js} +1 -1
  78. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-D_AtEYQL.js → chunk-KBJHAD2P-BoVe5IM6.js} +1 -1
  79. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DW4TwhKv.js → chunk-RYQCIY6F-C4q_0Lw9.js} +1 -1
  80. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CTQDmJcp.js → chunk-XXDRQBXY-Bb9TvjeY.js} +1 -1
  81. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CXNNsY2M.js +1 -0
  82. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CXNNsY2M.js +1 -0
  83. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-iMqPuBsL.js → cose-bilkent-JH36ORCC-DmNnL8Ff.js} +1 -1
  84. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DktIuDWs.js → cynefin-VYW2F7L2-Contvx58.js} +1 -1
  85. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BgVnjyEs.js → cynefinDiagram-MW4NZA55-Cigq7cjk.js} +1 -1
  86. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CR25E4bu.js → dagre-VZM6K2ZE-dEbC0nxb.js} +1 -1
  87. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CPM2h5HY.js → diagram-7IWD3JNH-CzIztESV.js} +1 -1
  88. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-DqF0x2g1.js → diagram-B4RE2ZJO-Bo14zbaY.js} +1 -1
  89. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-G8BYp2eb.js → diagram-LBJQPF4R-C4xUyil6.js} +1 -1
  90. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BbC4kT_D.js → diagram-Q27KOJAE-CaBY_uYa.js} +1 -1
  91. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CKZwZ-G5.js → diagram-UB23O5K3-ClUXUeiu.js} +1 -1
  92. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-kJDmheia.js → ebnfDiagram-BXEA7PRR-BbGCgjC6.js} +1 -1
  93. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DFTVHyxB.js → erDiagram-JOGREHBK-Dr-AcwSl.js} +1 -1
  94. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-BqdN5_os.js → flowDiagram-UKHOOZJN-BG8Gcfyc.js} +1 -1
  95. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-SYDjVakh.js → ganttDiagram-PKOTCBZU-CFVaPs5r.js} +1 -1
  96. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js → gitGraphDiagram-DS77QQ5N-ChJ8pm-O.js} +1 -1
  97. package/dist/worker/console/static/assets/index-B_5GhVIO.js +488 -0
  98. package/dist/worker/console/static/assets/index-C7YMBluX.css +1 -0
  99. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DIeAmJ45.js → infoDiagram-6WML65LV-C5J9-uCZ.js} +1 -1
  100. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-oikqUAlo.js → ishikawaDiagram-WSZJBQD7-BcfjqzDv.js} +1 -1
  101. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-mKEF_xYT.js → journeyDiagram-NVQOT4AX-BzzCO-rD.js} +1 -1
  102. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BdWJHPnN.js → kanban-definition-27J2QSJJ-NCHUZ46h.js} +1 -1
  103. package/dist/worker/console/static/assets/{linear-DGRlieMH.js → linear-BKZlWCUU.js} +1 -1
  104. package/dist/worker/console/static/assets/{mermaid.core-4P9yPC9c.js → mermaid.core-DbTaQjFE.js} +5 -5
  105. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-5PTSkos6.js → mindmap-definition-FAOFIHXS-CroYl5xQ.js} +1 -1
  106. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DONIFpG5.js → pegDiagram-VL7TDLO6-DgMBx-9k.js} +1 -1
  107. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-D_Z4S70M.js → pieDiagram-7S7Q4E2Y-CyWDmbq1.js} +1 -1
  108. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CAsjh8iY.js → quadrantDiagram-CIZ2JOQS-BgGEOqSP.js} +1 -1
  109. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Cx4Dd6ac.js → railroadDiagram-AXF67PYL-BMZ7oeeV.js} +1 -1
  110. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-cQb7mwyH.js → requirementDiagram-LRYGKXZP-DxqyGhcc.js} +1 -1
  111. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Cd-DhUdf.js → sankeyDiagram-W5VNT64P-DnSvNCG8.js} +1 -1
  112. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-AlKYRgIk.js → sequenceDiagram-SI44F4Z6-BiZh6f-R.js} +1 -1
  113. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-CUy_ET4e.js → sizeCapture-X5ZJPWSS-BKXSI8WB.js} +1 -1
  114. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CTuVe6Sa.js → stateDiagram-OKZ733FA-CfSgoYFm.js} +1 -1
  115. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DgHi3vjb.js +1 -0
  116. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-ChFLJb-v.js → swimlanes-SLNWSIFB-CyGADcHe.js} +2 -2
  117. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bgvdt1Y0.js +8 -0
  118. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DZZeiAPr.js → timeline-definition-Z64GVDOM-HmPaSPOK.js} +1 -1
  119. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-t4O91cZU.js → vennDiagram-T6HMQDX7-VA2VBbez.js} +1 -1
  120. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CV5qGHZu.js → wardleyDiagram-T6FBY63Y-BxjiBPZa.js} +1 -1
  121. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-DyXbytUI.js → xychartDiagram-ELKLHX3M--hUY-qFj.js} +1 -1
  122. package/dist/worker/console/static/index.html +2 -2
  123. package/dist/worker/console/static/pets/ember/pet.json +167 -0
  124. package/dist/worker/console/static/pets/ember/spritesheet.png +0 -0
  125. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +4 -3
  126. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +57 -9
  127. package/dist/worker/console/static-src/operator-chat/use-stat-dialog.js +95 -0
  128. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +6 -0
  129. package/dist/worker/console/workspace-context.js +15 -0
  130. package/dist/worker/console/workspace-initialization.js +25 -16
  131. package/dist/worker/delivery/package.js +4 -1
  132. package/dist/worker/materialize/harness-task-lineage.js +4 -2
  133. package/dist/worker/observe/static/console-theme.css +33 -29
  134. package/dist/worker/observe/static/operator-chrome.js +1 -42
  135. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +3 -1
  136. package/dist/workflows/dag/backend-test-layout.js +27 -7
  137. package/dist/workflows/dag/backend-test-markdown-workflow.js +3 -1
  138. package/dist/workflows/dag/backend-test-pytest-collection.js +4 -1
  139. package/dist/workflows/dag/final-verification.js +45 -10
  140. package/dist/workflows/dag/frontend-test-case-checklist.js +12 -3
  141. package/dist/workflows/dag/frontend-test-environment-probe.js +0 -2
  142. package/dist/workflows/dag/frontend-test-layout.js +41 -12
  143. package/dist/workflows/dag/hybrid/sources.js +35 -16
  144. package/dist/workflows/dag/hybrid/templates/backend-test.js +70 -55
  145. package/dist/workflows/dag/hybrid/templates/frontend-test.js +62 -41
  146. package/dist/workflows/dag/hybrid/templates/kg-bootstrap.js +6 -6
  147. package/dist/workflows/dag/hybrid/templates/knowledge-sync.js +5 -5
  148. package/dist/workflows/dag/skill-instructions.js +102 -32
  149. package/dist/workflows/dag/skill-snapshot.js +3 -0
  150. package/dist/workflows/dag/skills.js +7 -7
  151. package/dist/workflows/dag/test-artifact-namespace.js +73 -0
  152. package/dist/workflows/dag/validate.js +24 -5
  153. package/dist/workflows/dag/workspace-checkpoint.js +13 -1
  154. package/docs/architecture/runtime-boundaries.md +28 -22
  155. package/docs/init-surface.manifest.json +43 -295
  156. package/docs/templates/README.md +4 -4
  157. package/docs/templates/agent-dag.base.json +9 -9
  158. package/docs/templates/agent-dag.final-verification.json +6 -6
  159. package/docs/templates/agent-dag.supervised-implementation.json +12 -12
  160. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +3 -3
  161. package/docs/templates/backend-test-dag.json +708 -708
  162. package/docs/templates/backend-test-dag.review-cases.prompt.md +3 -3
  163. package/docs/templates/frontend-implementation-dag.json +2 -2
  164. package/docs/templates/frontend-test-case-checklist.md +2 -2
  165. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +7 -7
  166. package/docs/templates/frontend-test-dag.json +48 -39
  167. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +2 -2
  168. package/docs/templates/frontend-test-dag.retrospect.prompt.md +1 -1
  169. package/docs/templates/frontend-test-dag.review-cases.prompt.md +1 -1
  170. package/docs/templates/hybrid-dag.json +9 -9
  171. package/docs/templates/knowledge-sync-dag.json +8 -8
  172. package/package.json +12 -3
  173. package/resource-catalog.v1.json +1973 -0
  174. package/resources/agent-bridge/loop-agent/SKILL.md +40 -0
  175. package/resources/init-history/legacy-surface.v1.json +33 -0
  176. package/skills/fe-test-ui-scout/SKILL.md +4 -4
  177. package/skills/fe-test-ui-scout/references/ledger-schema.md +1 -1
  178. package/skills/init-update/SKILL.md +34 -0
  179. package/skills/init-update/agents/openai.yaml +6 -0
  180. package/skills/playwright-cli/SKILL.md +1 -1
  181. package/skills/playwright-cli-case-generator/SKILL.md +4 -4
  182. package/dist/worker/console/static/assets/channel-CyBY_yGk.js +0 -1
  183. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +0 -1
  184. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +0 -1
  185. package/dist/worker/console/static/assets/index-B28Onmfy.css +0 -1
  186. package/dist/worker/console/static/assets/index-CTMZsDmh.js +0 -486
  187. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +0 -1
  188. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +0 -8
@@ -0,0 +1,99 @@
1
+ import { access } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { checkInitUpdate } from "./init.js";
4
+ import { loadNormalizedProjectConfig } from "../governance/project-config.js";
5
+ import { CONTROLLER_RESOURCE_IDS, createControllerContextProvider, } from "../governance/resource-catalog.js";
6
+ import { LOOP_AGENT_PACKAGE_NAME, readPackageVersion } from "../shared/package-metadata.js";
7
+ async function exists(file) {
8
+ try {
9
+ await access(file);
10
+ return true;
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
16
+ export async function buildOperatorGuideDocument(repoRootInput) {
17
+ const repoRoot = path.resolve(repoRootInput);
18
+ const provider = await createControllerContextProvider();
19
+ const packageRoot = path.dirname(provider.list().length > 0
20
+ ? (await provider.readText(CONTROLLER_RESOURCE_IDS.loopAgentBridgeSkill)).absolutePath
21
+ : repoRoot);
22
+ const version = readPackageVersion(path.resolve(packageRoot, "..", "..", "..")) ?? "unknown";
23
+ let reproducibility = "unavailable";
24
+ try {
25
+ reproducibility = (await loadNormalizedProjectConfig(repoRoot)).verification.reproducibility;
26
+ }
27
+ catch {
28
+ // An uninitialized or malformed project is classified below by init assessment.
29
+ }
30
+ let status = "uninitialized";
31
+ let bridge = "missing";
32
+ let next = {
33
+ command: "loop-agent init --repo-root .",
34
+ reason: "initialize the minimal project surface",
35
+ };
36
+ if (await exists(path.join(repoRoot, "harness.json"))) {
37
+ const report = await checkInitUpdate({ repoRoot });
38
+ const bridgeDecision = report.humanDecisions.find((item) => item.reason.includes("bridge-path-conflict"));
39
+ bridge = bridgeDecision
40
+ ? "conflict"
41
+ : await exists(path.join(repoRoot, ".agents", "skills", "loop-agent", "SKILL.md"))
42
+ ? "ready"
43
+ : "missing";
44
+ if (report.humanDecisions.length > 0 || report.modelMergeTasks.length > 0) {
45
+ status = "blocked";
46
+ next = {
47
+ command: "loop-agent init upgrade --repo-root . --json",
48
+ reason: "complete the bounded migration decisions before starting new work",
49
+ };
50
+ }
51
+ else if (report.deterministicActions.length > 0) {
52
+ status = "needs-update";
53
+ next = {
54
+ command: "loop-agent init update --repo-root . --apply-safe --json",
55
+ reason: "apply the deterministic minimal-surface update",
56
+ };
57
+ }
58
+ else {
59
+ status = "ready";
60
+ next = {
61
+ command: "loop-agent task advance <task-id> --profile auto --json",
62
+ reason: "prepare or continue a governed task",
63
+ };
64
+ }
65
+ }
66
+ return {
67
+ schemaVersion: 1,
68
+ kind: "loop-agent-operator-guide",
69
+ controller: {
70
+ packageName: LOOP_AGENT_PACKAGE_NAME,
71
+ version,
72
+ resourceCatalogSha256: provider.identity.catalogDigest,
73
+ },
74
+ project: { repoRoot, status, configReproducibility: reproducibility, bridge },
75
+ dag: {
76
+ entry: "loop-agent task advance",
77
+ longRunningOwner: "console-operation-required",
78
+ builtinSkillSource: "builtin",
79
+ },
80
+ next,
81
+ };
82
+ }
83
+ export function renderOperatorGuideMarkdown(document) {
84
+ return [
85
+ "# loop-agent operator guide",
86
+ "",
87
+ `- Controller: \`${document.controller.packageName}@${document.controller.version}\``,
88
+ `- Project: \`${document.project.status}\``,
89
+ `- Bridge: \`${document.project.bridge}\``,
90
+ `- Verification: \`${document.project.configReproducibility}\``,
91
+ "",
92
+ "## Next",
93
+ "",
94
+ `\`${document.next.command}\``,
95
+ "",
96
+ document.next.reason,
97
+ "",
98
+ ].join("\n");
99
+ }
@@ -1,4 +1,5 @@
1
1
  import { buildOperatorCapabilitiesDocument, operatorFailed, operatorSucceeded, processExitCodeForOutcome, writeOperatorJson, } from "../shared/operator/index.js";
2
+ import { buildOperatorGuideDocument, renderOperatorGuideMarkdown, } from "./operator-guide.js";
2
3
  export function parseOperatorArgs(args) {
3
4
  const options = {};
4
5
  const positional = [];
@@ -7,19 +8,31 @@ export function parseOperatorArgs(args) {
7
8
  options.json = true;
8
9
  continue;
9
10
  }
11
+ if (token === "--markdown") {
12
+ options.markdown = true;
13
+ continue;
14
+ }
10
15
  if (token.startsWith("-")) {
11
- throw new Error(`unknown flag ${token}\nusage: operator capabilities --json`);
16
+ throw new Error(`unknown flag ${token}\nusage: operator <capabilities|guide> [--json|--markdown]`);
12
17
  }
13
18
  positional.push(token);
14
19
  }
15
20
  const subcommand = positional[0];
16
21
  if (!subcommand) {
17
- throw new Error("usage: operator <capabilities> --json");
22
+ throw new Error("usage: operator <capabilities|guide> [--json|--markdown]");
18
23
  }
19
24
  return { subcommand, options };
20
25
  }
21
- export async function runOperator(_repoRoot, args) {
26
+ export async function runOperator(repoRoot, args) {
22
27
  const { subcommand, options } = parseOperatorArgs(args);
28
+ if (subcommand === "guide") {
29
+ const guide = await buildOperatorGuideDocument(repoRoot);
30
+ if (options.markdown)
31
+ console.log(renderOperatorGuideMarkdown(guide));
32
+ else
33
+ emit(operatorSucceeded("operator guide", guide), true);
34
+ return;
35
+ }
23
36
  if (subcommand !== "capabilities") {
24
37
  const result = operatorFailed({
25
38
  command: "operator",
@@ -38,7 +38,7 @@ import { isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, is
38
38
  import { assessBackendTestMdPlanCompleteness, assessBackendTestMdWriterCompleteness, assessBackendTestPytestPlanCompleteness, assessBackendTestPytestWriterCompleteness, assessBackendTestShardChildCompleteness, backendTestWriterProgressRoleForTask, classifyBackendTestWriterCompletenessFailure, isBackendTestCompletenessRetryCandidate, isBackendTestMdPlanTask, isBackendTestPytestCollectionRepairOutcomeRecoveryCandidate, isBackendTestPytestPlanTask, isBackendTestShardChildTask, writeBackendTestWriterProgressArtifacts, } from "../workflows/dag/backend-test-writer-completeness.js";
39
39
  import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
40
40
  import { assessBackendTestPlanProtocol } from "../workflows/dag/backend-test-plan-protocol.js";
41
- import { frontendTestLayoutFromSpec } from "../workflows/dag/frontend-test-layout.js";
41
+ import { frontendTestContextRel, frontendTestLayoutFromSpec, } from "../workflows/dag/frontend-test-layout.js";
42
42
  import { redactSecrets, truncateUtf8Preview } from "../shared/preview.js";
43
43
  import { writeEffectiveContextReceipt } from "../workflows/dag/context-receipt.js";
44
44
  import { estimateFrontendPlanRequirementRecordCalls, isWriterThinkingExhausted, readWriterThinkingExhaustionEvidence, WRITER_BUDGET_EXHAUSTED_CATEGORY, WRITER_TOKEN_BUDGET, FRONTEND_PLAN_COVERAGE_MAX_CONCURRENCY, mapWithConcurrency, buildFrontendPlanWorkload, loadOrCreateFrontendPlanCoverageLayout, collectFrontendVerificationCommandFiles, aggregateParallelPiResults, combineSequentialPiResults, runFrontendScoutParallelSessions, runFrontendContractSegmentedSessions, runFrontendPlanSegmentedSessions, runFrontendReviewSegmentedSessions, runFrontendScoutSegmentedSessions, } from "./dag-pi/sessions/index.js";
@@ -1552,7 +1552,7 @@ async function writePiExtensionsArtifact(nodeArtifactsDir, requested, resolved,
1552
1552
  // best-effort run fact; node execution continues
1553
1553
  }
1554
1554
  }
1555
- async function resolveValidatedFrontendBaseUrlFromContext(workspaceRoot, runDir) {
1555
+ async function resolveValidatedFrontendBaseUrlFromContext(workspaceRoot, runDir, layout) {
1556
1556
  try {
1557
1557
  const capability = JSON.parse(await readFile(path.join(runDir, "preflight-frontend-browser-tool-shell", "frontend-browser-capability.json"), "utf8"));
1558
1558
  if (capability.status !== "ready" ||
@@ -1563,7 +1563,7 @@ async function resolveValidatedFrontendBaseUrlFromContext(workspaceRoot, runDir)
1563
1563
  catch {
1564
1564
  throw new Error("browser-command-capability-unavailable: missing or invalid frontend browser capability artifact");
1565
1565
  }
1566
- const contextPath = path.join(workspaceRoot, "testcase", "frontend", "rag", "context.md");
1566
+ const contextPath = path.join(workspaceRoot, ...frontendTestContextRel(layout).split("/"));
1567
1567
  let markdown;
1568
1568
  try {
1569
1569
  markdown = await readFile(contextPath, "utf8");
@@ -1759,8 +1759,13 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
1759
1759
  let beforeStatus;
1760
1760
  let beforeWorkspaceSnapshot;
1761
1761
  let beforePathFingerprints;
1762
- /** tools-only and filesystem-only keep the tool sandbox but skip Git baseline. */
1763
- const filesystemOnly = isWriteTask && meta.spec.backendTestWorkspaceControl === "filesystem-only";
1762
+ /** tools-only and filesystem-only keep the tool sandbox but skip Git baseline.
1763
+ * filesystem-only applies when the node froze it in writeGuardPolicy (D4:
1764
+ * any dedicated test writer under an ignored root) OR the backend-test spec
1765
+ * explicitly selected workspaceControl=filesystem-only. */
1766
+ const filesystemOnly = isWriteTask &&
1767
+ (input.task.writeGuardPolicy === "filesystem-only" ||
1768
+ meta.spec.backendTestWorkspaceControl === "filesystem-only");
1764
1769
  const skipGitWriteGuard = isWriteTask && (input.task.writeGuardPolicy === "tools-only" || filesystemOnly);
1765
1770
  if (isWriteTask && filesystemOnly) {
1766
1771
  try {
@@ -1880,7 +1885,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
1880
1885
  nodeId: input.task.id,
1881
1886
  caseId,
1882
1887
  evidenceDir,
1883
- baseUrl: await resolveValidatedFrontendBaseUrlFromContext(input.cwd, meta.runDir),
1888
+ baseUrl: await resolveValidatedFrontendBaseUrlFromContext(input.cwd, meta.runDir, layout),
1884
1889
  inputRoot: layout.fixturesDir,
1885
1890
  layout,
1886
1891
  };
@@ -8,7 +8,7 @@ import { computeExtensionToolAllowlistExtras } from "./pi-extension-resolver.js"
8
8
  import { BoundedTextPreview, classifyPiFailure, createPiJsonlStreamCollector, DEFAULT_ABORT_GRACE_MS, DEFAULT_STALL_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, extractPiEventStopReason, extractAssistantTextFromPiJson, } from "./pi-executor.js";
9
9
  import { serializeSessionEvent } from "./pi-event-serializer.js";
10
10
  import { PI_RECOMMENDED_COMPACTION, PI_RECOMMENDED_RETRY, } from "../shared/pi-retry-settings.js";
11
- import { createCompactionController, createContextPressureExtension, createSiftBridgeState, inMemoryCompactionSettings, isDagSiftEnabled, parseCompactionPolicy, parseSiftBridgeConfig, SIFT_RETRIEVE_TOOL_NAME, } from "../shared/pi-context-pressure/index.js";
11
+ import { createCompactionController, createContextPressureExtension, createSiftBridgeState, inMemoryCompactionSettings, isDagSiftEnabled, parseCompactionPolicy, parsePiToolResultPruneConfig, parseSiftBridgeConfig, SIFT_RETRIEVE_TOOL_NAME, } from "../shared/pi-context-pressure/index.js";
12
12
  import { DAG_TELEMETRY_SESSION_ID_ENV, injectWizardLocalTelemetryHeaders, isTelemetrySessionId, } from "../shared/wizard-local-telemetry.js";
13
13
  /** Maximum number of same-session continuations after provider output truncation. */
14
14
  export const PI_SDK_OUTPUT_LIMIT_CONTINUE_MAX = 3;
@@ -292,6 +292,7 @@ async function createSdkSession(sdk, input, shared) {
292
292
  // Prefer SettingsManager.inMemory so harness runs do not depend on ~/.pi or
293
293
  // project .pi/settings.json being present/trusted.
294
294
  const policy = parseCompactionPolicy();
295
+ const pruneSettings = parsePiToolResultPruneConfig();
295
296
  const compaction = {
296
297
  ...PI_RECOMMENDED_COMPACTION,
297
298
  ...inMemoryCompactionSettings(policy),
@@ -321,6 +322,7 @@ async function createSdkSession(sdk, input, shared) {
321
322
  policy,
322
323
  controller,
323
324
  sift,
325
+ ...(pruneSettings.enabled && policy.mode !== "off" ? { prune: pruneSettings.config } : {}),
324
326
  });
325
327
  const telemetryExtension = createDagWizardLocalTelemetryHeadersExtension(input.telemetrySessionId);
326
328
  const extensionFactories = [pressureExtension, telemetryExtension].filter((extension) => Boolean(extension));
@@ -1,6 +1,7 @@
1
1
  import { access as fsAccess, lstat, mkdir as fsMkdir, readFile as fsReadFile, realpath, writeFile as fsWriteFile, } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { pathMatchesPattern } from "../shared/git-progress.js";
4
+ import { isAnyTestArtifactPath, isHarnessBaselineForbiddenPattern, } from "../workflows/dag/test-artifact-namespace.js";
4
5
  export class PiWriterPathDeniedError extends Error {
5
6
  code;
6
7
  targetPath;
@@ -105,12 +106,60 @@ function repoRelativePath(root, absoluteTarget, raw, allowRoot) {
105
106
  }
106
107
  return relative;
107
108
  }
109
+ /**
110
+ * D3 test-artifact exception at the tool layer: a write target inside the
111
+ * canonical `.harness/testcase` namespace that the task's exclusive writeSet
112
+ * (or, for mkdir, a writeSet entry at/above it) explicitly owns is exempt
113
+ * from the BASELINE harness guard patterns (`.harness` / `.harness/**`) only.
114
+ * Explicit deeper forbidden patterns and the hard controller-facts guard stay
115
+ * authoritative, matching the DAG validator and the post-hoc write guard.
116
+ */
117
+ function testArtifactWriteExempt(relative, ctx) {
118
+ const forbidden = ctx.forbiddenPaths ?? [];
119
+ let baselineHit = false;
120
+ for (const pattern of forbidden) {
121
+ if (!matchesAnyPattern(relative, [pattern]))
122
+ continue;
123
+ if (!isHarnessBaselineForbiddenPattern(pattern))
124
+ return false;
125
+ baselineHit = true;
126
+ }
127
+ if (!baselineHit)
128
+ return false;
129
+ if (!isAnyTestArtifactPath(relative))
130
+ return false;
131
+ const writeSet = ctx.writeSet ?? [];
132
+ if ((ctx.writePolicy ?? "exclusive") === "exclusive") {
133
+ return matchesAnyPattern(relative, writeSet);
134
+ }
135
+ return writeSet.length === 0 || matchesAnyPattern(relative, writeSet);
136
+ }
137
+ function testArtifactMkdirExempt(relative, ctx) {
138
+ const forbidden = ctx.forbiddenPaths ?? [];
139
+ let baselineHit = false;
140
+ for (const pattern of forbidden) {
141
+ if (!pathMatchesPattern(relative, pattern))
142
+ continue;
143
+ if (!isHarnessBaselineForbiddenPattern(pattern))
144
+ return false;
145
+ baselineHit = true;
146
+ }
147
+ if (!baselineHit)
148
+ return false;
149
+ if (!isAnyTestArtifactPath(relative))
150
+ return false;
151
+ const writeSet = (ctx.writeSet ?? []).map(normalizeRepoRelative);
152
+ return writeSet.some((candidate) => isPathAtOrBelowDirectory(candidate, relative) ||
153
+ (relative.length > 0 && pathMatchesPattern(relative, candidate)));
154
+ }
108
155
  function assertWritableRelative(relative, ctx) {
109
156
  if (isHardForbidden(relative)) {
110
157
  throw new PiWriterPathDeniedError("hard-forbidden", relative, "writes under .harness/tasks/** or .harness/dag-runs/** are never allowed for Pi writers");
111
158
  }
112
159
  const forbidden = ctx.forbiddenPaths ?? [];
113
- if (forbidden.length > 0 && matchesAnyPattern(relative, forbidden)) {
160
+ if (forbidden.length > 0 &&
161
+ matchesAnyPattern(relative, forbidden) &&
162
+ !testArtifactWriteExempt(relative, ctx)) {
114
163
  throw new PiWriterPathDeniedError("forbidden", relative);
115
164
  }
116
165
  const writeSet = ctx.writeSet ?? [];
@@ -130,7 +179,8 @@ function assertMkdirRelative(relative, ctx) {
130
179
  const forbidden = ctx.forbiddenPaths ?? [];
131
180
  if (relative &&
132
181
  forbidden.length > 0 &&
133
- matchesAnyPattern(relative, forbidden)) {
182
+ matchesAnyPattern(relative, forbidden) &&
183
+ !testArtifactMkdirExempt(relative, ctx)) {
134
184
  throw new PiWriterPathDeniedError("forbidden", relative);
135
185
  }
136
186
  const writeSetCandidates = (ctx.writeSet ?? [])
@@ -149,8 +199,14 @@ function assertMkdirRelative(relative, ctx) {
149
199
  if (nonHardForbiddenCandidates.length === 0) {
150
200
  throw new PiWriterPathDeniedError("hard-forbidden", relative || ".");
151
201
  }
152
- if (forbidden.length > 0 &&
153
- nonHardForbiddenCandidates.every((candidate) => matchesAnyPattern(candidate, forbidden))) {
202
+ // D3 test-artifact exception: when the target directory is a canonical
203
+ // namespace root owned by the writeSet, baseline harness guard patterns do
204
+ // not disqualify allowedPaths candidates either.
205
+ const effectiveForbidden = testArtifactMkdirExempt(relative, ctx)
206
+ ? forbidden.filter((pattern) => !isHarnessBaselineForbiddenPattern(pattern))
207
+ : forbidden;
208
+ if (effectiveForbidden.length > 0 &&
209
+ nonHardForbiddenCandidates.every((candidate) => matchesAnyPattern(candidate, effectiveForbidden))) {
154
210
  throw new PiWriterPathDeniedError("forbidden", relative || ".");
155
211
  }
156
212
  }
@@ -1,57 +1,43 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { readFileSync, realpathSync, statSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { createRequire } from "node:module";
4
4
  const PACKAGE_NAME = "@playwright/cli";
5
+ const PACKAGE_VERSION = "0.1.20";
5
6
  function launcherFromManifest(manifestPath) {
6
- try {
7
- const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
8
- if (manifest.name !== PACKAGE_NAME)
9
- return undefined;
10
- const bin = typeof manifest.bin === "string"
11
- ? manifest.bin
12
- : manifest.bin && typeof manifest.bin === "object"
13
- ? manifest.bin["playwright-cli"]
14
- : undefined;
15
- if (typeof bin !== "string" || !bin || path.isAbsolute(bin) || bin.split(/[\\/]+/).includes(".."))
16
- return undefined;
17
- const packageRoot = path.dirname(manifestPath);
18
- const entryPath = path.resolve(packageRoot, bin);
19
- const relative = path.relative(packageRoot, entryPath);
20
- if (relative.startsWith("..") || path.isAbsolute(relative) || !existsSync(entryPath))
21
- return undefined;
22
- return { executable: process.execPath, argvPrefix: [entryPath], entryPath };
23
- }
24
- catch {
7
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
8
+ if (manifest.name !== PACKAGE_NAME)
25
9
  return undefined;
10
+ if (manifest.version !== PACKAGE_VERSION) {
11
+ throw new Error(`expected ${PACKAGE_NAME}@${PACKAGE_VERSION}, found ${String(manifest.version)}`);
26
12
  }
13
+ const bin = typeof manifest.bin === "string"
14
+ ? manifest.bin
15
+ : manifest.bin && typeof manifest.bin === "object"
16
+ ? manifest.bin["playwright-cli"]
17
+ : undefined;
18
+ if (typeof bin !== "string" || !bin || path.isAbsolute(bin) || path.win32.isAbsolute(bin) || bin.split(/[\\/]+/).includes(".."))
19
+ return undefined;
20
+ if (!/\.(?:c|m)?js$/i.test(bin))
21
+ return undefined;
22
+ const packageRoot = realpathSync(path.dirname(manifestPath));
23
+ const entryPath = realpathSync(path.resolve(packageRoot, bin));
24
+ const relative = path.relative(packageRoot, entryPath);
25
+ if (relative.startsWith("..") || path.isAbsolute(relative) || !statSync(entryPath).isFile())
26
+ return undefined;
27
+ return { executable: process.execPath, argvPrefix: [entryPath], entryPath };
27
28
  }
28
- function candidateManifestPaths(cwd) {
29
- const paths = new Set();
30
- for (const root of [cwd, process.cwd()]) {
31
- try {
32
- paths.add(createRequire(path.join(path.resolve(root), "package.json")).resolve(`${PACKAGE_NAME}/package.json`));
33
- }
34
- catch {
35
- // Keep searching controller-known module roots.
36
- }
37
- }
38
- // Npm's Windows/global shim is paired with node_modules in either the shim
39
- // directory or its parent (.bin). We read package metadata directly; the
40
- // .cmd file itself is never executed or parsed as a command string.
41
- for (const binDir of (process.env.PATH ?? "").split(path.delimiter).filter(Boolean)) {
42
- paths.add(path.join(binDir, "node_modules", ...PACKAGE_NAME.split("/"), "package.json"));
43
- paths.add(path.join(binDir, "..", ...PACKAGE_NAME.split("/"), "package.json"));
44
- }
45
- return [...paths];
46
- }
47
- /** Resolve only a package-declared entry; throws instead of falling back to a PATH executable. */
48
- export function resolvePlaywrightCliLauncher(cwd) {
49
- for (const manifestPath of candidateManifestPaths(cwd)) {
29
+ /** Resolve from this installation, independently of the target project's cwd/PATH. */
30
+ export function resolvePlaywrightCliLauncher(_cwd) {
31
+ try {
32
+ const manifestPath = createRequire(import.meta.url).resolve(`${PACKAGE_NAME}/package.json`);
50
33
  const launcher = launcherFromManifest(manifestPath);
51
- if (launcher)
52
- return launcher;
34
+ if (!launcher)
35
+ throw new Error("invalid package manifest or JavaScript entry");
36
+ return launcher;
37
+ }
38
+ catch (error) {
39
+ throw new Error(`playwright-cli-unavailable: bundled ${PACKAGE_NAME} installation is missing or invalid; reinstall loop-agent. ${error instanceof Error ? error.message : String(error)}`);
53
40
  }
54
- throw new Error("playwright-cli-unavailable: verified @playwright/cli JavaScript entry was not found");
55
41
  }
56
42
  export function checkPlaywrightCliLauncher(cwd) {
57
43
  try {
@@ -1,11 +1,11 @@
1
- import { spawn } from "node:child_process";
2
1
  import { createHash } from "node:crypto";
3
- import { constants, createReadStream } from "node:fs";
4
- import { access, lstat, readlink, unlink } from "node:fs/promises";
2
+ import { createReadStream } from "node:fs";
3
+ import { lstat, readlink, unlink } from "node:fs/promises";
5
4
  import path from "node:path";
6
5
  import { pathMatchesPattern } from "../shared/git-progress.js";
7
6
  import { expandWorkspaceStatus, hasGitmodulesFile, synthesizeExpandedPorcelain, } from "../shared/git-workspace-status.js";
8
7
  import { ResilientGitCommandError, runResilientGitCommand, } from "../shared/resilient-git.js";
8
+ import { isAnyTestArtifactPath, isHarnessBaselineForbiddenPattern, } from "../workflows/dag/test-artifact-namespace.js";
9
9
  async function sha256File(filePath) {
10
10
  const hash = createHash("sha256");
11
11
  for await (const chunk of createReadStream(filePath)) {
@@ -149,7 +149,27 @@ export function validateShellWriteGuard(input) {
149
149
  continue;
150
150
  }
151
151
  if (matchesAnyPattern(filePath, forbiddenPaths)) {
152
- violations.push(filePath);
152
+ // D3 test-artifact exception: a changed file inside the canonical
153
+ // `.harness/testcase` namespace that the task's exclusive writeSet
154
+ // explicitly owns is exempt from the BASELINE harness guard patterns
155
+ // (`.harness` / `.harness/**`) only — spec-level validation gates such
156
+ // writeSets behind a frozen dedicated-test layout. Any explicit
157
+ // (deeper or out-of-tree) matching forbidden pattern still wins.
158
+ const baselinePatternHit = forbiddenPaths.some((forbidden) => isHarnessBaselineForbiddenPattern(forbidden) &&
159
+ pathMatchesPattern(filePath, forbidden));
160
+ const explicitPatternHit = forbiddenPaths.some((forbidden) => !isHarnessBaselineForbiddenPattern(forbidden) &&
161
+ pathMatchesPattern(filePath, forbidden));
162
+ const ownedByWriteSet = writePolicy === "exclusive" &&
163
+ writeSet.length > 0 &&
164
+ matchesAnyPattern(filePath, writeSet);
165
+ if (explicitPatternHit ||
166
+ !baselinePatternHit ||
167
+ !isAnyTestArtifactPath(filePath) ||
168
+ !ownedByWriteSet) {
169
+ violations.push(filePath);
170
+ continue;
171
+ }
172
+ // Exempted: canonical test-artifact file owned by the writeSet.
153
173
  continue;
154
174
  }
155
175
  if (writePolicy === "read-only" || writePolicy === "none") {
@@ -380,37 +400,6 @@ export function deriveSameInstallationGitCandidates(primary) {
380
400
  }
381
401
  return Array.from(new Set([normalized, ...(fallback ? [fallback] : [])]));
382
402
  }
383
- async function resolveGitExecutableCandidates(platform, env) {
384
- if (platform !== "win32")
385
- return ["git"];
386
- const pathValue = env.PATH ?? env.Path ?? "";
387
- for (const rawEntry of pathValue.split(path.delimiter)) {
388
- const entry = rawEntry.trim().replace(/^"|"$/g, "");
389
- if (!entry)
390
- continue;
391
- const candidate = path.win32.join(entry, "git.exe");
392
- try {
393
- await access(candidate, constants.X_OK);
394
- const sameInstallation = deriveSameInstallationGitCandidates(candidate);
395
- const existing = [];
396
- for (const executable of sameInstallation) {
397
- try {
398
- await access(executable, constants.X_OK);
399
- existing.push(executable);
400
- }
401
- catch {
402
- // Optional same-installation fallback is absent.
403
- }
404
- }
405
- if (existing.length > 0)
406
- return existing;
407
- }
408
- catch {
409
- // Keep searching PATH entries.
410
- }
411
- }
412
- return ["git"];
413
- }
414
403
  export async function readGitStatusPorcelain(cwd, options = {}) {
415
404
  let parentPorcelain;
416
405
  try {
@@ -510,38 +499,6 @@ export async function readGitStatusPorcelainWithDependencies(cwd, options, depen
510
499
  attempts: attemptDiagnostics,
511
500
  });
512
501
  }
513
- function readGitStatusPorcelainOnce(cwd, executable) {
514
- return new Promise((resolve, reject) => {
515
- const child = spawn(executable, ["status", "--porcelain=v1", "--untracked-files=all"], {
516
- cwd,
517
- env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
518
- stdio: ["ignore", "pipe", "pipe"],
519
- windowsHide: true,
520
- });
521
- let stdout = "";
522
- let stderr = "";
523
- child.stdout.setEncoding("utf-8");
524
- child.stderr.setEncoding("utf-8");
525
- child.stdout.on("data", (chunk) => {
526
- stdout += chunk;
527
- });
528
- child.stderr.on("data", (chunk) => {
529
- stderr += chunk;
530
- });
531
- child.on("error", reject);
532
- child.on("close", (code, signal) => {
533
- if (code === 0) {
534
- resolve(stdout);
535
- return;
536
- }
537
- const detail = stderr.trim() || stdout.trim() || "no stderr/stdout";
538
- const error = new Error(`git status failed: ${detail}`);
539
- error.exitCode = code;
540
- error.signal = signal;
541
- reject(error);
542
- });
543
- });
544
- }
545
502
  export async function runPostRunWriteGuard(input) {
546
503
  const afterStatus = await readGitStatusPorcelain(input.rootCwd);
547
504
  const beforeSnapshot = snapshotGitStatusPorcelain(input.beforeStatus);
@@ -191,12 +191,21 @@ function extractIndexReferences(indexContent) {
191
191
  // `(${base})` and `` `base` ``. Bare mentions are intentionally NOT
192
192
  // treated as references to avoid false positives from partial matches.
193
193
  const linkMatches = [...line.matchAll(/\[[^\]]*\]\(([^)]+\.md)\)/g)];
194
+ // Link spans are blanked before the backtick scan: the index convention
195
+ // `[`archived.md`](../completed/archived.md)` puts a bare basename in the
196
+ // link TEXT, which is not an entry for this directory. Reading it as one
197
+ // reported every archived link as a stale active entry. Blanking (rather
198
+ // than filtering by content) keeps `[`local.md`](local.md)`, standalone
199
+ // `local.md` mentions, and mixed lines working, because those still
200
+ // contribute through their link target or an untouched backtick span.
201
+ let remainder = line;
194
202
  for (const match of linkMatches) {
195
203
  const reference = match[1] ? localIndexReference(match[1]) : null;
196
204
  if (reference)
197
205
  references.add(reference);
206
+ remainder = remainder.replace(match[0], " ".repeat(match[0].length));
198
207
  }
199
- const backtickMatches = [...line.matchAll(/`([^`]+\.md)`/g)];
208
+ const backtickMatches = [...remainder.matchAll(/`([^`]+\.md)`/g)];
200
209
  for (const match of backtickMatches) {
201
210
  const reference = match[1] ? localIndexReference(match[1]) : null;
202
211
  if (reference)
@@ -1,9 +1,4 @@
1
- import { readFile } from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { harnessManifestSchema } from '../shared/types.js';
1
+ import { loadNormalizedProjectConfig } from './project-config.js';
4
2
  export async function loadHarnessManifest(repoRoot) {
5
- const manifestPath = path.join(repoRoot, 'harness.json');
6
- const raw = await readFile(manifestPath, 'utf-8');
7
- const parsed = JSON.parse(raw);
8
- return harnessManifestSchema.parse(parsed);
3
+ return (await loadNormalizedProjectConfig(repoRoot)).manifest;
9
4
  }
@@ -209,14 +209,18 @@ export const nightSchedulerPolicySchema = z.object({
209
209
  export const workerPolicySchema = z.object({
210
210
  nightScheduler: nightSchedulerPolicySchema.optional().default({}),
211
211
  });
212
- export const CURSOR_HARNESS_EXECUTOR_REMOVED_ERROR = "harness executors.cursor is no longer supported; remove it and use executors.pi only (Cursor is available only via cursor-prompt sidecar)";
213
- export const harnessManifestSchema = z
212
+ export const verificationCommandsSchema = z
214
213
  .object({
214
+ quick: z.array(z.string().trim().min(1)).optional().default([]),
215
+ standard: z.array(z.string().trim().min(1)).optional().default([]),
216
+ full: z.array(z.string().trim().min(1)).optional().default([]),
217
+ })
218
+ .strict();
219
+ export const CURSOR_HARNESS_EXECUTOR_REMOVED_ERROR = "harness executors.cursor is no longer supported; remove it and use executors.pi only (Cursor is available only via cursor-prompt sidecar)";
220
+ const harnessManifestCommonShape = {
215
221
  $schema: z.string().optional(),
216
- version: z.number(),
217
222
  project: z.string(),
218
223
  adapter: z.string().optional(),
219
- governanceRoot: z.string(),
220
224
  features: z
221
225
  .object({
222
226
  goals: z.boolean().optional().default(true),
@@ -225,9 +229,7 @@ export const harnessManifestSchema = z
225
229
  .default({
226
230
  goals: true,
227
231
  }),
228
- entrypoints: z.record(z.string(), z.string().nullable()),
229
- artifacts: z.record(z.string(), z.string().nullable()),
230
- scripts: z.record(z.string(), z.string().nullable()),
232
+ verification: verificationCommandsSchema.optional().default({}),
231
233
  workflowPolicy: workflowPolicySchema,
232
234
  /** Optional; absent means Night Scheduler uses package defaults when enabled by callers. */
233
235
  workerPolicy: workerPolicySchema.optional(),
@@ -237,8 +239,25 @@ export const harnessManifestSchema = z
237
239
  .strict()
238
240
  .optional()
239
241
  .default({}),
240
- })
241
- .strict()
242
+ };
243
+ const harnessManifestV1Schema = z.object({
244
+ ...harnessManifestCommonShape,
245
+ version: z.literal(1),
246
+ governanceRoot: z.string(),
247
+ entrypoints: z.record(z.string(), z.string().nullable()),
248
+ artifacts: z.record(z.string(), z.string().nullable()),
249
+ scripts: z.record(z.string(), z.string().nullable()),
250
+ }).strict();
251
+ const harnessManifestV2Schema = z.object({
252
+ ...harnessManifestCommonShape,
253
+ version: z.literal(2),
254
+ governanceRoot: z.string().optional().default("docs"),
255
+ entrypoints: z.record(z.string(), z.string().nullable()).optional().default({}),
256
+ artifacts: z.record(z.string(), z.string().nullable()).optional().default({}),
257
+ scripts: z.record(z.string(), z.string().nullable()).optional().default({}),
258
+ }).strict();
259
+ export const harnessManifestSchema = z
260
+ .discriminatedUnion("version", [harnessManifestV1Schema, harnessManifestV2Schema])
242
261
  .superRefine((manifest, ctx) => {
243
262
  // Preserve the empty read-only compatibility projection for callers that
244
263
  // inspect parsed manifests, while rejecting this field in JSON input.