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