@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
|
@@ -5,15 +5,21 @@ import { lstat, mkdtemp, mkdir, readFile, readdir, readlink, realpath, rename, r
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import semver from "semver";
|
|
8
9
|
import { resolveAdapter } from "../adapters/index.js";
|
|
9
10
|
import { createNpmUpdateClient, } from "../shared/update/npm-client.js";
|
|
10
11
|
import { auditDocs } from "../governance/checks.js";
|
|
11
12
|
import { harnessManifestSchema } from "../governance/manifest-types.js";
|
|
13
|
+
import { ProjectCapabilityScanner } from "../governance/project-capability-scanner.js";
|
|
14
|
+
import { loadNormalizedProjectConfig } from "../governance/project-config.js";
|
|
15
|
+
import { CONTROLLER_RESOURCE_IDS, createControllerContextProvider, } from "../governance/resource-catalog.js";
|
|
12
16
|
import { isInitRuntimeActive, } from "../shared/runtime-activity.js";
|
|
13
17
|
import { MAX_OVERFLOW_RECOVERIES, MAX_SESSION_RETRIES, OPENCODE_CONTEXT_OVERFLOW_COMPACT_PLUGIN_PATH, OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH, PI_CONTEXT_OVERFLOW_EXTENSION_PATH, PI_PROJECT_SETTINGS_PATH, inspectProjectPiSettings, } from "./client-recovery.js";
|
|
14
|
-
import { applyInitUpdate, checkInitUpdate, initializeLoopAgentProject, runInitDoctor, } from "./init.js";
|
|
18
|
+
import { applyInitUpdate, applyInitDecisionBoundActions, checkInitUpdate, initializeLoopAgentProject, runInitDoctor, } from "./init.js";
|
|
15
19
|
const RUN_ROOT = ".harness/init-upgrades";
|
|
16
20
|
const GLOBAL_LOCK_NAME = ".writer-lock";
|
|
21
|
+
const PENDING_DECISION_FILE = "decision-application-pending.json";
|
|
22
|
+
const DECISION_REPLACE_REASON = "replace an explicitly approved legacy loop-agent Skill with the minimal external Agent bridge";
|
|
17
23
|
const INIT_SURFACE_STATE_PATH = ".harness/init-surface.json";
|
|
18
24
|
const RECOVERY_PATHS = [
|
|
19
25
|
OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH,
|
|
@@ -27,6 +33,80 @@ function sha256(value) {
|
|
|
27
33
|
function validRunId(runId) {
|
|
28
34
|
return /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(runId);
|
|
29
35
|
}
|
|
36
|
+
async function readRepoLocalDecisionFile(repoRoot, decisionFile) {
|
|
37
|
+
const absolutePath = path.resolve(repoRoot, decisionFile);
|
|
38
|
+
const relativePath = path.relative(repoRoot, absolutePath);
|
|
39
|
+
if (relativePath.length === 0 ||
|
|
40
|
+
relativePath.startsWith(`..${path.sep}`) ||
|
|
41
|
+
path.isAbsolute(relativePath)) {
|
|
42
|
+
throw new Error("init upgrade --decision-file must resolve inside repoRoot");
|
|
43
|
+
}
|
|
44
|
+
const info = await lstat(absolutePath).catch(() => undefined);
|
|
45
|
+
if (!info?.isFile() || info.isSymbolicLink()) {
|
|
46
|
+
throw new Error("init upgrade --decision-file must be a regular non-symlink file");
|
|
47
|
+
}
|
|
48
|
+
return await readFile(absolutePath);
|
|
49
|
+
}
|
|
50
|
+
async function readInitUpgradeDecisionFile(input) {
|
|
51
|
+
const raw = await readRepoLocalDecisionFile(input.repoRoot, input.decisionFile);
|
|
52
|
+
let parsed;
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(raw.toString("utf-8"));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new Error("init upgrade --decision-file is not valid JSON");
|
|
58
|
+
}
|
|
59
|
+
if (!isRecord(parsed) ||
|
|
60
|
+
parsed.schemaVersion !== 1 ||
|
|
61
|
+
parsed.runId !== input.runId ||
|
|
62
|
+
!Array.isArray(parsed.decisions)) {
|
|
63
|
+
throw new Error("init upgrade --decision-file has an invalid schema or runId binding");
|
|
64
|
+
}
|
|
65
|
+
const decisions = [];
|
|
66
|
+
const decisionPaths = new Set();
|
|
67
|
+
const pendingByPath = new Map(input.pending.map((decision) => [decision.path, decision]));
|
|
68
|
+
for (const value of parsed.decisions) {
|
|
69
|
+
const pending = isRecord(value) && typeof value.path === "string"
|
|
70
|
+
? pendingByPath.get(value.path)
|
|
71
|
+
: undefined;
|
|
72
|
+
const legacyDisposition = isRecord(value) &&
|
|
73
|
+
(value.disposition === "retain-project-content" ||
|
|
74
|
+
value.disposition === "preserve-unproven" ||
|
|
75
|
+
value.disposition === "delete-listed-file");
|
|
76
|
+
if (!isRecord(value) ||
|
|
77
|
+
typeof value.path !== "string" ||
|
|
78
|
+
(value.disposition !== "detach-as-project-content" &&
|
|
79
|
+
value.disposition !== "replace-with-bridge" &&
|
|
80
|
+
!legacyDisposition) ||
|
|
81
|
+
!isSha256(value.expectedSha256) ||
|
|
82
|
+
decisionPaths.has(value.path) ||
|
|
83
|
+
!pending ||
|
|
84
|
+
(Boolean(pending.candidateId) !== legacyDisposition) ||
|
|
85
|
+
(Boolean(pending.candidateId) &&
|
|
86
|
+
(value.candidateId !== pending.candidateId ||
|
|
87
|
+
value.expectedSha256 !== pending.expectedSha256))) {
|
|
88
|
+
throw new Error("init upgrade --decision-file contains an invalid or duplicate decision");
|
|
89
|
+
}
|
|
90
|
+
decisionPaths.add(value.path);
|
|
91
|
+
decisions.push({
|
|
92
|
+
path: value.path,
|
|
93
|
+
disposition: value.disposition,
|
|
94
|
+
expectedSha256: value.expectedSha256,
|
|
95
|
+
...(typeof value.candidateId === "string"
|
|
96
|
+
? { candidateId: value.candidateId }
|
|
97
|
+
: {}),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const pendingPaths = new Set(input.pending.map((decision) => decision.path));
|
|
101
|
+
if (decisionPaths.size !== pendingPaths.size ||
|
|
102
|
+
[...pendingPaths].some((pendingPath) => !decisionPaths.has(pendingPath))) {
|
|
103
|
+
throw new Error("init upgrade --decision-file must resolve every current human decision exactly once");
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
document: { schemaVersion: 1, runId: input.runId, decisions },
|
|
107
|
+
sha256: sha256(raw),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
30
110
|
function isRecord(value) {
|
|
31
111
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
32
112
|
}
|
|
@@ -129,8 +209,12 @@ function controllerStateProjection(state) {
|
|
|
129
209
|
queuedMergeTasks: cloneJson(state.queuedMergeTasks),
|
|
130
210
|
...(state.mergeGuard ? { mergeGuard: cloneJson(state.mergeGuard) } : {}),
|
|
131
211
|
mergeReceipts: cloneJson(state.mergeReceipts),
|
|
212
|
+
decisionReceipts: cloneJson(state.decisionReceipts ?? []),
|
|
132
213
|
humanDecisions: cloneJson(state.humanDecisions),
|
|
133
214
|
actionReceipts: cloneJson(state.actionReceipts),
|
|
215
|
+
coreState: state.coreState,
|
|
216
|
+
migrationState: state.migrationState,
|
|
217
|
+
...(state.legacyAudit ? { legacyAudit: cloneJson(state.legacyAudit) } : {}),
|
|
134
218
|
};
|
|
135
219
|
}
|
|
136
220
|
function sameJsonValue(left, right) {
|
|
@@ -149,6 +233,88 @@ function sameJsonValue(left, right) {
|
|
|
149
233
|
return (leftKeys.length === rightKeys.length &&
|
|
150
234
|
leftKeys.every((key, index) => key === rightKeys[index] && sameJsonValue(left[key], right[key])));
|
|
151
235
|
}
|
|
236
|
+
function isDecisionAction(value) {
|
|
237
|
+
return (isRecord(value) &&
|
|
238
|
+
typeof value.path === "string" &&
|
|
239
|
+
(value.disposition === "detach-as-project-content" ||
|
|
240
|
+
value.disposition === "replace-with-bridge" ||
|
|
241
|
+
value.disposition === "retain-project-content" ||
|
|
242
|
+
value.disposition === "preserve-unproven" ||
|
|
243
|
+
value.disposition === "delete-listed-file") &&
|
|
244
|
+
isSha256(value.expectedSha256) &&
|
|
245
|
+
(value.candidateId === undefined || typeof value.candidateId === "string"));
|
|
246
|
+
}
|
|
247
|
+
function assertPendingDecisionAuthority(authority, state) {
|
|
248
|
+
if (!authority)
|
|
249
|
+
return undefined;
|
|
250
|
+
const pending = authority.pending;
|
|
251
|
+
if (!isRecord(pending) ||
|
|
252
|
+
pending.schemaVersion !== 1 ||
|
|
253
|
+
pending.runId !== state.runId ||
|
|
254
|
+
pending.repoRoot !== state.repoRoot ||
|
|
255
|
+
!isSha256(pending.decisionFileSha256) ||
|
|
256
|
+
!isRecord(pending.document) ||
|
|
257
|
+
pending.document.schemaVersion !== 1 ||
|
|
258
|
+
pending.document.runId !== state.runId ||
|
|
259
|
+
!Array.isArray(pending.document.decisions) ||
|
|
260
|
+
pending.document.decisions.length === 0 ||
|
|
261
|
+
!pending.document.decisions.every(isDecisionAction) ||
|
|
262
|
+
!isRecord(pending.stateProjection) ||
|
|
263
|
+
!isSha256(pending.stateProjectionSha256) ||
|
|
264
|
+
canonicalSha256(pending.stateProjection) !== pending.stateProjectionSha256 ||
|
|
265
|
+
typeof pending.preparedAt !== "string" ||
|
|
266
|
+
!isSha256(authority.pendingSha256) ||
|
|
267
|
+
canonicalSha256(pending) !== authority.pendingSha256) {
|
|
268
|
+
throw new Error("pending init decision authority is invalid");
|
|
269
|
+
}
|
|
270
|
+
const pre = pending.stateProjection;
|
|
271
|
+
const current = controllerStateProjection(state);
|
|
272
|
+
if (sameJsonValue(current, pre))
|
|
273
|
+
return pending;
|
|
274
|
+
const priorDecisionReceipts = pre.decisionReceipts ?? [];
|
|
275
|
+
const currentDecisionReceipts = current.decisionReceipts ?? [];
|
|
276
|
+
const appended = currentDecisionReceipts.slice(priorDecisionReceipts.length);
|
|
277
|
+
if (currentDecisionReceipts.length !==
|
|
278
|
+
priorDecisionReceipts.length + pending.document.decisions.length ||
|
|
279
|
+
!sameJsonValue(currentDecisionReceipts.slice(0, priorDecisionReceipts.length), priorDecisionReceipts) ||
|
|
280
|
+
appended.some((receipt, index) => {
|
|
281
|
+
const decision = pending.document.decisions[index];
|
|
282
|
+
return (!decision ||
|
|
283
|
+
receipt.path !== decision.path ||
|
|
284
|
+
receipt.disposition !== decision.disposition ||
|
|
285
|
+
receipt.expectedSha256 !== decision.expectedSha256 ||
|
|
286
|
+
receipt.candidateId !== decision.candidateId ||
|
|
287
|
+
receipt.decisionFileSha256 !== pending.decisionFileSha256 ||
|
|
288
|
+
!isSha256(receipt.resultSha256) ||
|
|
289
|
+
typeof receipt.decidedAt !== "string");
|
|
290
|
+
})) {
|
|
291
|
+
throw new Error("pending init decision receipt projection is inconsistent");
|
|
292
|
+
}
|
|
293
|
+
const expectedPost = cloneJson(pre);
|
|
294
|
+
expectedPost.status = "in-progress";
|
|
295
|
+
expectedPost.humanDecisions = [];
|
|
296
|
+
expectedPost.decisionReceipts = cloneJson(currentDecisionReceipts);
|
|
297
|
+
expectedPost.actionReceipts = [
|
|
298
|
+
...cloneJson(pre.actionReceipts),
|
|
299
|
+
...pending.document.decisions
|
|
300
|
+
.filter((decision) => decision.disposition === "replace-with-bridge" ||
|
|
301
|
+
decision.disposition === "delete-listed-file")
|
|
302
|
+
.map((decision) => ({
|
|
303
|
+
type: decision.disposition === "replace-with-bridge"
|
|
304
|
+
? "replace-owned-file"
|
|
305
|
+
: "remove-owned-file",
|
|
306
|
+
path: decision.path,
|
|
307
|
+
expectedSha256: decision.expectedSha256,
|
|
308
|
+
reason: decision.disposition === "replace-with-bridge"
|
|
309
|
+
? DECISION_REPLACE_REASON
|
|
310
|
+
: "remove an explicitly approved hash-bound legacy init file",
|
|
311
|
+
})),
|
|
312
|
+
];
|
|
313
|
+
if (!sameJsonValue(current, expectedPost)) {
|
|
314
|
+
throw new Error("pending init decision state differs from controller authority");
|
|
315
|
+
}
|
|
316
|
+
return pending;
|
|
317
|
+
}
|
|
152
318
|
function isControllerIdentity(value) {
|
|
153
319
|
if (!isRecord(value))
|
|
154
320
|
return false;
|
|
@@ -205,8 +371,14 @@ function assertStateShape(state, repoRoot, runId) {
|
|
|
205
371
|
!Array.isArray(candidate.mergeTasks) ||
|
|
206
372
|
!Array.isArray(candidate.queuedMergeTasks) ||
|
|
207
373
|
!Array.isArray(candidate.mergeReceipts) ||
|
|
374
|
+
(candidate.decisionReceipts !== undefined &&
|
|
375
|
+
!Array.isArray(candidate.decisionReceipts)) ||
|
|
208
376
|
!Array.isArray(candidate.humanDecisions) ||
|
|
209
377
|
!Array.isArray(candidate.actionReceipts) ||
|
|
378
|
+
(candidate.coreState !== "ready" &&
|
|
379
|
+
candidate.coreState !== "blocked" &&
|
|
380
|
+
candidate.coreState !== "error") ||
|
|
381
|
+
!["clean", "retained", "review-required", "in-progress", "failed"].includes(candidate.migrationState) ||
|
|
210
382
|
!isHomeFingerprint(candidate.homeFingerprint) ||
|
|
211
383
|
(candidate.modelCatalog !== undefined &&
|
|
212
384
|
(!isRecord(candidate.modelCatalog) ||
|
|
@@ -257,6 +429,9 @@ function runGitQuery(cwd, args) {
|
|
|
257
429
|
function splitNulPaths(output) {
|
|
258
430
|
return output.split("\0").filter((entry) => entry.length > 0);
|
|
259
431
|
}
|
|
432
|
+
function quoteGitPath(pathName) {
|
|
433
|
+
return `'${pathName.replaceAll("'", `'"'"'`)}'`;
|
|
434
|
+
}
|
|
260
435
|
/**
|
|
261
436
|
* Gitignore migration assessment (PRD 2026-08-13): inventories tracked
|
|
262
437
|
* `.harness/**` / `.agents/**` with read-only git queries after the managed
|
|
@@ -273,7 +448,8 @@ export async function assessGitignoreMigration(repoRoot, input) {
|
|
|
273
448
|
needsAgentsReview: false,
|
|
274
449
|
followUp: [
|
|
275
450
|
"run `git status --short` and confirm only the intended untrack changes",
|
|
276
|
-
"confirm the .harness
|
|
451
|
+
"confirm the listed .harness files still exist locally after index-only untracking",
|
|
452
|
+
"keep every tracked .agents path at its existing tracking intent; the bridge remains shared",
|
|
277
453
|
"run `loop-agent init doctor` to re-check the gitignore loop-agent block",
|
|
278
454
|
],
|
|
279
455
|
};
|
|
@@ -329,7 +505,7 @@ export async function assessGitignoreMigration(repoRoot, input) {
|
|
|
329
505
|
const harnessTracked = trackedPaths.filter((entry) => entry.startsWith(".harness/"));
|
|
330
506
|
const agentsTracked = trackedPaths.filter((entry) => entry.startsWith(".agents/"));
|
|
331
507
|
const stagedRisk = splitNulPaths(staged.stdout);
|
|
332
|
-
const needsAgentsReview =
|
|
508
|
+
const needsAgentsReview = false;
|
|
333
509
|
if (stagedRisk.length > 0) {
|
|
334
510
|
// Fail closed: staged entries make any untrack advice ambiguous.
|
|
335
511
|
return {
|
|
@@ -344,16 +520,10 @@ export async function assessGitignoreMigration(repoRoot, input) {
|
|
|
344
520
|
};
|
|
345
521
|
}
|
|
346
522
|
const recommendedCommands = [];
|
|
347
|
-
|
|
348
|
-
recommendedCommands.push({
|
|
349
|
-
command: "git rm -r --cached --ignore-unmatch .harness",
|
|
350
|
-
note: "untracks tracked .harness runtime facts; index-only, working-tree files are kept",
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
if (agentsTracked.length > 0) {
|
|
523
|
+
for (const trackedPath of harnessTracked) {
|
|
354
524
|
recommendedCommands.push({
|
|
355
|
-
command:
|
|
356
|
-
note:
|
|
525
|
+
command: `git rm --cached --ignore-unmatch -- ${quoteGitPath(trackedPath)}`,
|
|
526
|
+
note: `untracks only ${trackedPath}; the working-tree file is kept`,
|
|
357
527
|
});
|
|
358
528
|
}
|
|
359
529
|
return {
|
|
@@ -364,9 +534,7 @@ export async function assessGitignoreMigration(repoRoot, input) {
|
|
|
364
534
|
stagedRisk,
|
|
365
535
|
needsAgentsReview,
|
|
366
536
|
recommendedCommands,
|
|
367
|
-
status: harnessTracked.length > 0
|
|
368
|
-
? "recommended"
|
|
369
|
-
: "clean",
|
|
537
|
+
status: harnessTracked.length > 0 ? "recommended" : "clean",
|
|
370
538
|
};
|
|
371
539
|
}
|
|
372
540
|
function runRoot(repoRoot) {
|
|
@@ -440,6 +608,7 @@ async function writeControllerAnchor(input) {
|
|
|
440
608
|
let authoritativeHomeFingerprint;
|
|
441
609
|
let authoritativeGitignoreMigrationSha256;
|
|
442
610
|
let authoritativeModelCatalogSha256;
|
|
611
|
+
let authoritativeDecisionAuthority;
|
|
443
612
|
if (input.initialize) {
|
|
444
613
|
if (await exists(location.file)) {
|
|
445
614
|
throw new Error("init upgrade controller authority anchor already exists");
|
|
@@ -449,8 +618,9 @@ async function writeControllerAnchor(input) {
|
|
|
449
618
|
}
|
|
450
619
|
authoritativeHomeFingerprint = cloneJson(input.state.homeFingerprint);
|
|
451
620
|
authoritativeModelCatalogSha256 = input.state.modelCatalog
|
|
452
|
-
? canonicalSha256(input.state.modelCatalog)
|
|
621
|
+
? canonicalSha256(cloneJson(input.state.modelCatalog))
|
|
453
622
|
: undefined;
|
|
623
|
+
authoritativeDecisionAuthority = undefined;
|
|
454
624
|
}
|
|
455
625
|
else {
|
|
456
626
|
const current = await readControllerAnchor(location.repoRoot, input.state);
|
|
@@ -464,6 +634,10 @@ async function writeControllerAnchor(input) {
|
|
|
464
634
|
}
|
|
465
635
|
authoritativeGitignoreMigrationSha256 = digest;
|
|
466
636
|
authoritativeModelCatalogSha256 = current.modelCatalogSha256;
|
|
637
|
+
authoritativeDecisionAuthority =
|
|
638
|
+
input.decisionAuthority === null
|
|
639
|
+
? undefined
|
|
640
|
+
: input.decisionAuthority ?? current.decisionAuthority;
|
|
467
641
|
}
|
|
468
642
|
await writeJsonAtomic(location.file, {
|
|
469
643
|
schemaVersion: 1,
|
|
@@ -478,6 +652,9 @@ async function writeControllerAnchor(input) {
|
|
|
478
652
|
? { gitignoreMigrationSha256: authoritativeGitignoreMigrationSha256 }
|
|
479
653
|
: {}),
|
|
480
654
|
...(input.mergeAuthority ? { mergeAuthority: input.mergeAuthority } : {}),
|
|
655
|
+
...(authoritativeDecisionAuthority
|
|
656
|
+
? { decisionAuthority: authoritativeDecisionAuthority }
|
|
657
|
+
: {}),
|
|
481
658
|
...(input.completion ? { completion: input.completion } : {}),
|
|
482
659
|
});
|
|
483
660
|
}
|
|
@@ -499,7 +676,7 @@ async function readControllerAnchor(repoRoot, state) {
|
|
|
499
676
|
throw new Error("init upgrade controller authority anchor binding is invalid");
|
|
500
677
|
}
|
|
501
678
|
const expectedModelCatalogSha256 = state.modelCatalog
|
|
502
|
-
? canonicalSha256(state.modelCatalog)
|
|
679
|
+
? canonicalSha256(cloneJson(state.modelCatalog))
|
|
503
680
|
: undefined;
|
|
504
681
|
if (parsed.modelCatalogSha256 !== expectedModelCatalogSha256) {
|
|
505
682
|
throw new Error("init upgrade model catalog differs from controller authority");
|
|
@@ -510,6 +687,59 @@ async function readControllerAnchor(repoRoot, state) {
|
|
|
510
687
|
}
|
|
511
688
|
return parsed;
|
|
512
689
|
}
|
|
690
|
+
async function readPendingDecisionApplication(input) {
|
|
691
|
+
const pending = assertPendingDecisionAuthority(input.anchor.decisionAuthority, input.state);
|
|
692
|
+
const pendingPath = path.join(input.directory, PENDING_DECISION_FILE);
|
|
693
|
+
if (!pending) {
|
|
694
|
+
await rm(pendingPath, { force: true });
|
|
695
|
+
return undefined;
|
|
696
|
+
}
|
|
697
|
+
let parsed;
|
|
698
|
+
try {
|
|
699
|
+
parsed = JSON.parse(await readFile(pendingPath, "utf-8"));
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
if (error.code !== "ENOENT") {
|
|
703
|
+
throw new Error("pending init decision record is missing or invalid");
|
|
704
|
+
}
|
|
705
|
+
await writeJsonAtomic(pendingPath, pending);
|
|
706
|
+
parsed = pending;
|
|
707
|
+
}
|
|
708
|
+
if (!sameJsonValue(parsed, pending) ||
|
|
709
|
+
canonicalSha256(parsed) !== input.anchor.decisionAuthority?.pendingSha256) {
|
|
710
|
+
throw new Error("pending init decision record differs from controller authority");
|
|
711
|
+
}
|
|
712
|
+
return pending;
|
|
713
|
+
}
|
|
714
|
+
async function readPendingDecisionProjection(input) {
|
|
715
|
+
const pending = assertPendingDecisionAuthority(input.anchor.decisionAuthority, input.state);
|
|
716
|
+
const pendingPath = path.join(input.directory, PENDING_DECISION_FILE);
|
|
717
|
+
let raw;
|
|
718
|
+
try {
|
|
719
|
+
raw = await readFile(pendingPath, "utf-8");
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
if (error.code === "ENOENT" && !pending) {
|
|
723
|
+
return undefined;
|
|
724
|
+
}
|
|
725
|
+
throw new Error("pending init decision record is missing or unreadable");
|
|
726
|
+
}
|
|
727
|
+
if (!pending) {
|
|
728
|
+
throw new Error("pending init decision record lacks controller authority");
|
|
729
|
+
}
|
|
730
|
+
let parsed;
|
|
731
|
+
try {
|
|
732
|
+
parsed = JSON.parse(raw);
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
throw new Error("pending init decision record is invalid");
|
|
736
|
+
}
|
|
737
|
+
if (!sameJsonValue(parsed, pending) ||
|
|
738
|
+
canonicalSha256(parsed) !== input.anchor.decisionAuthority?.pendingSha256) {
|
|
739
|
+
throw new Error("pending init decision record differs from controller authority");
|
|
740
|
+
}
|
|
741
|
+
return pending;
|
|
742
|
+
}
|
|
513
743
|
function mergeAuthorityFromAnchor(anchor, state) {
|
|
514
744
|
const authority = anchor.mergeAuthority;
|
|
515
745
|
if (!authority ||
|
|
@@ -633,17 +863,9 @@ async function homeFingerprint() {
|
|
|
633
863
|
}
|
|
634
864
|
}
|
|
635
865
|
function versionsNewer(candidate, current) {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
if (!left || !right)
|
|
640
|
-
return false;
|
|
641
|
-
for (let index = 0; index < left.length; index += 1) {
|
|
642
|
-
if (left[index] === right[index])
|
|
643
|
-
continue;
|
|
644
|
-
return left[index] > right[index];
|
|
645
|
-
}
|
|
646
|
-
return false;
|
|
866
|
+
return semver.valid(candidate) !== null && semver.valid(current) !== null
|
|
867
|
+
? semver.gt(candidate, current)
|
|
868
|
+
: false;
|
|
647
869
|
}
|
|
648
870
|
async function readState(repoRoot, runId) {
|
|
649
871
|
const file = path.join(runDirectory(repoRoot, runId), "state.json");
|
|
@@ -684,12 +906,22 @@ function toResult(state, verification, gitignoreMigration) {
|
|
|
684
906
|
versionGate: state.versionGate,
|
|
685
907
|
deterministicActions: state.actionReceipts,
|
|
686
908
|
modelMergeTasks: state.mergeTasks,
|
|
909
|
+
decisionReceipts: state.decisionReceipts ?? [],
|
|
687
910
|
humanDecisions: state.humanDecisions,
|
|
911
|
+
coreState: state.coreState,
|
|
912
|
+
migrationState: state.migrationState,
|
|
913
|
+
...(state.legacyAudit ? { legacyAudit: state.legacyAudit } : {}),
|
|
688
914
|
nextAction: state.nextAction,
|
|
689
915
|
...(verification ? { verification } : {}),
|
|
690
916
|
...(gitignoreMigration ? { gitignoreMigration } : {}),
|
|
691
917
|
};
|
|
692
918
|
}
|
|
919
|
+
function projectInitStates(state, report) {
|
|
920
|
+
state.coreState = report.coreState;
|
|
921
|
+
state.migrationState = report.migrationState;
|
|
922
|
+
if (report.legacyAudit)
|
|
923
|
+
state.legacyAudit = cloneJson(report.legacyAudit);
|
|
924
|
+
}
|
|
693
925
|
function renderReport(state, verification, gitignoreMigration) {
|
|
694
926
|
const lines = [
|
|
695
927
|
"# loop-agent init upgrade report",
|
|
@@ -708,10 +940,13 @@ function renderReport(state, verification, gitignoreMigration) {
|
|
|
708
940
|
`- version choice: \`${state.versionGate.choice ?? "pending"}\``,
|
|
709
941
|
`- deterministic actions applied: ${state.actionReceipts.length}`,
|
|
710
942
|
`- semantic merges accepted: ${state.mergeReceipts.length}`,
|
|
711
|
-
`-
|
|
943
|
+
`- decision-bound dispositions accepted: ${(state.decisionReceipts ?? []).length}`,
|
|
944
|
+
`- preserved user modifications: ${state.mergeReceipts.length + (state.decisionReceipts ?? []).filter((receipt) => receipt.disposition === "detach-as-project-content").length}`,
|
|
712
945
|
`- retired path actions: ${state.actionReceipts.filter((action) => action.type === "migrate-owned-file" || action.type === "remove-owned-file" || action.type === "remove-empty-directory").length}`,
|
|
713
946
|
`- remaining model merge tasks: ${state.mergeTasks.length + state.queuedMergeTasks.length}`,
|
|
714
947
|
`- remaining human decisions: ${state.humanDecisions.length}`,
|
|
948
|
+
`- core state: \`${state.coreState}\``,
|
|
949
|
+
`- migration state: \`${state.migrationState}\``,
|
|
715
950
|
`- gitignore migration assessment: \`.harness/init-upgrades/${state.runId}/gitignore-migration.json\` (index-only untrack guidance; the controller never modifies the Git index)`,
|
|
716
951
|
...(gitignoreMigration
|
|
717
952
|
? [
|
|
@@ -1225,12 +1460,12 @@ async function freezeVerificationSurface(repoRoot) {
|
|
|
1225
1460
|
collected.add(await assertSafeRegularRepoFile(repoRoot, relativePath));
|
|
1226
1461
|
};
|
|
1227
1462
|
await add("harness.json");
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
const
|
|
1232
|
-
for (const command of commands) {
|
|
1233
|
-
for (const input of
|
|
1463
|
+
if (await exists(path.join(repoRoot, ".loop-agent", "project.json"))) {
|
|
1464
|
+
await add(".loop-agent/project.json");
|
|
1465
|
+
}
|
|
1466
|
+
const verification = await resolveProjectVerification(repoRoot);
|
|
1467
|
+
for (const command of verification.commands) {
|
|
1468
|
+
for (const input of command.inputs) {
|
|
1234
1469
|
const normalized = await assertSafeRegularRepoFile(repoRoot, input);
|
|
1235
1470
|
if (normalized.endsWith(".sh"))
|
|
1236
1471
|
await staticScriptDependencies(repoRoot, normalized, collected);
|
|
@@ -1244,6 +1479,50 @@ async function freezeVerificationSurface(repoRoot) {
|
|
|
1244
1479
|
})));
|
|
1245
1480
|
return { schemaVersion: 1, files };
|
|
1246
1481
|
}
|
|
1482
|
+
async function resolveProjectVerification(repoRoot) {
|
|
1483
|
+
const config = await loadNormalizedProjectConfig(repoRoot);
|
|
1484
|
+
const configured = config.verification.quick.length > 0
|
|
1485
|
+
? config.verification.quick
|
|
1486
|
+
: config.verification.standard.length > 0
|
|
1487
|
+
? config.verification.standard
|
|
1488
|
+
: config.verification.full;
|
|
1489
|
+
if (configured.length > 0) {
|
|
1490
|
+
const commands = [];
|
|
1491
|
+
for (const command of configured) {
|
|
1492
|
+
commands.push({
|
|
1493
|
+
command,
|
|
1494
|
+
inputs: await commandRepositoryInputs(repoRoot, command),
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
return {
|
|
1498
|
+
source: config.verification.reproducibility === "tracked"
|
|
1499
|
+
? "tracked"
|
|
1500
|
+
: "local-only",
|
|
1501
|
+
commands,
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
const scan = await new ProjectCapabilityScanner().scan(repoRoot);
|
|
1505
|
+
if (scan.verification.status !== "ready") {
|
|
1506
|
+
throw new Error(`project verification is ${scan.verification.status}; configure .loop-agent/project.json verification before init upgrade`);
|
|
1507
|
+
}
|
|
1508
|
+
const candidates = scan.verification.commands;
|
|
1509
|
+
const selected = candidates.find((candidate) => candidate.id.endsWith(":test")) ??
|
|
1510
|
+
candidates.find((candidate) => candidate.id.endsWith(":typecheck")) ??
|
|
1511
|
+
candidates.find((candidate) => candidate.id.endsWith(":lint")) ??
|
|
1512
|
+
candidates[0];
|
|
1513
|
+
if (!selected) {
|
|
1514
|
+
throw new Error("project verification is unavailable; configure .loop-agent/project.json verification before init upgrade");
|
|
1515
|
+
}
|
|
1516
|
+
return {
|
|
1517
|
+
source: "scanner",
|
|
1518
|
+
commands: [
|
|
1519
|
+
{
|
|
1520
|
+
command: selected.command,
|
|
1521
|
+
inputs: scan.evidence.map((entry) => entry.path),
|
|
1522
|
+
},
|
|
1523
|
+
],
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1247
1526
|
async function assertFrozenVerificationSurface(repoRoot, surface) {
|
|
1248
1527
|
if (!isFrozenVerificationSurface(surface)) {
|
|
1249
1528
|
throw new Error("verification authority surface manifest is invalid");
|
|
@@ -1273,6 +1552,32 @@ function isControllerAuthorizedVerificationMergeTask(task, surface) {
|
|
|
1273
1552
|
taskPath.endsWith("/verification-matrix.md")) &&
|
|
1274
1553
|
Boolean(surface?.files.some((entry) => entry.path === taskPath)));
|
|
1275
1554
|
}
|
|
1555
|
+
function isControllerModelMatrixSelectionTask(task) {
|
|
1556
|
+
if (normalizeAllowedPath(task.path) !== "harness.json")
|
|
1557
|
+
return false;
|
|
1558
|
+
if (task.purpose === "pi-model-matrix-selection")
|
|
1559
|
+
return true;
|
|
1560
|
+
return (task.allowedPaths.length === 1 &&
|
|
1561
|
+
normalizeAllowedPath(task.allowedPaths[0] ?? "") === "harness.json" &&
|
|
1562
|
+
(task.reason ===
|
|
1563
|
+
"harness.json has no complete Pi model matrix that resolves against the current Pi model catalog" ||
|
|
1564
|
+
task.reason.startsWith("only unavailable Pi tiers need reselection: ")) &&
|
|
1565
|
+
(task.mergeRules.includes("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.") ||
|
|
1566
|
+
task.mergeRules.includes("Apply the complete matrix through init update --apply-safe with all three tier flags so code can revalidate it; valid existing tiers must keep their current effective reference.")));
|
|
1567
|
+
}
|
|
1568
|
+
function executorAuthorityWithoutPiModelTiers(value) {
|
|
1569
|
+
if (!isRecord(value))
|
|
1570
|
+
return value;
|
|
1571
|
+
const executors = { ...value };
|
|
1572
|
+
if (!isRecord(value.pi))
|
|
1573
|
+
return executors;
|
|
1574
|
+
const pi = { ...value.pi };
|
|
1575
|
+
delete pi.LOW;
|
|
1576
|
+
delete pi.MED;
|
|
1577
|
+
delete pi.HIGH;
|
|
1578
|
+
executors.pi = pi;
|
|
1579
|
+
return executors;
|
|
1580
|
+
}
|
|
1276
1581
|
async function assertFrozenVerificationSurfaceExcept(repoRoot, surface, exemptPath) {
|
|
1277
1582
|
if (!isFrozenVerificationSurface(surface)) {
|
|
1278
1583
|
throw new Error("verification authority surface manifest is invalid");
|
|
@@ -1294,20 +1599,34 @@ async function validateControllerAuthorizedVerificationMerge(input) {
|
|
|
1294
1599
|
if (!isControllerAuthorizedVerificationMergeTask(input.task, input.surface))
|
|
1295
1600
|
return;
|
|
1296
1601
|
const harness = await readVerificationHarness(input.repoRoot);
|
|
1297
|
-
const matrixPath = normalizeAllowedPath(verificationMatrixPath(harness));
|
|
1298
|
-
const frozenMatrixPath = frozenVerificationMatrixPath(input.surface);
|
|
1299
|
-
if (matrixPath !== frozenMatrixPath ||
|
|
1300
|
-
(taskPath !== "harness.json" && taskPath !== frozenMatrixPath)) {
|
|
1301
|
-
throw new Error("verification authority merge must retain the controller-frozen verification matrix path");
|
|
1302
|
-
}
|
|
1303
1602
|
if (taskPath === "harness.json") {
|
|
1304
1603
|
harnessManifestSchema.parse(harness);
|
|
1305
1604
|
const authority = input.guard?.verificationAuthority;
|
|
1605
|
+
const executorAuthorityChanged = authority && isControllerModelMatrixSelectionTask(input.task)
|
|
1606
|
+
? !sameJsonValue(executorAuthorityWithoutPiModelTiers(harness.executors ?? null), executorAuthorityWithoutPiModelTiers(authority.executors))
|
|
1607
|
+
: authority
|
|
1608
|
+
? !sameJsonValue(harness.executors ?? null, authority.executors)
|
|
1609
|
+
: false;
|
|
1306
1610
|
if (authority &&
|
|
1307
|
-
(
|
|
1611
|
+
(executorAuthorityChanged ||
|
|
1308
1612
|
!sameJsonValue(harness.workflowPolicy ?? null, authority.workflowPolicy))) {
|
|
1309
1613
|
throw new Error("verification authority harness.json merge cannot change controller executor or workflow authority");
|
|
1310
1614
|
}
|
|
1615
|
+
const verification = await resolveProjectVerification(input.repoRoot);
|
|
1616
|
+
const frozenPaths = new Set(input.surface.files.map((entry) => entry.path));
|
|
1617
|
+
for (const command of verification.commands) {
|
|
1618
|
+
for (const relativePath of command.inputs) {
|
|
1619
|
+
if (!frozenPaths.has(normalizeAllowedPath(relativePath))) {
|
|
1620
|
+
throw new Error(`verification authority harness.json merge introduced an unfrozen input: ${relativePath}`);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
const matrixPath = normalizeAllowedPath(verificationMatrixPath(harness));
|
|
1627
|
+
const frozenMatrixPath = frozenVerificationMatrixPath(input.surface);
|
|
1628
|
+
if (matrixPath !== frozenMatrixPath || taskPath !== frozenMatrixPath) {
|
|
1629
|
+
throw new Error("verification authority merge must retain the controller-frozen verification matrix path");
|
|
1311
1630
|
}
|
|
1312
1631
|
const command = await discoverQuickVerification(input.repoRoot);
|
|
1313
1632
|
tokenizeCommand(command);
|
|
@@ -1799,6 +2118,14 @@ const VERIFICATION_OUTPUT_LIMIT = 4 * 1024 * 1024;
|
|
|
1799
2118
|
const VERIFICATION_SUPERVISOR_SOURCE = `
|
|
1800
2119
|
import { spawn } from "node:child_process";
|
|
1801
2120
|
const specification = JSON.parse(Buffer.from(process.argv[2], "base64url").toString("utf8"));
|
|
2121
|
+
const windowsCommandScript = process.platform === "win32" && /\\.(?:cmd|bat)$/i.test(specification.command);
|
|
2122
|
+
const quoteWindowsCommandArgument = (value) => /^[A-Za-z0-9_@./:\\\\=,+-]+$/.test(value)
|
|
2123
|
+
? value
|
|
2124
|
+
: '"' + value.replaceAll('"', '""') + '"';
|
|
2125
|
+
const workerCommand = windowsCommandScript ? process.env.ComSpec || "cmd.exe" : specification.command;
|
|
2126
|
+
const workerArgs = windowsCommandScript
|
|
2127
|
+
? ["/d /s /c " + [specification.command, ...specification.args].map(quoteWindowsCommandArgument).join(" ")]
|
|
2128
|
+
: specification.args;
|
|
1802
2129
|
let worker;
|
|
1803
2130
|
let workerClosed = false;
|
|
1804
2131
|
let exitResult;
|
|
@@ -1820,12 +2147,13 @@ const finish = (message) => {
|
|
|
1820
2147
|
process.send({ ...message, stdioOpen: !workerClosed }, afterSend);
|
|
1821
2148
|
};
|
|
1822
2149
|
try {
|
|
1823
|
-
worker = spawn(
|
|
2150
|
+
worker = spawn(workerCommand, workerArgs, {
|
|
1824
2151
|
cwd: process.cwd(),
|
|
1825
2152
|
env: process.env,
|
|
1826
2153
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1827
2154
|
windowsHide: true,
|
|
1828
2155
|
shell: false,
|
|
2156
|
+
windowsVerbatimArguments: windowsCommandScript,
|
|
1829
2157
|
});
|
|
1830
2158
|
worker.stdout?.pipe(process.stdout);
|
|
1831
2159
|
worker.stderr?.pipe(process.stderr);
|
|
@@ -2291,7 +2619,18 @@ async function runVerificationCommand(input) {
|
|
|
2291
2619
|
const anchorBefore = await controllerAnchorSnapshot(repoRoot, state);
|
|
2292
2620
|
const piHomeBefore = await snapshotOptionalDirectory(path.join(os.homedir(), ".pi"));
|
|
2293
2621
|
const tokens = tokenizeCommand(commandText);
|
|
2294
|
-
|
|
2622
|
+
if (process.platform === "win32" &&
|
|
2623
|
+
tokens.some((token) => /[%!^]/.test(token))) {
|
|
2624
|
+
throw new Error(`unsafe Windows verification command: ${commandText}`);
|
|
2625
|
+
}
|
|
2626
|
+
if (input.authorityInputs) {
|
|
2627
|
+
for (const relativePath of input.authorityInputs) {
|
|
2628
|
+
await assertSafeRegularRepoFile(repoRoot, relativePath);
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
else {
|
|
2632
|
+
await commandRepositoryInputs(repoRoot, commandText);
|
|
2633
|
+
}
|
|
2295
2634
|
let command = tokens.shift();
|
|
2296
2635
|
if (process.platform === "win32" && command === "npm")
|
|
2297
2636
|
command = "npm.cmd";
|
|
@@ -2454,6 +2793,7 @@ async function runFinalVerification(state, authoritativeHomeFingerprint, surface
|
|
|
2454
2793
|
const finalCheck = await checkInitUpdate({
|
|
2455
2794
|
repoRoot: state.repoRoot,
|
|
2456
2795
|
modelCatalog,
|
|
2796
|
+
legacyAudit: true,
|
|
2457
2797
|
});
|
|
2458
2798
|
const doctor = await runInitDoctor({ repoRoot: state.repoRoot, modelCatalog });
|
|
2459
2799
|
let inspect;
|
|
@@ -2474,47 +2814,72 @@ async function runFinalVerification(state, authoritativeHomeFingerprint, surface
|
|
|
2474
2814
|
error: error instanceof Error ? error.message : String(error),
|
|
2475
2815
|
};
|
|
2476
2816
|
}
|
|
2477
|
-
|
|
2817
|
+
const normalized = await loadNormalizedProjectConfig(state.repoRoot);
|
|
2818
|
+
const documentedGovernance = Object.values(normalized.manifest.entrypoints ?? {}).some((value) => typeof value === "string" && value.length > 0);
|
|
2819
|
+
let docsAudit = {
|
|
2820
|
+
ok: true,
|
|
2821
|
+
status: "not-configured",
|
|
2822
|
+
};
|
|
2823
|
+
if (documentedGovernance) {
|
|
2824
|
+
try {
|
|
2825
|
+
const audit = await auditDocs(state.repoRoot);
|
|
2826
|
+
docsAudit = {
|
|
2827
|
+
ok: audit.summary.errorCount === 0,
|
|
2828
|
+
command: "loop-agent docs audit --repo-root .",
|
|
2829
|
+
errorCount: audit.summary.errorCount,
|
|
2830
|
+
warningCount: audit.summary.warningCount,
|
|
2831
|
+
};
|
|
2832
|
+
}
|
|
2833
|
+
catch (error) {
|
|
2834
|
+
docsAudit = {
|
|
2835
|
+
ok: false,
|
|
2836
|
+
command: "loop-agent docs audit --repo-root .",
|
|
2837
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2838
|
+
};
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
let packageSelfCheck;
|
|
2478
2842
|
try {
|
|
2479
|
-
const
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2843
|
+
const provider = await createControllerContextProvider();
|
|
2844
|
+
await provider.read(CONTROLLER_RESOURCE_IDS.loopAgentBridgeSkill);
|
|
2845
|
+
packageSelfCheck = {
|
|
2846
|
+
ok: true,
|
|
2847
|
+
catalogSha256: provider.identity.catalogDigest,
|
|
2848
|
+
resourceCount: provider.list().length,
|
|
2485
2849
|
};
|
|
2486
2850
|
}
|
|
2487
2851
|
catch (error) {
|
|
2488
|
-
|
|
2852
|
+
packageSelfCheck = {
|
|
2489
2853
|
ok: false,
|
|
2490
|
-
command: "loop-agent docs audit --repo-root .",
|
|
2491
2854
|
error: error instanceof Error ? error.message : String(error),
|
|
2492
2855
|
};
|
|
2493
2856
|
}
|
|
2494
|
-
|
|
2495
|
-
state,
|
|
2496
|
-
surface,
|
|
2497
|
-
commandText: "bash scripts/check-repo.sh",
|
|
2498
|
-
});
|
|
2499
|
-
let quickVerification;
|
|
2857
|
+
let projectVerification;
|
|
2500
2858
|
try {
|
|
2501
|
-
const
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2859
|
+
const plan = await resolveProjectVerification(state.repoRoot);
|
|
2860
|
+
const results = [];
|
|
2861
|
+
for (const item of plan.commands) {
|
|
2862
|
+
results.push(await runVerificationCommand({
|
|
2863
|
+
state,
|
|
2864
|
+
surface,
|
|
2865
|
+
commandText: item.command,
|
|
2866
|
+
authorityInputs: item.inputs,
|
|
2867
|
+
}));
|
|
2868
|
+
}
|
|
2869
|
+
projectVerification = {
|
|
2870
|
+
ok: results.length > 0 && results.every((result) => result.ok),
|
|
2871
|
+
source: plan.source,
|
|
2872
|
+
results,
|
|
2873
|
+
};
|
|
2507
2874
|
}
|
|
2508
2875
|
catch (error) {
|
|
2509
|
-
|
|
2876
|
+
projectVerification = {
|
|
2510
2877
|
ok: false,
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
stdout: "",
|
|
2514
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
2878
|
+
source: "unavailable",
|
|
2879
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2515
2880
|
};
|
|
2516
2881
|
}
|
|
2517
|
-
const recovery =
|
|
2882
|
+
const recovery = { ok: true, status: "not-installed" };
|
|
2518
2883
|
const projectPi = await inspectProjectPiSettings({ repoRoot: state.repoRoot });
|
|
2519
2884
|
const homeAfter = await homeFingerprint();
|
|
2520
2885
|
return {
|
|
@@ -2529,6 +2894,8 @@ async function runFinalVerification(state, authoritativeHomeFingerprint, surface
|
|
|
2529
2894
|
mergeReceiptCount: state.mergeReceipts.length,
|
|
2530
2895
|
},
|
|
2531
2896
|
initSurfaceClean: finalCheck.ok,
|
|
2897
|
+
coreState: finalCheck.coreState,
|
|
2898
|
+
migrationState: finalCheck.migrationState,
|
|
2532
2899
|
initSurface: {
|
|
2533
2900
|
deterministicActions: finalCheck.deterministicActions,
|
|
2534
2901
|
modelMergeTasks: finalCheck.modelMergeTasks,
|
|
@@ -2537,15 +2904,14 @@ async function runFinalVerification(state, authoritativeHomeFingerprint, surface
|
|
|
2537
2904
|
doctor: { ok: doctor.ok, checks: doctor.checks },
|
|
2538
2905
|
inspect,
|
|
2539
2906
|
docsAudit,
|
|
2540
|
-
|
|
2541
|
-
|
|
2907
|
+
packageSelfCheck,
|
|
2908
|
+
projectVerification,
|
|
2542
2909
|
recovery,
|
|
2543
2910
|
projectPi: {
|
|
2544
2911
|
reason: projectPi.reason,
|
|
2545
2912
|
action: projectPi.action,
|
|
2546
|
-
trustRequired:
|
|
2547
|
-
valid: projectPi.reason
|
|
2548
|
-
projectPi.reason === "enabled-false",
|
|
2913
|
+
trustRequired: projectPi.reason !== "missing-file",
|
|
2914
|
+
valid: projectPi.reason !== "invalid-json",
|
|
2549
2915
|
},
|
|
2550
2916
|
userHomeUnchanged: sameJsonValue(homeAfter, authoritativeHomeFingerprint),
|
|
2551
2917
|
mergeBoundaryClean: state.mergeTasks.length === 0 && state.queuedMergeTasks.length === 0,
|
|
@@ -2554,8 +2920,8 @@ async function runFinalVerification(state, authoritativeHomeFingerprint, surface
|
|
|
2554
2920
|
}
|
|
2555
2921
|
function authoritativeVerificationFailureReason(verification) {
|
|
2556
2922
|
const sources = [
|
|
2557
|
-
verification.
|
|
2558
|
-
verification.
|
|
2923
|
+
verification.projectVerification,
|
|
2924
|
+
verification.packageSelfCheck,
|
|
2559
2925
|
verification.inspect,
|
|
2560
2926
|
verification.docsAudit,
|
|
2561
2927
|
];
|
|
@@ -2594,28 +2960,25 @@ function verificationPassed(verification) {
|
|
|
2594
2960
|
const doctor = verification.doctor;
|
|
2595
2961
|
const inspect = verification.inspect;
|
|
2596
2962
|
const docsAudit = verification.docsAudit;
|
|
2597
|
-
const
|
|
2598
|
-
const
|
|
2963
|
+
const packageSelfCheck = verification.packageSelfCheck;
|
|
2964
|
+
const projectVerification = verification.projectVerification;
|
|
2599
2965
|
const projectPi = verification.projectPi;
|
|
2600
|
-
const recovery = verification.recovery;
|
|
2601
2966
|
return (verification.initSurfaceClean === true &&
|
|
2967
|
+
verification.coreState === "ready" &&
|
|
2968
|
+
(verification.migrationState === "clean" ||
|
|
2969
|
+
verification.migrationState === "retained") &&
|
|
2602
2970
|
isRecord(doctor) &&
|
|
2603
2971
|
doctor.ok === true &&
|
|
2604
2972
|
isRecord(inspect) &&
|
|
2605
2973
|
inspect.ok === true &&
|
|
2606
2974
|
isRecord(docsAudit) &&
|
|
2607
2975
|
docsAudit.ok === true &&
|
|
2608
|
-
isRecord(
|
|
2609
|
-
|
|
2610
|
-
isRecord(
|
|
2611
|
-
|
|
2976
|
+
isRecord(packageSelfCheck) &&
|
|
2977
|
+
packageSelfCheck.ok === true &&
|
|
2978
|
+
isRecord(projectVerification) &&
|
|
2979
|
+
projectVerification.ok === true &&
|
|
2612
2980
|
isRecord(projectPi) &&
|
|
2613
2981
|
projectPi.valid === true &&
|
|
2614
|
-
isRecord(recovery) &&
|
|
2615
|
-
RECOVERY_PATHS.every((recoveryPath) => {
|
|
2616
|
-
const entry = recovery[recoveryPath];
|
|
2617
|
-
return isRecord(entry) && entry.exists === true && entry.invariant === true;
|
|
2618
|
-
}) &&
|
|
2619
2982
|
verification.userHomeUnchanged === true &&
|
|
2620
2983
|
verification.mergeBoundaryClean === true &&
|
|
2621
2984
|
verification.humanDecisionClean === true);
|
|
@@ -2687,13 +3050,24 @@ async function readFreshCompletedVerification(repoRoot, state) {
|
|
|
2687
3050
|
return { verification, reportMarkdown: report.toString("utf-8") };
|
|
2688
3051
|
}
|
|
2689
3052
|
async function readValidatedStatusState(repoRoot, runId) {
|
|
2690
|
-
|
|
3053
|
+
let state = await readState(repoRoot, runId);
|
|
2691
3054
|
assertTerminalCoherence(state);
|
|
2692
3055
|
const directory = runDirectory(repoRoot, runId);
|
|
2693
3056
|
const anchor = await readControllerAnchor(repoRoot, state);
|
|
2694
3057
|
if (anchor.mergeAuthority) {
|
|
2695
3058
|
mergeAuthorityProjection(state, mergeAuthorityFromAnchor(anchor, state));
|
|
2696
3059
|
}
|
|
3060
|
+
const pendingDecision = await readPendingDecisionProjection({
|
|
3061
|
+
directory,
|
|
3062
|
+
state,
|
|
3063
|
+
anchor,
|
|
3064
|
+
});
|
|
3065
|
+
if (pendingDecision) {
|
|
3066
|
+
state = cloneJson(state);
|
|
3067
|
+
state.status = "in-progress";
|
|
3068
|
+
state.humanDecisions = [];
|
|
3069
|
+
state.nextAction = `Resume the authenticated pending decision application with: loop-agent init upgrade --continue --run-id ${state.runId}`;
|
|
3070
|
+
}
|
|
2697
3071
|
if (state.status === "completed" || state.status === "cancelled") {
|
|
2698
3072
|
if (!(await frozenControllerMatches(directory, state))) {
|
|
2699
3073
|
throw new Error("Controller identity drift detected for persisted init upgrade state");
|
|
@@ -2788,6 +3162,138 @@ async function activateModelMergeTask(input) {
|
|
|
2788
3162
|
await writeReport(directory, state, undefined, gitignoreMigration);
|
|
2789
3163
|
return toResult(state, undefined, gitignoreMigration);
|
|
2790
3164
|
}
|
|
3165
|
+
class DecisionApplicationPendingError extends Error {
|
|
3166
|
+
constructor(error) {
|
|
3167
|
+
super(`init decision application is pending recovery: ${error instanceof Error ? error.message : String(error)}`);
|
|
3168
|
+
this.name = "DecisionApplicationPendingError";
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
async function applyDecisionBoundFile(input) {
|
|
3172
|
+
let anchor = await readControllerAnchor(input.state.repoRoot, input.state);
|
|
3173
|
+
let pending = await readPendingDecisionApplication({
|
|
3174
|
+
directory: input.directory,
|
|
3175
|
+
state: input.state,
|
|
3176
|
+
anchor,
|
|
3177
|
+
});
|
|
3178
|
+
if (!pending) {
|
|
3179
|
+
if (!input.decisionFile) {
|
|
3180
|
+
throw new Error("init upgrade requires --decision-file for unresolved human decisions");
|
|
3181
|
+
}
|
|
3182
|
+
const parsed = await readInitUpgradeDecisionFile({
|
|
3183
|
+
repoRoot: input.state.repoRoot,
|
|
3184
|
+
runId: input.state.runId,
|
|
3185
|
+
decisionFile: input.decisionFile,
|
|
3186
|
+
pending: input.state.humanDecisions,
|
|
3187
|
+
});
|
|
3188
|
+
const stateProjection = controllerStateProjection(input.state);
|
|
3189
|
+
pending = {
|
|
3190
|
+
schemaVersion: 1,
|
|
3191
|
+
runId: input.state.runId,
|
|
3192
|
+
repoRoot: input.state.repoRoot,
|
|
3193
|
+
decisionFileSha256: parsed.sha256,
|
|
3194
|
+
document: cloneJson(parsed.document),
|
|
3195
|
+
stateProjection,
|
|
3196
|
+
stateProjectionSha256: canonicalSha256(stateProjection),
|
|
3197
|
+
preparedAt: new Date().toISOString(),
|
|
3198
|
+
};
|
|
3199
|
+
await writeJsonAtomic(path.join(input.directory, PENDING_DECISION_FILE), pending);
|
|
3200
|
+
await writeControllerAnchor({
|
|
3201
|
+
state: input.state,
|
|
3202
|
+
decisionAuthority: {
|
|
3203
|
+
pending,
|
|
3204
|
+
pendingSha256: canonicalSha256(pending),
|
|
3205
|
+
},
|
|
3206
|
+
});
|
|
3207
|
+
anchor = await readControllerAnchor(input.state.repoRoot, input.state);
|
|
3208
|
+
pending = await readPendingDecisionApplication({
|
|
3209
|
+
directory: input.directory,
|
|
3210
|
+
state: input.state,
|
|
3211
|
+
anchor,
|
|
3212
|
+
});
|
|
3213
|
+
if (!pending)
|
|
3214
|
+
throw new Error("pending init decision authority was not established");
|
|
3215
|
+
}
|
|
3216
|
+
else if (input.decisionFile) {
|
|
3217
|
+
try {
|
|
3218
|
+
const suppliedSha256 = sha256(await readRepoLocalDecisionFile(input.state.repoRoot, input.decisionFile));
|
|
3219
|
+
if (suppliedSha256 !== pending.decisionFileSha256) {
|
|
3220
|
+
throw new Error("init upgrade --decision-file differs from the authenticated pending decision");
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
catch (error) {
|
|
3224
|
+
throw new DecisionApplicationPendingError(error);
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
try {
|
|
3228
|
+
const applied = await applyInitDecisionBoundActions({
|
|
3229
|
+
repoRoot: input.state.repoRoot,
|
|
3230
|
+
actions: pending.document.decisions,
|
|
3231
|
+
resumeAuthority: {
|
|
3232
|
+
runId: pending.runId,
|
|
3233
|
+
pendingSha256: anchor.decisionAuthority.pendingSha256,
|
|
3234
|
+
},
|
|
3235
|
+
});
|
|
3236
|
+
await input.afterActionsApplied?.();
|
|
3237
|
+
const decidedAt = new Date().toISOString();
|
|
3238
|
+
const receipts = applied.map((result) => ({
|
|
3239
|
+
...result,
|
|
3240
|
+
decisionFileSha256: pending.decisionFileSha256,
|
|
3241
|
+
decidedAt,
|
|
3242
|
+
}));
|
|
3243
|
+
const existingReceiptKeys = new Set((input.state.decisionReceipts ?? []).map((receipt) => canonicalSha256({
|
|
3244
|
+
path: receipt.path,
|
|
3245
|
+
candidateId: receipt.candidateId,
|
|
3246
|
+
disposition: receipt.disposition,
|
|
3247
|
+
expectedSha256: receipt.expectedSha256,
|
|
3248
|
+
resultSha256: receipt.resultSha256,
|
|
3249
|
+
decisionFileSha256: receipt.decisionFileSha256,
|
|
3250
|
+
})));
|
|
3251
|
+
input.state.decisionReceipts = [
|
|
3252
|
+
...(input.state.decisionReceipts ?? []),
|
|
3253
|
+
...receipts.filter((receipt) => !existingReceiptKeys.has(canonicalSha256({
|
|
3254
|
+
path: receipt.path,
|
|
3255
|
+
candidateId: receipt.candidateId,
|
|
3256
|
+
disposition: receipt.disposition,
|
|
3257
|
+
expectedSha256: receipt.expectedSha256,
|
|
3258
|
+
resultSha256: receipt.resultSha256,
|
|
3259
|
+
decisionFileSha256: receipt.decisionFileSha256,
|
|
3260
|
+
}))),
|
|
3261
|
+
];
|
|
3262
|
+
for (const receipt of receipts) {
|
|
3263
|
+
if ((receipt.disposition === "replace-with-bridge" ||
|
|
3264
|
+
receipt.disposition === "delete-listed-file") &&
|
|
3265
|
+
!input.state.actionReceipts.some((action) => action.type ===
|
|
3266
|
+
(receipt.disposition === "replace-with-bridge"
|
|
3267
|
+
? "replace-owned-file"
|
|
3268
|
+
: "remove-owned-file") &&
|
|
3269
|
+
action.path === receipt.path &&
|
|
3270
|
+
action.expectedSha256 === receipt.expectedSha256)) {
|
|
3271
|
+
input.state.actionReceipts.push({
|
|
3272
|
+
type: receipt.disposition === "replace-with-bridge"
|
|
3273
|
+
? "replace-owned-file"
|
|
3274
|
+
: "remove-owned-file",
|
|
3275
|
+
path: receipt.path,
|
|
3276
|
+
expectedSha256: receipt.expectedSha256,
|
|
3277
|
+
reason: receipt.disposition === "replace-with-bridge"
|
|
3278
|
+
? DECISION_REPLACE_REASON
|
|
3279
|
+
: "remove an explicitly approved hash-bound legacy init file",
|
|
3280
|
+
});
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
input.state.humanDecisions = [];
|
|
3284
|
+
input.state.status = "in-progress";
|
|
3285
|
+
input.state.nextAction =
|
|
3286
|
+
"Decision-bound dispositions were recorded; re-plan the minimal surface and continue verification.";
|
|
3287
|
+
await writeJsonAtomic(path.join(input.directory, "decision-receipts.json"), input.state.decisionReceipts);
|
|
3288
|
+
await writeJsonAtomic(path.join(input.directory, "change-manifest.json"), input.state.actionReceipts);
|
|
3289
|
+
await saveState(input.directory, input.state);
|
|
3290
|
+
await writeControllerAnchor({ state: input.state, decisionAuthority: null });
|
|
3291
|
+
await rm(path.join(input.directory, PENDING_DECISION_FILE), { force: true });
|
|
3292
|
+
}
|
|
3293
|
+
catch (error) {
|
|
3294
|
+
throw new DecisionApplicationPendingError(error);
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
2791
3297
|
async function verifyAndCompleteUpgrade(input) {
|
|
2792
3298
|
const { directory, state, gitignoreMigration, modelCatalog } = input;
|
|
2793
3299
|
state.mergeTasks = [];
|
|
@@ -2798,6 +3304,7 @@ async function verifyAndCompleteUpgrade(input) {
|
|
|
2798
3304
|
let postMerge = await checkInitUpdate({
|
|
2799
3305
|
repoRoot: state.repoRoot,
|
|
2800
3306
|
modelCatalog,
|
|
3307
|
+
legacyAudit: true,
|
|
2801
3308
|
});
|
|
2802
3309
|
const seen = new Set();
|
|
2803
3310
|
for (let pass = 0; postMerge.deterministicActions.length > 0 && pass < 2; pass += 1) {
|
|
@@ -2809,8 +3316,8 @@ async function verifyAndCompleteUpgrade(input) {
|
|
|
2809
3316
|
const converged = await applyInitUpdate({
|
|
2810
3317
|
repoRoot: state.repoRoot,
|
|
2811
3318
|
applySafe: true,
|
|
2812
|
-
clientRecovery: "project",
|
|
2813
3319
|
modelCatalog,
|
|
3320
|
+
legacyAudit: true,
|
|
2814
3321
|
});
|
|
2815
3322
|
state.actionReceipts.push(...converged.applied);
|
|
2816
3323
|
await writeJsonAtomic(path.join(directory, "change-manifest.json"), state.actionReceipts);
|
|
@@ -2819,6 +3326,7 @@ async function verifyAndCompleteUpgrade(input) {
|
|
|
2819
3326
|
postMerge = await checkInitUpdate({
|
|
2820
3327
|
repoRoot: state.repoRoot,
|
|
2821
3328
|
modelCatalog,
|
|
3329
|
+
legacyAudit: true,
|
|
2822
3330
|
});
|
|
2823
3331
|
}
|
|
2824
3332
|
if (postMerge.deterministicActions.length > 0) {
|
|
@@ -2828,6 +3336,7 @@ async function verifyAndCompleteUpgrade(input) {
|
|
|
2828
3336
|
postMerge.humanDecisions.length > 0) {
|
|
2829
3337
|
throw new Error("accepted semantic merges introduced new non-deterministic init decisions; start a new bounded upgrade run");
|
|
2830
3338
|
}
|
|
3339
|
+
projectInitStates(state, postMerge);
|
|
2831
3340
|
}
|
|
2832
3341
|
state.phase = "VERIFY";
|
|
2833
3342
|
const anchor = await readControllerAnchor(state.repoRoot, state);
|
|
@@ -2839,7 +3348,7 @@ async function verifyAndCompleteUpgrade(input) {
|
|
|
2839
3348
|
state.status = "completed";
|
|
2840
3349
|
state.phase = "COMPLETED";
|
|
2841
3350
|
state.nextAction =
|
|
2842
|
-
"Init upgrade completed; reload the host session so
|
|
3351
|
+
"Init upgrade completed; reload the host session so the minimal bridge and current package resources are discovered.";
|
|
2843
3352
|
await writeReport(directory, state, verification, gitignoreMigration);
|
|
2844
3353
|
const finalReportSha256 = sha256(await readFile(path.join(directory, "final-report.md")));
|
|
2845
3354
|
await writeControllerAnchor({
|
|
@@ -2937,7 +3446,9 @@ async function executeUpgrade(directory, state, input) {
|
|
|
2937
3446
|
const pre = await checkInitUpdate({
|
|
2938
3447
|
repoRoot: state.repoRoot,
|
|
2939
3448
|
modelCatalog: state.modelCatalog,
|
|
3449
|
+
legacyAudit: true,
|
|
2940
3450
|
});
|
|
3451
|
+
projectInitStates(state, pre);
|
|
2941
3452
|
await writeJsonAtomic(path.join(directory, "pre-update-report.json"), pre);
|
|
2942
3453
|
await writeJsonAtomic(path.join(directory, "upgrade-plan.json"), {
|
|
2943
3454
|
deterministicActions: pre.deterministicActions,
|
|
@@ -2947,10 +3458,10 @@ async function executeUpgrade(directory, state, input) {
|
|
|
2947
3458
|
"init check-update",
|
|
2948
3459
|
"init doctor",
|
|
2949
3460
|
"loop-agent inspect",
|
|
2950
|
-
"
|
|
2951
|
-
"
|
|
2952
|
-
"
|
|
2953
|
-
"
|
|
3461
|
+
"package resource catalog self-check",
|
|
3462
|
+
"normalized project verification or bounded capability scan",
|
|
3463
|
+
"project docs audit when configured",
|
|
3464
|
+
"explicit adapter invariants when installed",
|
|
2954
3465
|
"Pi home fingerprint",
|
|
2955
3466
|
],
|
|
2956
3467
|
});
|
|
@@ -2959,9 +3470,7 @@ async function executeUpgrade(directory, state, input) {
|
|
|
2959
3470
|
if (pre.surfaceState === "missing") {
|
|
2960
3471
|
const initialized = await initializeLoopAgentProject({
|
|
2961
3472
|
repoRoot: state.repoRoot,
|
|
2962
|
-
profile: "full",
|
|
2963
3473
|
merge: true,
|
|
2964
|
-
clientRecovery: "project",
|
|
2965
3474
|
});
|
|
2966
3475
|
state.actionReceipts.push(...initialized.written.map((writtenPath) => ({
|
|
2967
3476
|
type: writtenPath === INIT_SURFACE_STATE_PATH
|
|
@@ -2969,23 +3478,25 @@ async function executeUpgrade(directory, state, input) {
|
|
|
2969
3478
|
: "write-generated-missing",
|
|
2970
3479
|
path: writtenPath,
|
|
2971
3480
|
reason: writtenPath === INIT_SURFACE_STATE_PATH
|
|
2972
|
-
? "recorded the
|
|
2973
|
-
: "
|
|
3481
|
+
? "recorded the minimal fresh-project initialization surface"
|
|
3482
|
+
: "minimal initialization wrote the required project surface",
|
|
2974
3483
|
})));
|
|
2975
3484
|
}
|
|
2976
3485
|
await assertInactive(input);
|
|
2977
3486
|
const safe = await applyInitUpdate({
|
|
2978
3487
|
repoRoot: state.repoRoot,
|
|
2979
3488
|
applySafe: true,
|
|
2980
|
-
clientRecovery: "project",
|
|
2981
3489
|
modelCatalog: state.modelCatalog,
|
|
3490
|
+
legacyAudit: true,
|
|
2982
3491
|
});
|
|
2983
3492
|
state.actionReceipts.push(...safe.applied);
|
|
2984
3493
|
await writeJsonAtomic(path.join(directory, "change-manifest.json"), state.actionReceipts);
|
|
2985
3494
|
let post = await checkInitUpdate({
|
|
2986
3495
|
repoRoot: state.repoRoot,
|
|
2987
3496
|
modelCatalog: state.modelCatalog,
|
|
3497
|
+
legacyAudit: true,
|
|
2988
3498
|
});
|
|
3499
|
+
projectInitStates(state, post);
|
|
2989
3500
|
// Converge remaining safe deterministic init actions before the final check.
|
|
2990
3501
|
// Project/governance-derived drift (e.g. a personalized project rename after
|
|
2991
3502
|
// an accepted merge) settles here instead of failing finalCheck. Model merge
|
|
@@ -3000,8 +3511,8 @@ async function executeUpgrade(directory, state, input) {
|
|
|
3000
3511
|
const converged = await applyInitUpdate({
|
|
3001
3512
|
repoRoot: state.repoRoot,
|
|
3002
3513
|
applySafe: true,
|
|
3003
|
-
clientRecovery: "project",
|
|
3004
3514
|
modelCatalog: state.modelCatalog,
|
|
3515
|
+
legacyAudit: true,
|
|
3005
3516
|
});
|
|
3006
3517
|
state.actionReceipts.push(...converged.applied);
|
|
3007
3518
|
await writeJsonAtomic(path.join(directory, "change-manifest.json"), state.actionReceipts);
|
|
@@ -3010,7 +3521,9 @@ async function executeUpgrade(directory, state, input) {
|
|
|
3010
3521
|
post = await checkInitUpdate({
|
|
3011
3522
|
repoRoot: state.repoRoot,
|
|
3012
3523
|
modelCatalog: state.modelCatalog,
|
|
3524
|
+
legacyAudit: true,
|
|
3013
3525
|
});
|
|
3526
|
+
projectInitStates(state, post);
|
|
3014
3527
|
}
|
|
3015
3528
|
if (post.deterministicActions.length > 0) {
|
|
3016
3529
|
throw new Error(`deterministic init update exceeded the 2-pass convergence limit: ${post.deterministicActions.map((action) => `${action.type}:${action.path}`).join(", ")}`);
|
|
@@ -3056,7 +3569,7 @@ async function executeUpgrade(directory, state, input) {
|
|
|
3056
3569
|
state.status = "needs-human-decision";
|
|
3057
3570
|
state.phase = "MODEL_MERGE";
|
|
3058
3571
|
state.nextAction =
|
|
3059
|
-
"Resolve
|
|
3572
|
+
"Resolve every file decision in a repo-local schemaVersion 1 decision file bound to this runId and each current SHA-256, then call init upgrade --continue --decision-file <path>. The controller preserves detached project content and only replaces the bridge after explicit approval. Resolve staged Git blockers before continuing.";
|
|
3060
3573
|
await writeJsonAtomic(path.join(directory, "merge-tasks.json"), state.mergeTasks);
|
|
3061
3574
|
await saveState(directory, state);
|
|
3062
3575
|
await writeReport(directory, state, undefined, gitignoreMigration);
|
|
@@ -3083,6 +3596,9 @@ async function executeUpgrade(directory, state, input) {
|
|
|
3083
3596
|
export async function runInitUpgrade(input) {
|
|
3084
3597
|
const repoRoot = path.resolve(input.repoRoot);
|
|
3085
3598
|
const mode = input.mode ?? "start";
|
|
3599
|
+
if (input.decisionFile && mode !== "continue") {
|
|
3600
|
+
throw new Error("init upgrade --decision-file requires --continue and --run-id");
|
|
3601
|
+
}
|
|
3086
3602
|
if ((mode === "status" || mode === "report") && !input.runId) {
|
|
3087
3603
|
throw new Error("init upgrade --status/--report requires --run-id");
|
|
3088
3604
|
}
|
|
@@ -3151,6 +3667,25 @@ export async function runInitUpgrade(input) {
|
|
|
3151
3667
|
if (mode === "continue") {
|
|
3152
3668
|
state = await readState(repoRoot, runId);
|
|
3153
3669
|
directory = runDirectory(repoRoot, runId);
|
|
3670
|
+
const decisionAnchor = await readControllerAnchor(repoRoot, state);
|
|
3671
|
+
const pendingDecision = await readPendingDecisionApplication({
|
|
3672
|
+
directory,
|
|
3673
|
+
state,
|
|
3674
|
+
anchor: decisionAnchor,
|
|
3675
|
+
});
|
|
3676
|
+
if (pendingDecision || input.decisionFile) {
|
|
3677
|
+
if ((!pendingDecision && state.phase !== "MODEL_MERGE") ||
|
|
3678
|
+
(!pendingDecision && state.status !== "needs-human-decision") ||
|
|
3679
|
+
(!pendingDecision && state.humanDecisions.length === 0)) {
|
|
3680
|
+
throw new Error("init upgrade --decision-file requires a paused needs-human-decision run");
|
|
3681
|
+
}
|
|
3682
|
+
await applyDecisionBoundFile({
|
|
3683
|
+
directory,
|
|
3684
|
+
state,
|
|
3685
|
+
decisionFile: input.decisionFile,
|
|
3686
|
+
afterActionsApplied: input.afterDecisionActionsApplied,
|
|
3687
|
+
});
|
|
3688
|
+
}
|
|
3154
3689
|
const recoverableMergeState = state.phase === "MODEL_MERGE" && state.status === "in-progress"
|
|
3155
3690
|
? cloneJson(state)
|
|
3156
3691
|
: undefined;
|
|
@@ -3217,10 +3752,13 @@ export async function runInitUpgrade(input) {
|
|
|
3217
3752
|
mergeTasks: [],
|
|
3218
3753
|
queuedMergeTasks: [],
|
|
3219
3754
|
mergeReceipts: [],
|
|
3755
|
+
decisionReceipts: [],
|
|
3220
3756
|
humanDecisions: [],
|
|
3221
3757
|
actionReceipts: [],
|
|
3222
3758
|
homeFingerprint: await homeFingerprint(),
|
|
3223
3759
|
modelCatalog: input.modelCatalog,
|
|
3760
|
+
coreState: "blocked",
|
|
3761
|
+
migrationState: "clean",
|
|
3224
3762
|
};
|
|
3225
3763
|
await writeControllerAnchor({ state, initialize: true });
|
|
3226
3764
|
await writeJsonAtomic(path.join(directory, "controller-identity.json"), {
|
|
@@ -3238,6 +3776,8 @@ export async function runInitUpgrade(input) {
|
|
|
3238
3776
|
catch (error) {
|
|
3239
3777
|
if (error instanceof VerificationProcessBoundaryError)
|
|
3240
3778
|
throw error;
|
|
3779
|
+
if (error instanceof DecisionApplicationPendingError)
|
|
3780
|
+
throw error;
|
|
3241
3781
|
if (!state)
|
|
3242
3782
|
throw error;
|
|
3243
3783
|
const nextAction = error instanceof Error ? error.message : String(error);
|