@tea-agent/loop-agent 0.44.0-next.13 → 0.44.0-next.15

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 (147) hide show
  1. package/CHANGELOG.md +13 -0
  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 +685 -330
  10. package/dist/commands/operator-guide.js +99 -0
  11. package/dist/commands/operator.js +16 -3
  12. package/dist/executors/pi-sdk-executor.js +3 -1
  13. package/dist/governance/harness.js +2 -7
  14. package/dist/governance/manifest-types.js +28 -9
  15. package/dist/governance/project-capability-scanner.js +263 -0
  16. package/dist/governance/project-config.js +96 -0
  17. package/dist/governance/resource-catalog.js +151 -0
  18. package/dist/infrastructure/console/app-data.js +6 -2
  19. package/dist/shared/operator/capabilities.js +2 -2
  20. package/dist/shared/package-metadata.js +23 -7
  21. package/dist/shared/pi-context-pressure/extension.js +23 -0
  22. package/dist/shared/pi-context-pressure/index.js +1 -0
  23. package/dist/shared/pi-context-pressure/telemetry.js +4 -0
  24. package/dist/shared/pi-context-pressure/tool-result-pruner.js +98 -0
  25. package/dist/shared/update/npm-client.js +49 -1
  26. package/dist/worker/cli.js +3 -5
  27. package/dist/worker/console/chat/browser-automation.js +114 -5
  28. package/dist/worker/console/chat/browser-routes.js +21 -0
  29. package/dist/worker/console/chat/chat-event-store.js +7 -6
  30. package/dist/worker/console/chat/context-insights.js +84 -19
  31. package/dist/worker/console/chat/db-connections.js +1 -1
  32. package/dist/worker/console/chat/pet/pet-affinity.js +169 -0
  33. package/dist/worker/console/chat/pet/pet-persist.js +109 -0
  34. package/dist/worker/console/chat/pet/pet-registry.js +142 -0
  35. package/dist/worker/console/chat/pet/pet-remarks.js +48 -0
  36. package/dist/worker/console/chat/pet/pet-routes.js +125 -0
  37. package/dist/worker/console/chat/pet/pet-service.js +262 -0
  38. package/dist/worker/console/chat/pet/pet-state.js +99 -0
  39. package/dist/worker/console/chat/pi-mode-loop-isolation.js +17 -2
  40. package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +2 -2
  41. package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +18 -3
  42. package/dist/worker/console/chat/pi-runtime.js +7 -2
  43. package/dist/worker/console/chat/routes.js +43 -7
  44. package/dist/worker/console/chat/scheduled-goal-request.js +14 -6
  45. package/dist/worker/console/chat/session-store.js +20 -3
  46. package/dist/worker/console/console-shutdown.js +34 -0
  47. package/dist/worker/console/server.js +2 -1
  48. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BA7qCL_r.js → abnfDiagram-N423BO3Z-DASc69AM.js} +1 -1
  49. package/dist/worker/console/static/assets/{arc-Df09ajwL.js → arc-h09p-LAc.js} +1 -1
  50. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CpF9sw5R.js → architectureDiagram-T3A2C74G-CZ3dtLkh.js} +1 -1
  51. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BtPys8ja.js → blockDiagram-VBNYF7ZC-DMrRhMXG.js} +1 -1
  52. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DZPnTWqT.js → c4Diagram-5PPSVZJV-DQxisHV6.js} +1 -1
  53. package/dist/worker/console/static/assets/channel-D1zgKZfg.js +1 -0
  54. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BD9D4h1p.js → chunk-2GRJ4B5K-CefVN9zh.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-Svn8yBmn.js → chunk-2Q5K7J3B-DmR1Koq2.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-B8EbQj-S.js → chunk-5RXB4S5H-NlzrSW7L.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-tENZpcpF.js → chunk-5VM5RSS4-DfelJv-0.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DBx71Lco.js → chunk-6Q2QTUOP-BK6RFy58.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-1v_QamMz.js → chunk-GF5L2VYU-BAeh9lz4.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BAMCNNBG.js → chunk-JWPE2WC7-ChPS5WzG.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BvuEury-.js → chunk-KBJHAD2P-BoVe5IM6.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-YqFuHp8s.js → chunk-RYQCIY6F-C4q_0Lw9.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-ouI5T7Mr.js → chunk-XXDRQBXY-Bb9TvjeY.js} +1 -1
  64. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CXNNsY2M.js +1 -0
  65. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CXNNsY2M.js +1 -0
  66. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-Bri0_Izk.js → cose-bilkent-JH36ORCC-DmNnL8Ff.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-e8Vhhi4X.js → cynefin-VYW2F7L2-Contvx58.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-tSIXGQiJ.js → cynefinDiagram-MW4NZA55-Cigq7cjk.js} +1 -1
  69. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Rl-sF5pt.js → dagre-VZM6K2ZE-dEbC0nxb.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Db6ddPz8.js → diagram-7IWD3JNH-CzIztESV.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Jwd_Xw5S.js → diagram-B4RE2ZJO-Bo14zbaY.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CeC_8InC.js → diagram-LBJQPF4R-C4xUyil6.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-LKPRaNq8.js → diagram-Q27KOJAE-CaBY_uYa.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-UB23O5K3-DlVQytMW.js → diagram-UB23O5K3-ClUXUeiu.js} +1 -1
  75. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DnfJZwO-.js → ebnfDiagram-BXEA7PRR-BbGCgjC6.js} +1 -1
  76. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-3axVSXca.js → erDiagram-JOGREHBK-Dr-AcwSl.js} +1 -1
  77. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-xe04-_L-.js → flowDiagram-UKHOOZJN-BG8Gcfyc.js} +1 -1
  78. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-hpmRINiD.js → ganttDiagram-PKOTCBZU-CFVaPs5r.js} +1 -1
  79. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-6HSNz9Ko.js → gitGraphDiagram-DS77QQ5N-ChJ8pm-O.js} +1 -1
  80. package/dist/worker/console/static/assets/index-B_5GhVIO.js +488 -0
  81. package/dist/worker/console/static/assets/index-C7YMBluX.css +1 -0
  82. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BF6ksWTX.js → infoDiagram-6WML65LV-C5J9-uCZ.js} +1 -1
  83. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DXisdeD7.js → ishikawaDiagram-WSZJBQD7-BcfjqzDv.js} +1 -1
  84. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BWu65wmu.js → journeyDiagram-NVQOT4AX-BzzCO-rD.js} +1 -1
  85. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-hfNhHqAI.js → kanban-definition-27J2QSJJ-NCHUZ46h.js} +1 -1
  86. package/dist/worker/console/static/assets/{linear-D6HTfQcp.js → linear-BKZlWCUU.js} +1 -1
  87. package/dist/worker/console/static/assets/{mermaid.core-Iv91R1WP.js → mermaid.core-DbTaQjFE.js} +5 -5
  88. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CfPChwjR.js → mindmap-definition-FAOFIHXS-CroYl5xQ.js} +1 -1
  89. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B7TcoqHz.js → pegDiagram-VL7TDLO6-DgMBx-9k.js} +1 -1
  90. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-ouyPKMtQ.js → pieDiagram-7S7Q4E2Y-CyWDmbq1.js} +1 -1
  91. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B9tuyLbh.js → quadrantDiagram-CIZ2JOQS-BgGEOqSP.js} +1 -1
  92. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Ck4q9nJs.js → railroadDiagram-AXF67PYL-BMZ7oeeV.js} +1 -1
  93. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-wXLX5Dnp.js → requirementDiagram-LRYGKXZP-DxqyGhcc.js} +1 -1
  94. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-D2XWw6_m.js → sankeyDiagram-W5VNT64P-DnSvNCG8.js} +1 -1
  95. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DzxKiug4.js → sequenceDiagram-SI44F4Z6-BiZh6f-R.js} +1 -1
  96. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWUqK5Us.js → sizeCapture-X5ZJPWSS-BKXSI8WB.js} +1 -1
  97. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-D5Pa4O5g.js → stateDiagram-OKZ733FA-CfSgoYFm.js} +1 -1
  98. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DgHi3vjb.js +1 -0
  99. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-VH7cRnsJ.js → swimlanes-SLNWSIFB-CyGADcHe.js} +2 -2
  100. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bgvdt1Y0.js +8 -0
  101. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-C4HM5NoZ.js → timeline-definition-Z64GVDOM-HmPaSPOK.js} +1 -1
  102. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Bu-4hOlA.js → vennDiagram-T6HMQDX7-VA2VBbez.js} +1 -1
  103. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CasFEvvl.js → wardleyDiagram-T6FBY63Y-BxjiBPZa.js} +1 -1
  104. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D-gpwPSA.js → xychartDiagram-ELKLHX3M--hUY-qFj.js} +1 -1
  105. package/dist/worker/console/static/index.html +2 -2
  106. package/dist/worker/console/static/pets/ember/pet.json +167 -0
  107. package/dist/worker/console/static/pets/ember/spritesheet.png +0 -0
  108. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +4 -3
  109. package/dist/worker/console/static-src/operator-chat/use-stat-dialog.js +95 -0
  110. package/dist/worker/console/workspace-context.js +14 -0
  111. package/dist/worker/console/workspace-initialization.js +25 -16
  112. package/dist/worker/delivery/package.js +4 -1
  113. package/dist/worker/observe/static/operator-chrome.js +1 -42
  114. package/dist/workflows/dag/backend-test-pytest-collection.js +4 -1
  115. package/dist/workflows/dag/final-verification.js +45 -10
  116. package/dist/workflows/dag/hybrid/sources.js +14 -14
  117. package/dist/workflows/dag/hybrid/templates/backend-test.js +5 -5
  118. package/dist/workflows/dag/hybrid/templates/frontend-test.js +4 -4
  119. package/dist/workflows/dag/hybrid/templates/kg-bootstrap.js +6 -6
  120. package/dist/workflows/dag/hybrid/templates/knowledge-sync.js +5 -5
  121. package/dist/workflows/dag/skill-instructions.js +102 -32
  122. package/dist/workflows/dag/skill-snapshot.js +3 -0
  123. package/dist/workflows/dag/skills.js +7 -7
  124. package/dist/workflows/dag/validate.js +2 -2
  125. package/dist/workflows/dag/workspace-checkpoint.js +13 -1
  126. package/docs/architecture/runtime-boundaries.md +28 -22
  127. package/docs/init-surface.manifest.json +43 -295
  128. package/docs/templates/agent-dag.base.json +9 -9
  129. package/docs/templates/agent-dag.final-verification.json +6 -6
  130. package/docs/templates/agent-dag.supervised-implementation.json +12 -12
  131. package/docs/templates/frontend-implementation-dag.json +2 -2
  132. package/docs/templates/frontend-test-dag.json +2 -2
  133. package/docs/templates/hybrid-dag.json +9 -9
  134. package/docs/templates/knowledge-sync-dag.json +8 -8
  135. package/package.json +6 -2
  136. package/resource-catalog.v1.json +1973 -0
  137. package/resources/agent-bridge/loop-agent/SKILL.md +40 -0
  138. package/resources/init-history/legacy-surface.v1.json +33 -0
  139. package/skills/init-update/SKILL.md +34 -0
  140. package/skills/init-update/agents/openai.yaml +6 -0
  141. package/dist/worker/console/static/assets/channel-DiTNLZUm.js +0 -1
  142. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DgL8aiyZ.js +0 -1
  143. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DgL8aiyZ.js +0 -1
  144. package/dist/worker/console/static/assets/index-C_8_gWal.css +0 -1
  145. package/dist/worker/console/static/assets/index-DybcVXCu.js +0 -488
  146. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-C0QIbqO4.js +0 -1
  147. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BB9CzRHB.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,17 +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
892
  ".playwright-cli/",
876
- ".agents/",
877
893
  ".task-pool/",
878
894
  ".worktrees/",
879
- `${governanceRoot}/`,
880
- "scripts/",
881
895
  "harness.json",
882
896
  GITIGNORE_BLOCK_END,
883
897
  ].join("\n");
@@ -923,16 +937,8 @@ function buildHarness(input) {
923
937
  delete pi.defaultModel;
924
938
  mergedExecutors.pi = pi;
925
939
  }
926
- const harness = {
927
- ...input.existing,
928
- $schema: expectedHarnessSchemaRef(input.governanceRoot),
929
- version: 1,
930
- project: input.projectName,
931
- adapter: "loop-agent",
932
- governanceRoot: input.governanceRoot,
933
- features: mergeRecord(input.template.features, input.existing.features),
934
- workflowPolicy: mergeRecord(input.template.workflowPolicy, input.existing.workflowPolicy),
935
- entrypoints: mergeRecord(input.existing.entrypoints, {
940
+ const legacyControllerMaps = {
941
+ entrypoints: {
936
942
  readme: "README.md",
937
943
  agents: "AGENTS.md",
938
944
  governanceIndex: `${input.governanceRoot}/README.md`,
@@ -940,16 +946,16 @@ function buildHarness(input) {
940
946
  workflow: `${input.governanceRoot}/feature-workflow.md`,
941
947
  verificationMatrix: `${input.governanceRoot}/verification-matrix.md`,
942
948
  loopAgentHarness: `${input.governanceRoot}/loop-agent-harness.md`,
943
- }),
944
- artifacts: mergeRecord(input.existing.artifacts, {
949
+ },
950
+ artifacts: {
945
951
  templatesDir: `${input.governanceRoot}/templates`,
946
952
  execPlansDir: `${input.governanceRoot}/exec-plans`,
947
953
  progressDir: `${input.governanceRoot}/progress`,
948
954
  reportsDir: `${input.governanceRoot}/reports`,
949
955
  decisionsDir: `${input.governanceRoot}/decisions`,
950
956
  taskPoolDir: ".harness/task-pool",
951
- }),
952
- scripts: mergeRecord(input.existing.scripts, {
957
+ },
958
+ scripts: {
953
959
  checkEngineeringStructure: "scripts/check-engineering-structure.sh",
954
960
  checkDocIndex: "scripts/check-doc-index.sh",
955
961
  checkDocLinks: "scripts/check-doc-links.sh",
@@ -965,7 +971,38 @@ function buildHarness(input) {
965
971
  quickVerify: "bash scripts/check-repo.sh",
966
972
  standardVerify: "bash scripts/ci-governance.sh",
967
973
  fullVerify: "bash scripts/ci.sh",
968
- }),
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
+ : {}),
969
1006
  executors: mergedExecutors,
970
1007
  };
971
1008
  // Fresh init owns only the current DAG-facing surface. Legacy step routing is
@@ -1002,6 +1039,16 @@ async function writeText(input) {
1002
1039
  await writeFile(target, input.content, "utf-8");
1003
1040
  input.written.push(input.relativePath);
1004
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
+ }
1005
1052
  async function copyFileIfMissing(input) {
1006
1053
  const source = path.join(input.assetRoot, input.sourceRelativePath);
1007
1054
  const target = path.join(input.repoRoot, input.targetRelativePath);
@@ -1203,38 +1250,21 @@ async function readPackageVersion(assetRoot) {
1203
1250
  async function readInitSurfaceManifest(assetRoot) {
1204
1251
  const manifestPath = path.join(assetRoot, "docs", "init-surface.manifest.json");
1205
1252
  const raw = await readFile(manifestPath, "utf-8");
1206
- const manifest = JSON.parse(raw);
1207
- const initFullRequired = manifest.initFullRequired ?? [];
1208
- const initSurface = manifest.initSurface ?? {};
1209
- const seenPaths = new Set();
1210
- const entries = [];
1211
- for (const relativePath of initFullRequired) {
1212
- if (seenPaths.has(relativePath))
1213
- continue;
1214
- seenPaths.add(relativePath);
1215
- entries.push({
1216
- path: relativePath,
1217
- mode: initSurface[relativePath] ?? inferInitSurfaceMode(relativePath),
1218
- });
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");
1219
1256
  }
1220
- // Fresh `init --profile full` copies the entire `docs/templates/` and
1221
- // `skills/` directories via copyDirMerge. Every file actually projected to a
1222
- // target project must be represented in the desired init surface so that
1223
- // check-update can detect missing or stale copies during controller upgrades.
1224
- // The static manifest intentionally only pins explicit modes for the subset
1225
- // that needs them; the rest are auto-discovered here as "copied" entries.
1226
- for (const discovered of await discoverCopiedSurfaceFiles(assetRoot)) {
1227
- if (seenPaths.has(discovered))
1228
- continue;
1229
- seenPaths.add(discovered);
1230
- entries.push({
1231
- path: discovered,
1232
- mode: initSurface[discovered] ?? inferInitSurfaceMode(discovered),
1233
- });
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 });
1234
1263
  }
1235
- if (!entries.some((entry) => entry.path === INIT_SURFACE_STATE_PATH)) {
1236
- 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");
1237
1266
  }
1267
+ const manifest = parsed;
1238
1268
  return { manifest, raw, sha256: sha256Text(raw), entries };
1239
1269
  }
1240
1270
  /**
@@ -1296,6 +1326,15 @@ function inferInitSurfaceMode(relativePath) {
1296
1326
  return "copied";
1297
1327
  }
1298
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
+ }
1299
1338
  const manifestPath = targetPathToManifestPath(input.entry.path, input.governanceRoot);
1300
1339
  if (input.entry.mode === "directory" || input.entry.mode === "state")
1301
1340
  return {};
@@ -1400,7 +1439,9 @@ async function readExistingSurfaceState(repoRoot) {
1400
1439
  if (!(await exists(filePath)))
1401
1440
  return undefined;
1402
1441
  const parsed = JSON.parse(await readFile(filePath, "utf-8"));
1403
- if (!isRecord(parsed) || parsed.schemaVersion !== 1)
1442
+ if (!isRecord(parsed) ||
1443
+ (parsed.schemaVersion !== 1 && parsed.schemaVersion !== 2) ||
1444
+ !isRecord(parsed.files))
1404
1445
  return undefined;
1405
1446
  return parsed;
1406
1447
  }
@@ -1635,21 +1676,37 @@ async function buildCurrentSurfaceState(input) {
1635
1676
  };
1636
1677
  }
1637
1678
  return {
1638
- schemaVersion: 1,
1679
+ schemaVersion: 2,
1639
1680
  controllerVersion,
1640
1681
  stateKind: input.stateKind,
1641
1682
  generatedAt: input.generatedAt ?? new Date().toISOString(),
1642
1683
  manifestSha256: manifest.sha256,
1643
1684
  files,
1685
+ dispositions: [],
1686
+ actionReceipts: [],
1644
1687
  };
1645
1688
  }
1646
1689
  async function writeInitSurfaceState(input) {
1647
1690
  const state = await buildCurrentSurfaceState(input);
1648
- 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) {
1649
1701
  for (const [pathName, file] of Object.entries(state.files)) {
1650
1702
  if (file.relationship !== "local-existing-unknown")
1651
1703
  continue;
1652
1704
  const priorFile = input.preserveOwnershipFrom.files[pathName];
1705
+ if (priorFile?.acceptedMerge) {
1706
+ file.acceptedMerge = { ...priorFile.acceptedMerge };
1707
+ }
1708
+ if (input.stateKind !== "recorded")
1709
+ continue;
1653
1710
  const priorHash = priorFile?.currentSha256;
1654
1711
  if (priorHash !== undefined) {
1655
1712
  file.currentSha256 = priorHash;
@@ -1657,10 +1714,9 @@ async function writeInitSurfaceState(input) {
1657
1714
  else {
1658
1715
  delete file.currentSha256;
1659
1716
  }
1660
- if (priorFile?.acceptedMerge) {
1661
- file.acceptedMerge = { ...priorFile.acceptedMerge };
1662
- }
1663
1717
  }
1718
+ }
1719
+ if (input.stateKind === "recorded" && input.preserveOwnershipFrom) {
1664
1720
  for (const [pathName, priorFile] of Object.entries(input.preserveOwnershipFrom.files)) {
1665
1721
  if (state.files[pathName] || priorFile.mode === "state")
1666
1722
  continue;
@@ -1685,6 +1741,13 @@ async function writeInitSurfaceState(input) {
1685
1741
  }
1686
1742
  const target = path.join(input.repoRoot, INIT_SURFACE_STATE_PATH);
1687
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
+ }
1688
1751
  await writeFile(target, `${JSON.stringify(state, null, 2)}\n`, "utf-8");
1689
1752
  return state;
1690
1753
  }
@@ -1903,7 +1966,10 @@ async function collectRetiredLayoutActions(input) {
1903
1966
  const retiredOwnedPaths = [];
1904
1967
  const currentPaths = new Set(Object.keys(input.currentState.files));
1905
1968
  const recordedPaths = new Set(Object.keys(input.recordedState.files));
1969
+ const disposedPaths = new Set((input.recordedState.dispositions ?? []).map((receipt) => receipt.path));
1906
1970
  for (const [oldPath, oldState] of Object.entries(input.recordedState.files)) {
1971
+ if (disposedPaths.has(oldPath))
1972
+ continue;
1907
1973
  const targetPath = normalizeRelativePath(manifestPathToTargetPath(oldPath, input.governanceRoot));
1908
1974
  if (targetPath === oldPath || !currentPaths.has(targetPath))
1909
1975
  continue;
@@ -1969,6 +2035,8 @@ async function collectRetiredLayoutActions(input) {
1969
2035
  for (const legacyRoot of ["docs", "skills"]) {
1970
2036
  for (const relativePath of await listRelativeFiles(path.join(input.repoRoot, legacyRoot))) {
1971
2037
  const oldPath = `${legacyRoot}/${relativePath}`;
2038
+ if (disposedPaths.has(oldPath))
2039
+ continue;
1972
2040
  if (recordedPaths.has(oldPath))
1973
2041
  continue;
1974
2042
  const targetPath = normalizeRelativePath(manifestPathToTargetPath(oldPath, input.governanceRoot));
@@ -2523,84 +2591,45 @@ function buildGovernanceDirectoryReadme(input) {
2523
2591
  }
2524
2592
  export function buildInitInstructions(input) {
2525
2593
  const projectName = input.projectName ?? path.basename(path.resolve(input.repoRoot));
2526
- const governanceRoot = input.governanceRoot ?? DEFAULT_GOVERNANCE_ROOT;
2527
2594
  return [
2528
2595
  "# loop-agent Initialization Instructions",
2529
2596
  "",
2530
- `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}\`.`,
2531
2598
  "",
2532
- "## Confirm With User",
2599
+ "## Project Inputs",
2533
2600
  "",
2534
2601
  `- Project name: default \`${projectName}\`.`,
2535
- `- Governance root: default \`${governanceRoot}\`.`,
2536
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.",
2537
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.",
2538
- "- 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.",
2539
- "- 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.",
2540
2605
  "",
2541
2606
  "## Initialization Upgrade Routing",
2542
2607
  "",
2543
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.",
2544
2609
  "Explicit `检查初始化更新`, `初始化更新校验`, or `只检查,不要修改` stays strictly read-only: run only `loop-agent init check-update --repo-root . --markdown` and do not create an upgrade run.",
2545
- "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.",
2546
- "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",
2547
2614
  "",
2548
- "## 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.",
2549
2619
  "",
2550
- '- 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.",
2551
2623
  `- ${DAG_HARD_GATE_TRIGGER}`,
2552
- "- 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.",
2553
- "- 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.",
2554
- "- Do not assume the target project is TypeScript, Node.js, frontend, backend, or any other specific stack.",
2555
- "- `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.",
2556
- "- Default verification entries: quick `bash scripts/check-repo.sh`, standard `bash scripts/ci-governance.sh`, full `bash scripts/ci.sh`.",
2557
- `- 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.`,
2558
- "- 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.",
2559
- "- 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.",
2560
- `- 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.`,
2561
- "- 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.",
2562
- "- 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.",
2563
- "- Do not copy examples by default; examples stay bundled in the tool and are available through `loop-agent examples`.",
2564
- "- Add or update a loop-agent managed block in AGENTS.md.",
2565
- "- 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.",
2566
- "- 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.",
2567
- "- Create `.harness/` runtime directories but do not create historical run facts.",
2568
- "",
2569
- "## Automatic Workflow For Model-Executed Init",
2570
- "",
2571
- "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.",
2572
- "",
2573
- "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.",
2574
- "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.",
2575
- "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.",
2576
- "4. Run deterministic init with all three tier flags and merge defaults.",
2577
- "5. Continue immediately into project inspection and adaptation.",
2578
- "6. Finish by running the init health checks and quick verification; `init doctor` must confirm the Pi model matrix before any DAG is started.",
2579
- "",
2580
- "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.",
2581
- "",
2582
- "## Required Model Adaptation",
2583
- "",
2584
- "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.",
2585
- "",
2586
- "Required post-init steps:",
2587
- "",
2588
- "1. Read the target project's existing README, manifest/build/config files, and top-level source/module directories.",
2589
- "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.",
2590
- `3. Update \`${governanceRoot}/verification-matrix.md\` with the target project's actual quick, standard, and full verification commands.`,
2591
- `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.`,
2592
- "5. Update `scripts/ci-tests.sh` only when the conservative generated detector is insufficient for the target project.",
2593
- "6. Run `loop-agent init doctor`, `loop-agent inspect`, `loop-agent docs audit`, and the quick verification command.",
2594
- "",
2595
- "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.",
2596
2626
  "",
2597
2627
  "## Suggested Command",
2598
2628
  "",
2599
2629
  "```bash",
2600
- `loop-agent init --repo-root ${input.repoRoot} --profile full --merge`,
2630
+ `loop-agent init --repo-root ${input.repoRoot} --merge`,
2601
2631
  "loop-agent init doctor --repo-root <target>",
2602
- "loop-agent inspect --repo-root <target>",
2603
- "loop-agent docs audit --repo-root <target>",
2632
+ "loop-agent --repo-root <target> operator guide --json",
2604
2633
  "```",
2605
2634
  ].join("\n");
2606
2635
  }
@@ -2608,8 +2637,6 @@ export async function initializeLoopAgentProject(options) {
2608
2637
  const repoRoot = path.resolve(options.repoRoot);
2609
2638
  const projectName = options.projectName ?? path.basename(repoRoot);
2610
2639
  const governanceRoot = options.governanceRoot ?? DEFAULT_GOVERNANCE_ROOT;
2611
- const profile = options.profile ?? "full";
2612
- const merge = options.merge ?? true;
2613
2640
  const model = normalizeInitModelReference(options);
2614
2641
  const modelMatrix = options.modelMatrix;
2615
2642
  const written = [];
@@ -2627,21 +2654,6 @@ export async function initializeLoopAgentProject(options) {
2627
2654
  }
2628
2655
  }
2629
2656
  await mkdir(repoRoot, { recursive: true });
2630
- const readmePath = path.join(repoRoot, "README.md");
2631
- const existingReadme = (await exists(readmePath))
2632
- ? await readFile(readmePath, "utf-8")
2633
- : undefined;
2634
- const readmeContent = existingReadme
2635
- ? mergeManagedBlock(existingReadme, buildManagedReadmeBlock({ projectName, governanceRoot }))
2636
- : buildTargetReadme({ projectName, governanceRoot });
2637
- await writeText({
2638
- repoRoot,
2639
- relativePath: "README.md",
2640
- content: readmeContent,
2641
- merge: true,
2642
- written,
2643
- skipped,
2644
- });
2645
2657
  const harness = buildHarness({
2646
2658
  existing: existingHarness,
2647
2659
  template,
@@ -2650,27 +2662,10 @@ export async function initializeLoopAgentProject(options) {
2650
2662
  model,
2651
2663
  modelMatrix,
2652
2664
  });
2653
- await writeText({
2665
+ await writeTextIfChanged({
2654
2666
  repoRoot,
2655
2667
  relativePath: "harness.json",
2656
2668
  content: `${JSON.stringify(harness, null, 2)}\n`,
2657
- merge: true,
2658
- written,
2659
- skipped,
2660
- });
2661
- const agentsPath = path.join(repoRoot, "AGENTS.md");
2662
- const existingAgents = (await exists(agentsPath))
2663
- ? await readFile(agentsPath, "utf-8")
2664
- : `# AGENTS.md\n`;
2665
- await writeText({
2666
- repoRoot,
2667
- relativePath: "AGENTS.md",
2668
- content: mergeManagedBlock(existingAgents, await buildManagedAgentsBlock({
2669
- assetRoot,
2670
- projectName,
2671
- governanceRoot,
2672
- })),
2673
- merge: true,
2674
2669
  written,
2675
2670
  skipped,
2676
2671
  });
@@ -2678,122 +2673,45 @@ export async function initializeLoopAgentProject(options) {
2678
2673
  const existingGitignore = (await exists(gitignorePath))
2679
2674
  ? await readFile(gitignorePath, "utf-8")
2680
2675
  : "";
2681
- await writeText({
2676
+ await writeTextIfChanged({
2682
2677
  repoRoot,
2683
2678
  relativePath: ".gitignore",
2684
2679
  content: mergeGitignoreManagedBlock(existingGitignore, buildManagedGitignoreBlock(governanceRoot)),
2685
- merge: true,
2686
2680
  written,
2687
2681
  skipped,
2688
2682
  });
2689
- for (const doc of CORE_DOC_FILES) {
2690
- const generated = buildGeneratedCoreDoc({
2691
- doc: doc.target,
2692
- projectName,
2693
- governanceRoot,
2694
- });
2695
- if (generated) {
2696
- await writeTextIfMissing({
2697
- repoRoot,
2698
- relativePath: path.join(governanceRoot, doc.target),
2699
- content: generated,
2700
- merge,
2701
- written,
2702
- skipped,
2703
- });
2704
- continue;
2705
- }
2706
- await copyFileIfMissing({
2707
- assetRoot,
2708
- repoRoot,
2709
- sourceRelativePath: path.join("docs", doc.source),
2710
- targetRelativePath: path.join(governanceRoot, doc.target),
2711
- merge,
2712
- written,
2713
- skipped,
2714
- });
2715
- }
2716
- for (const dir of GOVERNANCE_README_DIRS) {
2717
- await writeTextIfMissing({
2718
- repoRoot,
2719
- relativePath: path.join(governanceRoot, dir, "README.md"),
2720
- content: buildGovernanceDirectoryReadme({ dir, projectName }),
2721
- merge,
2722
- written,
2723
- skipped,
2724
- });
2725
- }
2726
- await copyDirMerge({
2683
+ const bridgeEntry = {
2684
+ path: ".agents/skills/loop-agent/SKILL.md",
2685
+ mode: "copied",
2686
+ };
2687
+ const bridge = await buildDesiredSurfaceContent({
2727
2688
  assetRoot,
2728
2689
  repoRoot,
2729
- sourceRelativePath: "docs/templates",
2730
- targetRelativePath: path.join(governanceRoot, "templates"),
2731
- written,
2732
- });
2733
- await writeTextIfMissing({
2734
- repoRoot,
2735
- relativePath: path.join(governanceRoot, "spec-registry.json"),
2736
- content: buildSpecRegistry(),
2737
- merge,
2738
- written,
2739
- skipped,
2690
+ projectName,
2691
+ governanceRoot,
2692
+ entry: bridgeEntry,
2740
2693
  });
2741
- if (profile === "full") {
2742
- // Project repo-local skills live only under `.agents/skills/`.
2743
- // The package still ships `skills/` as the built-in fallback source.
2744
- await copyDirMerge({
2745
- assetRoot,
2746
- repoRoot,
2747
- sourceRelativePath: "skills",
2748
- targetRelativePath: ".agents/skills",
2749
- written,
2750
- });
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);
2751
2704
  }
2752
2705
  else {
2753
- skipped.push(".agents/skills/");
2754
- }
2755
- skipped.push("examples/");
2756
- for (const [relativePath, content] of Object.entries(buildInitScriptFiles(governanceRoot))) {
2757
- await writeText({
2706
+ await writeTextIfChanged({
2758
2707
  repoRoot,
2759
- relativePath,
2760
- content,
2761
- merge: true,
2708
+ relativePath: bridgeEntry.path,
2709
+ content: bridge.content,
2762
2710
  written,
2763
2711
  skipped,
2764
2712
  });
2765
2713
  }
2766
- await ensureHarnessDirs(repoRoot, written);
2767
- await writeCompatPrompts({ repoRoot, merge, written, skipped });
2768
- const clientRecoveryMode = options.clientRecovery ?? "auto";
2769
- const clientRecovery = clientRecoveryMode === "off"
2770
- ? undefined
2771
- : await runClientRecovery({
2772
- repoRoot,
2773
- mode: clientRecoveryMode,
2774
- });
2775
- const clientRecoveryPaths = [
2776
- OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH,
2777
- OPENCODE_CONTEXT_OVERFLOW_COMPACT_PLUGIN_PATH,
2778
- PI_CONTEXT_OVERFLOW_EXTENSION_PATH,
2779
- PI_PROJECT_SETTINGS_PATH,
2780
- ];
2781
- if (clientRecoveryMode === "off") {
2782
- for (const recoveryPath of clientRecoveryPaths)
2783
- skipped.push(recoveryPath);
2784
- }
2785
- else if (clientRecovery) {
2786
- const installResults = [
2787
- clientRecovery.plugin,
2788
- clientRecovery.overflowPlugin,
2789
- clientRecovery.piExtension,
2790
- { written: clientRecovery.pi?.wrote === true },
2791
- ];
2792
- for (const [index, recoveryPath] of clientRecoveryPaths.entries()) {
2793
- if (installResults[index]?.written)
2794
- written.push(recoveryPath);
2795
- }
2796
- }
2714
+ const stateBefore = await fileSha256IfExists(path.join(repoRoot, INIT_SURFACE_STATE_PATH));
2797
2715
  await writeInitSurfaceState({
2798
2716
  repoRoot,
2799
2717
  projectName,
@@ -2801,15 +2719,18 @@ export async function initializeLoopAgentProject(options) {
2801
2719
  stateKind: "recorded",
2802
2720
  acceptCurrentHarnessAsMerge: model !== undefined || modelMatrix !== undefined,
2803
2721
  });
2804
- 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);
2805
2727
  return {
2806
2728
  repoRoot,
2807
2729
  projectName,
2808
2730
  governanceRoot,
2809
- profile,
2731
+ profile: "minimal",
2810
2732
  written,
2811
2733
  skipped,
2812
- clientRecovery,
2813
2734
  };
2814
2735
  }
2815
2736
  function actionForMissing(pathName, state) {
@@ -2878,13 +2799,17 @@ function recommendedNextFor(report) {
2878
2799
  next.push("loop-agent init update --repo-root <target> --bootstrap-surface");
2879
2800
  }
2880
2801
  if (report.deterministicActions.some((action) => action.type !== "bootstrap-surface")) {
2881
- 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");
2882
2805
  }
2883
2806
  if (report.modelMergeTasks.length > 0) {
2884
2807
  next.push("loop-agent init check-update --repo-root <target> --markdown");
2885
2808
  }
2886
2809
  if (report.humanDecisions.length > 0) {
2887
- 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.");
2888
2813
  }
2889
2814
  if (next.length === 0)
2890
2815
  next.push("No init update actions are needed.");
@@ -2906,9 +2831,8 @@ export async function checkInitUpdate(input) {
2906
2831
  projectName: input.projectName,
2907
2832
  governanceRoot: input.governanceRoot,
2908
2833
  });
2909
- const clientRecoveryMode = input.clientRecovery ?? "auto";
2910
- const skipClientRecoverySurfaces = clientRecoveryMode === "off";
2911
2834
  const recordedState = await readExistingSurfaceState(repoRoot);
2835
+ const disposedPaths = new Set((recordedState?.dispositions ?? []).map((receipt) => receipt.path));
2912
2836
  const assetRoot = await findPackageRoot();
2913
2837
  const surfaceState = recordedState?.stateKind ?? "missing";
2914
2838
  const currentState = await buildCurrentSurfaceState({
@@ -2947,10 +2871,6 @@ export async function checkInitUpdate(input) {
2947
2871
  });
2948
2872
  }
2949
2873
  for (const [pathName, state] of Object.entries(currentState.files)) {
2950
- // Mirror fresh init: client-recovery=off never reinstalls recovery surfaces.
2951
- if (skipClientRecoverySurfaces && isClientRecoverySurfacePath(pathName)) {
2952
- continue;
2953
- }
2954
2874
  // `.pi/settings.json` is a structured project configuration: fill only
2955
2875
  // absent nested recovery fields and preserve every explicit project value.
2956
2876
  if (pathName === PI_PROJECT_SETTINGS_PATH &&
@@ -2985,6 +2905,30 @@ export async function checkInitUpdate(input) {
2985
2905
  continue;
2986
2906
  }
2987
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
+ }
2988
2932
  const acceptedMerge = recordedState?.files[pathName]?.acceptedMerge;
2989
2933
  if (acceptedMerge) {
2990
2934
  // Generated acceptance is bound to the stable source anchor so that
@@ -3039,11 +2983,29 @@ export async function checkInitUpdate(input) {
3039
2983
  if (recordedState) {
3040
2984
  for (const [pathName, recordedFile] of Object.entries(recordedState.files)) {
3041
2985
  if (currentState.files[pathName] ||
2986
+ disposedPaths.has(pathName) ||
3042
2987
  retiredActionPaths.has(pathName) ||
3043
2988
  recordedFile.mode === "state" ||
3044
2989
  recordedFile.mode === "directory") {
3045
2990
  continue;
3046
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
+ }
3047
3009
  const currentSha256 = await fileSha256IfExists(path.join(repoRoot, pathName));
3048
3010
  if (!currentSha256)
3049
3011
  continue;
@@ -3074,15 +3036,6 @@ export async function checkInitUpdate(input) {
3074
3036
  const harness = JSON.parse(await readFile(harnessPath, "utf-8"));
3075
3037
  if (isRecord(harness))
3076
3038
  currentHarness = harness;
3077
- if (recordedState &&
3078
- isRecord(harness) &&
3079
- harness.$schema !== expectedHarnessSchemaRef(governanceRoot)) {
3080
- deterministicActions.push({
3081
- type: "add-harness-schema-ref",
3082
- path: "harness.json",
3083
- reason: "harness.json is missing or has a stale IDE JSON Schema reference",
3084
- });
3085
- }
3086
3039
  if (recordedState &&
3087
3040
  isRecord(harness) &&
3088
3041
  governanceRoot !== "docs" &&
@@ -3122,7 +3075,6 @@ export async function checkInitUpdate(input) {
3122
3075
  mergeTask.verification = [
3123
3076
  "loop-agent init check-update --repo-root . --json",
3124
3077
  "loop-agent inspect --repo-root .",
3125
- "bash scripts/check-repo.sh",
3126
3078
  ];
3127
3079
  const existingTask = modelMergeTasks.findIndex((task) => task.path === "harness.json");
3128
3080
  if (existingTask >= 0)
@@ -3142,8 +3094,13 @@ export async function checkInitUpdate(input) {
3142
3094
  entry: { path: "harness.json", mode: "generated" },
3143
3095
  });
3144
3096
  if (desired.content !== undefined) {
3145
- const desiredHarness = JSON.parse(desired.content);
3146
- 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
+ });
3147
3104
  if (!isDeepStrictEqual(reconciled, harness)) {
3148
3105
  deterministicActions.push({
3149
3106
  type: "reconcile-harness-structure",
@@ -3171,6 +3128,7 @@ export async function checkInitUpdate(input) {
3171
3128
  modelReadiness?.status === "reselection-required") {
3172
3129
  const state = currentState.files["harness.json"];
3173
3130
  const task = modelMergeTaskFor("harness.json", state, allPaths, recordedState);
3131
+ task.purpose = "pi-model-matrix-selection";
3174
3132
  task.reason =
3175
3133
  modelReadiness.status === "selection-required"
3176
3134
  ? "harness.json has no complete Pi model matrix that resolves against the current Pi model catalog"
@@ -3179,14 +3137,13 @@ export async function checkInitUpdate(input) {
3179
3137
  `Preserve these currently resolvable tiers byte-for-byte: ${modelReadiness.preserveTiers.join(", ") || "none"}.`,
3180
3138
  `Select replacements only for: ${modelReadiness.reselectTiers.join(", ")}.`,
3181
3139
  "Choose replacements from eligibleReferences using target-project needs, capability, context window, latency, and cost; tiers may intentionally share a model.",
3182
- "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.",
3183
3141
  "Do not add credentials or raw Pi configuration to harness.json.",
3184
3142
  ];
3185
3143
  task.verification = [
3186
3144
  "loop-agent init model-catalog --repo-root . --json",
3187
3145
  "loop-agent init check-update --repo-root . --json",
3188
3146
  "loop-agent init doctor --repo-root .",
3189
- "bash scripts/check-repo.sh",
3190
3147
  ];
3191
3148
  const existingTask = modelMergeTasks.findIndex((candidate) => candidate.path === "harness.json");
3192
3149
  if (existingTask >= 0)
@@ -3208,6 +3165,34 @@ export async function checkInitUpdate(input) {
3208
3165
  reason,
3209
3166
  });
3210
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);
3211
3196
  const partial = {
3212
3197
  repoRoot,
3213
3198
  controllerVersion: currentState.controllerVersion,
@@ -3215,6 +3200,9 @@ export async function checkInitUpdate(input) {
3215
3200
  deterministicActions,
3216
3201
  modelMergeTasks,
3217
3202
  humanDecisions,
3203
+ coreState: coreBlocked ? "blocked" : "ready",
3204
+ migrationState: legacyAudit?.migrationState ?? recordedState?.legacyAudit?.migrationState ?? "clean",
3205
+ ...(legacyAudit ? { legacyAudit } : {}),
3218
3206
  ...(modelReadiness ? { modelReadiness } : {}),
3219
3207
  summary: {
3220
3208
  missing: deterministicActions.filter((action) => action.type !== "bootstrap-surface").length,
@@ -3239,6 +3227,16 @@ export async function checkInitUpdate(input) {
3239
3227
  },
3240
3228
  };
3241
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
+ }
3242
3240
  async function applySafeAction(input) {
3243
3241
  if (input.action.type === "merge-pi-project-settings") {
3244
3242
  return (await applyProjectPiSettingsMerge({ repoRoot: input.repoRoot })).wrote === true;
@@ -3334,6 +3332,13 @@ async function applySafeAction(input) {
3334
3332
  return true;
3335
3333
  }
3336
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
+ }
3337
3342
  const target = path.join(input.repoRoot, input.action.path);
3338
3343
  if (!(await exists(target)))
3339
3344
  return false;
@@ -3344,6 +3349,28 @@ async function applySafeAction(input) {
3344
3349
  await rm(target, { force: true });
3345
3350
  return true;
3346
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
+ }
3347
3374
  if (input.action.type === "reconcile-harness-structure") {
3348
3375
  const target = path.join(input.repoRoot, "harness.json");
3349
3376
  if (!(await exists(target)))
@@ -3361,7 +3388,13 @@ async function applySafeAction(input) {
3361
3388
  });
3362
3389
  if (desired.content === undefined)
3363
3390
  return false;
3364
- 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
+ });
3365
3398
  if (isDeepStrictEqual(next, current))
3366
3399
  return false;
3367
3400
  await writeFile(target, `${JSON.stringify(next, null, 2)}\n`, "utf-8");
@@ -3426,6 +3459,13 @@ async function applySafeAction(input) {
3426
3459
  await writeFile(target, desired.content, "utf-8");
3427
3460
  return true;
3428
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
+ }
3429
3469
  if (await exists(target))
3430
3470
  return false;
3431
3471
  await writeFile(target, desired.content, "utf-8");
@@ -3440,7 +3480,6 @@ export async function applyInitUpdate(input) {
3440
3480
  });
3441
3481
  const applied = [];
3442
3482
  const skipped = [];
3443
- const clientRecoveryMode = input.clientRecovery ?? "auto";
3444
3483
  const requestedModelMatrix = input.modelMatrix ??
3445
3484
  (input.model
3446
3485
  ? { LOW: input.model, MED: input.model, HIGH: input.model }
@@ -3502,6 +3541,8 @@ export async function applyInitUpdate(input) {
3502
3541
  }
3503
3542
  if (input.applySafe) {
3504
3543
  const existingSurface = await readExistingSurfaceState(repoRoot);
3544
+ const actionReceipts = [];
3545
+ const dispositions = [];
3505
3546
  // Preserve source strength: recorded stays recorded so unchanged owned files
3506
3547
  // remain deterministic refresh candidates; bootstrap/inferred stays inferred.
3507
3548
  const preservedStateKind = existingSurface?.stateKind === "recorded"
@@ -3511,9 +3552,8 @@ export async function applyInitUpdate(input) {
3511
3552
  repoRoot,
3512
3553
  projectName,
3513
3554
  governanceRoot,
3514
- clientRecovery: clientRecoveryMode,
3515
- homeDir: input.homeDir,
3516
3555
  modelCatalog: input.modelCatalog,
3556
+ legacyAudit: input.legacyAudit,
3517
3557
  });
3518
3558
  for (const action of report.deterministicActions) {
3519
3559
  if (action.type === "bootstrap-surface") {
@@ -3523,8 +3563,35 @@ export async function applyInitUpdate(input) {
3523
3563
  // The report was produced immediately before this batch. Each action also
3524
3564
  // performs its own target check; missing-file writes refuse existing paths,
3525
3565
  // while managed blocks merge into the latest user-authored content.
3526
- 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 })) {
3527
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
+ }
3528
3595
  else
3529
3596
  skipped.push(action);
3530
3597
  }
@@ -3534,14 +3601,11 @@ export async function applyInitUpdate(input) {
3534
3601
  governanceRoot,
3535
3602
  stateKind: preservedStateKind,
3536
3603
  preserveOwnershipFrom: existingSurface,
3537
- });
3538
- }
3539
- // Pi user config is only mutated with explicit --client-recovery=user.
3540
- // Do not reinstall the project plugin here — that would bypass ownership and
3541
- // clobber user-modified plugins; plugin updates stay on deterministic actions.
3542
- if (clientRecoveryMode === "user") {
3543
- await applyPiRetryMerge({
3544
- 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
+ : {}),
3545
3609
  });
3546
3610
  }
3547
3611
  return {
@@ -3551,12 +3615,313 @@ export async function applyInitUpdate(input) {
3551
3615
  repoRoot,
3552
3616
  projectName,
3553
3617
  governanceRoot,
3554
- clientRecovery: clientRecoveryMode,
3555
- homeDir: input.homeDir,
3556
3618
  modelCatalog: input.modelCatalog,
3619
+ legacyAudit: input.legacyAudit,
3557
3620
  }),
3558
3621
  };
3559
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
+ }
3560
3925
  function formatClientRecoverySummary(report) {
3561
3926
  if (!report.clientRecovery?.pi)
3562
3927
  return [];
@@ -3582,6 +3947,8 @@ function formatCheckUpdateText(report) {
3582
3947
  `loop-agent init update check for ${report.repoRoot}`,
3583
3948
  `controllerVersion: ${report.controllerVersion}`,
3584
3949
  `surfaceState: ${report.surfaceState}`,
3950
+ `coreState: ${report.coreState}`,
3951
+ `migrationState: ${report.migrationState}`,
3585
3952
  `deterministicActions: ${report.deterministicActions.length}`,
3586
3953
  `modelMergeTasks: ${report.modelMergeTasks.length}`,
3587
3954
  `humanDecisions: ${report.humanDecisions.length}`,
@@ -3599,6 +3966,8 @@ function formatCheckUpdateMarkdown(report) {
3599
3966
  `- repoRoot: \`${report.repoRoot}\``,
3600
3967
  `- controllerVersion: \`${report.controllerVersion}\``,
3601
3968
  `- surfaceState: \`${report.surfaceState}\``,
3969
+ `- coreState: \`${report.coreState}\``,
3970
+ `- migrationState: \`${report.migrationState}\``,
3602
3971
  ...(report.modelReadiness
3603
3972
  ? [
3604
3973
  "",
@@ -3640,7 +4009,6 @@ export async function runInitDoctor(input) {
3640
4009
  try {
3641
4010
  const manifest = await loadHarnessManifest(repoRoot);
3642
4011
  add("harness.json", manifest.adapter === "loop-agent" || manifest.project === "loop-agent", `project=${manifest.project}`);
3643
- add("governance root", await exists(path.join(repoRoot, manifest.governanceRoot, "README.md")), manifest.governanceRoot);
3644
4012
  if (input.modelCatalog) {
3645
4013
  const readiness = assessInitPiModelReadiness(isRecord(manifest.executors?.pi)
3646
4014
  ? manifest.executors.pi
@@ -3653,31 +4021,14 @@ export async function runInitDoctor(input) {
3653
4021
  catch (error) {
3654
4022
  add("harness.json", false, error instanceof Error ? error.message : String(error));
3655
4023
  }
3656
- const agents = path.join(repoRoot, "AGENTS.md");
3657
- add("AGENTS.md loop-agent block", (await exists(agents)) &&
3658
- (await readFile(agents, "utf-8")).includes(MANAGED_BLOCK_START), "managed block present");
3659
- const readme = path.join(repoRoot, "README.md");
3660
- add("README loop-agent block", (await exists(readme)) &&
3661
- (await readFile(readme, "utf-8")).includes(MANAGED_BLOCK_START), "managed block present");
3662
- 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");
3663
4025
  const gitignorePath = path.join(repoRoot, ".gitignore");
3664
4026
  const gitignoreContent = (await exists(gitignorePath))
3665
4027
  ? await readFile(gitignorePath, "utf-8")
3666
4028
  : "";
3667
4029
  add("gitignore loop-agent block", gitignoreContent.includes(GITIGNORE_BLOCK_START) &&
3668
4030
  gitignoreContent.includes(".harness/"), ".gitignore managed runtime ignores");
3669
- const requiredScripts = Object.keys(INIT_SCRIPT_FILES);
3670
- const missingScripts = [];
3671
- for (const script of requiredScripts) {
3672
- if (!(await exists(path.join(repoRoot, script))))
3673
- missingScripts.push(script);
3674
- }
3675
- add("script matrix", missingScripts.length === 0, missingScripts.length === 0
3676
- ? `${requiredScripts.length} scripts`
3677
- : `missing: ${missingScripts.join(", ")}`);
3678
- add("compat prompts", (await exists(path.join(repoRoot, ".harness", "prompts", "analyze.md"))) &&
3679
- (await exists(path.join(repoRoot, ".harness", "prompts", "plan.md"))), ".harness/prompts/analyze.md");
3680
- 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);
3681
4032
  return {
3682
4033
  ok: checks.every((check) => check.ok),
3683
4034
  repoRoot,
@@ -3729,23 +4080,23 @@ function parseInitArgs(repoRoot, args) {
3729
4080
  let subcommand;
3730
4081
  let projectName;
3731
4082
  let governanceRoot;
3732
- let profile = "full";
3733
4083
  let merge = true;
3734
4084
  let provider;
3735
4085
  let model;
3736
4086
  let lowModel;
3737
4087
  let medModel;
3738
4088
  let highModel;
3739
- let clientRecovery = "auto";
3740
4089
  let json = false;
3741
4090
  let markdown = false;
3742
4091
  let bootstrapSurface = false;
3743
4092
  let applySafe = false;
4093
+ let legacyAudit = false;
3744
4094
  let runId;
3745
4095
  let upgradeStatus = false;
3746
4096
  let upgradeContinue = false;
3747
4097
  let upgradeReport = false;
3748
4098
  let versionChoice;
4099
+ let decisionFile;
3749
4100
  for (let i = 0; i < args.length; i += 1) {
3750
4101
  const arg = args[i];
3751
4102
  if (arg === undefined)
@@ -3769,10 +4120,8 @@ function parseInitArgs(repoRoot, args) {
3769
4120
  governanceRoot = args[++i] ?? governanceRoot;
3770
4121
  else if (arg.startsWith("--governance-root="))
3771
4122
  governanceRoot = arg.slice("--governance-root=".length);
3772
- else if (arg === "--profile")
3773
- profile = args[++i] ?? profile;
3774
- else if (arg.startsWith("--profile="))
3775
- 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");
3776
4125
  else if (arg === "--merge")
3777
4126
  merge = true;
3778
4127
  else if (arg === "--no-merge")
@@ -3797,10 +4146,9 @@ function parseInitArgs(repoRoot, args) {
3797
4146
  highModel = args[++i];
3798
4147
  else if (arg.startsWith("--high-model="))
3799
4148
  highModel = arg.slice("--high-model=".length);
3800
- else if (arg === "--client-recovery")
3801
- clientRecovery = parseClientRecoveryMode(args[++i]);
3802
- else if (arg.startsWith("--client-recovery="))
3803
- 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)");
3804
4152
  else if (arg === "--json")
3805
4153
  json = true;
3806
4154
  else if (arg === "--markdown")
@@ -3809,6 +4157,8 @@ function parseInitArgs(repoRoot, args) {
3809
4157
  bootstrapSurface = true;
3810
4158
  else if (arg === "--apply-safe")
3811
4159
  applySafe = true;
4160
+ else if (arg === "--legacy-audit")
4161
+ legacyAudit = true;
3812
4162
  else if (arg === "--run-id")
3813
4163
  runId = args[++i];
3814
4164
  else if (arg.startsWith("--run-id="))
@@ -3823,13 +4173,15 @@ function parseInitArgs(repoRoot, args) {
3823
4173
  versionChoice = args[++i];
3824
4174
  else if (arg.startsWith("--version-choice="))
3825
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);
3826
4180
  else if (arg.startsWith("-"))
3827
4181
  throw new Error(`unknown init flag: ${arg}`);
3828
4182
  else
3829
4183
  throw new Error(`unexpected init argument: ${arg}`);
3830
4184
  }
3831
- if (profile !== "full" && profile !== "minimal")
3832
- throw new Error("init --profile must be full or minimal");
3833
4185
  if (versionChoice && !["current", "upgrade", "cancel", "retry"].includes(versionChoice)) {
3834
4186
  throw new Error("init upgrade --version-choice must be current|upgrade|cancel|retry");
3835
4187
  }
@@ -3845,22 +4197,23 @@ function parseInitArgs(repoRoot, args) {
3845
4197
  repoRoot,
3846
4198
  projectName,
3847
4199
  governanceRoot,
3848
- profile,
4200
+ profile: "minimal",
3849
4201
  merge,
3850
4202
  provider,
3851
4203
  model: normalizedModel,
3852
4204
  modelMatrix,
3853
- clientRecovery,
3854
4205
  subcommand,
3855
4206
  json,
3856
4207
  markdown,
3857
4208
  bootstrapSurface,
3858
4209
  applySafe,
4210
+ legacyAudit,
3859
4211
  runId,
3860
4212
  upgradeStatus,
3861
4213
  upgradeContinue,
3862
4214
  upgradeReport,
3863
4215
  versionChoice,
4216
+ decisionFile,
3864
4217
  };
3865
4218
  }
3866
4219
  async function discoverInitCatalogForRepo(repoRoot) {
@@ -3948,6 +4301,7 @@ export async function runInit(repoRoot, rawArgs, dependencies) {
3948
4301
  runId: parsed.runId,
3949
4302
  mode,
3950
4303
  versionChoice: parsed.versionChoice,
4304
+ decisionFile: parsed.decisionFile,
3951
4305
  readRuntimeActivity: dependencies.readRuntimeActivity,
3952
4306
  modelCatalog,
3953
4307
  });
@@ -4004,6 +4358,8 @@ function formatInitUpgradeResult(result) {
4004
4358
  `phase: ${result.phase}`,
4005
4359
  `modelMergeTasks: ${result.modelMergeTasks.length}`,
4006
4360
  `humanDecisions: ${result.humanDecisions.length}`,
4361
+ `coreState: ${result.coreState}`,
4362
+ `migrationState: ${result.migrationState}`,
4007
4363
  `next: ${result.nextAction}`,
4008
4364
  ].join("\n");
4009
4365
  }
@@ -4014,6 +4370,8 @@ function formatInitUpdateResult(result) {
4014
4370
  `skipped: ${result.skipped.length}`,
4015
4371
  `remaining deterministicActions: ${result.report.deterministicActions.length}`,
4016
4372
  `remaining modelMergeTasks: ${result.report.modelMergeTasks.length}`,
4373
+ `coreState: ${result.report.coreState}`,
4374
+ `migrationState: ${result.report.migrationState}`,
4017
4375
  "recommendedNext:",
4018
4376
  ...result.report.recommendedNext.map((item) => `- ${item}`),
4019
4377
  ].join("\n");
@@ -4037,14 +4395,11 @@ function formatReconcileResult(result) {
4037
4395
  }
4038
4396
  function formatInitResult(result) {
4039
4397
  return [
4040
- `Initialized loop-agent harness at ${result.repoRoot}`,
4398
+ `Initialized minimal loop-agent project surface at ${result.repoRoot}`,
4041
4399
  `project: ${result.projectName}`,
4042
- `governanceRoot: ${result.governanceRoot}`,
4043
- `profile: ${result.profile}`,
4044
4400
  `written: ${result.written.length}`,
4045
4401
  `skipped: ${result.skipped.length}`,
4046
4402
  "next: loop-agent init doctor --repo-root <target>",
4047
- `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.`,
4048
4403
  ].join("\n");
4049
4404
  }
4050
4405
  export async function listInitializedFiles(repoRoot) {