@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
@@ -1,13 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
- import { access, copyFile, mkdir, readdir, readFile, rename, rm, rmdir, stat, writeFile, } from "node:fs/promises";
3
- import os from "node:os";
2
+ import { access, copyFile, lstat, mkdir, readdir, readFile, realpath, rename, rm, rmdir, stat, writeFile, } from "node:fs/promises";
4
3
  import path from "node:path";
5
4
  import { isDeepStrictEqual } from "node:util";
6
5
  import { fileURLToPath } from "node:url";
7
6
  import { isInitRuntimeActive, } from "../shared/runtime-activity.js";
8
7
  import { loadHarnessManifest } from "../governance/harness.js";
9
- import { OPENCODE_CONTEXT_OVERFLOW_COMPACT_PLUGIN_PATH, OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH, PI_CONTEXT_OVERFLOW_EXTENSION_PATH, PI_PROJECT_SETTINGS_PATH, applyPiRetryMerge, applyProjectPiSettingsMerge, buildOpenCodeContextOverflowCompactPluginSource, buildProjectPiSettingsSource, buildOpenCodeTransientRetryPluginSource, buildPiContextOverflowExtensionSource, inspectProjectPiSettings, parseClientRecoveryMode, runClientRecovery, } from "./client-recovery.js";
8
+ import { OPENCODE_CONTEXT_OVERFLOW_COMPACT_PLUGIN_PATH, OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH, PI_CONTEXT_OVERFLOW_EXTENSION_PATH, PI_PROJECT_SETTINGS_PATH, applyProjectPiSettingsMerge, buildOpenCodeContextOverflowCompactPluginSource, buildProjectPiSettingsSource, buildOpenCodeTransientRetryPluginSource, buildPiContextOverflowExtensionSource, inspectProjectPiSettings, } from "./client-recovery.js";
10
9
  import { assessInitPiModelReadiness, discoverInitModelCatalog, validateInitModelMatrixAgainstCatalog, } from "./init-model-catalog.js";
10
+ import { auditLegacyInitSurface, inspectContainedRegularFile, legacyAuditReceipt, removeContainedRegularFileWithSha256Cas, } from "./init-legacy-audit.js";
11
11
  const MANAGED_BLOCK_START = "<!-- LOOP_AGENT_INIT_START -->";
12
12
  const MANAGED_BLOCK_END = "<!-- LOOP_AGENT_INIT_END -->";
13
13
  const GITIGNORE_BLOCK_START = "# LOOP_AGENT_INIT_START";
@@ -817,6 +817,18 @@ function mergeRecord(base, patch) {
817
817
  ...patch,
818
818
  };
819
819
  }
820
+ function normalizeLegacyWorkflowPolicy(value) {
821
+ if (!isRecord(value))
822
+ return {};
823
+ const normalized = { ...value };
824
+ if (isRecord(normalized.dag)) {
825
+ const dag = { ...normalized.dag };
826
+ if (dag.defaultEntry === "dag run-task")
827
+ dag.defaultEntry = "task advance";
828
+ normalized.dag = dag;
829
+ }
830
+ return normalized;
831
+ }
820
832
  async function loadManagedAgentsTemplate(assetRoot) {
821
833
  if (managedAgentsTemplateCache !== undefined)
822
834
  return managedAgentsTemplateCache;
@@ -853,8 +865,13 @@ function mergeGitignoreManagedBlock(existing, block) {
853
865
  const start = existing.indexOf(GITIGNORE_BLOCK_START);
854
866
  const end = existing.indexOf(GITIGNORE_BLOCK_END);
855
867
  if (start >= 0 && end > start) {
856
- return (`${existing.slice(0, start).trimEnd()}\n\n${block}\n${existing.slice(end + GITIGNORE_BLOCK_END.length).trimStart()}`.trimEnd() +
857
- "\n");
868
+ return `${[
869
+ existing.slice(0, start).trimEnd(),
870
+ block,
871
+ existing.slice(end + GITIGNORE_BLOCK_END.length).trimStart(),
872
+ ]
873
+ .filter((part) => part.length > 0)
874
+ .join("\n\n")}\n`;
858
875
  }
859
876
  if (!existing.trim())
860
877
  return `${block}\n`;
@@ -867,16 +884,14 @@ function mergeGitignoreManagedBlock(existing, block) {
867
884
  * commit surface (they carry the governance handoff and are not ignored);
868
885
  * `.opencode/` and `.pi/` project adapters are left to the user's discretion.
869
886
  */
870
- export function buildManagedGitignoreBlock(governanceRoot = DEFAULT_GOVERNANCE_ROOT) {
887
+ export function buildManagedGitignoreBlock(_governanceRoot = DEFAULT_GOVERNANCE_ROOT) {
871
888
  return [
872
889
  GITIGNORE_BLOCK_START,
873
- "# loop-agent init surface: rebuildable outputs; AGENTS.md and README.md stay shared",
890
+ "# loop-agent local runtime and machine-specific execution config",
874
891
  ".harness/",
875
- ".agents/",
892
+ ".playwright-cli/",
876
893
  ".task-pool/",
877
894
  ".worktrees/",
878
- `${governanceRoot}/`,
879
- "scripts/",
880
895
  "harness.json",
881
896
  GITIGNORE_BLOCK_END,
882
897
  ].join("\n");
@@ -922,16 +937,8 @@ function buildHarness(input) {
922
937
  delete pi.defaultModel;
923
938
  mergedExecutors.pi = pi;
924
939
  }
925
- const harness = {
926
- ...input.existing,
927
- $schema: expectedHarnessSchemaRef(input.governanceRoot),
928
- version: 1,
929
- project: input.projectName,
930
- adapter: "loop-agent",
931
- governanceRoot: input.governanceRoot,
932
- features: mergeRecord(input.template.features, input.existing.features),
933
- workflowPolicy: mergeRecord(input.template.workflowPolicy, input.existing.workflowPolicy),
934
- entrypoints: mergeRecord(input.existing.entrypoints, {
940
+ const legacyControllerMaps = {
941
+ entrypoints: {
935
942
  readme: "README.md",
936
943
  agents: "AGENTS.md",
937
944
  governanceIndex: `${input.governanceRoot}/README.md`,
@@ -939,16 +946,16 @@ function buildHarness(input) {
939
946
  workflow: `${input.governanceRoot}/feature-workflow.md`,
940
947
  verificationMatrix: `${input.governanceRoot}/verification-matrix.md`,
941
948
  loopAgentHarness: `${input.governanceRoot}/loop-agent-harness.md`,
942
- }),
943
- artifacts: mergeRecord(input.existing.artifacts, {
949
+ },
950
+ artifacts: {
944
951
  templatesDir: `${input.governanceRoot}/templates`,
945
952
  execPlansDir: `${input.governanceRoot}/exec-plans`,
946
953
  progressDir: `${input.governanceRoot}/progress`,
947
954
  reportsDir: `${input.governanceRoot}/reports`,
948
955
  decisionsDir: `${input.governanceRoot}/decisions`,
949
956
  taskPoolDir: ".harness/task-pool",
950
- }),
951
- scripts: mergeRecord(input.existing.scripts, {
957
+ },
958
+ scripts: {
952
959
  checkEngineeringStructure: "scripts/check-engineering-structure.sh",
953
960
  checkDocIndex: "scripts/check-doc-index.sh",
954
961
  checkDocLinks: "scripts/check-doc-links.sh",
@@ -964,7 +971,38 @@ function buildHarness(input) {
964
971
  quickVerify: "bash scripts/check-repo.sh",
965
972
  standardVerify: "bash scripts/ci-governance.sh",
966
973
  fullVerify: "bash scripts/ci.sh",
967
- }),
974
+ },
975
+ };
976
+ const preserveCustomMap = (name) => {
977
+ const current = isRecord(input.existing[name]) ? input.existing[name] : {};
978
+ return Object.fromEntries(Object.entries(current).filter(([key, value]) => legacyControllerMaps[name]?.[key] !== value));
979
+ };
980
+ const harness = {
981
+ version: 2,
982
+ project: input.projectName,
983
+ adapter: "loop-agent",
984
+ ...(isRecord(input.existing.features)
985
+ ? { features: input.existing.features }
986
+ : {}),
987
+ workflowPolicy: mergeRecord(input.template.workflowPolicy, normalizeLegacyWorkflowPolicy(input.existing.workflowPolicy)),
988
+ ...(Object.keys(preserveCustomMap("entrypoints")).length > 0
989
+ ? { entrypoints: preserveCustomMap("entrypoints") }
990
+ : {}),
991
+ ...(Object.keys(preserveCustomMap("artifacts")).length > 0
992
+ ? { artifacts: preserveCustomMap("artifacts") }
993
+ : {}),
994
+ ...(Object.keys(preserveCustomMap("scripts")).length > 0
995
+ ? { scripts: preserveCustomMap("scripts") }
996
+ : {}),
997
+ ...(isRecord(input.existing.verification)
998
+ ? { verification: input.existing.verification }
999
+ : {}),
1000
+ ...(isRecord(input.existing.workerPolicy)
1001
+ ? { workerPolicy: input.existing.workerPolicy }
1002
+ : {}),
1003
+ ...(isRecord(input.existing.worktree)
1004
+ ? { worktree: input.existing.worktree }
1005
+ : {}),
968
1006
  executors: mergedExecutors,
969
1007
  };
970
1008
  // Fresh init owns only the current DAG-facing surface. Legacy step routing is
@@ -1001,6 +1039,16 @@ async function writeText(input) {
1001
1039
  await writeFile(target, input.content, "utf-8");
1002
1040
  input.written.push(input.relativePath);
1003
1041
  }
1042
+ async function writeTextIfChanged(input) {
1043
+ const target = path.join(input.repoRoot, input.relativePath);
1044
+ await mkdir(path.dirname(target), { recursive: true });
1045
+ if ((await exists(target)) && (await readFile(target, "utf-8")) === input.content) {
1046
+ input.skipped.push(input.relativePath);
1047
+ return;
1048
+ }
1049
+ await writeFile(target, input.content, "utf-8");
1050
+ input.written.push(input.relativePath);
1051
+ }
1004
1052
  async function copyFileIfMissing(input) {
1005
1053
  const source = path.join(input.assetRoot, input.sourceRelativePath);
1006
1054
  const target = path.join(input.repoRoot, input.targetRelativePath);
@@ -1202,38 +1250,21 @@ async function readPackageVersion(assetRoot) {
1202
1250
  async function readInitSurfaceManifest(assetRoot) {
1203
1251
  const manifestPath = path.join(assetRoot, "docs", "init-surface.manifest.json");
1204
1252
  const raw = await readFile(manifestPath, "utf-8");
1205
- const manifest = JSON.parse(raw);
1206
- const initFullRequired = manifest.initFullRequired ?? [];
1207
- const initSurface = manifest.initSurface ?? {};
1208
- const seenPaths = new Set();
1209
- const entries = [];
1210
- for (const relativePath of initFullRequired) {
1211
- if (seenPaths.has(relativePath))
1212
- continue;
1213
- seenPaths.add(relativePath);
1214
- entries.push({
1215
- path: relativePath,
1216
- mode: initSurface[relativePath] ?? inferInitSurfaceMode(relativePath),
1217
- });
1253
+ const parsed = JSON.parse(raw);
1254
+ if (parsed.version !== 2 || !isRecord(parsed.initSurface)) {
1255
+ throw new Error("INIT_SURFACE_MANIFEST_INVALID: expected version 2 initSurface contract");
1218
1256
  }
1219
- // Fresh `init --profile full` copies the entire `docs/templates/` and
1220
- // `skills/` directories via copyDirMerge. Every file actually projected to a
1221
- // target project must be represented in the desired init surface so that
1222
- // check-update can detect missing or stale copies during controller upgrades.
1223
- // The static manifest intentionally only pins explicit modes for the subset
1224
- // that needs them; the rest are auto-discovered here as "copied" entries.
1225
- for (const discovered of await discoverCopiedSurfaceFiles(assetRoot)) {
1226
- if (seenPaths.has(discovered))
1227
- continue;
1228
- seenPaths.add(discovered);
1229
- entries.push({
1230
- path: discovered,
1231
- mode: initSurface[discovered] ?? inferInitSurfaceMode(discovered),
1232
- });
1257
+ const entries = [];
1258
+ for (const [entryPath, mode] of Object.entries(parsed.initSurface)) {
1259
+ if (!["generated", "copied", "managed-block", "directory", "state"].includes(String(mode))) {
1260
+ throw new Error(`INIT_SURFACE_MANIFEST_INVALID: unsupported mode for ${entryPath}`);
1261
+ }
1262
+ entries.push({ path: normalizeRelativePath(entryPath), mode: mode });
1233
1263
  }
1234
- if (!entries.some((entry) => entry.path === INIT_SURFACE_STATE_PATH)) {
1235
- entries.push({ path: INIT_SURFACE_STATE_PATH, mode: "state" });
1264
+ if (entries.length === 0 || !entries.some((entry) => entry.path === INIT_SURFACE_STATE_PATH)) {
1265
+ throw new Error("INIT_SURFACE_MANIFEST_INVALID: state entry is required");
1236
1266
  }
1267
+ const manifest = parsed;
1237
1268
  return { manifest, raw, sha256: sha256Text(raw), entries };
1238
1269
  }
1239
1270
  /**
@@ -1295,6 +1326,15 @@ function inferInitSurfaceMode(relativePath) {
1295
1326
  return "copied";
1296
1327
  }
1297
1328
  async function buildDesiredSurfaceContent(input) {
1329
+ if (input.entry.path === ".agents/skills/loop-agent/SKILL.md") {
1330
+ const bridgePath = path.join(input.assetRoot, "resources", "agent-bridge", "loop-agent", "SKILL.md");
1331
+ const fallbackPath = path.join(input.assetRoot, "skills", "loop-agent", "SKILL.md");
1332
+ const sourcePath = (await exists(bridgePath)) ? bridgePath : fallbackPath;
1333
+ return {
1334
+ content: await readFile(sourcePath, "utf-8"),
1335
+ sourcePath,
1336
+ };
1337
+ }
1298
1338
  const manifestPath = targetPathToManifestPath(input.entry.path, input.governanceRoot);
1299
1339
  if (input.entry.mode === "directory" || input.entry.mode === "state")
1300
1340
  return {};
@@ -1399,7 +1439,9 @@ async function readExistingSurfaceState(repoRoot) {
1399
1439
  if (!(await exists(filePath)))
1400
1440
  return undefined;
1401
1441
  const parsed = JSON.parse(await readFile(filePath, "utf-8"));
1402
- if (!isRecord(parsed) || parsed.schemaVersion !== 1)
1442
+ if (!isRecord(parsed) ||
1443
+ (parsed.schemaVersion !== 1 && parsed.schemaVersion !== 2) ||
1444
+ !isRecord(parsed.files))
1403
1445
  return undefined;
1404
1446
  return parsed;
1405
1447
  }
@@ -1634,21 +1676,37 @@ async function buildCurrentSurfaceState(input) {
1634
1676
  };
1635
1677
  }
1636
1678
  return {
1637
- schemaVersion: 1,
1679
+ schemaVersion: 2,
1638
1680
  controllerVersion,
1639
1681
  stateKind: input.stateKind,
1640
1682
  generatedAt: input.generatedAt ?? new Date().toISOString(),
1641
1683
  manifestSha256: manifest.sha256,
1642
1684
  files,
1685
+ dispositions: [],
1686
+ actionReceipts: [],
1643
1687
  };
1644
1688
  }
1645
1689
  async function writeInitSurfaceState(input) {
1646
1690
  const state = await buildCurrentSurfaceState(input);
1647
- if (input.stateKind === "recorded" && input.preserveOwnershipFrom) {
1691
+ state.dispositions = [
1692
+ ...(input.preserveOwnershipFrom?.dispositions ?? []),
1693
+ ...(input.dispositions ?? []),
1694
+ ];
1695
+ state.actionReceipts = [
1696
+ ...(input.preserveOwnershipFrom?.actionReceipts ?? []),
1697
+ ...(input.actionReceipts ?? []),
1698
+ ];
1699
+ state.legacyAudit = input.legacyAudit ?? input.preserveOwnershipFrom?.legacyAudit;
1700
+ if (input.preserveOwnershipFrom) {
1648
1701
  for (const [pathName, file] of Object.entries(state.files)) {
1649
1702
  if (file.relationship !== "local-existing-unknown")
1650
1703
  continue;
1651
1704
  const priorFile = input.preserveOwnershipFrom.files[pathName];
1705
+ if (priorFile?.acceptedMerge) {
1706
+ file.acceptedMerge = { ...priorFile.acceptedMerge };
1707
+ }
1708
+ if (input.stateKind !== "recorded")
1709
+ continue;
1652
1710
  const priorHash = priorFile?.currentSha256;
1653
1711
  if (priorHash !== undefined) {
1654
1712
  file.currentSha256 = priorHash;
@@ -1656,10 +1714,9 @@ async function writeInitSurfaceState(input) {
1656
1714
  else {
1657
1715
  delete file.currentSha256;
1658
1716
  }
1659
- if (priorFile?.acceptedMerge) {
1660
- file.acceptedMerge = { ...priorFile.acceptedMerge };
1661
- }
1662
1717
  }
1718
+ }
1719
+ if (input.stateKind === "recorded" && input.preserveOwnershipFrom) {
1663
1720
  for (const [pathName, priorFile] of Object.entries(input.preserveOwnershipFrom.files)) {
1664
1721
  if (state.files[pathName] || priorFile.mode === "state")
1665
1722
  continue;
@@ -1684,6 +1741,13 @@ async function writeInitSurfaceState(input) {
1684
1741
  }
1685
1742
  const target = path.join(input.repoRoot, INIT_SURFACE_STATE_PATH);
1686
1743
  await mkdir(path.dirname(target), { recursive: true });
1744
+ const previous = await readExistingSurfaceState(input.repoRoot);
1745
+ if (previous?.schemaVersion === 2) {
1746
+ const previousComparable = { ...previous, generatedAt: "" };
1747
+ const nextComparable = { ...state, generatedAt: "" };
1748
+ if (JSON.stringify(previousComparable) === JSON.stringify(nextComparable))
1749
+ return previous;
1750
+ }
1687
1751
  await writeFile(target, `${JSON.stringify(state, null, 2)}\n`, "utf-8");
1688
1752
  return state;
1689
1753
  }
@@ -1902,7 +1966,10 @@ async function collectRetiredLayoutActions(input) {
1902
1966
  const retiredOwnedPaths = [];
1903
1967
  const currentPaths = new Set(Object.keys(input.currentState.files));
1904
1968
  const recordedPaths = new Set(Object.keys(input.recordedState.files));
1969
+ const disposedPaths = new Set((input.recordedState.dispositions ?? []).map((receipt) => receipt.path));
1905
1970
  for (const [oldPath, oldState] of Object.entries(input.recordedState.files)) {
1971
+ if (disposedPaths.has(oldPath))
1972
+ continue;
1906
1973
  const targetPath = normalizeRelativePath(manifestPathToTargetPath(oldPath, input.governanceRoot));
1907
1974
  if (targetPath === oldPath || !currentPaths.has(targetPath))
1908
1975
  continue;
@@ -1968,6 +2035,8 @@ async function collectRetiredLayoutActions(input) {
1968
2035
  for (const legacyRoot of ["docs", "skills"]) {
1969
2036
  for (const relativePath of await listRelativeFiles(path.join(input.repoRoot, legacyRoot))) {
1970
2037
  const oldPath = `${legacyRoot}/${relativePath}`;
2038
+ if (disposedPaths.has(oldPath))
2039
+ continue;
1971
2040
  if (recordedPaths.has(oldPath))
1972
2041
  continue;
1973
2042
  const targetPath = normalizeRelativePath(manifestPathToTargetPath(oldPath, input.governanceRoot));
@@ -2522,84 +2591,45 @@ function buildGovernanceDirectoryReadme(input) {
2522
2591
  }
2523
2592
  export function buildInitInstructions(input) {
2524
2593
  const projectName = input.projectName ?? path.basename(path.resolve(input.repoRoot));
2525
- const governanceRoot = input.governanceRoot ?? DEFAULT_GOVERNANCE_ROOT;
2526
2594
  return [
2527
2595
  "# loop-agent Initialization Instructions",
2528
2596
  "",
2529
- `Initialize the target repository as a loop-agent harness project: \`${input.repoRoot}\`.`,
2597
+ `Initialize the target repository as a minimal loop-agent project: \`${input.repoRoot}\`.`,
2530
2598
  "",
2531
- "## Confirm With User",
2599
+ "## Project Inputs",
2532
2600
  "",
2533
2601
  `- Project name: default \`${projectName}\`.`,
2534
- `- Governance root: default \`${governanceRoot}\`.`,
2535
2602
  "- Discover a credential-safe Pi/OpenCode model catalog with `loop-agent init model-catalog --repo-root <target> --json`. The command exposes only allowlisted model metadata, Pi setup status, and credential-readiness booleans, never raw config or credentials.",
2536
2603
  "- Preserve every existing LOW/MED/HIGH reference that still resolves in Pi. Select only missing or removed tiers from `eligibleForHarness=true`, then apply the complete matrix with `--low-model`, `--med-model`, and `--high-model`; update code keeps valid tiers unchanged.",
2537
- "- If `piSetup.status` is `model-definition-required`, ask only for the missing provider/model id, API/base URL, context window, maximum output, inputs/reasoning, cost constraints, and credential method. Help merge Pi models.json without exposing credential values, rerun the catalog, and only then initialize. If definitions exist but credentials are missing, configure auth instead of rewriting harness tiers.",
2538
- "- Default: merge existing AGENTS.md, harness.json, and loop-agent governance docs instead of overwriting user content.",
2604
+ "- Ask only when credentials, cost/compliance constraints, a bridge-path conflict, or an ownership decision cannot be inferred safely.",
2539
2605
  "",
2540
2606
  "## Initialization Upgrade Routing",
2541
2607
  "",
2542
2608
  "When the user says `loop-agent初始化更新`, `loop-agent 初始化更新`, `更新 loop-agent 初始化内容`, or an equivalent write request, run `loop-agent init upgrade --repo-root . --json` as the single controller-owned entry. Complete returned single-file allowedPaths merge tasks and call `--continue` until a stable terminal result; do not stop at check-update, needs-safe-update, needs-model-merge, or verification-pending.",
2543
2609
  "Explicit `检查初始化更新`, `初始化更新校验`, or `只检查,不要修改` stays strictly read-only: run only `loop-agent init check-update --repo-root . --markdown` and do not create an upgrade run.",
2544
- "Upgrade manages project `.opencode/plugins/`, `.pi/extensions/`, and `.pi/settings.json` by nested merge. Pi must trust the project before loading these settings; do not read or write `~/.pi/agent/settings.json` by default.",
2545
- "The upgrade also writes a read-only gitignore migration assessment (`.harness/init-upgrades/<run-id>/gitignore-migration.json`): tracked `.harness/**` and `.agents/**` get index-only `git rm -r --cached --ignore-unmatch` guidance (working-tree files are kept). Pause for human review when `.agents` content is tracked or when staged changes / git query failures block safe guidance; never run git rm yourself.",
2610
+ "The upgrade removes only controller-owned bytes that still match their recorded hash. Modified or unproven legacy files are preserved for an explicit bounded decision.",
2611
+ "Do not change the Git index implicitly. Follow only the path-specific migration guidance returned by the controller.",
2612
+ "",
2613
+ "## Minimal Managed Surface",
2546
2614
  "",
2547
- "## Apply Defaults",
2615
+ "Fresh init manages only `.gitignore`, `harness.json`, `.agents/skills/loop-agent/SKILL.md`, and `.harness/init-surface.json`.",
2616
+ "Do not create or modify root `README.md`, `AGENTS.md`, project `docs/`, project `scripts/`, client adapters, or mirrored built-in Skills.",
2617
+ "General governance, templates, checks, and built-in Skills come from the installed loop-agent package. Project facts continue to come from the repository's own files.",
2618
+ "The bridge Skill is the only default repo-local integration for external Agents. It routes them to `loop-agent --repo-root . operator guide --json`; it is not a copy of the controller policy.",
2548
2619
  "",
2549
- '- Use the current loop-agent harness.json as the default template, but write target project name plus `adapter: "loop-agent"` and a Pi-only `executors.pi.LOW/MED/HIGH` matrix. Do not write `defaultModel` when all three tiers exist, and never project `model`, `models`, `modelProfiles`, `modelRouting`, `verify`, or `sequentialWorkflowRole`.',
2620
+ "## Verification And DAG",
2621
+ "",
2622
+ "Project verification comes from `.loop-agent/project.json`, additive local `harness.json` commands, or bounded capability scanning. Never assume deleted projected scripts still exist.",
2550
2623
  `- ${DAG_HARD_GATE_TRIGGER}`,
2551
- "- Generate the target project's loop-agent script matrix from templates: structure check, docs index/link checks, active plan status, exec-plan index sync, harness runtime cleanliness, architecture boundaries, skill entry integrity, governance CI, project-test CI, and full CI.",
2552
- "- Copy or project only stack-agnostic governance scripts. For project-specific verification, packaging, release, or maintenance commands, generate the target-project version from templates plus the target repository's actual files instead of copying loop-agent's own TypeScript-specific scripts.",
2553
- "- Do not assume the target project is TypeScript, Node.js, frontend, backend, or any other specific stack.",
2554
- "- `scripts/ci-tests.sh` must be language-neutral: detect common project verification entrypoints conservatively, run only commands that exist, and clearly report when project-specific verification needs model/user adaptation.",
2555
- "- Default verification entries: quick `bash scripts/check-repo.sh`, standard `bash scripts/ci-governance.sh`, full `bash scripts/ci.sh`.",
2556
- `- Generate target-project versions of \`${governanceRoot}/README.md\`, \`${governanceRoot}/development-principles.md\`, \`${governanceRoot}/architecture/runtime-boundaries.md\`, \`${governanceRoot}/feature-workflow.md\`, \`${governanceRoot}/verification-matrix.md\`, and \`${governanceRoot}/loop-agent-harness.md\`; copy only generic methodology/template material as-is.`,
2557
- "- After deterministic initialization, inspect README/config/build files (for example package.json, pyproject.toml, go.mod, Cargo.toml, pom.xml, Gradle files, Makefile, .sln/.csproj, or project-specific scripts) and adapt `scripts/ci-tests.sh` plus the verification matrix to the real project.",
2558
- "- Enrich the root `README.md`: keep the deterministic project title and the loop-agent managed block intact, and fill the human-authored sections (项目概览, 技术栈与目录结构, 开发与验证) from the target project's actual files. The root README must serve both as a human-first project entry and as an agent work entry; replace the initialization-model supplement comments when the project files provide the information.",
2559
- `- Populate \`${governanceRoot}/verification-matrix.md\` with the target project's actual quick, standard, and full verification commands derived from its real language and toolchain, keeping the governance rows intact.`,
2560
- "- Project repo-local skills live in `.agents/skills/`. Do not create a root `skills/` directory in the target project; the package's bundled `skills/` remains the built-in fallback.",
2561
- "- Merge a loop-agent managed block into `.gitignore` that ignores the full rebuildable init surface (`.harness/`, `.agents/`, `.task-pool/`, `.worktrees/`, `ai_workspace/loop-agent/`, `scripts/`, `harness.json`); keep `AGENTS.md` and `README.md` on the shared commit surface and keep all user rules outside the block untouched.",
2562
- "- Do not copy examples by default; examples stay bundled in the tool and are available through `loop-agent examples`.",
2563
- "- Add or update a loop-agent managed block in AGENTS.md.",
2564
- "- The generated AGENTS.md must include documentation convergence and structured DAG write-boundary rules so target projects keep the same working discipline as this repository.",
2565
- "- The generated target docs must surface the production readiness checklist and the operator recovery path: `dag report`, `dag doctor --run-id --markdown`, and failed-run `dag closeout-draft` failure handoff.",
2566
- "- Create `.harness/` runtime directories but do not create historical run facts.",
2567
- "",
2568
- "## Automatic Workflow For Model-Executed Init",
2569
- "",
2570
- "When a model/agent is asked to initialize a target project, treat initialization as one automated workflow. Do not run deterministic init and then wait for another user turn to fill project-specific content.",
2571
- "",
2572
- "1. Run `init model-catalog` and inspect `piSetup`. When Pi has no usable model setup, collect only the missing provider/model metadata or credential method, help configure Pi, and rerun the catalog before writing harness.json.",
2573
- "2. Inspect the target repository. Preserve existing tiers that still resolve in Pi and select only missing/removed tiers from credential-ready Pi-compatible entries. OpenCode-only entries are context, not proof that the Pi executor can use them.",
2574
- "3. Resolve with the user only inputs that remain genuinely unsafe to assume: material credential/cost/compliance choices, non-default governance root, overwrite policy, or deployment side effects.",
2575
- "4. Run deterministic init with all three tier flags and merge defaults.",
2576
- "5. Continue immediately into project inspection and adaptation.",
2577
- "6. Finish by running the init health checks and quick verification; `init doctor` must confirm the Pi model matrix before any DAG is started.",
2578
- "",
2579
- "A model reference that remains in Pi is project-owned and must not be reset merely because init templates changed. If its credentials are missing, preserve the tier and repair authentication. If a reference has left Pi, replace only that tier. Ask the user only when required model metadata, credentials, cost, compliance, or availability cannot be inferred. An ambiguous old model configuration stays in a bounded harness.json model merge/human decision; never silently reinterpret it.",
2580
- "",
2581
- "## Required Model Adaptation",
2582
- "",
2583
- "Do not stop after the deterministic `loop-agent init` command. When these instructions are executed by an agent/model, the initialization is not complete until the target project has been inspected and the generated scaffolding has been adapted.",
2584
- "",
2585
- "Required post-init steps:",
2586
- "",
2587
- "1. Read the target project's existing README, manifest/build/config files, and top-level source/module directories.",
2588
- "2. Replace the root README's `初始化模型补充` comments when the information can be inferred from files. At minimum, fill project overview, technology stack/directory structure, and development/verification commands.",
2589
- `3. Update \`${governanceRoot}/verification-matrix.md\` with the target project's actual quick, standard, and full verification commands.`,
2590
- `4. Confirm the generated docs expose \`${governanceRoot}/templates/production-readiness-checklist.md\` plus the operator recovery path: \`dag report\`, \`dag doctor --run-id --markdown\`, and failed-run \`dag closeout-draft\` failure handoff.`,
2591
- "5. Update `scripts/ci-tests.sh` only when the conservative generated detector is insufficient for the target project.",
2592
- "6. Run `loop-agent init doctor`, `loop-agent inspect`, `loop-agent docs audit`, and the quick verification command.",
2593
- "",
2594
- "Completion rule: if target files provide enough evidence, the final README should not leave generic `初始化模型补充` comments in the sections that can be filled. If a section truly cannot be inferred, write a short explicit note such as `尚未从仓库文件中识别到 ...` instead of asking the user to fill it later.",
2624
+ "- A real DAG must be owned by Console operation or another durable foreground controller. Do not launch a long-running DAG from a transient Agent shell.",
2625
+ "- Run `loop-agent init doctor`, `loop-agent --repo-root . operator guide --json`, and the selected project verification before starting new governed work.",
2595
2626
  "",
2596
2627
  "## Suggested Command",
2597
2628
  "",
2598
2629
  "```bash",
2599
- `loop-agent init --repo-root ${input.repoRoot} --profile full --merge`,
2630
+ `loop-agent init --repo-root ${input.repoRoot} --merge`,
2600
2631
  "loop-agent init doctor --repo-root <target>",
2601
- "loop-agent inspect --repo-root <target>",
2602
- "loop-agent docs audit --repo-root <target>",
2632
+ "loop-agent --repo-root <target> operator guide --json",
2603
2633
  "```",
2604
2634
  ].join("\n");
2605
2635
  }
@@ -2607,8 +2637,6 @@ export async function initializeLoopAgentProject(options) {
2607
2637
  const repoRoot = path.resolve(options.repoRoot);
2608
2638
  const projectName = options.projectName ?? path.basename(repoRoot);
2609
2639
  const governanceRoot = options.governanceRoot ?? DEFAULT_GOVERNANCE_ROOT;
2610
- const profile = options.profile ?? "full";
2611
- const merge = options.merge ?? true;
2612
2640
  const model = normalizeInitModelReference(options);
2613
2641
  const modelMatrix = options.modelMatrix;
2614
2642
  const written = [];
@@ -2626,21 +2654,6 @@ export async function initializeLoopAgentProject(options) {
2626
2654
  }
2627
2655
  }
2628
2656
  await mkdir(repoRoot, { recursive: true });
2629
- const readmePath = path.join(repoRoot, "README.md");
2630
- const existingReadme = (await exists(readmePath))
2631
- ? await readFile(readmePath, "utf-8")
2632
- : undefined;
2633
- const readmeContent = existingReadme
2634
- ? mergeManagedBlock(existingReadme, buildManagedReadmeBlock({ projectName, governanceRoot }))
2635
- : buildTargetReadme({ projectName, governanceRoot });
2636
- await writeText({
2637
- repoRoot,
2638
- relativePath: "README.md",
2639
- content: readmeContent,
2640
- merge: true,
2641
- written,
2642
- skipped,
2643
- });
2644
2657
  const harness = buildHarness({
2645
2658
  existing: existingHarness,
2646
2659
  template,
@@ -2649,27 +2662,10 @@ export async function initializeLoopAgentProject(options) {
2649
2662
  model,
2650
2663
  modelMatrix,
2651
2664
  });
2652
- await writeText({
2665
+ await writeTextIfChanged({
2653
2666
  repoRoot,
2654
2667
  relativePath: "harness.json",
2655
2668
  content: `${JSON.stringify(harness, null, 2)}\n`,
2656
- merge: true,
2657
- written,
2658
- skipped,
2659
- });
2660
- const agentsPath = path.join(repoRoot, "AGENTS.md");
2661
- const existingAgents = (await exists(agentsPath))
2662
- ? await readFile(agentsPath, "utf-8")
2663
- : `# AGENTS.md\n`;
2664
- await writeText({
2665
- repoRoot,
2666
- relativePath: "AGENTS.md",
2667
- content: mergeManagedBlock(existingAgents, await buildManagedAgentsBlock({
2668
- assetRoot,
2669
- projectName,
2670
- governanceRoot,
2671
- })),
2672
- merge: true,
2673
2669
  written,
2674
2670
  skipped,
2675
2671
  });
@@ -2677,122 +2673,45 @@ export async function initializeLoopAgentProject(options) {
2677
2673
  const existingGitignore = (await exists(gitignorePath))
2678
2674
  ? await readFile(gitignorePath, "utf-8")
2679
2675
  : "";
2680
- await writeText({
2676
+ await writeTextIfChanged({
2681
2677
  repoRoot,
2682
2678
  relativePath: ".gitignore",
2683
2679
  content: mergeGitignoreManagedBlock(existingGitignore, buildManagedGitignoreBlock(governanceRoot)),
2684
- merge: true,
2685
2680
  written,
2686
2681
  skipped,
2687
2682
  });
2688
- for (const doc of CORE_DOC_FILES) {
2689
- const generated = buildGeneratedCoreDoc({
2690
- doc: doc.target,
2691
- projectName,
2692
- governanceRoot,
2693
- });
2694
- if (generated) {
2695
- await writeTextIfMissing({
2696
- repoRoot,
2697
- relativePath: path.join(governanceRoot, doc.target),
2698
- content: generated,
2699
- merge,
2700
- written,
2701
- skipped,
2702
- });
2703
- continue;
2704
- }
2705
- await copyFileIfMissing({
2706
- assetRoot,
2707
- repoRoot,
2708
- sourceRelativePath: path.join("docs", doc.source),
2709
- targetRelativePath: path.join(governanceRoot, doc.target),
2710
- merge,
2711
- written,
2712
- skipped,
2713
- });
2714
- }
2715
- for (const dir of GOVERNANCE_README_DIRS) {
2716
- await writeTextIfMissing({
2717
- repoRoot,
2718
- relativePath: path.join(governanceRoot, dir, "README.md"),
2719
- content: buildGovernanceDirectoryReadme({ dir, projectName }),
2720
- merge,
2721
- written,
2722
- skipped,
2723
- });
2724
- }
2725
- await copyDirMerge({
2683
+ const bridgeEntry = {
2684
+ path: ".agents/skills/loop-agent/SKILL.md",
2685
+ mode: "copied",
2686
+ };
2687
+ const bridge = await buildDesiredSurfaceContent({
2726
2688
  assetRoot,
2727
2689
  repoRoot,
2728
- sourceRelativePath: "docs/templates",
2729
- targetRelativePath: path.join(governanceRoot, "templates"),
2730
- written,
2731
- });
2732
- await writeTextIfMissing({
2733
- repoRoot,
2734
- relativePath: path.join(governanceRoot, "spec-registry.json"),
2735
- content: buildSpecRegistry(),
2736
- merge,
2737
- written,
2738
- skipped,
2690
+ projectName,
2691
+ governanceRoot,
2692
+ entry: bridgeEntry,
2739
2693
  });
2740
- if (profile === "full") {
2741
- // Project repo-local skills live only under `.agents/skills/`.
2742
- // The package still ships `skills/` as the built-in fallback source.
2743
- await copyDirMerge({
2744
- assetRoot,
2745
- repoRoot,
2746
- sourceRelativePath: "skills",
2747
- targetRelativePath: ".agents/skills",
2748
- written,
2749
- });
2694
+ if (bridge.content === undefined) {
2695
+ throw new Error("AGENT_BRIDGE_RESOURCE_MISSING: bundled loop-agent bridge is unavailable");
2696
+ }
2697
+ const bridgeTarget = path.join(repoRoot, bridgeEntry.path);
2698
+ if (await exists(bridgeTarget)) {
2699
+ const existingBridge = await readFile(bridgeTarget, "utf-8");
2700
+ if (existingBridge !== bridge.content) {
2701
+ throw new Error("BRIDGE_PATH_CONFLICT: .agents/skills/loop-agent/SKILL.md already contains user or legacy content; run init upgrade to resolve it explicitly");
2702
+ }
2703
+ skipped.push(bridgeEntry.path);
2750
2704
  }
2751
2705
  else {
2752
- skipped.push(".agents/skills/");
2753
- }
2754
- skipped.push("examples/");
2755
- for (const [relativePath, content] of Object.entries(buildInitScriptFiles(governanceRoot))) {
2756
- await writeText({
2706
+ await writeTextIfChanged({
2757
2707
  repoRoot,
2758
- relativePath,
2759
- content,
2760
- merge: true,
2708
+ relativePath: bridgeEntry.path,
2709
+ content: bridge.content,
2761
2710
  written,
2762
2711
  skipped,
2763
2712
  });
2764
2713
  }
2765
- await ensureHarnessDirs(repoRoot, written);
2766
- await writeCompatPrompts({ repoRoot, merge, written, skipped });
2767
- const clientRecoveryMode = options.clientRecovery ?? "auto";
2768
- const clientRecovery = clientRecoveryMode === "off"
2769
- ? undefined
2770
- : await runClientRecovery({
2771
- repoRoot,
2772
- mode: clientRecoveryMode,
2773
- });
2774
- const clientRecoveryPaths = [
2775
- OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH,
2776
- OPENCODE_CONTEXT_OVERFLOW_COMPACT_PLUGIN_PATH,
2777
- PI_CONTEXT_OVERFLOW_EXTENSION_PATH,
2778
- PI_PROJECT_SETTINGS_PATH,
2779
- ];
2780
- if (clientRecoveryMode === "off") {
2781
- for (const recoveryPath of clientRecoveryPaths)
2782
- skipped.push(recoveryPath);
2783
- }
2784
- else if (clientRecovery) {
2785
- const installResults = [
2786
- clientRecovery.plugin,
2787
- clientRecovery.overflowPlugin,
2788
- clientRecovery.piExtension,
2789
- { written: clientRecovery.pi?.wrote === true },
2790
- ];
2791
- for (const [index, recoveryPath] of clientRecoveryPaths.entries()) {
2792
- if (installResults[index]?.written)
2793
- written.push(recoveryPath);
2794
- }
2795
- }
2714
+ const stateBefore = await fileSha256IfExists(path.join(repoRoot, INIT_SURFACE_STATE_PATH));
2796
2715
  await writeInitSurfaceState({
2797
2716
  repoRoot,
2798
2717
  projectName,
@@ -2800,15 +2719,18 @@ export async function initializeLoopAgentProject(options) {
2800
2719
  stateKind: "recorded",
2801
2720
  acceptCurrentHarnessAsMerge: model !== undefined || modelMatrix !== undefined,
2802
2721
  });
2803
- written.push(INIT_SURFACE_STATE_PATH);
2722
+ const stateAfter = await fileSha256IfExists(path.join(repoRoot, INIT_SURFACE_STATE_PATH));
2723
+ if (stateBefore !== stateAfter)
2724
+ written.push(INIT_SURFACE_STATE_PATH);
2725
+ else
2726
+ skipped.push(INIT_SURFACE_STATE_PATH);
2804
2727
  return {
2805
2728
  repoRoot,
2806
2729
  projectName,
2807
2730
  governanceRoot,
2808
- profile,
2731
+ profile: "minimal",
2809
2732
  written,
2810
2733
  skipped,
2811
- clientRecovery,
2812
2734
  };
2813
2735
  }
2814
2736
  function actionForMissing(pathName, state) {
@@ -2877,13 +2799,17 @@ function recommendedNextFor(report) {
2877
2799
  next.push("loop-agent init update --repo-root <target> --bootstrap-surface");
2878
2800
  }
2879
2801
  if (report.deterministicActions.some((action) => action.type !== "bootstrap-surface")) {
2880
- next.push("loop-agent init update --repo-root <target> --apply-safe");
2802
+ next.push(report.legacyAudit
2803
+ ? "loop-agent init update --repo-root <target> --apply-safe --legacy-audit"
2804
+ : "loop-agent init update --repo-root <target> --apply-safe");
2881
2805
  }
2882
2806
  if (report.modelMergeTasks.length > 0) {
2883
2807
  next.push("loop-agent init check-update --repo-root <target> --markdown");
2884
2808
  }
2885
2809
  if (report.humanDecisions.length > 0) {
2886
- next.push("Review humanDecisions before applying semantic or policy-sensitive changes.");
2810
+ next.push(report.humanDecisions.some((decision) => decision.candidateId)
2811
+ ? "Invoke /skill:init-update to review hash-bound legacy candidates."
2812
+ : "Review humanDecisions before applying semantic or policy-sensitive changes.");
2887
2813
  }
2888
2814
  if (next.length === 0)
2889
2815
  next.push("No init update actions are needed.");
@@ -2905,9 +2831,8 @@ export async function checkInitUpdate(input) {
2905
2831
  projectName: input.projectName,
2906
2832
  governanceRoot: input.governanceRoot,
2907
2833
  });
2908
- const clientRecoveryMode = input.clientRecovery ?? "auto";
2909
- const skipClientRecoverySurfaces = clientRecoveryMode === "off";
2910
2834
  const recordedState = await readExistingSurfaceState(repoRoot);
2835
+ const disposedPaths = new Set((recordedState?.dispositions ?? []).map((receipt) => receipt.path));
2911
2836
  const assetRoot = await findPackageRoot();
2912
2837
  const surfaceState = recordedState?.stateKind ?? "missing";
2913
2838
  const currentState = await buildCurrentSurfaceState({
@@ -2946,10 +2871,6 @@ export async function checkInitUpdate(input) {
2946
2871
  });
2947
2872
  }
2948
2873
  for (const [pathName, state] of Object.entries(currentState.files)) {
2949
- // Mirror fresh init: client-recovery=off never reinstalls recovery surfaces.
2950
- if (skipClientRecoverySurfaces && isClientRecoverySurfacePath(pathName)) {
2951
- continue;
2952
- }
2953
2874
  // `.pi/settings.json` is a structured project configuration: fill only
2954
2875
  // absent nested recovery fields and preserve every explicit project value.
2955
2876
  if (pathName === PI_PROJECT_SETTINGS_PATH &&
@@ -2984,6 +2905,30 @@ export async function checkInitUpdate(input) {
2984
2905
  continue;
2985
2906
  }
2986
2907
  if (state.relationship === "local-existing-unknown") {
2908
+ if (pathName === ".agents/skills/loop-agent/SKILL.md") {
2909
+ const recordedBridge = recordedState?.files[pathName];
2910
+ const decisionOwnedBridge = recordedState?.actionReceipts?.some((receipt) => receipt.type === "replace-owned-file" &&
2911
+ receipt.path === pathName &&
2912
+ receipt.afterSha256 === state.currentSha256);
2913
+ if ((recordedState?.stateKind === "recorded" &&
2914
+ recordedBridge?.currentSha256 === state.currentSha256) ||
2915
+ decisionOwnedBridge) {
2916
+ deterministicActions.push({
2917
+ type: "replace-owned-file",
2918
+ path: pathName,
2919
+ expectedSha256: state.currentSha256,
2920
+ mode: state.mode,
2921
+ reason: "replace the unchanged controller-owned loop-agent Skill with the current external Agent bridge",
2922
+ });
2923
+ }
2924
+ else {
2925
+ humanDecisions.push({
2926
+ path: pathName,
2927
+ reason: "bridge-path-conflict: existing loop-agent Skill is not proven controller-owned and unchanged",
2928
+ });
2929
+ }
2930
+ continue;
2931
+ }
2987
2932
  const acceptedMerge = recordedState?.files[pathName]?.acceptedMerge;
2988
2933
  if (acceptedMerge) {
2989
2934
  // Generated acceptance is bound to the stable source anchor so that
@@ -3038,11 +2983,29 @@ export async function checkInitUpdate(input) {
3038
2983
  if (recordedState) {
3039
2984
  for (const [pathName, recordedFile] of Object.entries(recordedState.files)) {
3040
2985
  if (currentState.files[pathName] ||
2986
+ disposedPaths.has(pathName) ||
3041
2987
  retiredActionPaths.has(pathName) ||
3042
2988
  recordedFile.mode === "state" ||
3043
2989
  recordedFile.mode === "directory") {
3044
2990
  continue;
3045
2991
  }
2992
+ if (recordedFile.mode === "managed-block") {
2993
+ const target = path.join(repoRoot, pathName);
2994
+ if (!(await exists(target)))
2995
+ continue;
2996
+ const content = await readFile(target, "utf-8");
2997
+ const block = pathName === ".gitignore"
2998
+ ? extractGitignoreManagedBlock(content)
2999
+ : extractManagedBlock(content);
3000
+ if (block) {
3001
+ deterministicActions.push({
3002
+ type: "strip-managed-block",
3003
+ path: pathName,
3004
+ reason: "remove the retired loop-agent managed block while preserving surrounding project content",
3005
+ });
3006
+ }
3007
+ continue;
3008
+ }
3046
3009
  const currentSha256 = await fileSha256IfExists(path.join(repoRoot, pathName));
3047
3010
  if (!currentSha256)
3048
3011
  continue;
@@ -3073,15 +3036,6 @@ export async function checkInitUpdate(input) {
3073
3036
  const harness = JSON.parse(await readFile(harnessPath, "utf-8"));
3074
3037
  if (isRecord(harness))
3075
3038
  currentHarness = harness;
3076
- if (recordedState &&
3077
- isRecord(harness) &&
3078
- harness.$schema !== expectedHarnessSchemaRef(governanceRoot)) {
3079
- deterministicActions.push({
3080
- type: "add-harness-schema-ref",
3081
- path: "harness.json",
3082
- reason: "harness.json is missing or has a stale IDE JSON Schema reference",
3083
- });
3084
- }
3085
3039
  if (recordedState &&
3086
3040
  isRecord(harness) &&
3087
3041
  governanceRoot !== "docs" &&
@@ -3121,7 +3075,6 @@ export async function checkInitUpdate(input) {
3121
3075
  mergeTask.verification = [
3122
3076
  "loop-agent init check-update --repo-root . --json",
3123
3077
  "loop-agent inspect --repo-root .",
3124
- "bash scripts/check-repo.sh",
3125
3078
  ];
3126
3079
  const existingTask = modelMergeTasks.findIndex((task) => task.path === "harness.json");
3127
3080
  if (existingTask >= 0)
@@ -3141,8 +3094,13 @@ export async function checkInitUpdate(input) {
3141
3094
  entry: { path: "harness.json", mode: "generated" },
3142
3095
  });
3143
3096
  if (desired.content !== undefined) {
3144
- const desiredHarness = JSON.parse(desired.content);
3145
- const reconciled = reconcileHarnessStructure(harness, desiredHarness);
3097
+ const template = await readJsonIfExists(path.join(assetRoot, "harness.json"));
3098
+ const reconciled = buildHarness({
3099
+ existing: harness,
3100
+ template,
3101
+ projectName,
3102
+ governanceRoot,
3103
+ });
3146
3104
  if (!isDeepStrictEqual(reconciled, harness)) {
3147
3105
  deterministicActions.push({
3148
3106
  type: "reconcile-harness-structure",
@@ -3170,6 +3128,7 @@ export async function checkInitUpdate(input) {
3170
3128
  modelReadiness?.status === "reselection-required") {
3171
3129
  const state = currentState.files["harness.json"];
3172
3130
  const task = modelMergeTaskFor("harness.json", state, allPaths, recordedState);
3131
+ task.purpose = "pi-model-matrix-selection";
3173
3132
  task.reason =
3174
3133
  modelReadiness.status === "selection-required"
3175
3134
  ? "harness.json has no complete Pi model matrix that resolves against the current Pi model catalog"
@@ -3178,14 +3137,13 @@ export async function checkInitUpdate(input) {
3178
3137
  `Preserve these currently resolvable tiers byte-for-byte: ${modelReadiness.preserveTiers.join(", ") || "none"}.`,
3179
3138
  `Select replacements only for: ${modelReadiness.reselectTiers.join(", ")}.`,
3180
3139
  "Choose replacements from eligibleReferences using target-project needs, capability, context window, latency, and cost; tiers may intentionally share a model.",
3181
- "Apply the complete matrix through init update --apply-safe with all three tier flags so code can revalidate it; valid existing tiers must keep their current effective reference.",
3140
+ "Write the complete matrix directly to harness.json; do not run init update because this task authorizes only harness.json. The controller will revalidate all three tiers on continue.",
3182
3141
  "Do not add credentials or raw Pi configuration to harness.json.",
3183
3142
  ];
3184
3143
  task.verification = [
3185
3144
  "loop-agent init model-catalog --repo-root . --json",
3186
3145
  "loop-agent init check-update --repo-root . --json",
3187
3146
  "loop-agent init doctor --repo-root .",
3188
- "bash scripts/check-repo.sh",
3189
3147
  ];
3190
3148
  const existingTask = modelMergeTasks.findIndex((candidate) => candidate.path === "harness.json");
3191
3149
  if (existingTask >= 0)
@@ -3207,6 +3165,34 @@ export async function checkInitUpdate(input) {
3207
3165
  reason,
3208
3166
  });
3209
3167
  }
3168
+ let legacyAudit;
3169
+ if (input.legacyAudit && recordedState) {
3170
+ legacyAudit = await auditLegacyInitSurface({
3171
+ repoRoot,
3172
+ assetRoot,
3173
+ surface: recordedState,
3174
+ });
3175
+ for (const candidate of legacyAudit.candidates) {
3176
+ if (candidate.automaticAction && !deterministicActions.some((action) => action.path === candidate.path)) {
3177
+ deterministicActions.push({
3178
+ type: "remove-owned-file",
3179
+ path: candidate.path,
3180
+ expectedSha256: candidate.rawSha256,
3181
+ candidateId: candidate.candidateId,
3182
+ evidenceClass: candidate.evidenceClass,
3183
+ reason: candidate.reason,
3184
+ });
3185
+ }
3186
+ else if (!candidate.terminalDisposition && !candidate.automaticAction && !humanDecisions.some((decision) => decision.path === candidate.path)) {
3187
+ humanDecisions.push(legacyDecision(candidate));
3188
+ }
3189
+ }
3190
+ }
3191
+ const legacyCandidatePaths = new Set(legacyAudit?.candidates.map((candidate) => candidate.path) ?? []);
3192
+ const blockingModelMerge = modelMergeTasks.some((task) => task.path !== "harness.json" || modelReadiness?.status !== "ready");
3193
+ const coreBlocked = deterministicActions.some((action) => !legacyCandidatePaths.has(action.path)) ||
3194
+ blockingModelMerge ||
3195
+ humanDecisions.some((decision) => !decision.candidateId);
3210
3196
  const partial = {
3211
3197
  repoRoot,
3212
3198
  controllerVersion: currentState.controllerVersion,
@@ -3214,6 +3200,9 @@ export async function checkInitUpdate(input) {
3214
3200
  deterministicActions,
3215
3201
  modelMergeTasks,
3216
3202
  humanDecisions,
3203
+ coreState: coreBlocked ? "blocked" : "ready",
3204
+ migrationState: legacyAudit?.migrationState ?? recordedState?.legacyAudit?.migrationState ?? "clean",
3205
+ ...(legacyAudit ? { legacyAudit } : {}),
3217
3206
  ...(modelReadiness ? { modelReadiness } : {}),
3218
3207
  summary: {
3219
3208
  missing: deterministicActions.filter((action) => action.type !== "bootstrap-surface").length,
@@ -3238,6 +3227,16 @@ export async function checkInitUpdate(input) {
3238
3227
  },
3239
3228
  };
3240
3229
  }
3230
+ function legacyDecision(candidate) {
3231
+ return {
3232
+ path: candidate.path,
3233
+ reason: candidate.reason,
3234
+ candidateId: candidate.candidateId,
3235
+ expectedSha256: candidate.rawSha256,
3236
+ evidenceClass: candidate.evidenceClass,
3237
+ consumers: candidate.consumers,
3238
+ };
3239
+ }
3241
3240
  async function applySafeAction(input) {
3242
3241
  if (input.action.type === "merge-pi-project-settings") {
3243
3242
  return (await applyProjectPiSettingsMerge({ repoRoot: input.repoRoot })).wrote === true;
@@ -3333,6 +3332,13 @@ async function applySafeAction(input) {
3333
3332
  return true;
3334
3333
  }
3335
3334
  if (input.action.type === "remove-owned-file") {
3335
+ if (input.action.candidateId && input.action.expectedSha256) {
3336
+ return ((await removeContainedRegularFileWithSha256Cas({
3337
+ repoRoot: input.repoRoot,
3338
+ relativePath: input.action.path,
3339
+ expectedSha256: input.action.expectedSha256,
3340
+ })) === "removed");
3341
+ }
3336
3342
  const target = path.join(input.repoRoot, input.action.path);
3337
3343
  if (!(await exists(target)))
3338
3344
  return false;
@@ -3343,6 +3349,28 @@ async function applySafeAction(input) {
3343
3349
  await rm(target, { force: true });
3344
3350
  return true;
3345
3351
  }
3352
+ if (input.action.type === "strip-managed-block") {
3353
+ const target = path.join(input.repoRoot, input.action.path);
3354
+ if (!(await exists(target)))
3355
+ return false;
3356
+ const current = await readFile(target, "utf-8");
3357
+ const startMarker = input.action.path === ".gitignore"
3358
+ ? GITIGNORE_BLOCK_START
3359
+ : MANAGED_BLOCK_START;
3360
+ const endMarker = input.action.path === ".gitignore"
3361
+ ? GITIGNORE_BLOCK_END
3362
+ : MANAGED_BLOCK_END;
3363
+ const start = current.indexOf(startMarker);
3364
+ const end = current.indexOf(endMarker, start + startMarker.length);
3365
+ if (start < 0 || end < start)
3366
+ return false;
3367
+ const next = `${current.slice(0, start)}${current.slice(end + endMarker.length)}`;
3368
+ if (next.trim().length === 0)
3369
+ await rm(target, { force: true });
3370
+ else
3371
+ await writeFile(target, next, "utf-8");
3372
+ return true;
3373
+ }
3346
3374
  if (input.action.type === "reconcile-harness-structure") {
3347
3375
  const target = path.join(input.repoRoot, "harness.json");
3348
3376
  if (!(await exists(target)))
@@ -3360,7 +3388,13 @@ async function applySafeAction(input) {
3360
3388
  });
3361
3389
  if (desired.content === undefined)
3362
3390
  return false;
3363
- const next = reconcileHarnessStructure(current, JSON.parse(desired.content));
3391
+ const template = await readJsonIfExists(path.join(assetRoot, "harness.json"));
3392
+ const next = buildHarness({
3393
+ existing: current,
3394
+ template,
3395
+ projectName: input.projectName,
3396
+ governanceRoot: input.governanceRoot,
3397
+ });
3364
3398
  if (isDeepStrictEqual(next, current))
3365
3399
  return false;
3366
3400
  await writeFile(target, `${JSON.stringify(next, null, 2)}\n`, "utf-8");
@@ -3425,6 +3459,13 @@ async function applySafeAction(input) {
3425
3459
  await writeFile(target, desired.content, "utf-8");
3426
3460
  return true;
3427
3461
  }
3462
+ if (input.action.type === "replace-owned-file") {
3463
+ if (input.action.expectedSha256 &&
3464
+ (await fileSha256IfExists(target)) !== input.action.expectedSha256)
3465
+ return false;
3466
+ await writeFile(target, desired.content, "utf-8");
3467
+ return true;
3468
+ }
3428
3469
  if (await exists(target))
3429
3470
  return false;
3430
3471
  await writeFile(target, desired.content, "utf-8");
@@ -3439,7 +3480,6 @@ export async function applyInitUpdate(input) {
3439
3480
  });
3440
3481
  const applied = [];
3441
3482
  const skipped = [];
3442
- const clientRecoveryMode = input.clientRecovery ?? "auto";
3443
3483
  const requestedModelMatrix = input.modelMatrix ??
3444
3484
  (input.model
3445
3485
  ? { LOW: input.model, MED: input.model, HIGH: input.model }
@@ -3501,6 +3541,8 @@ export async function applyInitUpdate(input) {
3501
3541
  }
3502
3542
  if (input.applySafe) {
3503
3543
  const existingSurface = await readExistingSurfaceState(repoRoot);
3544
+ const actionReceipts = [];
3545
+ const dispositions = [];
3504
3546
  // Preserve source strength: recorded stays recorded so unchanged owned files
3505
3547
  // remain deterministic refresh candidates; bootstrap/inferred stays inferred.
3506
3548
  const preservedStateKind = existingSurface?.stateKind === "recorded"
@@ -3510,9 +3552,8 @@ export async function applyInitUpdate(input) {
3510
3552
  repoRoot,
3511
3553
  projectName,
3512
3554
  governanceRoot,
3513
- clientRecovery: clientRecoveryMode,
3514
- homeDir: input.homeDir,
3515
3555
  modelCatalog: input.modelCatalog,
3556
+ legacyAudit: input.legacyAudit,
3516
3557
  });
3517
3558
  for (const action of report.deterministicActions) {
3518
3559
  if (action.type === "bootstrap-surface") {
@@ -3522,8 +3563,35 @@ export async function applyInitUpdate(input) {
3522
3563
  // The report was produced immediately before this batch. Each action also
3523
3564
  // performs its own target check; missing-file writes refuse existing paths,
3524
3565
  // while managed blocks merge into the latest user-authored content.
3525
- if (await applySafeAction({ repoRoot, projectName, governanceRoot, action }))
3566
+ const beforeSha256 = await fileSha256IfExists(path.join(repoRoot, action.path));
3567
+ if (await applySafeAction({ repoRoot, projectName, governanceRoot, action })) {
3526
3568
  applied.push(action);
3569
+ const afterSha256 = await fileSha256IfExists(path.join(repoRoot, action.path));
3570
+ actionReceipts.push({
3571
+ type: action.type,
3572
+ path: action.path,
3573
+ ...(beforeSha256 ? { beforeSha256 } : {}),
3574
+ ...(afterSha256 ? { afterSha256 } : {}),
3575
+ appliedAt: new Date().toISOString(),
3576
+ });
3577
+ if (action.type === "remove-owned-file" ||
3578
+ action.type === "strip-managed-block") {
3579
+ const previous = existingSurface?.files[action.path];
3580
+ if ((previous || action.candidateId) && existingSurface) {
3581
+ dispositions.push({
3582
+ path: action.path,
3583
+ disposition: "removed",
3584
+ previousMode: previous?.mode ?? "copied",
3585
+ ...(previous?.currentSha256 || action.expectedSha256
3586
+ ? { recordedSha256: previous?.currentSha256 ?? action.expectedSha256 }
3587
+ : {}),
3588
+ ...(afterSha256 ? { resultSha256: afterSha256 } : {}),
3589
+ controllerVersion: existingSurface.controllerVersion,
3590
+ recordedAt: new Date().toISOString(),
3591
+ });
3592
+ }
3593
+ }
3594
+ }
3527
3595
  else
3528
3596
  skipped.push(action);
3529
3597
  }
@@ -3533,14 +3601,11 @@ export async function applyInitUpdate(input) {
3533
3601
  governanceRoot,
3534
3602
  stateKind: preservedStateKind,
3535
3603
  preserveOwnershipFrom: existingSurface,
3536
- });
3537
- }
3538
- // Pi user config is only mutated with explicit --client-recovery=user.
3539
- // Do not reinstall the project plugin here — that would bypass ownership and
3540
- // clobber user-modified plugins; plugin updates stay on deterministic actions.
3541
- if (clientRecoveryMode === "user") {
3542
- await applyPiRetryMerge({
3543
- homeDir: input.homeDir ?? os.homedir(),
3604
+ dispositions,
3605
+ actionReceipts,
3606
+ ...(report.legacyAudit
3607
+ ? { legacyAudit: legacyAuditReceipt(report.legacyAudit, applied.filter((action) => Boolean(action.candidateId)).length) }
3608
+ : {}),
3544
3609
  });
3545
3610
  }
3546
3611
  return {
@@ -3550,12 +3615,313 @@ export async function applyInitUpdate(input) {
3550
3615
  repoRoot,
3551
3616
  projectName,
3552
3617
  governanceRoot,
3553
- clientRecovery: clientRecoveryMode,
3554
- homeDir: input.homeDir,
3555
3618
  modelCatalog: input.modelCatalog,
3619
+ legacyAudit: input.legacyAudit,
3556
3620
  }),
3557
3621
  };
3558
3622
  }
3623
+ function canonicalInitAuthorityJson(value) {
3624
+ if (value === null || typeof value !== "object")
3625
+ return JSON.stringify(value);
3626
+ if (Array.isArray(value)) {
3627
+ return `[${value.map(canonicalInitAuthorityJson).join(",")}]`;
3628
+ }
3629
+ const record = value;
3630
+ return `{${Object.keys(record)
3631
+ .sort()
3632
+ .map((key) => `${JSON.stringify(key)}:${canonicalInitAuthorityJson(record[key])}`)
3633
+ .join(",")}}`;
3634
+ }
3635
+ async function assertInitDecisionResumeAuthority(input) {
3636
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(input.authority.runId) ||
3637
+ !/^[a-f0-9]{64}$/.test(input.authority.pendingSha256)) {
3638
+ throw new Error("init decision resume authority is invalid");
3639
+ }
3640
+ const repoRoot = await realpath(input.repoRoot).catch(() => {
3641
+ throw new Error("init decision resume authority requires a readable Git repository");
3642
+ });
3643
+ const marker = path.join(repoRoot, ".git");
3644
+ const markerStat = await lstat(marker).catch(() => undefined);
3645
+ let gitDirectory;
3646
+ if (markerStat?.isDirectory()) {
3647
+ gitDirectory = await realpath(marker);
3648
+ }
3649
+ else if (markerStat?.isFile()) {
3650
+ const text = await readFile(marker, "utf-8").catch(() => "");
3651
+ const match = /^gitdir:\s*(.+?)\s*\r?\n?$/.exec(text);
3652
+ if (!match?.[1]) {
3653
+ throw new Error("init decision resume authority rejected an invalid Git worktree file");
3654
+ }
3655
+ gitDirectory = await realpath(path.resolve(path.dirname(marker), match[1]));
3656
+ }
3657
+ else {
3658
+ throw new Error("init decision resume authority requires a Git repository");
3659
+ }
3660
+ const anchorRoot = await realpath(path.join(gitDirectory, "loop-agent-init-upgrades")).catch(() => {
3661
+ throw new Error("init decision resume authority anchor is missing");
3662
+ });
3663
+ const relativeAnchorRoot = path.relative(gitDirectory, anchorRoot);
3664
+ if (relativeAnchorRoot === "" ||
3665
+ relativeAnchorRoot === ".." ||
3666
+ relativeAnchorRoot.startsWith(`..${path.sep}`) ||
3667
+ path.isAbsolute(relativeAnchorRoot)) {
3668
+ throw new Error("init decision resume authority anchor escapes Git directory");
3669
+ }
3670
+ const anchorPath = path.join(anchorRoot, `${sha256Text(`${repoRoot}\0${input.authority.runId}`)}.json`);
3671
+ let anchor;
3672
+ try {
3673
+ anchor = JSON.parse(await readFile(anchorPath, "utf-8"));
3674
+ }
3675
+ catch {
3676
+ throw new Error("init decision resume authority anchor is missing or invalid");
3677
+ }
3678
+ if (!isRecord(anchor) || anchor.schemaVersion !== 1) {
3679
+ throw new Error("init decision resume authority anchor is invalid");
3680
+ }
3681
+ const decisionAuthority = anchor.decisionAuthority;
3682
+ if (!isRecord(decisionAuthority) || !isRecord(decisionAuthority.pending)) {
3683
+ throw new Error("init decision resume authority is not pending");
3684
+ }
3685
+ const pending = decisionAuthority.pending;
3686
+ if (anchor.repoRoot !== repoRoot ||
3687
+ anchor.runId !== input.authority.runId ||
3688
+ decisionAuthority.pendingSha256 !== input.authority.pendingSha256 ||
3689
+ sha256Text(canonicalInitAuthorityJson(pending)) !==
3690
+ input.authority.pendingSha256 ||
3691
+ pending.schemaVersion !== 1 ||
3692
+ pending.repoRoot !== repoRoot ||
3693
+ pending.runId !== input.authority.runId ||
3694
+ !isRecord(pending.document) ||
3695
+ pending.document.schemaVersion !== 1 ||
3696
+ pending.document.runId !== input.authority.runId ||
3697
+ !Array.isArray(pending.document.decisions) ||
3698
+ !isDeepStrictEqual(pending.document.decisions, input.actions)) {
3699
+ throw new Error("init decision resume authority does not match pending actions");
3700
+ }
3701
+ }
3702
+ export async function applyInitDecisionBoundActions(input) {
3703
+ const repoRoot = path.resolve(input.repoRoot);
3704
+ if (input.resumeAuthority) {
3705
+ await assertInitDecisionResumeAuthority({
3706
+ repoRoot,
3707
+ actions: input.actions,
3708
+ authority: input.resumeAuthority,
3709
+ });
3710
+ }
3711
+ const { projectName, governanceRoot } = await resolveInitProjectContext({
3712
+ repoRoot,
3713
+ });
3714
+ const existingSurface = await readExistingSurfaceState(repoRoot);
3715
+ if (!existingSurface) {
3716
+ throw new Error("init decision-bound actions require an existing init surface");
3717
+ }
3718
+ const report = await checkInitUpdate({ repoRoot, legacyAudit: true });
3719
+ const pending = new Map(report.humanDecisions.map((decision) => [decision.path, decision]));
3720
+ const seen = new Set();
3721
+ const validated = [];
3722
+ const assetRoot = await findPackageRoot();
3723
+ for (const action of input.actions) {
3724
+ const normalized = normalizeRelativePath(action.path);
3725
+ if (normalized !== action.path ||
3726
+ path.isAbsolute(action.path) ||
3727
+ action.path.startsWith("../") ||
3728
+ action.path.includes("/../")) {
3729
+ throw new Error(`invalid init decision path: ${action.path}`);
3730
+ }
3731
+ if (seen.has(action.path)) {
3732
+ throw new Error(`duplicate init decision path: ${action.path}`);
3733
+ }
3734
+ seen.add(action.path);
3735
+ const pendingDecision = pending.get(action.path);
3736
+ if (!pendingDecision && !input.resumeAuthority) {
3737
+ throw new Error(`init decision is not currently pending: ${action.path}`);
3738
+ }
3739
+ const isBridge = action.path === ".agents/skills/loop-agent/SKILL.md";
3740
+ const isLegacyCandidate = Boolean(action.candidateId);
3741
+ if (isLegacyCandidate) {
3742
+ if (!input.resumeAuthority &&
3743
+ (!pendingDecision?.candidateId ||
3744
+ pendingDecision.candidateId !== action.candidateId ||
3745
+ pendingDecision.expectedSha256 !== action.expectedSha256)) {
3746
+ throw new Error(`stale or mismatched legacy init candidate decision: ${action.path}`);
3747
+ }
3748
+ if (action.disposition !== "retain-project-content" &&
3749
+ action.disposition !== "preserve-unproven" &&
3750
+ action.disposition !== "delete-listed-file") {
3751
+ throw new Error(`unsupported legacy init disposition for ${action.path}: ${action.disposition}`);
3752
+ }
3753
+ if (action.disposition === "delete-listed-file" &&
3754
+ (pendingDecision?.consumers?.length ?? 0) > 0) {
3755
+ throw new Error(`legacy init candidate still has consumers and cannot be deleted: ${action.path}`);
3756
+ }
3757
+ }
3758
+ else if ((isBridge && action.disposition !== "replace-with-bridge") ||
3759
+ (!isBridge && action.disposition !== "detach-as-project-content")) {
3760
+ throw new Error(`unsupported init decision disposition for ${action.path}: ${action.disposition}`);
3761
+ }
3762
+ const absolutePath = path.resolve(repoRoot, action.path);
3763
+ const relative = path.relative(repoRoot, absolutePath);
3764
+ if (relative.length === 0 ||
3765
+ relative.startsWith(`..${path.sep}`) ||
3766
+ path.isAbsolute(relative)) {
3767
+ throw new Error(`init decision path escapes the repository: ${action.path}`);
3768
+ }
3769
+ const inspected = await inspectContainedRegularFile(repoRoot, action.path);
3770
+ if (!inspected &&
3771
+ input.resumeAuthority &&
3772
+ isLegacyCandidate &&
3773
+ action.disposition === "delete-listed-file") {
3774
+ validated.push({
3775
+ action,
3776
+ absolutePath,
3777
+ currentSha256: action.expectedSha256,
3778
+ alreadyAbsent: true,
3779
+ });
3780
+ continue;
3781
+ }
3782
+ if (!inspected || "conflict" in inspected) {
3783
+ throw new Error(`init decision target is not a regular file: ${action.path}`);
3784
+ }
3785
+ const currentSha256 = sha256Buffer(inspected.bytes);
3786
+ let desiredBridgeContent;
3787
+ let desiredBridgeSha256;
3788
+ if (isBridge) {
3789
+ const desired = await buildDesiredSurfaceContent({
3790
+ assetRoot,
3791
+ repoRoot,
3792
+ projectName,
3793
+ governanceRoot,
3794
+ entry: {
3795
+ path: ".agents/skills/loop-agent/SKILL.md",
3796
+ mode: "copied",
3797
+ },
3798
+ });
3799
+ if (desired.content === undefined) {
3800
+ throw new Error("AGENT_BRIDGE_RESOURCE_MISSING: bundled loop-agent bridge is unavailable");
3801
+ }
3802
+ desiredBridgeContent = desired.content;
3803
+ desiredBridgeSha256 = sha256Text(desired.content);
3804
+ }
3805
+ if (currentSha256 !== action.expectedSha256 &&
3806
+ !(input.resumeAuthority && desiredBridgeSha256 === currentSha256)) {
3807
+ throw new Error(`stale init decision for ${action.path}: expected ${action.expectedSha256}, observed ${currentSha256}`);
3808
+ }
3809
+ validated.push({
3810
+ action,
3811
+ absolutePath,
3812
+ currentSha256,
3813
+ desiredBridgeContent,
3814
+ desiredBridgeSha256,
3815
+ });
3816
+ }
3817
+ const dispositionReceipts = [];
3818
+ const actionReceipts = [];
3819
+ const results = [];
3820
+ for (const entry of validated) {
3821
+ let resultSha256 = entry.currentSha256;
3822
+ if (entry.action.disposition === "replace-with-bridge") {
3823
+ if (!entry.desiredBridgeContent || !entry.desiredBridgeSha256) {
3824
+ throw new Error("AGENT_BRIDGE_RESOURCE_MISSING: bundled loop-agent bridge is unavailable");
3825
+ }
3826
+ resultSha256 = entry.desiredBridgeSha256;
3827
+ if (entry.currentSha256 !== resultSha256) {
3828
+ await writeFile(entry.absolutePath, entry.desiredBridgeContent, "utf-8");
3829
+ }
3830
+ const alreadyRecorded = existingSurface.actionReceipts?.some((receipt) => receipt.type === "replace-owned-file" &&
3831
+ receipt.path === entry.action.path &&
3832
+ receipt.beforeSha256 === entry.action.expectedSha256 &&
3833
+ receipt.afterSha256 === resultSha256);
3834
+ if (!alreadyRecorded) {
3835
+ actionReceipts.push({
3836
+ type: "replace-owned-file",
3837
+ path: entry.action.path,
3838
+ beforeSha256: entry.action.expectedSha256,
3839
+ afterSha256: resultSha256,
3840
+ appliedAt: new Date().toISOString(),
3841
+ });
3842
+ }
3843
+ }
3844
+ else if (entry.action.disposition === "delete-listed-file") {
3845
+ if (!entry.alreadyAbsent) {
3846
+ const removal = await removeContainedRegularFileWithSha256Cas({
3847
+ repoRoot,
3848
+ relativePath: entry.action.path,
3849
+ expectedSha256: entry.action.expectedSha256,
3850
+ });
3851
+ if (removal === "rejected") {
3852
+ throw new Error(`legacy init decision target changed or escaped containment before deletion: ${entry.action.path}`);
3853
+ }
3854
+ }
3855
+ dispositionReceipts.push({
3856
+ path: entry.action.path,
3857
+ disposition: "removed",
3858
+ previousMode: existingSurface.files[entry.action.path]?.mode ?? "copied",
3859
+ recordedSha256: entry.action.expectedSha256,
3860
+ controllerVersion: existingSurface.controllerVersion,
3861
+ recordedAt: new Date().toISOString(),
3862
+ });
3863
+ actionReceipts.push({
3864
+ type: "remove-owned-file",
3865
+ path: entry.action.path,
3866
+ beforeSha256: entry.action.expectedSha256,
3867
+ appliedAt: new Date().toISOString(),
3868
+ });
3869
+ }
3870
+ else {
3871
+ const disposition = entry.action.disposition === "retain-project-content"
3872
+ ? "retained-project-content"
3873
+ : entry.action.disposition === "preserve-unproven"
3874
+ ? "preserved-unproven-legacy"
3875
+ : "detached-as-project-content";
3876
+ const alreadyRecorded = existingSurface.dispositions?.some((receipt) => receipt.path === entry.action.path &&
3877
+ receipt.disposition === disposition &&
3878
+ receipt.recordedSha256 === entry.action.expectedSha256 &&
3879
+ receipt.resultSha256 === resultSha256);
3880
+ if (!alreadyRecorded) {
3881
+ dispositionReceipts.push({
3882
+ path: entry.action.path,
3883
+ disposition,
3884
+ previousMode: existingSurface.files[entry.action.path]?.mode ?? "copied",
3885
+ recordedSha256: entry.action.expectedSha256,
3886
+ resultSha256,
3887
+ controllerVersion: existingSurface.controllerVersion,
3888
+ recordedAt: new Date().toISOString(),
3889
+ });
3890
+ }
3891
+ }
3892
+ results.push({
3893
+ path: entry.action.path,
3894
+ disposition: entry.action.disposition,
3895
+ expectedSha256: entry.action.expectedSha256,
3896
+ resultSha256,
3897
+ ...(entry.action.candidateId
3898
+ ? { candidateId: entry.action.candidateId }
3899
+ : {}),
3900
+ });
3901
+ }
3902
+ await writeInitSurfaceState({
3903
+ repoRoot,
3904
+ projectName,
3905
+ governanceRoot,
3906
+ stateKind: existingSurface.stateKind,
3907
+ preserveOwnershipFrom: existingSurface,
3908
+ dispositions: dispositionReceipts,
3909
+ actionReceipts,
3910
+ });
3911
+ const postDecisionReport = await checkInitUpdate({ repoRoot, legacyAudit: true });
3912
+ if (postDecisionReport.legacyAudit) {
3913
+ const updatedSurface = await readExistingSurfaceState(repoRoot);
3914
+ await writeInitSurfaceState({
3915
+ repoRoot,
3916
+ projectName,
3917
+ governanceRoot,
3918
+ stateKind: existingSurface.stateKind,
3919
+ preserveOwnershipFrom: updatedSurface,
3920
+ legacyAudit: legacyAuditReceipt(postDecisionReport.legacyAudit, validated.filter((entry) => entry.action.disposition === "delete-listed-file").length),
3921
+ });
3922
+ }
3923
+ return results;
3924
+ }
3559
3925
  function formatClientRecoverySummary(report) {
3560
3926
  if (!report.clientRecovery?.pi)
3561
3927
  return [];
@@ -3581,6 +3947,8 @@ function formatCheckUpdateText(report) {
3581
3947
  `loop-agent init update check for ${report.repoRoot}`,
3582
3948
  `controllerVersion: ${report.controllerVersion}`,
3583
3949
  `surfaceState: ${report.surfaceState}`,
3950
+ `coreState: ${report.coreState}`,
3951
+ `migrationState: ${report.migrationState}`,
3584
3952
  `deterministicActions: ${report.deterministicActions.length}`,
3585
3953
  `modelMergeTasks: ${report.modelMergeTasks.length}`,
3586
3954
  `humanDecisions: ${report.humanDecisions.length}`,
@@ -3598,6 +3966,8 @@ function formatCheckUpdateMarkdown(report) {
3598
3966
  `- repoRoot: \`${report.repoRoot}\``,
3599
3967
  `- controllerVersion: \`${report.controllerVersion}\``,
3600
3968
  `- surfaceState: \`${report.surfaceState}\``,
3969
+ `- coreState: \`${report.coreState}\``,
3970
+ `- migrationState: \`${report.migrationState}\``,
3601
3971
  ...(report.modelReadiness
3602
3972
  ? [
3603
3973
  "",
@@ -3639,7 +4009,6 @@ export async function runInitDoctor(input) {
3639
4009
  try {
3640
4010
  const manifest = await loadHarnessManifest(repoRoot);
3641
4011
  add("harness.json", manifest.adapter === "loop-agent" || manifest.project === "loop-agent", `project=${manifest.project}`);
3642
- add("governance root", await exists(path.join(repoRoot, manifest.governanceRoot, "README.md")), manifest.governanceRoot);
3643
4012
  if (input.modelCatalog) {
3644
4013
  const readiness = assessInitPiModelReadiness(isRecord(manifest.executors?.pi)
3645
4014
  ? manifest.executors.pi
@@ -3652,31 +4021,14 @@ export async function runInitDoctor(input) {
3652
4021
  catch (error) {
3653
4022
  add("harness.json", false, error instanceof Error ? error.message : String(error));
3654
4023
  }
3655
- const agents = path.join(repoRoot, "AGENTS.md");
3656
- add("AGENTS.md loop-agent block", (await exists(agents)) &&
3657
- (await readFile(agents, "utf-8")).includes(MANAGED_BLOCK_START), "managed block present");
3658
- const readme = path.join(repoRoot, "README.md");
3659
- add("README loop-agent block", (await exists(readme)) &&
3660
- (await readFile(readme, "utf-8")).includes(MANAGED_BLOCK_START), "managed block present");
3661
- add("repo-local skills", await exists(path.join(repoRoot, ".agents", "skills", "loop-agent", "SKILL.md")), ".agents/skills/loop-agent/SKILL.md");
4024
+ add("external Agent bridge", await exists(path.join(repoRoot, ".agents", "skills", "loop-agent", "SKILL.md")), ".agents/skills/loop-agent/SKILL.md");
3662
4025
  const gitignorePath = path.join(repoRoot, ".gitignore");
3663
4026
  const gitignoreContent = (await exists(gitignorePath))
3664
4027
  ? await readFile(gitignorePath, "utf-8")
3665
4028
  : "";
3666
4029
  add("gitignore loop-agent block", gitignoreContent.includes(GITIGNORE_BLOCK_START) &&
3667
4030
  gitignoreContent.includes(".harness/"), ".gitignore managed runtime ignores");
3668
- const requiredScripts = Object.keys(INIT_SCRIPT_FILES);
3669
- const missingScripts = [];
3670
- for (const script of requiredScripts) {
3671
- if (!(await exists(path.join(repoRoot, script))))
3672
- missingScripts.push(script);
3673
- }
3674
- add("script matrix", missingScripts.length === 0, missingScripts.length === 0
3675
- ? `${requiredScripts.length} scripts`
3676
- : `missing: ${missingScripts.join(", ")}`);
3677
- add("compat prompts", (await exists(path.join(repoRoot, ".harness", "prompts", "analyze.md"))) &&
3678
- (await exists(path.join(repoRoot, ".harness", "prompts", "plan.md"))), ".harness/prompts/analyze.md");
3679
- add("harness runtime dirs", await exists(path.join(repoRoot, ".harness", "dag-runs", "active")), ".harness/dag-runs/active");
4031
+ add("init surface state", await exists(path.join(repoRoot, INIT_SURFACE_STATE_PATH)), INIT_SURFACE_STATE_PATH);
3680
4032
  return {
3681
4033
  ok: checks.every((check) => check.ok),
3682
4034
  repoRoot,
@@ -3728,23 +4080,23 @@ function parseInitArgs(repoRoot, args) {
3728
4080
  let subcommand;
3729
4081
  let projectName;
3730
4082
  let governanceRoot;
3731
- let profile = "full";
3732
4083
  let merge = true;
3733
4084
  let provider;
3734
4085
  let model;
3735
4086
  let lowModel;
3736
4087
  let medModel;
3737
4088
  let highModel;
3738
- let clientRecovery = "auto";
3739
4089
  let json = false;
3740
4090
  let markdown = false;
3741
4091
  let bootstrapSurface = false;
3742
4092
  let applySafe = false;
4093
+ let legacyAudit = false;
3743
4094
  let runId;
3744
4095
  let upgradeStatus = false;
3745
4096
  let upgradeContinue = false;
3746
4097
  let upgradeReport = false;
3747
4098
  let versionChoice;
4099
+ let decisionFile;
3748
4100
  for (let i = 0; i < args.length; i += 1) {
3749
4101
  const arg = args[i];
3750
4102
  if (arg === undefined)
@@ -3768,10 +4120,8 @@ function parseInitArgs(repoRoot, args) {
3768
4120
  governanceRoot = args[++i] ?? governanceRoot;
3769
4121
  else if (arg.startsWith("--governance-root="))
3770
4122
  governanceRoot = arg.slice("--governance-root=".length);
3771
- else if (arg === "--profile")
3772
- profile = args[++i] ?? profile;
3773
- else if (arg.startsWith("--profile="))
3774
- profile = arg.slice("--profile=".length);
4123
+ else if (arg === "--profile" || arg.startsWith("--profile="))
4124
+ throw new Error("INIT_PROFILE_REMOVED: minimal initialization is now the only init architecture; remove --profile");
3775
4125
  else if (arg === "--merge")
3776
4126
  merge = true;
3777
4127
  else if (arg === "--no-merge")
@@ -3796,10 +4146,9 @@ function parseInitArgs(repoRoot, args) {
3796
4146
  highModel = args[++i];
3797
4147
  else if (arg.startsWith("--high-model="))
3798
4148
  highModel = arg.slice("--high-model=".length);
3799
- else if (arg === "--client-recovery")
3800
- clientRecovery = parseClientRecoveryMode(args[++i]);
3801
- else if (arg.startsWith("--client-recovery="))
3802
- clientRecovery = parseClientRecoveryMode(arg.slice("--client-recovery=".length));
4149
+ else if (arg === "--client-recovery" ||
4150
+ arg.startsWith("--client-recovery="))
4151
+ throw new Error("INIT_CLIENT_RECOVERY_REMOVED: default init no longer installs client recovery files; remove --client-recovery (explicit client adapters are not available in this release)");
3803
4152
  else if (arg === "--json")
3804
4153
  json = true;
3805
4154
  else if (arg === "--markdown")
@@ -3808,6 +4157,8 @@ function parseInitArgs(repoRoot, args) {
3808
4157
  bootstrapSurface = true;
3809
4158
  else if (arg === "--apply-safe")
3810
4159
  applySafe = true;
4160
+ else if (arg === "--legacy-audit")
4161
+ legacyAudit = true;
3811
4162
  else if (arg === "--run-id")
3812
4163
  runId = args[++i];
3813
4164
  else if (arg.startsWith("--run-id="))
@@ -3822,13 +4173,15 @@ function parseInitArgs(repoRoot, args) {
3822
4173
  versionChoice = args[++i];
3823
4174
  else if (arg.startsWith("--version-choice="))
3824
4175
  versionChoice = arg.slice("--version-choice=".length);
4176
+ else if (arg === "--decision-file")
4177
+ decisionFile = args[++i];
4178
+ else if (arg.startsWith("--decision-file="))
4179
+ decisionFile = arg.slice("--decision-file=".length);
3825
4180
  else if (arg.startsWith("-"))
3826
4181
  throw new Error(`unknown init flag: ${arg}`);
3827
4182
  else
3828
4183
  throw new Error(`unexpected init argument: ${arg}`);
3829
4184
  }
3830
- if (profile !== "full" && profile !== "minimal")
3831
- throw new Error("init --profile must be full or minimal");
3832
4185
  if (versionChoice && !["current", "upgrade", "cancel", "retry"].includes(versionChoice)) {
3833
4186
  throw new Error("init upgrade --version-choice must be current|upgrade|cancel|retry");
3834
4187
  }
@@ -3844,22 +4197,23 @@ function parseInitArgs(repoRoot, args) {
3844
4197
  repoRoot,
3845
4198
  projectName,
3846
4199
  governanceRoot,
3847
- profile,
4200
+ profile: "minimal",
3848
4201
  merge,
3849
4202
  provider,
3850
4203
  model: normalizedModel,
3851
4204
  modelMatrix,
3852
- clientRecovery,
3853
4205
  subcommand,
3854
4206
  json,
3855
4207
  markdown,
3856
4208
  bootstrapSurface,
3857
4209
  applySafe,
4210
+ legacyAudit,
3858
4211
  runId,
3859
4212
  upgradeStatus,
3860
4213
  upgradeContinue,
3861
4214
  upgradeReport,
3862
4215
  versionChoice,
4216
+ decisionFile,
3863
4217
  };
3864
4218
  }
3865
4219
  async function discoverInitCatalogForRepo(repoRoot) {
@@ -3947,6 +4301,7 @@ export async function runInit(repoRoot, rawArgs, dependencies) {
3947
4301
  runId: parsed.runId,
3948
4302
  mode,
3949
4303
  versionChoice: parsed.versionChoice,
4304
+ decisionFile: parsed.decisionFile,
3950
4305
  readRuntimeActivity: dependencies.readRuntimeActivity,
3951
4306
  modelCatalog,
3952
4307
  });
@@ -4003,6 +4358,8 @@ function formatInitUpgradeResult(result) {
4003
4358
  `phase: ${result.phase}`,
4004
4359
  `modelMergeTasks: ${result.modelMergeTasks.length}`,
4005
4360
  `humanDecisions: ${result.humanDecisions.length}`,
4361
+ `coreState: ${result.coreState}`,
4362
+ `migrationState: ${result.migrationState}`,
4006
4363
  `next: ${result.nextAction}`,
4007
4364
  ].join("\n");
4008
4365
  }
@@ -4013,6 +4370,8 @@ function formatInitUpdateResult(result) {
4013
4370
  `skipped: ${result.skipped.length}`,
4014
4371
  `remaining deterministicActions: ${result.report.deterministicActions.length}`,
4015
4372
  `remaining modelMergeTasks: ${result.report.modelMergeTasks.length}`,
4373
+ `coreState: ${result.report.coreState}`,
4374
+ `migrationState: ${result.report.migrationState}`,
4016
4375
  "recommendedNext:",
4017
4376
  ...result.report.recommendedNext.map((item) => `- ${item}`),
4018
4377
  ].join("\n");
@@ -4036,14 +4395,11 @@ function formatReconcileResult(result) {
4036
4395
  }
4037
4396
  function formatInitResult(result) {
4038
4397
  return [
4039
- `Initialized loop-agent harness at ${result.repoRoot}`,
4398
+ `Initialized minimal loop-agent project surface at ${result.repoRoot}`,
4040
4399
  `project: ${result.projectName}`,
4041
- `governanceRoot: ${result.governanceRoot}`,
4042
- `profile: ${result.profile}`,
4043
4400
  `written: ${result.written.length}`,
4044
4401
  `skipped: ${result.skipped.length}`,
4045
4402
  "next: loop-agent init doctor --repo-root <target>",
4046
- `model-executed init: continue automatically by inspecting the target project, adapting README.md, ${result.governanceRoot}/verification-matrix.md, and scripts/ci-tests.sh when needed, then run inspect/docs audit/quick verification.`,
4047
4403
  ].join("\n");
4048
4404
  }
4049
4405
  export async function listInitializedFiles(repoRoot) {