@tea-agent/loop-agent 0.28.8 → 0.28.9
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 +15 -0
- package/dist/application/task-lifecycle/advance.js +24 -1
- package/dist/application/task-lifecycle/plan-transitions.js +7 -4
- package/dist/cli/program.js +2 -2
- package/dist/commands/init.js +232 -65
- package/dist/executors/dag-pi-executor.js +25 -0
- package/dist/executors/model-routing.js +0 -58
- package/dist/executors/pi-sdk-executor.js +14 -1
- package/dist/executors/shell-executor.js +123 -36
- package/dist/governance/manifest-types.js +18 -51
- package/dist/task/source-prepare/build-draft.js +10 -9
- package/dist/task/source-prepare/prepare.js +107 -4
- package/dist/workflows/dag/init-hybrid.js +184 -46
- package/dist/workflows/dag/node-execution.js +176 -21
- package/dist/workflows/dag/types.js +58 -0
- package/docs/governance/README.md +1 -1
- package/docs/templates/harness.schema.json +4 -8
- package/package.json +1 -1
- package/skills/loop-agent/references/command-reference.md +5 -2
- package/skills/loop-agent/references/model-routing.md +14 -7
package/CHANGELOG.md
CHANGED
|
@@ -37,6 +37,13 @@ function hasEngineeringBoundary(input) {
|
|
|
37
37
|
input.fromText ||
|
|
38
38
|
input.fromDraftPath);
|
|
39
39
|
}
|
|
40
|
+
/** Inputs that must produce a new managed-contract revision when explicit. */
|
|
41
|
+
function hasContractChangingInput(input) {
|
|
42
|
+
return Boolean(hasEngineeringBoundary(input) ||
|
|
43
|
+
input.title ||
|
|
44
|
+
input.taskKind ||
|
|
45
|
+
input.featureId);
|
|
46
|
+
}
|
|
40
47
|
function uniqueTransitions(existing, extra) {
|
|
41
48
|
const set = new Set(existing);
|
|
42
49
|
for (const item of extra)
|
|
@@ -197,6 +204,10 @@ export async function advanceTaskLifecycle(input) {
|
|
|
197
204
|
* never reaches prepare-contract / gate.
|
|
198
205
|
*/
|
|
199
206
|
let importedPrdThisCall = false;
|
|
207
|
+
/** A managed contract mutation is prepared at most once per invocation. */
|
|
208
|
+
let preparedContractThisCall = false;
|
|
209
|
+
/** A successful managed-contract mutation invalidates the prior DAG/gate. */
|
|
210
|
+
let regenerateDagThisCall = false;
|
|
200
211
|
let snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
|
|
201
212
|
let record = (await loadLifecycleRecord(input.repoRoot, input.taskId)) ??
|
|
202
213
|
emptyLifecycleRecord(input.taskId);
|
|
@@ -251,6 +262,9 @@ export async function advanceTaskLifecycle(input) {
|
|
|
251
262
|
verifyCommands: applied.verifyCommands ?? input.verifyCommands,
|
|
252
263
|
};
|
|
253
264
|
const hasBoundary = hasEngineeringBoundary(effectiveInput);
|
|
265
|
+
let refreshManagedContractThisCall = Boolean(hasContractChangingInput(effectiveInput) &&
|
|
266
|
+
(snapshot.contract?.effectiveStatus === "managed" ||
|
|
267
|
+
Boolean(snapshot.contract?.revision && snapshot.contract.revision > 0)));
|
|
254
268
|
if (applied.accepted) {
|
|
255
269
|
warnings.push({
|
|
256
270
|
code: "RECOMMENDATIONS_ACCEPTED",
|
|
@@ -341,11 +355,13 @@ export async function advanceTaskLifecycle(input) {
|
|
|
341
355
|
snapshot: current,
|
|
342
356
|
hasPrdInput: hasPrd && !importedPrdThisCall,
|
|
343
357
|
hasEngineeringBoundary: hasBoundary,
|
|
358
|
+
refreshManagedContract: refreshManagedContractThisCall,
|
|
359
|
+
forceDagRefresh: regenerateDagThisCall,
|
|
344
360
|
approveGate: consumeApproveGate || approvedThisCall,
|
|
345
361
|
rejectGate: false,
|
|
346
362
|
dryRun,
|
|
347
363
|
skipFinalize: Boolean(input.skipFinalize),
|
|
348
|
-
forcePrepareContract: Boolean(input.fromDraftPath),
|
|
364
|
+
forcePrepareContract: Boolean(input.fromDraftPath) && !preparedContractThisCall,
|
|
349
365
|
});
|
|
350
366
|
if (dryRun) {
|
|
351
367
|
const plan = planOnce(snapshot);
|
|
@@ -496,6 +512,12 @@ export async function advanceTaskLifecycle(input) {
|
|
|
496
512
|
});
|
|
497
513
|
break;
|
|
498
514
|
}
|
|
515
|
+
preparedContractThisCall = true;
|
|
516
|
+
if (refreshManagedContractThisCall || input.fromDraftPath) {
|
|
517
|
+
// The old DAG and gate were bound to the pre-mutation contract.
|
|
518
|
+
refreshManagedContractThisCall = false;
|
|
519
|
+
regenerateDagThisCall = true;
|
|
520
|
+
}
|
|
499
521
|
record = appendTransition(record, "contract-managed");
|
|
500
522
|
completed.push("contract-managed");
|
|
501
523
|
await saveLifecycleRecord(input.repoRoot, record);
|
|
@@ -540,6 +562,7 @@ export async function advanceTaskLifecycle(input) {
|
|
|
540
562
|
});
|
|
541
563
|
break;
|
|
542
564
|
}
|
|
565
|
+
regenerateDagThisCall = false;
|
|
543
566
|
record = appendTransition(record, "dag-generated");
|
|
544
567
|
completed.push("dag-generated");
|
|
545
568
|
await saveLifecycleRecord(input.repoRoot, record);
|
|
@@ -53,7 +53,7 @@ export function planTransitions(input) {
|
|
|
53
53
|
// Prefer prepare once PRDs are on disk; re-import only when caller still
|
|
54
54
|
// signals hasPrdInput (advance.ts latches this after one import per call).
|
|
55
55
|
const shouldImportPrd = input.hasPrdInput;
|
|
56
|
-
if (input.forcePrepareContract) {
|
|
56
|
+
if (input.forcePrepareContract || input.refreshManagedContract) {
|
|
57
57
|
if (shouldImportPrd) {
|
|
58
58
|
actions.push({ type: "import-prd" });
|
|
59
59
|
expectedTransitions.push("prd-imported");
|
|
@@ -106,11 +106,14 @@ export function planTransitions(input) {
|
|
|
106
106
|
}
|
|
107
107
|
const dagExists = snapshot.dagDraft?.exists === true;
|
|
108
108
|
const strictValidated = snapshot.dagDraft?.strictValidated === true;
|
|
109
|
-
|
|
109
|
+
const dagRefreshRequired = Boolean(input.forceDagRefresh ||
|
|
110
|
+
input.refreshManagedContract ||
|
|
111
|
+
input.forcePrepareContract);
|
|
112
|
+
if (!dagExists || dagRefreshRequired) {
|
|
110
113
|
actions.push({ type: "generate-dag" });
|
|
111
114
|
expectedTransitions.push("dag-generated");
|
|
112
115
|
}
|
|
113
|
-
if (!strictValidated) {
|
|
116
|
+
if (!strictValidated || dagRefreshRequired) {
|
|
114
117
|
actions.push({ type: "strict-validate-dag" });
|
|
115
118
|
expectedTransitions.push("dag-strict-validated");
|
|
116
119
|
}
|
|
@@ -127,7 +130,7 @@ export function planTransitions(input) {
|
|
|
127
130
|
snapshot.lifecycleState === "needs-attention" ||
|
|
128
131
|
snapshot.lifecycleState === "awaiting-decision";
|
|
129
132
|
const approvedForCurrent = postRunState || (input.approveGate && gate !== null);
|
|
130
|
-
if (!approvedForCurrent && (strictValidated || !dagExists)) {
|
|
133
|
+
if (!approvedForCurrent && (strictValidated || !dagExists || dagRefreshRequired)) {
|
|
131
134
|
// Gate opens after generate+validate in the advance loop; plan includes open.
|
|
132
135
|
if (!input.approveGate) {
|
|
133
136
|
actions.push({ type: "open-write-set-gate" });
|
package/dist/cli/program.js
CHANGED
|
@@ -588,8 +588,8 @@ export function buildLoopAgentProgram(options) {
|
|
|
588
588
|
.option("--profile <profile>", "full|minimal", "full")
|
|
589
589
|
.option("--merge", "merge existing files")
|
|
590
590
|
.option("--no-merge", "skip existing files")
|
|
591
|
-
.option("--provider <provider>", "model provider")
|
|
592
|
-
.option("--model <model>", "model
|
|
591
|
+
.option("--provider <provider>", "provider paired with a bare --model (otherwise use provider/model)")
|
|
592
|
+
.option("--model <model>", "qualified provider/model, or a bare model paired with --provider")
|
|
593
593
|
.option("--json", "print JSON")
|
|
594
594
|
.option("--markdown", "print Markdown")
|
|
595
595
|
.option("--bootstrap-surface", "write an inferred .harness/init-surface.json baseline")
|
package/dist/commands/init.js
CHANGED
|
@@ -923,15 +923,23 @@ function buildHarness(input) {
|
|
|
923
923
|
delete pi.requiresApiKey;
|
|
924
924
|
mergedExecutors.pi = pi;
|
|
925
925
|
}
|
|
926
|
-
//
|
|
927
|
-
//
|
|
928
|
-
if (input.
|
|
926
|
+
// Init always writes a Pi tier matrix. `defaultModel` remains runtime
|
|
927
|
+
// compatibility for old projects only and is never fresh-init output.
|
|
928
|
+
if (input.model) {
|
|
929
929
|
const piExisting = isRecord(mergedExecutors.pi) ? mergedExecutors.pi : {};
|
|
930
930
|
mergedExecutors.pi = {
|
|
931
931
|
...piExisting,
|
|
932
|
-
|
|
932
|
+
LOW: input.model,
|
|
933
|
+
MED: input.model,
|
|
934
|
+
HIGH: input.model,
|
|
933
935
|
};
|
|
934
936
|
}
|
|
937
|
+
if (isRecord(mergedExecutors.pi)) {
|
|
938
|
+
const pi = { ...mergedExecutors.pi };
|
|
939
|
+
if (hasCompletePiTierMatrix(pi))
|
|
940
|
+
delete pi.defaultModel;
|
|
941
|
+
mergedExecutors.pi = pi;
|
|
942
|
+
}
|
|
935
943
|
const harness = {
|
|
936
944
|
...input.existing,
|
|
937
945
|
$schema: expectedHarnessSchemaRef(input.governanceRoot),
|
|
@@ -977,29 +985,17 @@ function buildHarness(input) {
|
|
|
977
985
|
}),
|
|
978
986
|
executors: mergedExecutors,
|
|
979
987
|
};
|
|
980
|
-
//
|
|
981
|
-
//
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
Object.keys(input.existing.modelProfiles).length > 0) {
|
|
992
|
-
harness.modelProfiles = input.existing.modelProfiles;
|
|
993
|
-
}
|
|
994
|
-
else {
|
|
995
|
-
delete harness.modelProfiles;
|
|
996
|
-
}
|
|
997
|
-
if (isRecord(input.existing.modelRouting) &&
|
|
998
|
-
Object.keys(input.existing.modelRouting).length > 0) {
|
|
999
|
-
harness.modelRouting = input.existing.modelRouting;
|
|
1000
|
-
}
|
|
1001
|
-
else {
|
|
1002
|
-
delete harness.modelRouting;
|
|
988
|
+
// Fresh init owns only the current DAG-facing surface. Legacy step routing is
|
|
989
|
+
// diagnosed by check-update rather than copied into a new harness projection.
|
|
990
|
+
for (const legacyField of [
|
|
991
|
+
"model",
|
|
992
|
+
"models",
|
|
993
|
+
"modelProfiles",
|
|
994
|
+
"modelRouting",
|
|
995
|
+
"verify",
|
|
996
|
+
"sequentialWorkflowRole",
|
|
997
|
+
]) {
|
|
998
|
+
delete harness[legacyField];
|
|
1003
999
|
}
|
|
1004
1000
|
return harness;
|
|
1005
1001
|
}
|
|
@@ -1133,6 +1129,59 @@ function sha256Buffer(value) {
|
|
|
1133
1129
|
function isRecord(value) {
|
|
1134
1130
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1135
1131
|
}
|
|
1132
|
+
/** A complete matrix has three effective values, not just three present keys. */
|
|
1133
|
+
function hasCompletePiTierMatrix(pi) {
|
|
1134
|
+
return ["LOW", "MED", "HIGH"].every((tier) => {
|
|
1135
|
+
const value = pi[tier];
|
|
1136
|
+
if (typeof value === "string")
|
|
1137
|
+
return value.length > 0 && value !== "default";
|
|
1138
|
+
if (!isRecord(value))
|
|
1139
|
+
return false;
|
|
1140
|
+
const keys = Object.keys(value);
|
|
1141
|
+
if (!keys.every((key) => key === "model" || key === "thinking"))
|
|
1142
|
+
return false;
|
|
1143
|
+
if (typeof value.model !== "string" || value.model.length === 0)
|
|
1144
|
+
return false;
|
|
1145
|
+
if (value.model === "default")
|
|
1146
|
+
return false;
|
|
1147
|
+
return value.thinking === undefined || typeof value.thinking === "string";
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
/** Resolve init CLI input to one unambiguous Pi `provider/model` reference. */
|
|
1151
|
+
export function normalizeInitModelReference(input) {
|
|
1152
|
+
const provider = input.provider?.trim();
|
|
1153
|
+
const model = input.model?.trim();
|
|
1154
|
+
if (input.provider !== undefined && !provider) {
|
|
1155
|
+
throw new Error("init --provider must be a non-empty provider id");
|
|
1156
|
+
}
|
|
1157
|
+
if (input.model !== undefined && !model) {
|
|
1158
|
+
throw new Error("init --model must be a non-empty model reference");
|
|
1159
|
+
}
|
|
1160
|
+
if (provider && !model) {
|
|
1161
|
+
throw new Error("init --provider requires --model");
|
|
1162
|
+
}
|
|
1163
|
+
if (!model)
|
|
1164
|
+
return undefined;
|
|
1165
|
+
const slash = model.indexOf("/");
|
|
1166
|
+
if (slash < 0) {
|
|
1167
|
+
if (!provider) {
|
|
1168
|
+
throw new Error("init --model must be a provider/model reference or be paired with --provider");
|
|
1169
|
+
}
|
|
1170
|
+
if (/\s|\//.test(provider) || /\s/.test(model)) {
|
|
1171
|
+
throw new Error("init provider/model values must not contain whitespace");
|
|
1172
|
+
}
|
|
1173
|
+
return `${provider}/${model}`;
|
|
1174
|
+
}
|
|
1175
|
+
const modelProvider = model.slice(0, slash);
|
|
1176
|
+
const modelId = model.slice(slash + 1);
|
|
1177
|
+
if (!modelProvider || !modelId || /\s/.test(modelProvider) || /\s/.test(modelId)) {
|
|
1178
|
+
throw new Error("init --model must use a non-empty provider/model reference");
|
|
1179
|
+
}
|
|
1180
|
+
if (provider && provider !== modelProvider) {
|
|
1181
|
+
throw new Error("init --provider conflicts with the provider qualified in --model");
|
|
1182
|
+
}
|
|
1183
|
+
return model;
|
|
1184
|
+
}
|
|
1136
1185
|
function normalizeRelativePath(relativePath) {
|
|
1137
1186
|
return relativePath.split(path.sep).join("/");
|
|
1138
1187
|
}
|
|
@@ -1506,6 +1555,17 @@ async function writeInitSurfaceState(input) {
|
|
|
1506
1555
|
}
|
|
1507
1556
|
}
|
|
1508
1557
|
}
|
|
1558
|
+
if (input.acceptCurrentHarnessAsMerge) {
|
|
1559
|
+
const harness = state.files["harness.json"];
|
|
1560
|
+
if (harness?.relationship === "local-existing-unknown" &&
|
|
1561
|
+
harness.currentSha256 !== undefined &&
|
|
1562
|
+
harness.sourceSha256 !== undefined) {
|
|
1563
|
+
harness.acceptedMerge = {
|
|
1564
|
+
currentSha256: harness.currentSha256,
|
|
1565
|
+
desiredSha256: harness.sourceSha256,
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1509
1569
|
const target = path.join(input.repoRoot, INIT_SURFACE_STATE_PATH);
|
|
1510
1570
|
await mkdir(path.dirname(target), { recursive: true });
|
|
1511
1571
|
await writeFile(target, `${JSON.stringify(state, null, 2)}\n`, "utf-8");
|
|
@@ -1596,39 +1656,103 @@ function collectSafeRetiredDirectories(paths) {
|
|
|
1596
1656
|
}
|
|
1597
1657
|
return [...dirs].sort((left, right) => right.split("/").length - left.split("/").length);
|
|
1598
1658
|
}
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1659
|
+
const PI_MODEL_TIERS = ["LOW", "MED", "HIGH"];
|
|
1660
|
+
const LEGACY_MODEL_FIELDS = [
|
|
1661
|
+
"model",
|
|
1662
|
+
"models",
|
|
1663
|
+
"modelProfiles",
|
|
1664
|
+
"modelRouting",
|
|
1665
|
+
"provider",
|
|
1666
|
+
"verify",
|
|
1667
|
+
"sequentialWorkflowRole",
|
|
1668
|
+
];
|
|
1669
|
+
function isQualifiedModelReference(value) {
|
|
1670
|
+
if (typeof value !== "string" || value.trim() !== value)
|
|
1671
|
+
return false;
|
|
1672
|
+
const slash = value.indexOf("/");
|
|
1673
|
+
return (slash > 0 &&
|
|
1674
|
+
slash < value.length - 1 &&
|
|
1675
|
+
!/\s/.test(value.slice(0, slash)) &&
|
|
1676
|
+
!/\s/.test(value.slice(slash + 1)));
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Only a single, qualified old default can be expanded automatically. All
|
|
1680
|
+
* routing/profile/tier ambiguity remains in the target for a bounded merge.
|
|
1681
|
+
*/
|
|
1682
|
+
function assessHarnessModelMigration(harness) {
|
|
1683
|
+
const hasLegacyFields = LEGACY_MODEL_FIELDS.some((field) => field in harness);
|
|
1684
|
+
const executors = harness.executors;
|
|
1685
|
+
if (executors !== undefined && !isRecord(executors)) {
|
|
1686
|
+
return hasLegacyFields
|
|
1687
|
+
? { kind: "ambiguous", reason: "executors is not an object" }
|
|
1688
|
+
: { kind: "none" };
|
|
1689
|
+
}
|
|
1690
|
+
const executorRecord = executors ?? {};
|
|
1691
|
+
if ("cursor" in executorRecord) {
|
|
1692
|
+
return {
|
|
1693
|
+
kind: "ambiguous",
|
|
1694
|
+
reason: "executors.cursor has user-owned runtime semantics",
|
|
1695
|
+
};
|
|
1605
1696
|
}
|
|
1606
|
-
if (isRecord(
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
}
|
|
1697
|
+
if ("pi" in executorRecord && !isRecord(executorRecord.pi)) {
|
|
1698
|
+
return {
|
|
1699
|
+
kind: "ambiguous",
|
|
1700
|
+
reason: "executors.pi is not an object",
|
|
1701
|
+
};
|
|
1612
1702
|
}
|
|
1613
|
-
|
|
1703
|
+
const pi = isRecord(executorRecord.pi) ? executorRecord.pi : {};
|
|
1704
|
+
const hasDefaultModel = "defaultModel" in pi;
|
|
1705
|
+
const hasTier = PI_MODEL_TIERS.some((tier) => tier in pi);
|
|
1706
|
+
if (["requiresApiKey", "provider", "model"].some((field) => field in pi) ||
|
|
1707
|
+
Object.keys(pi).some((field) => !["description", "enabled", "defaultModel", ...PI_MODEL_TIERS].includes(field))) {
|
|
1708
|
+
return {
|
|
1709
|
+
kind: "ambiguous",
|
|
1710
|
+
reason: "executors.pi contains unknown or deprecated model semantics",
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
if (hasLegacyFields) {
|
|
1714
|
+
return {
|
|
1715
|
+
kind: "ambiguous",
|
|
1716
|
+
reason: "legacy top-level model or workflow fields cannot be proven equivalent",
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
if (!hasDefaultModel)
|
|
1720
|
+
return { kind: "none" };
|
|
1721
|
+
if (hasTier) {
|
|
1722
|
+
return {
|
|
1723
|
+
kind: "ambiguous",
|
|
1724
|
+
reason: "existing LOW/MED/HIGH tier configuration cannot be replaced by defaultModel automatically",
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
if (pi.defaultModel === "default")
|
|
1728
|
+
return { kind: "none" };
|
|
1729
|
+
if (!isQualifiedModelReference(pi.defaultModel)) {
|
|
1730
|
+
return {
|
|
1731
|
+
kind: "ambiguous",
|
|
1732
|
+
reason: "defaultModel is not a complete provider/model reference",
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
return { kind: "safe", model: pi.defaultModel, removeDefaultModel: true };
|
|
1736
|
+
}
|
|
1737
|
+
function needsHarnessModelMigration(harness) {
|
|
1738
|
+
return assessHarnessModelMigration(harness).kind === "safe";
|
|
1614
1739
|
}
|
|
1615
1740
|
function migrateHarnessModelFields(harness) {
|
|
1741
|
+
const assessment = assessHarnessModelMigration(harness);
|
|
1742
|
+
if (assessment.kind !== "safe") {
|
|
1743
|
+
throw new Error("ambiguous harness model migration must be merged manually");
|
|
1744
|
+
}
|
|
1616
1745
|
const next = { ...harness };
|
|
1617
|
-
const
|
|
1618
|
-
|
|
1619
|
-
delete next.models;
|
|
1620
|
-
delete next.modelProfiles;
|
|
1621
|
-
delete next.modelRouting;
|
|
1746
|
+
for (const field of LEGACY_MODEL_FIELDS)
|
|
1747
|
+
delete next[field];
|
|
1622
1748
|
const executors = isRecord(next.executors) ? { ...next.executors } : {};
|
|
1623
|
-
delete executors.cursor;
|
|
1624
1749
|
const pi = isRecord(executors.pi) ? { ...executors.pi } : {};
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
if (typeof pi[level] !== "string")
|
|
1629
|
-
pi[level] = legacyModel;
|
|
1630
|
-
}
|
|
1750
|
+
if (assessment.model) {
|
|
1751
|
+
for (const tier of PI_MODEL_TIERS)
|
|
1752
|
+
pi[tier] = assessment.model;
|
|
1631
1753
|
}
|
|
1754
|
+
if (assessment.removeDefaultModel)
|
|
1755
|
+
delete pi.defaultModel;
|
|
1632
1756
|
executors.pi = pi;
|
|
1633
1757
|
next.executors = executors;
|
|
1634
1758
|
return next;
|
|
@@ -2177,6 +2301,14 @@ function buildTargetLoopAgentHarness(input) {
|
|
|
2177
2301
|
"",
|
|
2178
2302
|
"Use `loop-agent task advance` / `loop-agent task status` for non-trivial implementation work; advanced arbitrary DagSpec uses `loop-agent dag execute`.",
|
|
2179
2303
|
"",
|
|
2304
|
+
"## Pi Model Matrix",
|
|
2305
|
+
"",
|
|
2306
|
+
"New DAG work reads only `harness.json.executors.pi.LOW`, `MED`, and `HIGH`. Each tier must be a model string or `{ model, thinking? }`; use explicit `provider/model` references when choosing a provider. Fresh init does not write `defaultModel`, `models`, `modelProfiles`, or `modelRouting`. `defaultModel` remains a runtime fallback only for compatible older projects.",
|
|
2307
|
+
"",
|
|
2308
|
+
"```json",
|
|
2309
|
+
'{ "executors": { "pi": { "LOW": "provider/low", "MED": "provider/medium", "HIGH": "provider/high" } } }',
|
|
2310
|
+
"```",
|
|
2311
|
+
"",
|
|
2180
2312
|
"## Adaptive Liveness",
|
|
2181
2313
|
"",
|
|
2182
2314
|
"- Healthy Pi nodes are no longer stopped by a fixed 30-minute deadline. The default 4h absolute max is a final safety bound and cannot be extended by synthetic heartbeats.",
|
|
@@ -2273,7 +2405,7 @@ export function buildInitInstructions(input) {
|
|
|
2273
2405
|
"",
|
|
2274
2406
|
`- Project name: default \`${projectName}\`.`,
|
|
2275
2407
|
`- Governance root: default \`${governanceRoot}\`.`,
|
|
2276
|
-
"- Confirm
|
|
2408
|
+
"- Confirm the Pi LOW/MED/HIGH matrix. Explicit input must be `--model provider/model` or a paired `--provider provider --model model`; do not guess a provider for a bare model.",
|
|
2277
2409
|
"- Default: merge existing AGENTS.md, harness.json, and loop-agent governance docs instead of overwriting user content.",
|
|
2278
2410
|
"",
|
|
2279
2411
|
"## Initialization Upgrade Routing",
|
|
@@ -2284,7 +2416,7 @@ export function buildInitInstructions(input) {
|
|
|
2284
2416
|
"",
|
|
2285
2417
|
"## Apply Defaults",
|
|
2286
2418
|
"",
|
|
2287
|
-
'- Use the current loop-agent harness.json as the default template, but write target project name plus `adapter: "loop-agent"`.',
|
|
2419
|
+
'- Use the current loop-agent harness.json as the default template, but write target project name plus `adapter: "loop-agent"` and a Pi-only `executors.pi.LOW/MED/HIGH` matrix. Do not write `defaultModel` when all three tiers exist, and never project `model`, `models`, `modelProfiles`, `modelRouting`, `verify`, or `sequentialWorkflowRole`.',
|
|
2288
2420
|
`- ${DAG_HARD_GATE_TRIGGER}`,
|
|
2289
2421
|
"- Generate the target project's loop-agent script matrix from templates: structure check, docs index/link checks, active plan status, exec-plan index sync, harness runtime cleanliness, architecture boundaries, skill entry integrity, governance CI, project-test CI, and full CI.",
|
|
2290
2422
|
"- Copy or project only stack-agnostic governance scripts. For project-specific verification, packaging, release, or maintenance commands, generate the target-project version from templates plus the target repository's actual files instead of copying loop-agent's own TypeScript-specific scripts.",
|
|
@@ -2312,7 +2444,7 @@ export function buildInitInstructions(input) {
|
|
|
2312
2444
|
"3. Continue immediately into project inspection and adaptation.",
|
|
2313
2445
|
"4. Finish by running the init health checks and quick verification.",
|
|
2314
2446
|
"",
|
|
2315
|
-
"If provider/model is not specified and the existing
|
|
2447
|
+
"If provider/model is not specified and the existing tier matrix is usable, proceed with it and record the assumption in the handoff. Ask the user only when the choice affects credentials, cost, availability, or an explicitly requested model backend. An ambiguous old model configuration must stay in a bounded harness.json model merge/human decision; never silently reinterpret it.",
|
|
2316
2448
|
"",
|
|
2317
2449
|
"## Required Model Adaptation",
|
|
2318
2450
|
"",
|
|
@@ -2345,11 +2477,21 @@ export async function initializeLoopAgentProject(options) {
|
|
|
2345
2477
|
const governanceRoot = options.governanceRoot ?? DEFAULT_GOVERNANCE_ROOT;
|
|
2346
2478
|
const profile = options.profile ?? "full";
|
|
2347
2479
|
const merge = options.merge ?? true;
|
|
2480
|
+
const model = normalizeInitModelReference(options);
|
|
2348
2481
|
const written = [];
|
|
2349
2482
|
const skipped = [];
|
|
2350
2483
|
const assetRoot = await findPackageRoot();
|
|
2351
2484
|
const template = await readJsonIfExists(path.join(assetRoot, "harness.json"));
|
|
2352
|
-
|
|
2485
|
+
let existingHarness = await readJsonIfExists(path.join(repoRoot, "harness.json"));
|
|
2486
|
+
if (Object.keys(existingHarness).length > 0) {
|
|
2487
|
+
const migration = assessHarnessModelMigration(existingHarness);
|
|
2488
|
+
if (migration.kind === "ambiguous") {
|
|
2489
|
+
throw new Error(`init refuses to overwrite ambiguous legacy model configuration: ${migration.reason}; run init check-update and complete the harness.json model merge`);
|
|
2490
|
+
}
|
|
2491
|
+
if (migration.kind === "safe") {
|
|
2492
|
+
existingHarness = migrateHarnessModelFields(existingHarness);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2353
2495
|
await mkdir(repoRoot, { recursive: true });
|
|
2354
2496
|
const readmePath = path.join(repoRoot, "README.md");
|
|
2355
2497
|
const existingReadme = (await exists(readmePath))
|
|
@@ -2371,8 +2513,7 @@ export async function initializeLoopAgentProject(options) {
|
|
|
2371
2513
|
template,
|
|
2372
2514
|
projectName,
|
|
2373
2515
|
governanceRoot,
|
|
2374
|
-
|
|
2375
|
-
model: options.model,
|
|
2516
|
+
model,
|
|
2376
2517
|
});
|
|
2377
2518
|
await writeText({
|
|
2378
2519
|
repoRoot,
|
|
@@ -2515,6 +2656,7 @@ export async function initializeLoopAgentProject(options) {
|
|
|
2515
2656
|
projectName,
|
|
2516
2657
|
governanceRoot,
|
|
2517
2658
|
stateKind: "recorded",
|
|
2659
|
+
acceptCurrentHarnessAsMerge: model !== undefined,
|
|
2518
2660
|
});
|
|
2519
2661
|
written.push(INIT_SURFACE_STATE_PATH);
|
|
2520
2662
|
return {
|
|
@@ -2764,12 +2906,36 @@ export async function checkInitUpdate(input) {
|
|
|
2764
2906
|
reason: `replace the legacy default governanceRoot docs with ${governanceRoot}`,
|
|
2765
2907
|
});
|
|
2766
2908
|
}
|
|
2767
|
-
if (isRecord(harness)
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2909
|
+
if (isRecord(harness)) {
|
|
2910
|
+
const modelMigration = assessHarnessModelMigration(harness);
|
|
2911
|
+
if (modelMigration.kind === "safe") {
|
|
2912
|
+
deterministicActions.push({
|
|
2913
|
+
type: "migrate-harness-model-fields",
|
|
2914
|
+
path: "harness.json",
|
|
2915
|
+
reason: "expand one proven-equivalent legacy model into the Pi LOW/MED/HIGH matrix",
|
|
2916
|
+
});
|
|
2917
|
+
}
|
|
2918
|
+
else if (modelMigration.kind === "ambiguous") {
|
|
2919
|
+
const state = currentState.files["harness.json"];
|
|
2920
|
+
const mergeTask = modelMergeTaskFor("harness.json", state, allPaths, recordedState);
|
|
2921
|
+
mergeTask.reason = `legacy model configuration requires a human/model merge: ${modelMigration.reason}`;
|
|
2922
|
+
mergeTask.mergeRules = [
|
|
2923
|
+
"Preserve every existing harness.json field unless the user explicitly approves its migration.",
|
|
2924
|
+
"Map only unambiguous complete provider/model references to executors.pi.LOW, MED, and HIGH.",
|
|
2925
|
+
"Do not retain defaultModel when all three tiers are present; do not invent a provider for bare model ids.",
|
|
2926
|
+
"Do not restore models, modelProfiles, modelRouting, cursor, or other legacy routing fields.",
|
|
2927
|
+
];
|
|
2928
|
+
mergeTask.verification = [
|
|
2929
|
+
"loop-agent init check-update --repo-root . --json",
|
|
2930
|
+
"loop-agent inspect --repo-root .",
|
|
2931
|
+
"bash scripts/check-repo.sh",
|
|
2932
|
+
];
|
|
2933
|
+
const existingTask = modelMergeTasks.findIndex((task) => task.path === "harness.json");
|
|
2934
|
+
if (existingTask >= 0)
|
|
2935
|
+
modelMergeTasks[existingTask] = mergeTask;
|
|
2936
|
+
else
|
|
2937
|
+
modelMergeTasks.push(mergeTask);
|
|
2938
|
+
}
|
|
2773
2939
|
}
|
|
2774
2940
|
}
|
|
2775
2941
|
catch {
|
|
@@ -3273,6 +3439,7 @@ function parseInitArgs(repoRoot, args) {
|
|
|
3273
3439
|
if (versionChoice && !["current", "upgrade", "cancel", "retry"].includes(versionChoice)) {
|
|
3274
3440
|
throw new Error("init upgrade --version-choice must be current|upgrade|cancel|retry");
|
|
3275
3441
|
}
|
|
3442
|
+
const normalizedModel = normalizeInitModelReference({ provider, model });
|
|
3276
3443
|
return {
|
|
3277
3444
|
repoRoot,
|
|
3278
3445
|
projectName,
|
|
@@ -3280,7 +3447,7 @@ function parseInitArgs(repoRoot, args) {
|
|
|
3280
3447
|
profile,
|
|
3281
3448
|
merge,
|
|
3282
3449
|
provider,
|
|
3283
|
-
model,
|
|
3450
|
+
model: normalizedModel,
|
|
3284
3451
|
clientRecovery,
|
|
3285
3452
|
subcommand,
|
|
3286
3453
|
json,
|
|
@@ -311,6 +311,21 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
311
311
|
const persona = resolveDagPiPersona(input.task);
|
|
312
312
|
const step = resolveDagPiStepName(input.task);
|
|
313
313
|
const isWriteTask = isDagPiWriteTask(input.task);
|
|
314
|
+
if (isWriteTask && input.task.finalWriteSetApproval) {
|
|
315
|
+
const binding = input.task.finalWriteSetApproval;
|
|
316
|
+
const resolved = input.task.resolvedFinalWriteSetApproval;
|
|
317
|
+
if (!resolved ||
|
|
318
|
+
resolved.approvalSourceNodeId !== binding.approvalSourceNodeId ||
|
|
319
|
+
!/^[a-f0-9]{64}$/.test(resolved.approvalDigest)) {
|
|
320
|
+
return {
|
|
321
|
+
ok: false,
|
|
322
|
+
stdout: "",
|
|
323
|
+
stderr: "final-write-set-approval-invalid: writer authorization was not resolved before Pi execution",
|
|
324
|
+
failureCategory: "final-write-set-approval-invalid",
|
|
325
|
+
durationMs: Date.now() - started,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
}
|
|
314
329
|
const allowedSteps = isWriteTask ? PI_WRITE_STEPS : SAFE_PI_STEPS;
|
|
315
330
|
if (!allowedSteps.has(step)) {
|
|
316
331
|
return {
|
|
@@ -657,6 +672,9 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
657
672
|
runDir: meta.runDir,
|
|
658
673
|
nodeId: input.task.id,
|
|
659
674
|
writerNodeId: input.task.id,
|
|
675
|
+
effectiveWriteSet: input.task.writeSet ?? [],
|
|
676
|
+
approvalSourceNodeId: input.task.resolvedFinalWriteSetApproval?.approvalSourceNodeId,
|
|
677
|
+
approvalDigest: input.task.resolvedFinalWriteSetApproval?.approvalDigest,
|
|
660
678
|
changedFiles: changeManifestChangedFiles,
|
|
661
679
|
beforeStatus,
|
|
662
680
|
afterStatus: changeManifestAfterStatus ?? "",
|
|
@@ -966,6 +984,13 @@ async function persistWriterChangeManifest(input) {
|
|
|
966
984
|
const manifest = {
|
|
967
985
|
schemaVersion: 1,
|
|
968
986
|
writerNodeId: input.writerNodeId,
|
|
987
|
+
effectiveWriteSet: [...input.effectiveWriteSet],
|
|
988
|
+
...(input.approvalSourceNodeId
|
|
989
|
+
? { approvalSourceNodeId: input.approvalSourceNodeId }
|
|
990
|
+
: {}),
|
|
991
|
+
...(input.approvalDigest
|
|
992
|
+
? { approvalDigest: input.approvalDigest }
|
|
993
|
+
: {}),
|
|
969
994
|
changedFiles: input.changedFiles,
|
|
970
995
|
beforeStatusSha256: createHash("sha256")
|
|
971
996
|
.update(input.beforeStatus)
|
|
@@ -59,61 +59,3 @@ export function resolveExecutorModelMatrices(manifest) {
|
|
|
59
59
|
export function resolvePiModelMatrix(manifest) {
|
|
60
60
|
return resolveExecutorModelMatrices(manifest).pi;
|
|
61
61
|
}
|
|
62
|
-
export function resolveModelSelection(manifest, taskConfig, step, options) {
|
|
63
|
-
const retryAttempt = options?.retryAttempt ?? 0;
|
|
64
|
-
if (Object.keys(manifest.modelProfiles ?? {}).length === 0) {
|
|
65
|
-
return {
|
|
66
|
-
modelConfig: manifest.models?.[step],
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
const profileName = resolveProfileName(manifest, taskConfig.complexity, step, retryAttempt);
|
|
70
|
-
const profile = profileName
|
|
71
|
-
? manifest.modelProfiles?.[profileName]
|
|
72
|
-
: undefined;
|
|
73
|
-
if (!profile) {
|
|
74
|
-
return {
|
|
75
|
-
modelConfig: manifest.models?.[step],
|
|
76
|
-
profileName,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
return {
|
|
80
|
-
modelConfig: {
|
|
81
|
-
provider: profile.provider,
|
|
82
|
-
model: profile.model,
|
|
83
|
-
thinking: profile.thinking,
|
|
84
|
-
timeoutMs: profile.timeoutMs ?? manifest.models?.[step]?.timeoutMs,
|
|
85
|
-
fallback: profile.fallback,
|
|
86
|
-
},
|
|
87
|
-
profileName,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
function resolveProfileName(manifest, complexity, step, retryAttempt) {
|
|
91
|
-
if (step === "implement" &&
|
|
92
|
-
retryAttempt > 0 &&
|
|
93
|
-
manifest.modelRouting?.implementRetry) {
|
|
94
|
-
return manifest.modelRouting.implementRetry;
|
|
95
|
-
}
|
|
96
|
-
const route = manifest.modelRouting?.[step];
|
|
97
|
-
if (!route) {
|
|
98
|
-
return undefined;
|
|
99
|
-
}
|
|
100
|
-
return route[complexity];
|
|
101
|
-
}
|
|
102
|
-
export function formatModelSelectionLabel(modelConfig, profileName) {
|
|
103
|
-
if (!modelConfig) {
|
|
104
|
-
return profileName ? `${profileName}` : "default";
|
|
105
|
-
}
|
|
106
|
-
const base = modelConfig.provider && modelConfig.model
|
|
107
|
-
? `${modelConfig.provider}/${modelConfig.model}`
|
|
108
|
-
: (modelConfig.model ?? "default");
|
|
109
|
-
return profileName ? `${profileName}:${base}` : base;
|
|
110
|
-
}
|
|
111
|
-
export function formatFallbackLabel(profile) {
|
|
112
|
-
if (!profile?.fallback) {
|
|
113
|
-
return undefined;
|
|
114
|
-
}
|
|
115
|
-
const fallback = profile.fallback;
|
|
116
|
-
return fallback.provider && fallback.model
|
|
117
|
-
? `${fallback.provider}/${fallback.model}`
|
|
118
|
-
: fallback.model;
|
|
119
|
-
}
|