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