@tea-agent/loop-agent 0.38.2 → 0.38.4
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/AGENTS.md +4 -1
- package/CHANGELOG.md +171 -96
- package/README.md +11 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/task-lifecycle/advance.js +4 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +16 -5
- package/dist/cli/program.js +16 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli.js +2 -1
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +428 -27
- package/dist/commands/inspect-next.js +8 -0
- package/dist/commands/task-advance.js +31 -0
- package/dist/executors/dag-pi-executor.js +76 -10
- package/dist/executors/pi-event-serializer.js +42 -1
- package/dist/executors/pi-executor.js +51 -0
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +51 -12
- package/dist/executors/pi-sdk-executor.js +187 -59
- package/dist/executors/shell-executor.js +265 -15
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/shared/dag-prompt-override.js +28 -0
- package/dist/shared/operator/capabilities.js +32 -0
- package/dist/shared/playwright-cli-command-policy.js +15 -0
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +41 -8
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/contract/constants.js +1 -0
- package/dist/task/contract/diff.js +26 -0
- package/dist/task/contract/project.js +5 -0
- package/dist/task/contract/schema.js +6 -1
- package/dist/task/source-prepare/build-draft.js +15 -0
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/worker/cli.js +31 -1
- package/dist/worker/console/chat/routes.js +1 -0
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- package/dist/worker/console/operator-actions.js +116 -0
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/pi-readiness.js +4 -4
- package/dist/worker/console/routes.js +43 -2
- package/dist/worker/console/security.js +63 -9
- package/dist/worker/console/server.js +36 -5
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4bmo3iY.js → abnfDiagram-N423BO3Z-gauhD9kD.js} +1 -1
- package/dist/worker/console/static/assets/{arc-SAD7McOS.js → arc-pm3c2Rsl.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DAVLUdK1.js → architectureDiagram-T3A2C74G-D_JAg3wH.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CPPdv4vj.js → blockDiagram-VBNYF7ZC-Dk7-ex52.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-D1GRGZax.js → c4Diagram-5PPSVZJV-Uval9oRI.js} +1 -1
- package/dist/worker/console/static/assets/channel-CVrnUdZl.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D_Bq2ZqK.js → chunk-2GRJ4B5K-BSYjHs-v.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CPLVtamK.js → chunk-2Q5K7J3B-KGUuBXI8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DnaZWcL3.js → chunk-5RXB4S5H-JqAtxD9N.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BRTmFgnq.js → chunk-5VM5RSS4-D6EJOVQK.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BRlfYZoH.js → chunk-6Q2QTUOP-B1JriSjm.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CjNybD4u.js → chunk-GF5L2VYU-D62b96Jd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D3e-74fG.js → chunk-JWPE2WC7-BsNKEC_r.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Br3ir6f2.js → chunk-KBJHAD2P-B56v6-ZP.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-1NgVuRbh.js → chunk-RYQCIY6F-D2KPnd8K.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BTYuvuWN.js → chunk-XXDRQBXY-kzKSCaaL.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-aX91WTya.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-aX91WTya.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BC6Bgosf.js → cose-bilkent-JH36ORCC-C6N6KNJd.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DV-HypfL.js → cynefin-VYW2F7L2-DiyymT2D.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DhTaddO-.js → cynefinDiagram-MW4NZA55-BeKAXvf3.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BnEcOmiI.js → dagre-VZM6K2ZE-TkIdXfPF.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-xphGRVKp.js → diagram-7IWD3JNH-BFmSG3ks.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CrdD1mRG.js → diagram-B4RE2ZJO-DqpJT2Fs.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-D4YdLsaF.js → diagram-LBJQPF4R-r5ATN43t.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-LCKyGZ5U.js → diagram-Q27KOJAE-DEcSCMuR.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BeiTe7t0.js → diagram-UB23O5K3-VOvYRLVc.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BO2cq8GS.js → ebnfDiagram-BXEA7PRR-D7f4Cb1u.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CmWww3Nt.js → erDiagram-JOGREHBK-ziUf3U0G.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-C2kkUYg5.js → flowDiagram-UKHOOZJN-BoRdQ-1y.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DY_JZ2iQ.js → ganttDiagram-PKOTCBZU-B1-GncNm.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BizhCSw9.js → gitGraphDiagram-DS77QQ5N-CTW-ph8W.js} +1 -1
- package/dist/worker/console/static/assets/index-C8rOIAJr.css +1 -0
- package/dist/worker/console/static/assets/{index-BXN89VPX.js → index-DEbyrEfW.js} +147 -68
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Dlo2eK_d.js → infoDiagram-6WML65LV-BFA337tZ.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js → ishikawaDiagram-WSZJBQD7-BtEujgRR.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DiMDe5Yh.js → journeyDiagram-NVQOT4AX-C8ZwD2Jp.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-sN553oIN.js → kanban-definition-27J2QSJJ-Ck-z8B11.js} +1 -1
- package/dist/worker/console/static/assets/{linear-D9Py31Ld.js → linear-CAyXjQjN.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-D7Occzk_.js → mermaid.core-CcWKn6tS.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DjDRboiR.js → mindmap-definition-FAOFIHXS-C2NuDO7E.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Cgc4qlzp.js → pegDiagram-VL7TDLO6-BFub3PDz.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DxZj8pKd.js → pieDiagram-7S7Q4E2Y-BJx5THZ5.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CJ50MngR.js → quadrantDiagram-CIZ2JOQS-DhalkDWV.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BY-OD8Tr.js → railroadDiagram-AXF67PYL-CVcBM36o.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CQFButfK.js → requirementDiagram-LRYGKXZP-D_vy4n0w.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BB_QUK_b.js → sankeyDiagram-W5VNT64P-DGYdJL1D.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CqhXZjyx.js → sequenceDiagram-SI44F4Z6-C1xJFg3k.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-2l1U4rgJ.js → sizeCapture-X5ZJPWSS-D_Fc43df.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BX1FbrhJ.js → stateDiagram-OKZ733FA-BhGbIjbu.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DUt1lyFL.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DOSnT7hb.js → swimlanes-SLNWSIFB-DVkNcO-V.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BM6SGr7j.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DVJQA7AN.js → timeline-definition-Z64GVDOM-BQU9ufV0.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-74uxl9gx.js → vennDiagram-T6HMQDX7-BCQhRmYB.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DsBxlcg_.js → wardleyDiagram-T6FBY63Y-DWerOyHO.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-snuW9Mjw.js → xychartDiagram-ELKLHX3M-DxLeebQU.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- package/dist/worker/observe/node-input.js +74 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +36 -3
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +66 -0
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/format.js +23 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.js +4 -0
- package/dist/worker/observe/static/state.js +13 -2
- package/dist/worker/observe/static/styles.css +73 -0
- package/dist/worker/observe/static/views/dag-inspector.js +612 -8
- package/dist/worker/observe/static/views/dag.js +8 -1
- package/dist/worker/observe/static/views/failures.js +5 -2
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +76 -6
- package/dist/workflows/dag/backend-test-scenario-partitions.js +73 -1
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/failure-category.js +12 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +145 -40
- package/dist/workflows/dag/frontend-prewrite-gate.js +87 -135
- package/dist/workflows/dag/frontend-repair.js +14 -0
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
- package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
- package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
- package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
- package/dist/workflows/dag/frontend-verification-trace.js +29 -4
- package/dist/workflows/dag/init-hybrid.js +196 -117
- package/dist/workflows/dag/node-execution.js +139 -10
- package/dist/workflows/dag/prompt.js +15 -1
- package/dist/workflows/dag/rerun-plan.js +134 -3
- package/dist/workflows/dag/rerun-run.js +58 -6
- package/dist/workflows/dag/retry-policy.js +91 -0
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +32 -1
- package/dist/workflows/dag/validate.js +41 -1
- package/docs/init-surface.manifest.json +5 -0
- package/docs/operations/README.md +1 -1
- package/docs/operations/local-development-environment.md +1 -5
- package/docs/skills/vetted-skill-registry.md +14 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/backend-test-dag.json +2 -2
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/harness.json +2 -2
- package/package.json +12 -4
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/analyze-product-requirements/SKILL.md +8 -5
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
- package/skills/analyze-product-requirements/references/example.md +50 -0
- package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
- package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +65 -0
- package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
- package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
- package/skills/improve-codebase-architecture/SKILL.md +81 -0
- package/skills/improve-codebase-architecture/deepening.md +37 -0
- package/skills/improve-codebase-architecture/html-report.md +123 -0
- package/skills/improve-codebase-architecture/interface-design.md +44 -0
- package/skills/improve-codebase-architecture/language.md +53 -0
- package/skills/loop-agent/references/command-reference.md +11 -4
- package/skills/playwright-cli/SKILL.md +22 -0
- package/skills/playwright-cli-case-generator/SKILL.md +20 -1
- package/dist/worker/console/static/assets/channel-R6oQIDkw.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +0 -1
- package/dist/worker/console/static/assets/index-CuH5CNFu.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +0 -8
package/dist/commands/init.js
CHANGED
|
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { isInitRuntimeActive, } from "../shared/runtime-activity.js";
|
|
8
8
|
import { loadHarnessManifest } from "../governance/harness.js";
|
|
9
9
|
import { OPENCODE_CONTEXT_OVERFLOW_COMPACT_PLUGIN_PATH, OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH, PI_CONTEXT_OVERFLOW_EXTENSION_PATH, PI_PROJECT_SETTINGS_PATH, applyPiRetryMerge, applyProjectPiSettingsMerge, buildOpenCodeContextOverflowCompactPluginSource, buildProjectPiSettingsSource, buildOpenCodeTransientRetryPluginSource, buildPiContextOverflowExtensionSource, inspectProjectPiSettings, parseClientRecoveryMode, runClientRecovery, } from "./client-recovery.js";
|
|
10
|
+
import { assessInitPiModelReadiness, discoverInitModelCatalog, validateInitModelMatrixAgainstCatalog, } from "./init-model-catalog.js";
|
|
10
11
|
const MANAGED_BLOCK_START = "<!-- LOOP_AGENT_INIT_START -->";
|
|
11
12
|
const MANAGED_BLOCK_END = "<!-- LOOP_AGENT_INIT_END -->";
|
|
12
13
|
const GITIGNORE_BLOCK_START = "# LOOP_AGENT_INIT_START";
|
|
@@ -889,13 +890,13 @@ function buildHarness(input) {
|
|
|
889
890
|
}
|
|
890
891
|
// Init always writes a Pi tier matrix. `defaultModel` remains runtime
|
|
891
892
|
// compatibility for old projects only and is never fresh-init output.
|
|
892
|
-
if (input.model) {
|
|
893
|
+
if (input.model || input.modelMatrix) {
|
|
893
894
|
const piExisting = isRecord(mergedExecutors.pi) ? mergedExecutors.pi : {};
|
|
894
895
|
mergedExecutors.pi = {
|
|
895
896
|
...piExisting,
|
|
896
|
-
LOW: input.model,
|
|
897
|
-
MED: input.model,
|
|
898
|
-
HIGH: input.model,
|
|
897
|
+
LOW: input.modelMatrix?.LOW ?? input.model,
|
|
898
|
+
MED: input.modelMatrix?.MED ?? input.model,
|
|
899
|
+
HIGH: input.modelMatrix?.HIGH ?? input.model,
|
|
899
900
|
};
|
|
900
901
|
}
|
|
901
902
|
if (isRecord(mergedExecutors.pi)) {
|
|
@@ -1128,6 +1129,28 @@ export function normalizeInitModelReference(input) {
|
|
|
1128
1129
|
}
|
|
1129
1130
|
return model;
|
|
1130
1131
|
}
|
|
1132
|
+
export function normalizeInitModelMatrix(input) {
|
|
1133
|
+
const tierInputs = [input.lowModel, input.medModel, input.highModel];
|
|
1134
|
+
if (tierInputs.every((value) => value === undefined))
|
|
1135
|
+
return undefined;
|
|
1136
|
+
if (input.provider !== undefined || input.model !== undefined) {
|
|
1137
|
+
throw new Error("init --low-model/--med-model/--high-model cannot be combined with --provider or --model");
|
|
1138
|
+
}
|
|
1139
|
+
if (tierInputs.some((value) => value === undefined)) {
|
|
1140
|
+
throw new Error("init model matrix requires --low-model, --med-model, and --high-model together");
|
|
1141
|
+
}
|
|
1142
|
+
const lowModel = normalizeInitModelReference({ model: input.lowModel });
|
|
1143
|
+
const medModel = normalizeInitModelReference({ model: input.medModel });
|
|
1144
|
+
const highModel = normalizeInitModelReference({ model: input.highModel });
|
|
1145
|
+
if (!lowModel || !medModel || !highModel) {
|
|
1146
|
+
throw new Error("init model matrix contains an empty model reference");
|
|
1147
|
+
}
|
|
1148
|
+
return {
|
|
1149
|
+
LOW: lowModel,
|
|
1150
|
+
MED: medModel,
|
|
1151
|
+
HIGH: highModel,
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1131
1154
|
function normalizeRelativePath(relativePath) {
|
|
1132
1155
|
return relativePath.split(path.sep).join("/");
|
|
1133
1156
|
}
|
|
@@ -1363,6 +1386,7 @@ async function readExistingSurfaceState(repoRoot) {
|
|
|
1363
1386
|
/** The IDE schema reference remains a structural update signal. */
|
|
1364
1387
|
const HARNESS_STRUCTURAL_VALUE_KEYS = new Set(["$schema"]);
|
|
1365
1388
|
const PI_MODEL_ROUTING_KEYS = new Set(["LOW", "MED", "HIGH", "defaultModel"]);
|
|
1389
|
+
const HARNESS_OPEN_MAP_PATHS = new Set(["entrypoints", "artifacts", "scripts"]);
|
|
1366
1390
|
function isPiModelRoutingPath(path) {
|
|
1367
1391
|
return path.length === 2 && path[0] === "executors" && path[1] === "pi";
|
|
1368
1392
|
}
|
|
@@ -1400,6 +1424,48 @@ function harnessJsonShapeMatches(current, desired, path = []) {
|
|
|
1400
1424
|
}
|
|
1401
1425
|
return true;
|
|
1402
1426
|
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Align harness.json object properties with the current generated contract while
|
|
1429
|
+
* preserving project-owned scalar/array values. Pi tier routing is deliberately
|
|
1430
|
+
* opaque: existing LOW/MED/HIGH values survive byte-for-value, and missing tiers
|
|
1431
|
+
* stay missing until model readiness selects them from the Pi catalog.
|
|
1432
|
+
*/
|
|
1433
|
+
function reconcileHarnessStructure(current, desired, path = []) {
|
|
1434
|
+
if (!isRecord(desired)) {
|
|
1435
|
+
if (HARNESS_STRUCTURAL_VALUE_KEYS.has(path.at(-1) ?? ""))
|
|
1436
|
+
return desired;
|
|
1437
|
+
if (Array.isArray(desired) &&
|
|
1438
|
+
Array.isArray(current)) {
|
|
1439
|
+
return current;
|
|
1440
|
+
}
|
|
1441
|
+
return current !== undefined && typeof current === typeof desired
|
|
1442
|
+
? current
|
|
1443
|
+
: desired;
|
|
1444
|
+
}
|
|
1445
|
+
const currentRecord = isRecord(current) ? current : {};
|
|
1446
|
+
const next = {};
|
|
1447
|
+
for (const [key, desiredValue] of Object.entries(desired)) {
|
|
1448
|
+
if (isPiModelRoutingPath(path) && PI_MODEL_ROUTING_KEYS.has(key)) {
|
|
1449
|
+
if (key in currentRecord)
|
|
1450
|
+
next[key] = currentRecord[key];
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
next[key] = reconcileHarnessStructure(currentRecord[key], desiredValue, [...path, key]);
|
|
1454
|
+
}
|
|
1455
|
+
if (isPiModelRoutingPath(path)) {
|
|
1456
|
+
for (const key of PI_MODEL_ROUTING_KEYS) {
|
|
1457
|
+
if (key in currentRecord && !(key in next))
|
|
1458
|
+
next[key] = currentRecord[key];
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
if (path.length === 1 && HARNESS_OPEN_MAP_PATHS.has(path[0])) {
|
|
1462
|
+
for (const [key, value] of Object.entries(currentRecord)) {
|
|
1463
|
+
if (!(key in next))
|
|
1464
|
+
next[key] = value;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
return next;
|
|
1468
|
+
}
|
|
1403
1469
|
/**
|
|
1404
1470
|
* Stable package/source anchor for generated desired content: sha256 of the
|
|
1405
1471
|
* desired content re-rendered through buildDesiredSurfaceContent with fixed
|
|
@@ -1574,6 +1640,13 @@ async function writeInitSurfaceState(input) {
|
|
|
1574
1640
|
file.acceptedMerge = { ...priorFile.acceptedMerge };
|
|
1575
1641
|
}
|
|
1576
1642
|
}
|
|
1643
|
+
for (const [pathName, priorFile] of Object.entries(input.preserveOwnershipFrom.files)) {
|
|
1644
|
+
if (state.files[pathName] || priorFile.mode === "state")
|
|
1645
|
+
continue;
|
|
1646
|
+
if (await exists(path.join(input.repoRoot, pathName))) {
|
|
1647
|
+
state.files[pathName] = { ...priorFile };
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1577
1650
|
}
|
|
1578
1651
|
if (input.acceptCurrentHarnessAsMerge) {
|
|
1579
1652
|
const harness = state.files["harness.json"];
|
|
@@ -1725,7 +1798,8 @@ function assessHarnessModelMigration(harness) {
|
|
|
1725
1798
|
}
|
|
1726
1799
|
const pi = isRecord(executorRecord.pi) ? executorRecord.pi : {};
|
|
1727
1800
|
const hasDefaultModel = "defaultModel" in pi;
|
|
1728
|
-
const
|
|
1801
|
+
const tierCount = PI_MODEL_TIERS.filter((tier) => tier in pi).length;
|
|
1802
|
+
const hasTier = tierCount > 0;
|
|
1729
1803
|
if (["requiresApiKey", "provider", "model"].some((field) => field in pi) ||
|
|
1730
1804
|
Object.keys(pi).some((field) => !["description", "enabled", "defaultModel", ...PI_MODEL_TIERS].includes(field))) {
|
|
1731
1805
|
return {
|
|
@@ -1739,8 +1813,16 @@ function assessHarnessModelMigration(harness) {
|
|
|
1739
1813
|
reason: "legacy top-level model or workflow fields cannot be proven equivalent",
|
|
1740
1814
|
};
|
|
1741
1815
|
}
|
|
1816
|
+
if (tierCount > 0 && tierCount < PI_MODEL_TIERS.length) {
|
|
1817
|
+
return {
|
|
1818
|
+
kind: "ambiguous",
|
|
1819
|
+
reason: "executors.pi contains an incomplete LOW/MED/HIGH model matrix",
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1742
1822
|
if (!hasDefaultModel)
|
|
1743
1823
|
return { kind: "none" };
|
|
1824
|
+
if (tierCount === PI_MODEL_TIERS.length)
|
|
1825
|
+
return { kind: "none" };
|
|
1744
1826
|
if (hasTier) {
|
|
1745
1827
|
return {
|
|
1746
1828
|
kind: "ambiguous",
|
|
@@ -2428,7 +2510,9 @@ export function buildInitInstructions(input) {
|
|
|
2428
2510
|
"",
|
|
2429
2511
|
`- Project name: default \`${projectName}\`.`,
|
|
2430
2512
|
`- Governance root: default \`${governanceRoot}\`.`,
|
|
2431
|
-
"-
|
|
2513
|
+
"- 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.",
|
|
2514
|
+
"- 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.",
|
|
2515
|
+
"- If `piSetup.status` is `model-definition-required`, ask only for the missing provider/model id, API/base URL, context window, maximum output, inputs/reasoning, cost constraints, and credential method. Help merge Pi models.json without exposing credential values, rerun the catalog, and only then initialize. If definitions exist but credentials are missing, configure auth instead of rewriting harness tiers.",
|
|
2432
2516
|
"- Default: merge existing AGENTS.md, harness.json, and loop-agent governance docs instead of overwriting user content.",
|
|
2433
2517
|
"",
|
|
2434
2518
|
"## Initialization Upgrade Routing",
|
|
@@ -2463,12 +2547,14 @@ export function buildInitInstructions(input) {
|
|
|
2463
2547
|
"",
|
|
2464
2548
|
"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.",
|
|
2465
2549
|
"",
|
|
2466
|
-
"1.
|
|
2467
|
-
"2.
|
|
2468
|
-
"3.
|
|
2469
|
-
"4.
|
|
2550
|
+
"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.",
|
|
2551
|
+
"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.",
|
|
2552
|
+
"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.",
|
|
2553
|
+
"4. Run deterministic init with all three tier flags and merge defaults.",
|
|
2554
|
+
"5. Continue immediately into project inspection and adaptation.",
|
|
2555
|
+
"6. Finish by running the init health checks and quick verification; `init doctor` must confirm the Pi model matrix before any DAG is started.",
|
|
2470
2556
|
"",
|
|
2471
|
-
"
|
|
2557
|
+
"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.",
|
|
2472
2558
|
"",
|
|
2473
2559
|
"## Required Model Adaptation",
|
|
2474
2560
|
"",
|
|
@@ -2502,6 +2588,7 @@ export async function initializeLoopAgentProject(options) {
|
|
|
2502
2588
|
const profile = options.profile ?? "full";
|
|
2503
2589
|
const merge = options.merge ?? true;
|
|
2504
2590
|
const model = normalizeInitModelReference(options);
|
|
2591
|
+
const modelMatrix = options.modelMatrix;
|
|
2505
2592
|
const written = [];
|
|
2506
2593
|
const skipped = [];
|
|
2507
2594
|
const assetRoot = await findPackageRoot();
|
|
@@ -2538,6 +2625,7 @@ export async function initializeLoopAgentProject(options) {
|
|
|
2538
2625
|
projectName,
|
|
2539
2626
|
governanceRoot,
|
|
2540
2627
|
model,
|
|
2628
|
+
modelMatrix,
|
|
2541
2629
|
});
|
|
2542
2630
|
await writeText({
|
|
2543
2631
|
repoRoot,
|
|
@@ -2680,7 +2768,7 @@ export async function initializeLoopAgentProject(options) {
|
|
|
2680
2768
|
projectName,
|
|
2681
2769
|
governanceRoot,
|
|
2682
2770
|
stateKind: "recorded",
|
|
2683
|
-
acceptCurrentHarnessAsMerge: model !== undefined,
|
|
2771
|
+
acceptCurrentHarnessAsMerge: model !== undefined || modelMatrix !== undefined,
|
|
2684
2772
|
});
|
|
2685
2773
|
written.push(INIT_SURFACE_STATE_PATH);
|
|
2686
2774
|
return {
|
|
@@ -2743,11 +2831,7 @@ function modelMergeTaskFor(pathName, state, allPaths, recordedState) {
|
|
|
2743
2831
|
"Keep writes inside allowedPaths only.",
|
|
2744
2832
|
],
|
|
2745
2833
|
verification: [
|
|
2746
|
-
|
|
2747
|
-
"loop-agent init doctor --repo-root .",
|
|
2748
|
-
"loop-agent inspect --repo-root .",
|
|
2749
|
-
"loop-agent docs audit --repo-root .",
|
|
2750
|
-
"bash scripts/check-repo.sh",
|
|
2834
|
+
`git diff --check -- ${pathName}`,
|
|
2751
2835
|
],
|
|
2752
2836
|
evidence: {
|
|
2753
2837
|
baseSha256: recordedState?.files[pathName]?.currentSha256,
|
|
@@ -2900,15 +2984,52 @@ export async function checkInitUpdate(input) {
|
|
|
2900
2984
|
(state.mode === "copied" || state.mode === "generated") &&
|
|
2901
2985
|
state.currentSha256 !== undefined &&
|
|
2902
2986
|
recordedState.files[pathName]?.currentSha256 === state.currentSha256) {
|
|
2987
|
+
if (pathName === "harness.json") {
|
|
2988
|
+
// harness.json always contains project-owned routing and configuration.
|
|
2989
|
+
// Even when its bytes still match the recorded baseline, package evolution
|
|
2990
|
+
// must use a semantic merge instead of replacing the whole file.
|
|
2991
|
+
modelMergeTasks.push(modelMergeTaskFor(pathName, state, allPaths, recordedState));
|
|
2992
|
+
}
|
|
2993
|
+
else {
|
|
2994
|
+
deterministicActions.push({
|
|
2995
|
+
type: "refresh-owned-file",
|
|
2996
|
+
path: pathName,
|
|
2997
|
+
mode: state.mode,
|
|
2998
|
+
reason: "refresh an unchanged recorded init file to the current generated or bundled content",
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
else {
|
|
3003
|
+
modelMergeTasks.push(modelMergeTaskFor(pathName, state, allPaths, recordedState));
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
const retiredActionPaths = new Set(retiredLayoutActions.map((action) => action.path));
|
|
3008
|
+
if (recordedState) {
|
|
3009
|
+
for (const [pathName, recordedFile] of Object.entries(recordedState.files)) {
|
|
3010
|
+
if (currentState.files[pathName] ||
|
|
3011
|
+
retiredActionPaths.has(pathName) ||
|
|
3012
|
+
recordedFile.mode === "state" ||
|
|
3013
|
+
recordedFile.mode === "directory") {
|
|
3014
|
+
continue;
|
|
3015
|
+
}
|
|
3016
|
+
const currentSha256 = await fileSha256IfExists(path.join(repoRoot, pathName));
|
|
3017
|
+
if (!currentSha256)
|
|
3018
|
+
continue;
|
|
3019
|
+
if (recordedFile.currentSha256 &&
|
|
3020
|
+
currentSha256 === recordedFile.currentSha256) {
|
|
2903
3021
|
deterministicActions.push({
|
|
2904
|
-
type: "
|
|
3022
|
+
type: "remove-owned-file",
|
|
2905
3023
|
path: pathName,
|
|
2906
|
-
|
|
2907
|
-
reason: "
|
|
3024
|
+
expectedSha256: currentSha256,
|
|
3025
|
+
reason: "remove an unchanged recorded init file that is no longer part of the current initialization surface",
|
|
2908
3026
|
});
|
|
2909
3027
|
}
|
|
2910
|
-
else {
|
|
2911
|
-
|
|
3028
|
+
else if (!humanDecisions.some((decision) => decision.path === pathName)) {
|
|
3029
|
+
humanDecisions.push({
|
|
3030
|
+
path: pathName,
|
|
3031
|
+
reason: "this recorded init file was removed from the current surface but has local changes; review whether to preserve or delete it",
|
|
3032
|
+
});
|
|
2912
3033
|
}
|
|
2913
3034
|
}
|
|
2914
3035
|
}
|
|
@@ -2916,9 +3037,12 @@ export async function checkInitUpdate(input) {
|
|
|
2916
3037
|
// Safe harness hygiene: strip obsolete pi.requiresApiKey without full harness rewrite.
|
|
2917
3038
|
// Cursor requiresApiKey is intentionally kept when present.
|
|
2918
3039
|
const harnessPath = path.join(repoRoot, "harness.json");
|
|
3040
|
+
let currentHarness;
|
|
2919
3041
|
if (await exists(harnessPath)) {
|
|
2920
3042
|
try {
|
|
2921
3043
|
const harness = JSON.parse(await readFile(harnessPath, "utf-8"));
|
|
3044
|
+
if (isRecord(harness))
|
|
3045
|
+
currentHarness = harness;
|
|
2922
3046
|
if (recordedState &&
|
|
2923
3047
|
isRecord(harness) &&
|
|
2924
3048
|
harness.$schema !== expectedHarnessSchemaRef(governanceRoot)) {
|
|
@@ -2942,10 +3066,11 @@ export async function checkInitUpdate(input) {
|
|
|
2942
3066
|
// A harness that already matches the current generated surface except for
|
|
2943
3067
|
// opaque Pi routing fields is modern, not a legacy-routing candidate.
|
|
2944
3068
|
// Keep the explicit migration assessment for every other harness shape.
|
|
3069
|
+
let modelMigration = { kind: "none" };
|
|
2945
3070
|
if (isRecord(harness) &&
|
|
2946
3071
|
currentState.files["harness.json"]?.relationship !==
|
|
2947
3072
|
"matches-current-generated") {
|
|
2948
|
-
|
|
3073
|
+
modelMigration = assessHarnessModelMigration(harness);
|
|
2949
3074
|
if (modelMigration.kind === "safe") {
|
|
2950
3075
|
deterministicActions.push({
|
|
2951
3076
|
type: "migrate-harness-model-fields",
|
|
@@ -2975,11 +3100,83 @@ export async function checkInitUpdate(input) {
|
|
|
2975
3100
|
modelMergeTasks.push(mergeTask);
|
|
2976
3101
|
}
|
|
2977
3102
|
}
|
|
3103
|
+
if (recordedState &&
|
|
3104
|
+
isRecord(harness) &&
|
|
3105
|
+
modelMigration.kind !== "ambiguous") {
|
|
3106
|
+
const desired = await buildDesiredSurfaceContent({
|
|
3107
|
+
assetRoot,
|
|
3108
|
+
repoRoot,
|
|
3109
|
+
projectName,
|
|
3110
|
+
governanceRoot,
|
|
3111
|
+
entry: { path: "harness.json", mode: "generated" },
|
|
3112
|
+
});
|
|
3113
|
+
if (desired.content !== undefined) {
|
|
3114
|
+
const desiredHarness = JSON.parse(desired.content);
|
|
3115
|
+
const reconciled = reconcileHarnessStructure(harness, desiredHarness);
|
|
3116
|
+
if (!isDeepStrictEqual(reconciled, harness)) {
|
|
3117
|
+
deterministicActions.push({
|
|
3118
|
+
type: "reconcile-harness-structure",
|
|
3119
|
+
path: "harness.json",
|
|
3120
|
+
reason: "add current harness properties, remove retired properties, and preserve project-owned values",
|
|
3121
|
+
});
|
|
3122
|
+
const existingTask = modelMergeTasks.findIndex((task) => task.path === "harness.json");
|
|
3123
|
+
if (existingTask >= 0)
|
|
3124
|
+
modelMergeTasks.splice(existingTask, 1);
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
2978
3128
|
}
|
|
2979
3129
|
catch {
|
|
2980
3130
|
// leave malformed harness for model merge / doctor
|
|
2981
3131
|
}
|
|
2982
3132
|
}
|
|
3133
|
+
const modelReadiness = input.modelCatalog
|
|
3134
|
+
? assessInitPiModelReadiness(isRecord(currentHarness?.executors) &&
|
|
3135
|
+
isRecord(currentHarness.executors.pi)
|
|
3136
|
+
? currentHarness.executors.pi
|
|
3137
|
+
: undefined, input.modelCatalog)
|
|
3138
|
+
: undefined;
|
|
3139
|
+
if (modelReadiness?.status === "selection-required" ||
|
|
3140
|
+
modelReadiness?.status === "reselection-required") {
|
|
3141
|
+
const state = currentState.files["harness.json"];
|
|
3142
|
+
const task = modelMergeTaskFor("harness.json", state, allPaths, recordedState);
|
|
3143
|
+
task.reason =
|
|
3144
|
+
modelReadiness.status === "selection-required"
|
|
3145
|
+
? "harness.json has no complete Pi model matrix that resolves against the current Pi model catalog"
|
|
3146
|
+
: `only unavailable Pi tiers need reselection: ${modelReadiness.reselectTiers.join(", ")}`;
|
|
3147
|
+
task.mergeRules = [
|
|
3148
|
+
`Preserve these currently resolvable tiers byte-for-byte: ${modelReadiness.preserveTiers.join(", ") || "none"}.`,
|
|
3149
|
+
`Select replacements only for: ${modelReadiness.reselectTiers.join(", ")}.`,
|
|
3150
|
+
"Choose replacements from eligibleReferences using target-project needs, capability, context window, latency, and cost; tiers may intentionally share a model.",
|
|
3151
|
+
"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.",
|
|
3152
|
+
"Do not add credentials or raw Pi configuration to harness.json.",
|
|
3153
|
+
];
|
|
3154
|
+
task.verification = [
|
|
3155
|
+
"loop-agent init model-catalog --repo-root . --json",
|
|
3156
|
+
"loop-agent init check-update --repo-root . --json",
|
|
3157
|
+
"loop-agent init doctor --repo-root .",
|
|
3158
|
+
"bash scripts/check-repo.sh",
|
|
3159
|
+
];
|
|
3160
|
+
const existingTask = modelMergeTasks.findIndex((candidate) => candidate.path === "harness.json");
|
|
3161
|
+
if (existingTask >= 0)
|
|
3162
|
+
modelMergeTasks[existingTask] = task;
|
|
3163
|
+
else
|
|
3164
|
+
modelMergeTasks.push(task);
|
|
3165
|
+
}
|
|
3166
|
+
else if (modelReadiness &&
|
|
3167
|
+
modelReadiness.status !== "ready") {
|
|
3168
|
+
const setup = input.modelCatalog?.piSetup;
|
|
3169
|
+
const reason = setup?.status === "model-definition-required"
|
|
3170
|
+
? `${setup.reason} Ask for: ${setup.requiredUserInput.join("; ")}.`
|
|
3171
|
+
: setup?.status === "credentials-required" ||
|
|
3172
|
+
setup?.status === "discovery-failed"
|
|
3173
|
+
? setup.reason
|
|
3174
|
+
: "One or more configured Pi tiers are known but not credential-ready; enable their provider without changing the tier selection.";
|
|
3175
|
+
humanDecisions.push({
|
|
3176
|
+
path: "~/.pi/agent/models.json",
|
|
3177
|
+
reason,
|
|
3178
|
+
});
|
|
3179
|
+
}
|
|
2983
3180
|
const partial = {
|
|
2984
3181
|
repoRoot,
|
|
2985
3182
|
controllerVersion: currentState.controllerVersion,
|
|
@@ -2987,6 +3184,7 @@ export async function checkInitUpdate(input) {
|
|
|
2987
3184
|
deterministicActions,
|
|
2988
3185
|
modelMergeTasks,
|
|
2989
3186
|
humanDecisions,
|
|
3187
|
+
...(modelReadiness ? { modelReadiness } : {}),
|
|
2990
3188
|
summary: {
|
|
2991
3189
|
missing: deterministicActions.filter((action) => action.type !== "bootstrap-surface").length,
|
|
2992
3190
|
modelMerge: modelMergeTasks.length,
|
|
@@ -2994,6 +3192,9 @@ export async function checkInitUpdate(input) {
|
|
|
2994
3192
|
},
|
|
2995
3193
|
};
|
|
2996
3194
|
const recommendedNext = recommendedNextFor(partial);
|
|
3195
|
+
if (modelReadiness && modelReadiness.status !== "ready") {
|
|
3196
|
+
recommendedNext.unshift("loop-agent init model-catalog --repo-root <target> --json");
|
|
3197
|
+
}
|
|
2997
3198
|
// Project settings are part of the surface. Do not inspect user home by default.
|
|
2998
3199
|
const piInspection = await inspectProjectPiSettings({ repoRoot });
|
|
2999
3200
|
return {
|
|
@@ -3105,9 +3306,36 @@ async function applySafeAction(input) {
|
|
|
3105
3306
|
const target = path.join(input.repoRoot, input.action.path);
|
|
3106
3307
|
if (!(await exists(target)))
|
|
3107
3308
|
return false;
|
|
3309
|
+
if (input.action.expectedSha256 &&
|
|
3310
|
+
(await fileSha256IfExists(target)) !== input.action.expectedSha256) {
|
|
3311
|
+
return false;
|
|
3312
|
+
}
|
|
3108
3313
|
await rm(target, { force: true });
|
|
3109
3314
|
return true;
|
|
3110
3315
|
}
|
|
3316
|
+
if (input.action.type === "reconcile-harness-structure") {
|
|
3317
|
+
const target = path.join(input.repoRoot, "harness.json");
|
|
3318
|
+
if (!(await exists(target)))
|
|
3319
|
+
return false;
|
|
3320
|
+
const current = JSON.parse(await readFile(target, "utf-8"));
|
|
3321
|
+
if (!isRecord(current))
|
|
3322
|
+
return false;
|
|
3323
|
+
const assetRoot = await findPackageRoot();
|
|
3324
|
+
const desired = await buildDesiredSurfaceContent({
|
|
3325
|
+
assetRoot,
|
|
3326
|
+
repoRoot: input.repoRoot,
|
|
3327
|
+
projectName: input.projectName,
|
|
3328
|
+
governanceRoot: input.governanceRoot,
|
|
3329
|
+
entry: { path: "harness.json", mode: "generated" },
|
|
3330
|
+
});
|
|
3331
|
+
if (desired.content === undefined)
|
|
3332
|
+
return false;
|
|
3333
|
+
const next = reconcileHarnessStructure(current, JSON.parse(desired.content));
|
|
3334
|
+
if (isDeepStrictEqual(next, current))
|
|
3335
|
+
return false;
|
|
3336
|
+
await writeFile(target, `${JSON.stringify(next, null, 2)}\n`, "utf-8");
|
|
3337
|
+
return true;
|
|
3338
|
+
}
|
|
3111
3339
|
if (input.action.type === "remove-empty-directory") {
|
|
3112
3340
|
const target = path.join(input.repoRoot, input.action.path);
|
|
3113
3341
|
if (!(await isDirectoryEmpty(target)))
|
|
@@ -3162,6 +3390,8 @@ async function applySafeAction(input) {
|
|
|
3162
3390
|
return true;
|
|
3163
3391
|
}
|
|
3164
3392
|
if (input.action.type === "refresh-owned-file") {
|
|
3393
|
+
if (input.action.path === "harness.json")
|
|
3394
|
+
return false;
|
|
3165
3395
|
await writeFile(target, desired.content, "utf-8");
|
|
3166
3396
|
return true;
|
|
3167
3397
|
}
|
|
@@ -3180,6 +3410,52 @@ export async function applyInitUpdate(input) {
|
|
|
3180
3410
|
const applied = [];
|
|
3181
3411
|
const skipped = [];
|
|
3182
3412
|
const clientRecoveryMode = input.clientRecovery ?? "auto";
|
|
3413
|
+
const requestedModelMatrix = input.modelMatrix ??
|
|
3414
|
+
(input.model
|
|
3415
|
+
? { LOW: input.model, MED: input.model, HIGH: input.model }
|
|
3416
|
+
: undefined);
|
|
3417
|
+
if (requestedModelMatrix) {
|
|
3418
|
+
if (!input.modelCatalog) {
|
|
3419
|
+
throw new Error("init update model selection requires a fresh Pi model catalog");
|
|
3420
|
+
}
|
|
3421
|
+
validateInitModelMatrixAgainstCatalog(requestedModelMatrix, input.modelCatalog);
|
|
3422
|
+
const harnessPath = path.join(repoRoot, "harness.json");
|
|
3423
|
+
const harness = JSON.parse(await readFile(harnessPath, "utf-8"));
|
|
3424
|
+
if (!isRecord(harness) ||
|
|
3425
|
+
!isRecord(harness.executors) ||
|
|
3426
|
+
!isRecord(harness.executors.pi)) {
|
|
3427
|
+
throw new Error("init update cannot select models until harness.json executors.pi exists");
|
|
3428
|
+
}
|
|
3429
|
+
const readiness = assessInitPiModelReadiness(harness.executors.pi, input.modelCatalog);
|
|
3430
|
+
if (readiness.status === "credentials-required" ||
|
|
3431
|
+
readiness.status === "model-definition-required" ||
|
|
3432
|
+
readiness.status === "discovery-failed") {
|
|
3433
|
+
throw new Error(`init update cannot change harness model routing while Pi model readiness is ${readiness.status}`);
|
|
3434
|
+
}
|
|
3435
|
+
for (const tier of readiness.preserveTiers) {
|
|
3436
|
+
const currentReference = readiness.tiers[tier].resolvedReference;
|
|
3437
|
+
if (currentReference &&
|
|
3438
|
+
requestedModelMatrix[tier] !== currentReference) {
|
|
3439
|
+
throw new Error(`init update preserves the current Pi ${tier} model because it is still in the Pi catalog: ${currentReference}`);
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
if (readiness.reselectTiers.length > 0) {
|
|
3443
|
+
const nextPi = { ...harness.executors.pi };
|
|
3444
|
+
for (const tier of readiness.reselectTiers) {
|
|
3445
|
+
nextPi[tier] = requestedModelMatrix[tier];
|
|
3446
|
+
}
|
|
3447
|
+
const next = {
|
|
3448
|
+
...harness,
|
|
3449
|
+
executors: { ...harness.executors, pi: nextPi },
|
|
3450
|
+
};
|
|
3451
|
+
await writeFile(harnessPath, `${JSON.stringify(next, null, 2)}\n`, "utf-8");
|
|
3452
|
+
applied.push({
|
|
3453
|
+
type: "write-harness-model-matrix",
|
|
3454
|
+
path: "harness.json",
|
|
3455
|
+
reason: `replace only unavailable Pi tiers: ${readiness.reselectTiers.join(", ")}`,
|
|
3456
|
+
});
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3183
3459
|
if (input.bootstrapSurface) {
|
|
3184
3460
|
await writeInitSurfaceState({
|
|
3185
3461
|
repoRoot,
|
|
@@ -3206,6 +3482,7 @@ export async function applyInitUpdate(input) {
|
|
|
3206
3482
|
governanceRoot,
|
|
3207
3483
|
clientRecovery: clientRecoveryMode,
|
|
3208
3484
|
homeDir: input.homeDir,
|
|
3485
|
+
modelCatalog: input.modelCatalog,
|
|
3209
3486
|
});
|
|
3210
3487
|
for (const action of report.deterministicActions) {
|
|
3211
3488
|
if (action.type === "bootstrap-surface") {
|
|
@@ -3245,6 +3522,7 @@ export async function applyInitUpdate(input) {
|
|
|
3245
3522
|
governanceRoot,
|
|
3246
3523
|
clientRecovery: clientRecoveryMode,
|
|
3247
3524
|
homeDir: input.homeDir,
|
|
3525
|
+
modelCatalog: input.modelCatalog,
|
|
3248
3526
|
}),
|
|
3249
3527
|
};
|
|
3250
3528
|
}
|
|
@@ -3258,6 +3536,16 @@ function formatClientRecoverySummary(report) {
|
|
|
3258
3536
|
`clientRecovery.pi.path: ${pi.path}`,
|
|
3259
3537
|
];
|
|
3260
3538
|
}
|
|
3539
|
+
function formatModelReadinessSummary(report) {
|
|
3540
|
+
const readiness = report.modelReadiness;
|
|
3541
|
+
if (!readiness)
|
|
3542
|
+
return [];
|
|
3543
|
+
return [
|
|
3544
|
+
`modelReadiness.status: ${readiness.status}`,
|
|
3545
|
+
`modelReadiness.preserveTiers: ${readiness.preserveTiers.join(",") || "none"}`,
|
|
3546
|
+
`modelReadiness.reselectTiers: ${readiness.reselectTiers.join(",") || "none"}`,
|
|
3547
|
+
];
|
|
3548
|
+
}
|
|
3261
3549
|
function formatCheckUpdateText(report) {
|
|
3262
3550
|
return [
|
|
3263
3551
|
`loop-agent init update check for ${report.repoRoot}`,
|
|
@@ -3266,6 +3554,7 @@ function formatCheckUpdateText(report) {
|
|
|
3266
3554
|
`deterministicActions: ${report.deterministicActions.length}`,
|
|
3267
3555
|
`modelMergeTasks: ${report.modelMergeTasks.length}`,
|
|
3268
3556
|
`humanDecisions: ${report.humanDecisions.length}`,
|
|
3557
|
+
...formatModelReadinessSummary(report),
|
|
3269
3558
|
...formatClientRecoverySummary(report),
|
|
3270
3559
|
"recommendedNext:",
|
|
3271
3560
|
...report.recommendedNext.map((item) => `- ${item}`),
|
|
@@ -3279,6 +3568,14 @@ function formatCheckUpdateMarkdown(report) {
|
|
|
3279
3568
|
`- repoRoot: \`${report.repoRoot}\``,
|
|
3280
3569
|
`- controllerVersion: \`${report.controllerVersion}\``,
|
|
3281
3570
|
`- surfaceState: \`${report.surfaceState}\``,
|
|
3571
|
+
...(report.modelReadiness
|
|
3572
|
+
? [
|
|
3573
|
+
"",
|
|
3574
|
+
"## Pi Model Readiness",
|
|
3575
|
+
"",
|
|
3576
|
+
...formatModelReadinessSummary(report).map((line) => `- ${line}`),
|
|
3577
|
+
]
|
|
3578
|
+
: []),
|
|
3282
3579
|
...(piLines.length > 0
|
|
3283
3580
|
? ["", "## Client Recovery (Pi user config, read-only)", "", ...piLines]
|
|
3284
3581
|
: []),
|
|
@@ -3313,6 +3610,14 @@ export async function runInitDoctor(input) {
|
|
|
3313
3610
|
const manifest = await loadHarnessManifest(repoRoot);
|
|
3314
3611
|
add("harness.json", manifest.adapter === "loop-agent" || manifest.project === "loop-agent", `project=${manifest.project}`);
|
|
3315
3612
|
add("governance root", await exists(path.join(repoRoot, manifest.governanceRoot, "README.md")), manifest.governanceRoot);
|
|
3613
|
+
if (input.modelCatalog) {
|
|
3614
|
+
const readiness = assessInitPiModelReadiness(isRecord(manifest.executors?.pi)
|
|
3615
|
+
? manifest.executors.pi
|
|
3616
|
+
: undefined, input.modelCatalog);
|
|
3617
|
+
add("Pi model matrix", readiness.status === "ready", readiness.status === "ready"
|
|
3618
|
+
? "LOW/MED/HIGH resolve against the current Pi model catalog"
|
|
3619
|
+
: `${readiness.status}; preserve=${readiness.preserveTiers.join(",") || "none"}; reselect=${readiness.reselectTiers.join(",") || "none"}`);
|
|
3620
|
+
}
|
|
3316
3621
|
}
|
|
3317
3622
|
catch (error) {
|
|
3318
3623
|
add("harness.json", false, error instanceof Error ? error.message : String(error));
|
|
@@ -3354,6 +3659,7 @@ export async function runInitReconcile(input) {
|
|
|
3354
3659
|
repoRoot,
|
|
3355
3660
|
projectName: input.projectName,
|
|
3356
3661
|
governanceRoot: input.governanceRoot,
|
|
3662
|
+
modelCatalog: input.modelCatalog,
|
|
3357
3663
|
});
|
|
3358
3664
|
if (preReport.surfaceState === "missing") {
|
|
3359
3665
|
return { status: "needs-baseline", report: preReport };
|
|
@@ -3374,6 +3680,7 @@ export async function runInitReconcile(input) {
|
|
|
3374
3680
|
projectName: input.projectName,
|
|
3375
3681
|
governanceRoot: input.governanceRoot,
|
|
3376
3682
|
applySafe: true,
|
|
3683
|
+
modelCatalog: input.modelCatalog,
|
|
3377
3684
|
});
|
|
3378
3685
|
const status = update.report.ok
|
|
3379
3686
|
? "clean"
|
|
@@ -3395,6 +3702,9 @@ function parseInitArgs(repoRoot, args) {
|
|
|
3395
3702
|
let merge = true;
|
|
3396
3703
|
let provider;
|
|
3397
3704
|
let model;
|
|
3705
|
+
let lowModel;
|
|
3706
|
+
let medModel;
|
|
3707
|
+
let highModel;
|
|
3398
3708
|
let clientRecovery = "auto";
|
|
3399
3709
|
let json = false;
|
|
3400
3710
|
let markdown = false;
|
|
@@ -3409,6 +3719,7 @@ function parseInitArgs(repoRoot, args) {
|
|
|
3409
3719
|
const arg = args[i];
|
|
3410
3720
|
if ((arg === "instructions" ||
|
|
3411
3721
|
arg === "doctor" ||
|
|
3722
|
+
arg === "model-catalog" ||
|
|
3412
3723
|
arg === "check-update" ||
|
|
3413
3724
|
arg === "update" ||
|
|
3414
3725
|
arg === "reconcile" ||
|
|
@@ -3441,6 +3752,18 @@ function parseInitArgs(repoRoot, args) {
|
|
|
3441
3752
|
model = args[++i];
|
|
3442
3753
|
else if (arg.startsWith("--model="))
|
|
3443
3754
|
model = arg.slice("--model=".length);
|
|
3755
|
+
else if (arg === "--low-model")
|
|
3756
|
+
lowModel = args[++i];
|
|
3757
|
+
else if (arg.startsWith("--low-model="))
|
|
3758
|
+
lowModel = arg.slice("--low-model=".length);
|
|
3759
|
+
else if (arg === "--med-model")
|
|
3760
|
+
medModel = args[++i];
|
|
3761
|
+
else if (arg.startsWith("--med-model="))
|
|
3762
|
+
medModel = arg.slice("--med-model=".length);
|
|
3763
|
+
else if (arg === "--high-model")
|
|
3764
|
+
highModel = args[++i];
|
|
3765
|
+
else if (arg.startsWith("--high-model="))
|
|
3766
|
+
highModel = arg.slice("--high-model=".length);
|
|
3444
3767
|
else if (arg === "--client-recovery")
|
|
3445
3768
|
clientRecovery = parseClientRecoveryMode(args[++i]);
|
|
3446
3769
|
else if (arg.startsWith("--client-recovery="))
|
|
@@ -3478,6 +3801,13 @@ function parseInitArgs(repoRoot, args) {
|
|
|
3478
3801
|
throw new Error("init upgrade --version-choice must be current|upgrade|cancel|retry");
|
|
3479
3802
|
}
|
|
3480
3803
|
const normalizedModel = normalizeInitModelReference({ provider, model });
|
|
3804
|
+
const modelMatrix = normalizeInitModelMatrix({
|
|
3805
|
+
provider,
|
|
3806
|
+
model,
|
|
3807
|
+
lowModel,
|
|
3808
|
+
medModel,
|
|
3809
|
+
highModel,
|
|
3810
|
+
});
|
|
3481
3811
|
return {
|
|
3482
3812
|
repoRoot,
|
|
3483
3813
|
projectName,
|
|
@@ -3486,6 +3816,7 @@ function parseInitArgs(repoRoot, args) {
|
|
|
3486
3816
|
merge,
|
|
3487
3817
|
provider,
|
|
3488
3818
|
model: normalizedModel,
|
|
3819
|
+
modelMatrix,
|
|
3489
3820
|
clientRecovery,
|
|
3490
3821
|
subcommand,
|
|
3491
3822
|
json,
|
|
@@ -3499,21 +3830,49 @@ function parseInitArgs(repoRoot, args) {
|
|
|
3499
3830
|
versionChoice,
|
|
3500
3831
|
};
|
|
3501
3832
|
}
|
|
3833
|
+
async function discoverInitCatalogForRepo(repoRoot) {
|
|
3834
|
+
const harness = await readJsonIfExists(path.join(repoRoot, "harness.json"));
|
|
3835
|
+
const pi = isRecord(harness.executors) && isRecord(harness.executors.pi)
|
|
3836
|
+
? harness.executors.pi
|
|
3837
|
+
: undefined;
|
|
3838
|
+
const includePiReferences = pi
|
|
3839
|
+
? PI_MODEL_TIERS.flatMap((tier) => {
|
|
3840
|
+
const value = pi[tier];
|
|
3841
|
+
const model = typeof value === "string"
|
|
3842
|
+
? value
|
|
3843
|
+
: isRecord(value) && typeof value.model === "string"
|
|
3844
|
+
? value.model
|
|
3845
|
+
: undefined;
|
|
3846
|
+
return model && isQualifiedModelReference(model) ? [model] : [];
|
|
3847
|
+
})
|
|
3848
|
+
: [];
|
|
3849
|
+
return discoverInitModelCatalog({ repoRoot, includePiReferences });
|
|
3850
|
+
}
|
|
3502
3851
|
export async function runInit(repoRoot, rawArgs, dependencies) {
|
|
3503
3852
|
const parsed = parseInitArgs(repoRoot, rawArgs);
|
|
3504
3853
|
if (parsed.subcommand === "instructions") {
|
|
3505
3854
|
console.log(buildInitInstructions(parsed));
|
|
3506
3855
|
return;
|
|
3507
3856
|
}
|
|
3857
|
+
if (parsed.subcommand === "model-catalog") {
|
|
3858
|
+
const catalog = await discoverInitCatalogForRepo(parsed.repoRoot);
|
|
3859
|
+
console.log(JSON.stringify(catalog, null, 2));
|
|
3860
|
+
return;
|
|
3861
|
+
}
|
|
3508
3862
|
if (parsed.subcommand === "doctor") {
|
|
3509
|
-
const
|
|
3863
|
+
const modelCatalog = await discoverInitCatalogForRepo(parsed.repoRoot);
|
|
3864
|
+
const report = await runInitDoctor({
|
|
3865
|
+
repoRoot: parsed.repoRoot,
|
|
3866
|
+
modelCatalog,
|
|
3867
|
+
});
|
|
3510
3868
|
console.log(JSON.stringify(report, null, 2));
|
|
3511
3869
|
if (!report.ok)
|
|
3512
3870
|
process.exitCode = 1;
|
|
3513
3871
|
return;
|
|
3514
3872
|
}
|
|
3515
3873
|
if (parsed.subcommand === "check-update") {
|
|
3516
|
-
const
|
|
3874
|
+
const modelCatalog = await discoverInitCatalogForRepo(parsed.repoRoot);
|
|
3875
|
+
const report = await checkInitUpdate({ ...parsed, modelCatalog });
|
|
3517
3876
|
if (parsed.json)
|
|
3518
3877
|
console.log(JSON.stringify(report, null, 2));
|
|
3519
3878
|
else if (parsed.markdown)
|
|
@@ -3526,7 +3885,11 @@ export async function runInit(repoRoot, rawArgs, dependencies) {
|
|
|
3526
3885
|
if (!parsed.bootstrapSurface && !parsed.applySafe) {
|
|
3527
3886
|
throw new Error("usage: init update [--bootstrap-surface] [--apply-safe]");
|
|
3528
3887
|
}
|
|
3529
|
-
|
|
3888
|
+
if ((parsed.model || parsed.modelMatrix) && !parsed.applySafe) {
|
|
3889
|
+
throw new Error("init update model selection requires --apply-safe");
|
|
3890
|
+
}
|
|
3891
|
+
const modelCatalog = await discoverInitCatalogForRepo(parsed.repoRoot);
|
|
3892
|
+
const result = await applyInitUpdate({ ...parsed, modelCatalog });
|
|
3530
3893
|
console.log(parsed.json
|
|
3531
3894
|
? JSON.stringify(result, null, 2)
|
|
3532
3895
|
: formatInitUpdateResult(result));
|
|
@@ -3537,12 +3900,23 @@ export async function runInit(repoRoot, rawArgs, dependencies) {
|
|
|
3537
3900
|
const selected = [parsed.upgradeStatus, parsed.upgradeContinue, parsed.upgradeReport].filter(Boolean).length;
|
|
3538
3901
|
if (selected > 1)
|
|
3539
3902
|
throw new Error("init upgrade accepts only one of --status, --continue, or --report");
|
|
3903
|
+
const mode = parsed.upgradeStatus
|
|
3904
|
+
? "status"
|
|
3905
|
+
: parsed.upgradeContinue
|
|
3906
|
+
? "continue"
|
|
3907
|
+
: parsed.upgradeReport
|
|
3908
|
+
? "report"
|
|
3909
|
+
: "start";
|
|
3910
|
+
const modelCatalog = mode === "start"
|
|
3911
|
+
? await discoverInitCatalogForRepo(parsed.repoRoot)
|
|
3912
|
+
: undefined;
|
|
3540
3913
|
const result = await runInitUpgrade({
|
|
3541
3914
|
repoRoot: parsed.repoRoot,
|
|
3542
3915
|
runId: parsed.runId,
|
|
3543
|
-
mode
|
|
3916
|
+
mode,
|
|
3544
3917
|
versionChoice: parsed.versionChoice,
|
|
3545
3918
|
readRuntimeActivity: dependencies.readRuntimeActivity,
|
|
3919
|
+
modelCatalog,
|
|
3546
3920
|
});
|
|
3547
3921
|
if ("markdown" in result)
|
|
3548
3922
|
console.log(result.markdown);
|
|
@@ -3551,8 +3925,10 @@ export async function runInit(repoRoot, rawArgs, dependencies) {
|
|
|
3551
3925
|
return;
|
|
3552
3926
|
}
|
|
3553
3927
|
if (parsed.subcommand === "reconcile") {
|
|
3928
|
+
const modelCatalog = await discoverInitCatalogForRepo(parsed.repoRoot);
|
|
3554
3929
|
const result = await runInitReconcile({
|
|
3555
3930
|
...parsed,
|
|
3931
|
+
modelCatalog,
|
|
3556
3932
|
readRuntimeActivity: dependencies.readRuntimeActivity,
|
|
3557
3933
|
});
|
|
3558
3934
|
console.log(parsed.json
|
|
@@ -3560,6 +3936,31 @@ export async function runInit(repoRoot, rawArgs, dependencies) {
|
|
|
3560
3936
|
: formatReconcileResult(result));
|
|
3561
3937
|
return;
|
|
3562
3938
|
}
|
|
3939
|
+
const catalog = await discoverInitCatalogForRepo(parsed.repoRoot);
|
|
3940
|
+
const requestedMatrix = parsed.modelMatrix ??
|
|
3941
|
+
(parsed.model
|
|
3942
|
+
? { LOW: parsed.model, MED: parsed.model, HIGH: parsed.model }
|
|
3943
|
+
: undefined);
|
|
3944
|
+
if (requestedMatrix) {
|
|
3945
|
+
validateInitModelMatrixAgainstCatalog(requestedMatrix, catalog);
|
|
3946
|
+
}
|
|
3947
|
+
else {
|
|
3948
|
+
const harness = await readJsonIfExists(path.join(parsed.repoRoot, "harness.json"));
|
|
3949
|
+
const readiness = assessInitPiModelReadiness(isRecord(harness.executors) && isRecord(harness.executors.pi)
|
|
3950
|
+
? harness.executors.pi
|
|
3951
|
+
: undefined, catalog);
|
|
3952
|
+
if (readiness.status !== "ready") {
|
|
3953
|
+
const setup = catalog.piSetup;
|
|
3954
|
+
if (setup.status === "model-definition-required") {
|
|
3955
|
+
throw new Error(`Pi model setup is required before init. Ask the user for: ${setup.requiredUserInput.join("; ")}. Help merge the definition into ${setup.configPath} without storing credential values, rerun init model-catalog, then initialize with all three tier flags.`);
|
|
3956
|
+
}
|
|
3957
|
+
if (setup.status === "credentials-required" ||
|
|
3958
|
+
setup.status === "discovery-failed") {
|
|
3959
|
+
throw new Error(`${setup.reason} ${setup.nextSteps.join(" ")}`);
|
|
3960
|
+
}
|
|
3961
|
+
throw new Error(`Pi model selection is required before init (${readiness.status}). Run init model-catalog, preserve tiers still present in Pi, select only missing tiers, and pass --low-model/--med-model/--high-model.`);
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3563
3964
|
const result = await initializeLoopAgentProject(parsed);
|
|
3564
3965
|
console.log(parsed.json ? JSON.stringify(result, null, 2) : formatInitResult(result));
|
|
3565
3966
|
}
|